anycoder-ce7b6184 / index.html
AiCoderv2's picture
Upload folder using huggingface_hub
a919902 verified
raw
history blame
34.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>AnyCoder Chat — AI Chatbot</title>
<style>
:root {
--bg: #0b1020;
--bg2: #0f1427;
--panel: rgba(255, 255, 255, 0.06);
--panel-strong: rgba(255, 255, 255, 0.12);
--text: #e6e8f0;
--muted: #a9b1c6;
--accent: #7c5cff;
--accent-2: #00d4ff;
--danger: #ff5c7c;
--success: #2bd67b;
--shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
--radius: 16px;
--radius-sm: 10px;
--radius-lg: 20px;
--blur: 12px;
--glass: rgba(255, 255, 255, 0.06);
--glass-strong: rgba(255, 255, 255, 0.12);
}
.theme-light {
--bg: #f7f8fc;
--bg2: #eef1fb;
--panel: rgba(255, 255, 255, 0.7);
--panel-strong: rgba(255, 255, 255, 0.9);
--text: #14151a;
--muted: #4b4f5c;
--accent: #6b5cff;
--accent-2: #00a6ff;
--danger: #e11d48;
--success: #16a34a;
--glass: rgba(255, 255, 255, 0.7);
--glass-strong: rgba(255, 255, 255, 0.9);
--shadow: 0 10px 30px rgba(10, 20, 50, 0.15);
}
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
}
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji";
color: var(--text);
background: radial-gradient(1200px 700px at 10% 10%, rgba(124, 92, 255, 0.15), transparent 60%),
radial-gradient(900px 600px at 90% 20%, rgba(0, 212, 255, 0.12), transparent 60%),
linear-gradient(180deg, var(--bg), var(--bg2));
background-attachment: fixed;
overflow: hidden;
}
.app {
display: grid;
grid-template-rows: auto 1fr auto;
height: 100dvh;
max-height: 100dvh;
}
/* Header */
.header {
display: flex;
align-items: center;
gap: 14px;
padding: 14px clamp(12px, 2vw, 24px);
position: sticky;
top: 0;
z-index: 5;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0));
backdrop-filter: blur(8px);
}
.brand {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
background: var(--panel);
border: 1px solid var(--panel-strong);
border-radius: var(--radius);
box-shadow: var(--shadow);
backdrop-filter: blur(var(--blur));
}
.logo {
width: 36px;
height: 36px;
border-radius: 10px;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
display: grid;
place-items: center;
color: white;
font-weight: 900;
letter-spacing: 0.5px;
box-shadow: 0 8px 20px rgba(124, 92, 255, 0.35), inset 0 0 20px rgba(255, 255, 255, 0.2);
}
.brand h1 {
margin: 0;
font-size: 18px;
letter-spacing: 0.3px;
}
.brand small {
display: block;
color: var(--muted);
font-size: 12px;
margin-top: 2px;
}
.header-actions {
margin-left: auto;
display: flex;
align-items: center;
gap: 8px;
}
.btn {
border: 1px solid var(--panel-strong);
background: var(--panel);
color: var(--text);
padding: 10px 14px;
border-radius: 12px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
transition: all 0.2s ease;
backdrop-filter: blur(var(--blur));
box-shadow: var(--shadow);
user-select: none;
}
.btn:hover {
transform: translateY(-1px);
background: var(--glass-strong);
}
.btn:active {
transform: translateY(0);
}
.btn.primary {
background: linear-gradient(135deg, var(--accent), var(--accent-2));
border-color: transparent;
color: white;
box-shadow: 0 10px 25px rgba(124, 92, 255, 0.35);
}
.btn.ghost {
background: transparent;
border-color: var(--panel-strong);
}
.btn.small {
padding: 8px 10px;
border-radius: 10px;
font-size: 13px;
}
.btn .icon {
width: 18px;
height: 18px;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
/* API Key Modal */
.modal {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.8);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal.show {
display: flex;
}
.modal-content {
background: var(--panel);
border: 1px solid var(--panel-strong);
border-radius: var(--radius);
padding: 24px;
max-width: 500px;
width: 90%;
box-shadow: var(--shadow);
backdrop-filter: blur(var(--blur));
}
.modal-content h3 {
margin: 0 0 16px 0;
color: var(--text);
}
.modal-content input {
width: 100%;
padding: 12px;
border: 1px solid var(--panel-strong);
border-radius: 8px;
background: var(--glass);
color: var(--text);
font: inherit;
margin-bottom: 16px;
}
.modal-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
}
/* Chat area */
.chat {
position: relative;
overflow: hidden;
}
.messages {
position: absolute;
inset: 0;
overflow-y: auto;
padding: 16px clamp(10px, 2vw, 24px) 24px;
scroll-behavior: smooth;
}
.messages::-webkit-scrollbar {
width: 10px;
}
.messages::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, var(--panel-strong), transparent);
border-radius: 8px;
}
.empty {
height: 100%;
display: grid;
place-items: center;
pointer-events: none;
color: var(--muted);
}
.empty-inner {
text-align: center;
max-width: 680px;
padding: 30px;
border-radius: var(--radius);
background: var(--panel);
border: 1px solid var(--panel-strong);
backdrop-filter: blur(var(--blur));
box-shadow: var(--shadow);
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
justify-content: center;
}
.chip {
padding: 8px 12px;
border-radius: 999px;
background: var(--glass);
border: 1px solid var(--panel-strong);
cursor: pointer;
transition: all 0.2s ease;
font-size: 13px;
}
.chip:hover {
transform: translateY(-1px);
background: var(--glass-strong);
}
.msg {
display: grid;
grid-template-columns: 36px 1fr;
gap: 10px;
margin: 10px auto;
max-width: min(900px, 92vw);
align-items: flex-start;
}
.msg.user {
grid-template-columns: 1fr 36px;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 10px;
background: var(--panel);
border: 1px solid var(--panel-strong);
display: grid;
place-items: center;
backdrop-filter: blur(var(--blur));
}
.avatar.ai {
background: linear-gradient(135deg, rgba(124, 92, 255, 0.3), rgba(0, 212, 255, 0.3));
border-color: transparent;
}
.bubble {
padding: 12px 14px;
border-radius: 14px;
background: var(--panel);
border: 1px solid var(--panel-strong);
backdrop-filter: blur(var(--blur));
box-shadow: var(--shadow);
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
}
.msg.user .bubble {
background: linear-gradient(135deg, rgba(124, 92, 255, 0.15), rgba(0, 212, 255, 0.15));
border-color: transparent;
}
.bubble .meta {
font-size: 12px;
color: var(--muted);
margin-bottom: 6px;
}
.bubble .content {
min-height: 20px;
}
.bubble pre {
background: rgba(0, 0, 0, 0.35);
padding: 10px 12px;
border-radius: 10px;
overflow-x: auto;
border: 1px solid rgba(255, 255, 255, 0.12);
}
.bubble code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
background: rgba(255, 255, 255, 0.08);
padding: 2px 6px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.typing {
display: inline-flex;
gap: 4px;
align-items: center;
}
.dot {
width: 6px;
height: 6px;
background: var(--muted);
border-radius: 50%;
opacity: 0.7;
animation: blink 1.2s infinite ease-in-out;
}
.dot:nth-child(2) {
animation-delay: 0.15s;
}
.dot:nth-child(3) {
animation-delay: 0.3s;
}
@keyframes blink {
0%,
80%,
100% {
transform: translateY(0);
opacity: 0.7;
}
40% {
transform: translateY(-3px);
opacity: 1;
}
}
/* Composer */
.composer {
position: sticky;
bottom: 0;
padding: 12px clamp(10px, 2vw, 24px) 16px;
background: linear-gradient(0deg, rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0));
backdrop-filter: blur(8px);
}
.composer-inner {
margin: 0 auto;
max-width: min(980px, 95vw);
background: var(--panel);
border: 1px solid var(--panel-strong);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
padding: 10px;
display: grid;
grid-template-columns: 1fr auto;
gap: 8px;
backdrop-filter: blur(var(--blur));
}
.input-wrap {
display: flex;
align-items: flex-end;
gap: 8px;
padding: 6px;
background: var(--glass);
border: 1px solid var(--panel-strong);
border-radius: 12px;
}
textarea {
width: 100%;
resize: none;
border: none;
outline: none;
background: transparent;
color: var(--text);
font: inherit;
line-height: 1.4;
max-height: 180px;
padding: 8px;
}
.send {
display: flex;
align-items: center;
gap: 8px;
}
.hint {
margin-top: 6px;
color: var(--muted);
font-size: 12px;
display: flex;
align-items: center;
gap: 10px;
justify-content: space-between;
padding: 0 4px;
flex-wrap: wrap;
}
.kbd {
border: 1px solid var(--panel-strong);
background: var(--panel);
padding: 2px 6px;
border-radius: 6px;
font-size: 12px;
color: var(--muted);
}
/* Error styling */
.error {
border: 1px solid var(--danger) !important;
background: rgba(255, 92, 124, 0.1) !important;
}
.error-message {
color: var(--danger);
font-size: 12px;
margin-top: 4px;
}
/* Status indicator */
.status {
position: fixed;
top: 20px;
right: 20px;
padding: 8px 12px;
border-radius: 8px;
font-size: 12px;
z-index: 100;
backdrop-filter: blur(var(--blur));
border: 1px solid var(--panel-strong);
}
.status.connected {
background: var(--success);
color: white;
}
.status.disconnected {
background: var(--danger);
color: white;
}
/* Utility */
.row {
display: flex;
align-items: center;
gap: 10px;
}
.spacer {
flex: 1;
}
/* Responsive tweaks */
@media (max-width: 700px) {
.brand h1 {
font-size: 16px;
}
.brand small {
display: none;
}
.msg {
max-width: 96vw;
}
.composer-inner {
grid-template-columns: 1fr;
}
.send {
justify-content: flex-end;
}
.modal-content {
padding: 16px;
}
}
</style>
</head>
<body>
<div class="app" id="app">
<header class="header">
<div class="brand" title="AnyCoder Chat">
<div class="logo">AI</div>
<div>
<h1>AnyCoder Chat</h1>
<small>AI Chatbot Interface</small>
</div>
</div>
<div class="header-actions">
<button class="btn small ghost" id="settingsBtn" title="API Settings">
<svg class="icon" viewBox="0 0 24 24" fill="none">
<path d="M12 15a3 3 0 100-6 3 3 0 000 6z" stroke="currentColor" stroke-width="2"/>
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 01-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09a1.65 1.65 0 00-1-1.51 1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09a1.65 1.65 0 001.51-1 1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Settings
</button>
<button class="btn small ghost" id="newChatBtn" title="Start a new chat">
<svg class="icon" viewBox="0 0 24 24" fill="none"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
New
</button>
<button class="btn small ghost" id="exportBtn" title="Export chat as JSON">
<svg class="icon" viewBox="0 0 24 24" fill="none"><path d="M12 16V4m0 0l-4 4m4-4l4 4M4 20h16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
Export
</button>
<button class="btn small ghost" id="themeBtn" title="Toggle theme">
<svg class="icon" id="themeIcon" viewBox="0 0 24 24" fill="none"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
Theme
</button>
<a class="btn small" href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank"
rel="noopener noreferrer" title="Visit AnyCoder on Hugging Face">
<svg class="icon" viewBox="0 0 24 24" fill="none">
<path d="M14 3h7v7M10 14L21 3M21 14v7H3V3h7" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
Built with anycoder
</a>
</div>
</header>
<!-- API Key Modal -->
<div class="modal" id="apiModal">
<div class="modal-content">
<h3>API Configuration</h3>
<p style="color: var(--muted); margin-bottom: 16px;">
To use this chat application, you need to provide a valid API key. Choose your preferred API service:
</p>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 8px; font-weight: 500;">API Service:</label>
<select id="apiService" style="width: 100%; padding: 8px; border-radius: 6px; background: var(--glass); color: var(--text); border: 1px solid var(--panel-strong);">
<option value="demo">Demo Mode (No API calls)</option>
<option value="openai">OpenAI API</option>
<option value="poe">Poe.com API</option>
</select>
</div>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 8px; font-weight: 500;">API Key:</label>
<input type="password" id="apiKey" placeholder="Enter your API key" />
<small style="color: var(--muted); display: block; margin-top: 4px;">
Your API key is stored locally and never sent anywhere except to the API service.
</small>
</div>
<div class="modal-actions">
<button class="btn ghost" id="cancelApiBtn">Cancel</button>
<button class="btn primary" id="saveApiBtn">Save</button>
</div>
</div>
</div>
<!-- Status Indicator -->
<div class="status disconnected" id="statusIndicator" style="display: none;">
API not configured
</div>
<main class="chat" id="chat">
<div class="messages" id="messages"></div>
<div class="empty" id="empty">
<div class="empty-inner">
<h2 style="margin:0 0 8px 0">Hi! I'm AnyCoder Chat 👋</h2>
<p style="margin:0;color:var(--muted)">I'm ready to help you with coding questions, brainstorming ideas, or just having a conversation. Click "Settings" above to configure your API key, or try the demo mode!</p>
<div class="chips" id="chips">
<div class="chip">Explain closures in JavaScript</div>
<div class="chip">Help me write a Python script</div>
<div class="chip">Summarize: "Effective Remote Work"</div>
<div class="chip">Brainstorm app features</div>
<div class="chip">Draft a polite email</div>
</div>
</div>
</div>
</main>
<footer class="composer">
<div class="composer-inner">
<div class="input-wrap">
<textarea id="input" rows="1" placeholder="Message AnyCoder... (Configure API in settings)"></textarea>
</div>
<div class="send">
<div class="row">
<button class="btn ghost small" id="stopBtn" title="Stop generating" style="display:none">
<svg class="icon" viewBox="0 0 24 24" fill="none"><rect x="6" y="6" width="12" height="12" rx="2" stroke="currentColor" stroke-width="2"/></svg>
Stop
</button>
</div>
<button class="btn primary" id="sendBtn" title="Send message">
<svg class="icon" viewBox="0 0 24 24" fill="none"><path d="M22 2L11 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M22 2l-7 20-4-9-9-4 20-7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
Send
</button>
</div>
</div>
<div class="hint">
<div class="row" style="gap:6px">
<span class="kbd">Enter</span> to send • <span class="kbd">Shift + Enter</span> for newline
</div>
<div class="spacer"></div>
<div id="apiStatus">Demo Mode - No API calls</div>
</div>
</footer>
</div>
<script>
// Configuration
const store = {
get(key, fallback) { try { return JSON.parse(localStorage.getItem(key)) ?? fallback; } catch { return fallback; } },
set(key, value) { localStorage.setItem(key, JSON.stringify(value)); },
remove(key) { localStorage.removeItem(key); }
};
// Utility: escape HTML
const escapeHtml = (str) => str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
// Utility: minimal markdown renderer (safe-ish)
function renderMarkdown(md) {
if (!md) return "";
// Extract code blocks first to avoid double-processing inside them
const codeBlocks = [];
md = md.replace(/```([\s\S]*?)```/g, (_, code) => {
const placeholder = `@@CODEBLOCK_${codeBlocks.length}@@`;
codeBlocks.push(code);
return placeholder;
});
// Escape HTML
md = escapeHtml(md);
// Restore code blocks with <pre><code>
codeBlocks.forEach((code, i) => {
const safe = escapeHtml(code);
md = md.replace(`@@CODEBLOCK_${i}@@`, `<pre><code>${safe}</code></pre>`);
});
// Inline code: `code`
md = md.replace(/`([^`]+)`/g, `<code>$1</code>`);
// Bold: **text**
md = md.replace(/\*\*([^*]+)\*\*/g, `<strong>$1</strong>`);
// Italic: *text* (avoid overlapping with bold)
md = md.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<em>$2</em>');
// Links: [text](url)
md = md.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, `<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>`);
// Line breaks -> paragraphs
md = md.split(/\n{2,}/).map(chunk => `<p>${chunk.replace(/\n/g, "<br>")}</p>`).join("");
return md;
}
// Utility: simple UUID
const uid = () => (crypto?.randomUUID?.() || `id_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`);
// Elements
const els = {
app: document.getElementById('app'),
messages: document.getElementById('messages'),
empty: document.getElementById('empty'),
chips: document.getElementById('chips'),
input: document.getElementById('input'),
sendBtn: document.getElementById('sendBtn'),
stopBtn: document.getElementById('stopBtn'),
newChatBtn: document.getElementById('newChatBtn'),
exportBtn: document.getElementById('exportBtn'),
themeBtn: document.getElementById('themeBtn'),
themeIcon: document.getElementById('themeIcon'),
chat: document.getElementById('chat'),
apiModal: document.getElementById('apiModal'),
apiService: document.getElementById('apiService'),
apiKey: document.getElementById('apiKey'),
saveApiBtn: document.getElementById('saveApiBtn'),
cancelApiBtn: document.getElementById('cancelApiBtn'),
settingsBtn: document.getElementById('settingsBtn'),
statusIndicator: document.getElementById('statusIndicator'),
apiStatus: document.getElementById('apiStatus')
};
// Theme
function applyTheme(theme) {
if (theme === 'light') document.body.classList.add('theme-light');
else document.body.classList.remove('theme-light');
els.themeIcon.innerHTML = theme === 'light'
? '<path d="M12 3v2m0 14v2m9-9h-2M5 12H3m14.95 6.95l-1.414-1.414M7.464 7.464L6.05 6.05m11.314 0l-1.414 1.414M7.464 16.536l-1.414 1.414" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="2"/>'
: '<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
}
const savedTheme = store.get('anycoder_theme', 'dark');
applyTheme(savedTheme);
els.themeBtn.addEventListener('click', () => {
const next = document.body.classList.contains('theme-light') ? 'dark' : 'light';
store.set('anycoder_theme', next);
applyTheme(next);
});
// API Configuration
const APIConfig = {
service: store.get('anycoder_api_service', 'demo'),
key: store.get('anycoder_api_key', ''),
get isConfigured() {
return this.service !== 'demo' && this.key.trim() !== '';
},
save() {
store.set('anycoder_api_service', this.service);
store.set('anycoder_api_key', this.key);
updateAPIStatus();
}
};
function updateAPIStatus() {
const statusEl = els.statusIndicator;
const apiStatusEl = els.apiStatus;
if (!APIConfig.isConfigured) {
statusEl.textContent = 'Demo Mode';
statusEl.className = 'status disconnected';
statusEl.style.display = 'block';
apiStatusEl.textContent = 'Demo Mode - No API calls';
els.input.placeholder = 'Message AnyCoder... (Configure API in settings)';
} else {
const serviceNames = {
openai: 'OpenAI API',
poe: 'Poe.com API'
};
statusEl.textContent = `Connected to ${serviceNames[APIConfig.service] || APIConfig.service}`;
statusEl.className = 'status connected';
statusEl.style.display = 'block';
apiStatusEl.textContent = `Using ${serviceNames[APIConfig.service] || APIConfig.service}`;
els.input.placeholder = `Message AnyCoder... (Connected to ${serviceNames[APIConfig.service] || APIConfig.service})`;
}
}
// API Modal
function showAPIModal() {
els.apiService.value = APIConfig.service;
els.apiKey.value = APIConfig.key;
els.apiModal.classList.add('show');
}
function hideAPIModal() {
els.apiModal.classList.remove('show');
}
els.settingsBtn.addEventListener('click', showAPIModal);
els.cancelApiBtn.addEventListener('click', hideAPIModal);
els.saveApiBtn.addEventListener('click', () => {
APIConfig.service = els.apiService.value;
APIConfig.key = els.apiKey.value.trim();
APIConfig.save();
hideAPIModal();
});
// Chat state
const ChatState = {
data: store.get('anycoder_chat', []),
get isEmpty() { return this.data.length === 0; },
save() { store.set('anycoder_chat', this.data); }
};
// Renderers
function messageElement(msg, { streaming = false } = {}) {
const isUser = msg.role === 'user';
const el = document.createElement('div');
el.className = `msg ${isUser ? 'user' : 'ai'}`;
el.dataset.id = msg.id;
const avatar = document.createElement('div');
avatar.className = `avatar ${isUser ? '' : 'ai'}`;
avatar.innerHTML = isUser
? '<svg class="icon" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="8" r="4" stroke="currentColor" stroke-width="2"/><path d="M4 20c0-4 4-6 8-6s8 2 8 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>'
: '<svg class="icon" viewBox="0 0 24 24" fill="none"><path d="M12 3l8 4v5c0 5-3.5 9-8 9S4 17 4 12V7l8-4z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="M9 12l2 2 4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
const bubble = document.createElement('div');
bubble.className = 'bubble';
const meta = document.createElement('div');
meta.className = 'meta';
meta.textContent = isUser ? 'You' : 'AnyCoder AI';
const content = document.createElement('div');
content.className = 'content';
if (isUser) {
content.textContent = msg.content;
} else {
content.innerHTML = renderMarkdown(msg.content);
}
bubble.appendChild(meta);
bubble.appendChild(content);
if (isUser) {
el.appendChild(bubble);
el.appendChild(avatar);
} else {
el.appendChild(avatar);
el.appendChild(bubble);
}
// Typing indicator if streaming
if (!isUser && streaming) {
const typing = document.createElement('div');
typing.className = 'meta';
typing.style.marginTop = '6px';
typing.innerHTML = '<span class="typing"><span class="dot"></span><span class="dot"></span><span class="dot"></span></span>';
bubble.appendChild(typing);
}
return el;
}
function scrollToBottom() {
els.messages.scrollTop = els.messages.scrollHeight;
}
function refreshEmpty() {
els.empty.style.display = ChatState.isEmpty ? 'grid' : 'none';
}
function renderAll() {
els.messages.innerHTML = '';
ChatState.data.forEach(msg => {
els.messages.appendChild(messageElement(msg));
});
refreshEmpty();
scrollToBottom();
}
// Demo AI responses
const demoResponses = [
"That's a great question! I'd be happy to help you with that. Here's what I think:\n\n**Key points:**\n- This is a demo response\n- The API is not currently configured\n- You can configure your API key in the settings\n\nLet me know if you'd like to explore this further!",
"Interesting! In a real implementation, this would connect to an AI service. For now, I'm running in demo mode.\n\n**What I can do:**\n- Process your questions\n- Provide helpful responses\n- Format with markdown\n\nWould you like me to help with something specific?",
"I understand what you're asking! This interface is ready to connect to various AI APIs including OpenAI, Claude, or others. Currently, it's running in demo mode.\n\n**To get started:**\n1. Click the Settings button\n2. Choose your preferred API service\n3. Enter your API key\n4. Start chatting!\n\nWhat would you like to work on?",
"That's a thoughtful question! I'm currently operating in demo mode, which means I'm providing pre-programmed responses rather than connecting to a live AI service.\n\n**In a full implementation, I could:**\n- Answer coding questions\n- Help with debugging\n- Explain complex concepts\n- Brainstorm ideas\n\nHow can I assist you today?"
];
function getDemoResponse(userMessage) {
const responses = demoResponses;
const randomIndex = Math.floor(Math.random() * responses.length);
const baseResponse = responses[randomIndex];
return baseResponse + `\n\n---\n\n**Your message was:** "${userMessage}"\n\n*Configure an API key in Settings to get real AI responses!*`;
}
// API Integration
const AIAPI = {
abortController: null,
async generateResponse(messages) {
if (!APIConfig.isConfigured) {
// Demo mode
const lastUserMessage = messages.filter(m => m.role === 'user').pop();
const response = getDemoResponse(lastUserMessage?.content || 'Hello');
// Simulate typing delay
await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000));
return response;
}
this.abortController = new AbortController();
try {
let response;
if (APIConfig.service === 'openai') {
response = await this.openAIRequest(messages);
} else if (APIConfig.service === 'poe') {
response = await this.poeRequest(messages);
} else {
throw new Error('Unsupported API service');
}
return response;
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request was cancelled');
}
throw error;
} finally {
this.abortController = null;
}
},
async openAIRequest(messages) {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${APIConfig.key}`
},
body: JSON.stringify({
model: 'gpt-3.5-turbo',
messages: messages,
temperature: 0.7,
max_tokens: 2000
}),
signal: this.abortController.signal
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error?.message || `OpenAI API request failed with status ${response.status}`);
}
const data = await response.json();
return data.choices?.[0]?.message?.content || 'No response generated';
},
async poeRequest(messages) {
// Note: Poe.com API might require different endpoints and authentication
// This is a placeholder implementation
const response = await fetch('https://api.poe.com/v1/gql_POST', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${APIConfig.key}`
},
body: JSON.stringify({
query: 'mutation CreateMessage($chatId: ID!, $query: String!, $source: String) { messageCreate(chatId: $chatId, query: $query, source: $source) { chat { threadId } } }',
variables: {
chatId: 'temp-chat-id',
query: messages.map(m => `${m.role}: ${m.content}`).join('\n')
}
}),
signal: this.abortController.signal
});
if (!response.ok) {
throw new Error(`Poe.com API request failed with status ${response.status}`);
}
const data = await response.json();
// This would need to be adapted based on Poe.com's actual API
return data.data?.messageCreate?.response || 'No response generated';
},
stop() {
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
}
};
// Actions
async function sendMessage() {
const text = els.input.value.trim();
if (!text) return;
// Clear any previous errors
els.input.classList.remove('error');
const existingError = els.input.parentNode.querySelector('.error-message');
if (existingError) existingError.remove();
// Add user message
const userMsg = { id: uid(), role: 'user', content: text, ts: Date.now() };
ChatState.data.push(userMsg);
ChatState.save();
els.messages.appendChild(messageElement(userMsg));
refreshEmpty();
scrollToBottom();
// Clear input
els.input.value = "";
autoResizeTextarea();
els.input.focus();
// Generate AI response
els.sendBtn.disabled = true;
els.stopBtn.style.display = 'inline-flex';
// Create AI message placeholder
const tempMsgId = uid();
const tempMsg = { id: tempMsgId, role: 'assistant', content: '' };
ChatState.data.push(tempMsg);
ChatState.save();
const el = messageElement(tempMsg, { streaming: true });
els.messages.appendChild(el);
refreshEmpty();
scrollToBottom();
const contentEl = el.querySelector('.content');
try {
// Prepare messages for API (last 10 messages to avoid token limits)
const apiMessages = ChatState.data.slice(-10).map(msg => ({
role: msg.role,
content: msg.content
}));
// Get response
const response = await AIAPI.generateResponse(apiMessages);
// Update message
tempMsg.content = response;
contentEl.innerHTML = renderMarkdown(response);
// Remove typing indicator
const typingIndicator = el.querySelector('.meta:last-child .typing');
if (typingIndicator) {
typingIndicator.parentNode.remove();
}
ChatState.save();