Spaces:
Running
Running
| <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; } | |
| /* 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); | |
| } | |
| /* 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; } | |
| } | |
| </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>Your friendly AI assistant</small> | |
| </div> | |
| </div> | |
| <div class="header-actions"> | |
| <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> | |
| <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)">Ask me to explain code, brainstorm ideas, draft emails, or just chat. I can format responses with Markdown, code blocks, and links.</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..."></textarea> | |
| </div> | |
| <div class="send"> | |
| <div class="row"> | |
| <label for="speed" style="font-size:12px;color:var(--muted);display:flex;align-items:center;gap:6px"> | |
| Speed | |
| <input id="speed" type="range" min="0" max="50" value="10" /> | |
| </label> | |
| <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 • Ask for code, summaries, or explanations | |
| </div> | |
| <div class="spacer"></div> | |
| <div>Local demo chatbot — no external API required</div> | |
| </div> | |
| </footer> | |
| </div> | |
| <script> | |
| // Utility: local storage wrapper | |
| 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, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">"); | |
| // 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'), | |
| speed: document.getElementById('speed'), | |
| chat: document.getElementById('chat') | |
| }; | |
| // 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); | |
| }); | |
| // 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'; | |
| 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) { | |
| // user: [avatar right] | |
| 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(); | |
| } | |
| // AI Engine (local, no external APIs) | |
| const AI = { | |
| abort: null, | |
| async respond(prompt, opts = {}) { | |
| // Streaming: return a controller with write/close | |
| const controller = { | |
| write: (chunk) => {}, | |
| close: () => {} | |
| }; | |
| const text = buildResponse(prompt); | |
| const speed = Number(els.speed.value) || 10; // 0..50 (higher is faster) | |
| const minDelay = 5; // min ms per chunk | |
| const maxDelay = 60; // max ms per chunk | |
| // Map speed: 0 slow -> maxDelay, 50 fast -> minDelay | |
| const delay = speed === 0 ? 30 : Math.max(minDelay, maxDelay - speed); | |
| const chunkSize = Math.max(2, Math.floor(60 - speed)); // chars per chunk | |
| let i = 0; | |
| 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'); | |
| function writeNext() { | |
| const end = Math.min(text.length, i + chunkSize); | |
| const chunk = text.slice(i, end); | |
| i = end; | |
| // append chunk as plain text, then re-render markdown for preview? To keep it simple, we'll append plain safe HTML. | |
| // But we want markdown; accumulate into tempMsg.content and re-render. | |
| tempMsg.content = text.slice(0, i); | |
| contentEl.innerHTML = renderMarkdown(tempMsg.content); | |
| scrollToBottom(); | |
| if (i < text.length) { | |
| AI.abort = setTimeout(writeNext, delay); | |
| } else { | |
| controller.close?.(); | |
| } | |
| } | |
| AI.abort = setTimeout(writeNext, delay); | |
| controller.close = () => { | |
| if (AI.abort) clearTimeout(AI.abort); | |
| // Ensure final render | |
| tempMsg.content = text; | |
| contentEl.innerHTML = renderMarkdown(tempMsg.content); | |
| // Remove typing indicator | |
| const t = el.querySelector('.meta:last-child'); | |
| if (t && t.querySelector('.typing')) t.remove(); | |
| ChatState.save(); | |
| AI.abort = null; | |
| }; | |
| return controller; | |
| }, | |
| stop() { | |
| if (AI.abort) clearTimeout(AI.abort); | |
| AI.abort = null; | |
| } | |
| }; | |
| function buildResponse(prompt) { | |
| const p = prompt.trim(); | |
| const lower = p.toLowerCase(); | |
| // Commands | |
| if (/^(\/help|\/h)$/.test(lower)) { | |
| return [ | |
| "Here are some things you can try:", | |
| "", | |
| "- Ask for explanations: “Explain closures in JavaScript”", | |
| "- Get code help: “Write a Python script to download files”", | |
| "- Brainstorm: “Ideas for a habit tracker app”", | |
| "- Draft text: “Write a polite email to reschedule a meeting”", | |
| "- Summaries: “Summarize the key points of [topic]”", | |
| "", | |
| "Tips:", | |
| "- Shift+Enter for a newline", | |
| "- Use backticks for inline code and triple backticks for code blocks", | |
| "- Use **bold** and *italic*", | |
| "" | |
| ].join("\n"); | |
| } | |
| if (/^(\/clear|\/c)$/.test(lower)) { | |
| ChatState.data = []; | |
| ChatState.save(); | |
| renderAll(); | |
| return "Cleared the chat."; | |
| } | |
| if (/^(\/time|\/date)$/.test(lower)) { | |
| const now = new Date(); | |
| return `Current date and time: ${now.toLocaleString()}`; | |
| } | |
| if (/^(\/joke)$/.test(lower)) { | |
| return joke(); | |
| } | |
| // Heuristic responses | |
| if (lower.includes('weather')) { | |
| return "I don't have live weather data in this demo, but here's what I'd do: " + | |
| "I'd call a weather API (like OpenWeatherMap) with your location, parse the JSON, and display conditions. " + | |
| "You can also show hourly and daily forecasts with charts."; | |
| } | |
| if (lower.includes('remind')) { | |
| return "Reminders aren't persisted in this offline demo, but you could store them in localStorage or IndexedDB. " + | |
| "For scheduling notifications, use the Notification API and setTimeout or a service worker."; | |
| } | |
| if (lower.includes('password') || lower.includes('random')) { | |
| return generatePassword(); | |
| } | |
| if (lower.includes('explain') && lower.includes('closure')) { | |
| return "A closure is a function paired with the lexical environment in which it was declared. " + | |
| "It lets the inner function remember variables from the outer scope even after the outer function has finished executing.\n\n" + | |
| "Example:\n```js\nfunction counter() {\n let n = 0;\n return () => ++n;\n}\nconst inc = counter();\ninc(); // 1\ninc(); // 2\n```\nHere, the returned function closes over `n`, keeping it alive between calls."; | |
| } | |
| if (lower.includes('write') && (lower.includes('python') || lower.includes('script'))) { | |
| return `Here's a tiny Python script to download a file:\n\`\`\`python\nimport requests\n\nurl = "https://example.com/file.pdf"\nresp = requests.get(url)\nwith open("file.pdf", "wb") as f:\n f.write(resp.content)\nprint("Saved to file.pdf")\n\`\`\`\nTip: Use tqdm for progress bars and pathlib for paths.`; | |
| } | |
| if (lower.includes('brainstorm') || lower.includes('ideas')) { | |
| return "Let’s brainstorm features for a habit tracker app:\n" + | |
| "- Daily streaks with visual badges\n" + | |
| "- Habit templates and categories\n" + | |
| "- Reminders with the Notification API\n" + | |
| "- Calendar heatmap view\n" + | |
| "- Sync via a backend (or locally with IndexedDB)\n" + | |
| "- Smart suggestions based on your schedule\n" + | |
| "- Gamification: points, levels, challenges"; | |
| } | |
| if (lower.includes('email') || lower.includes('draft')) { | |
| return "Draft email example:\n\n" + | |
| "Subject: Rescheduling our meeting\n\n" + | |
| "Hi [Name],\n\n" + | |
| "I hope you’re doing well. Due to an unexpected conflict, I need to reschedule our meeting originally planned for [Day, Time]. " + | |
| "Would [Alternative Day, Time] work for you? I’m happy to adjust.\n\n" + | |
| "Thanks for your understanding!\n\n" + | |
| "Best,\n[Your Name]"; | |
| } | |
| if (lower.includes('summarize') || lower.includes('summary')) { | |
| return "To summarize effectively:\n" + | |
| "1) Identify the thesis or main claim\n" + | |
| "2) Extract key arguments and evidence\n" + | |
| "3) Note counterpoints if relevant\n" + | |
| "4) Conclude with implications or takeaways\n\n" + | |
| "For long texts, chunk by sections and summarize each, then combine."; | |
| } | |
| // Fallback helpful response | |
| const tips = [ | |
| "Here are some tips to get better results:", | |
| "- Be specific about what you want (goal, constraints, examples)", | |
| "- Mention your audience or tone (e.g., beginner-friendly)", | |
| "- Provide sample input/output formats", | |
| "- For code, specify language and error messages (if any)" | |
| ]; | |
| const examples = [ | |
| "", | |
| "Examples you can try:", | |
| "- Explain closures in JavaScript with a small example", | |
| "- Write a Python script to download files with a progress bar", | |
| "- Brainstorm app features for a habit tracker", | |
| "- Draft a polite email to reschedule a meeting", | |
| "" | |
| ].join("\n"); | |
| const fallback = [ | |
| `You said: “${prompt}”`, | |
| "", | |
| "I can help with coding, explanations, brainstorming, drafting, and more. I support Markdown, code blocks, and links.", | |
| tips.join("\n"), | |
| examples | |
| ].join("\n"); | |
| return fallback; | |
| } | |
| function joke() { | |
| const jokes = [ | |
| "Why do programmers prefer dark mode? Because light attracts bugs.", | |
| "There are 10 kinds of people: those who understand binary and those who don’t.", | |
| "A SQL query walks into a bar, walks up to two tables and asks: 'Can I join you?'", | |
| "To understand recursion, you must first understand recursion." | |
| ]; | |
| return jokes[Math.floor(Math.random() * jokes.length)]; | |
| } | |
| function generatePassword() { | |
| const len = 16; | |
| const chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*"; | |
| let out = ""; | |
| const cryptoObj = window.crypto || window.msCrypto; | |
| if (cryptoObj?.getRandomValues) { | |
| const arr = new Uint32Array(len); | |
| cryptoObj.getRandomValues(arr); | |
| for (let i = 0; i < len; i++) { | |
| out += chars[arr[i] % chars.length]; | |
| } | |
| } else { | |
| for (let i = 0; i < len; i++) { | |
| out += chars[Math.floor(Math.random() * chars.length)]; | |
| } | |
| } | |
| return `Generated password (length ${len}):\n\`${out}\`\nTip: Use a password manager and enable 2FA.`; | |
| } | |
| // Actions | |
| async function sendMessage() { | |
| const text = els.input.value.trim(); | |
| if (!text) return; | |
| // 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'; | |
| const controller = await AI.respond(text); | |
| els.stopBtn.onclick = () => { | |
| AI.stop(); | |
| // finalize message | |
| controller.close?.(); | |
| els.stopBtn.style.display = 'none'; | |
| els.sendBtn.disabled = false; | |
| }; | |
| // When streaming ends | |
| const waitEnd = () => new Promise(resolve => { | |
| const check = () => { | |
| if (!AI.abort) resolve(); | |
| else setTimeout(check, 80); | |
| }; | |
| check(); | |
| }); | |
| await waitEnd(); | |
| els.stopBtn.style.display = 'none'; | |
| els.sendBtn.disabled = false; | |
| } | |
| function newChat() { | |
| ChatState.data = []; | |
| ChatState.save(); | |
| renderAll(); | |
| } | |
| function exportChat() { | |
| const blob = new Blob([JSON.stringify(ChatState.data, null, 2)], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `anycoder_chat_${new Date().toISOString().replace(/[:.]/g, '-')}.json`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| a.remove(); | |
| URL.revokeObjectURL(url); | |
| } | |
| // Input behaviors | |
| function autoResizeTextarea() { | |
| const ta = els.input; | |
| ta.style.height = 'auto'; | |
| ta.style.height = Math.min(180, ta.scrollHeight) + 'px'; | |
| } | |
| els.input.addEventListener('input', autoResizeTextarea); | |
| els.input.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage(); | |
| } | |
| }); | |
| els.sendBtn.addEventListener('click', sendMessage); | |
| els.newChatBtn.addEventListener('click', newChat); | |
| els.exportBtn.addEventListener('click', exportChat); | |
| // Chips | |
| els.chips.addEventListener('click', (e) => { | |
| const chip = e.target.closest('.chip'); | |
| if (!chip) return; | |
| els.input.value = chip.textContent.trim(); | |
| autoResizeTextarea(); | |
| els.input.focus(); | |
| }); | |
| // Stop generation when switching tabs (saves CPU) | |
| document.addEventListener('visibilitychange', () => { | |
| if (document.hidden) AI.stop(); | |
| }); | |
| // Initialize | |
| renderAll(); | |
| autoResizeTextarea(); | |
| // Preload a welcome message if empty | |
| if (ChatState.isEmpty) { | |
| const welcome = { id: uid(), role: 'assistant', content: | |
| `Welcome! I’m your built‑in demo assistant. | |
| - Ask me to explain code, brainstorm ideas, draft text, or summarize topics. | |
| - Use Markdown: **bold**, *italic*, \`inline code\`, and triple backticks for code blocks. | |
| - Try: /help, /time, /joke, /clear`}; | |
| ChatState.data.push(welcome); | |
| ChatState.save(); | |
| renderAll(); | |
| } | |
| </script> | |
| </body> | |
| </html> |