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); | |
| } | |
| /* Error styling */ | |
| .error { | |
| border: 1px solid var(--danger) ; | |
| background: rgba(255, 92, 124, 0.1) ; | |
| } | |
| .error-message { | |
| color: var(--danger); | |
| font-size: 12px; | |
| margin-top: 4px; | |
| } | |
| /* 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>Powered by Poe.com API</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 powered by Claude on Poe.com 👋</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. Connected to the claude-haiku-cheap model.</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"> | |
| <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 • Connected to Poe.com API | |
| </div> | |
| <div class="spacer"></div> | |
| <div>Using claude-haiku-cheap model via Poe.com</div> | |
| </div> | |
| </footer> | |
| </div> | |
| <script> | |
| // Configuration | |
| const POE_API_KEY = '5AvcWZHjdjrxOuRodUjyTZ2TE_D0tdrN-XantWcBY4E'; | |
| const POE_BASE_URL = 'https://api.poe.com/v1'; | |
| const MODEL_NAME = 'claude-haiku-cheap'; | |
| // 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'), | |
| 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' : 'Claude (via Poe.com)'; | |
| 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(); | |
| } | |
| // Poe.com API integration | |
| const PoeAPI = { | |
| abortController: null, | |
| async chatCompletion(messages) { | |
| this.abortController = new AbortController(); | |
| try { | |
| const response = await fetch(`${POE_BASE_URL}/chat/completions`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${POE_API_KEY}` | |
| }, | |
| body: JSON.stringify({ | |
| model: MODEL_NAME, | |
| messages: messages, | |
| stream: true, | |
| 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 || `API request failed with status ${response.status}`); | |
| } | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let buffer = ''; | |
| let fullContent = ''; | |
| const processChunk = async () => { | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| buffer += decoder.decode(value, { stream: true }); | |
| const lines = buffer.split('\n'); | |
| buffer = lines.pop() || ''; | |
| for (const line of lines) { | |
| const trimmed = line.trim(); | |
| if (!trimmed) continue; | |
| if (trimmed.startsWith('data: ')) { | |
| const data = trimmed.slice(6); | |
| if (data === '[DONE]') { | |
| return { content: fullContent, done: true }; | |
| } | |
| try { | |
| const parsed = JSON.parse(data); | |
| if (parsed.choices?.[0]?.delta?.content) { | |
| fullContent += parsed.choices[0].delta.content; | |
| yield { content: fullContent, done: false }; | |
| } | |
| } catch (e) { | |
| // Ignore JSON parse errors for incomplete chunks | |
| } | |
| } | |
| } | |
| } | |
| return { content: fullContent, done: true }; | |
| }; | |
| return { | |
| [Symbol.asyncIterator]: async function* () { | |
| for await (const chunk of processChunk()) { | |
| yield chunk; | |
| } | |
| } | |
| }; | |
| } catch (error) { | |
| if (error.name === 'AbortError') { | |
| throw new Error('Request was cancelled'); | |
| } | |
| throw error; | |
| } | |
| }, | |
| 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 streaming response | |
| const stream = await PoeAPI.chatCompletion(apiMessages); | |
| let finalContent = ''; | |
| for await (const chunk of stream) { | |
| finalContent = chunk.content; | |
| contentEl.innerHTML = renderMarkdown(finalContent); | |
| scrollToBottom(); | |
| } | |
| // Final update | |
| tempMsg.content = finalContent; | |
| contentEl.innerHTML = renderMarkdown(finalContent); | |
| // Remove typing indicator | |
| const typingIndicator = el.querySelector('.meta:last-child .typing'); | |
| if (typingIndicator) { | |
| typingIndicator.parentNode.remove(); | |
| } | |
| ChatState.save(); | |
| } catch (error) { | |
| console.error('API Error:', error); | |
| // Show error message | |
| tempMsg.content = `❌ **Error**: ${error.message}\n\nPlease check your internet connection and try again.`; | |
| contentEl.innerHTML = renderMarkdown(tempMsg.content); | |
| // Remove typing indicator | |
| const typingIndicator = el.querySelector('.meta:last-child .typing'); | |
| if (typingIndicator) { | |
| typingIndicator.parentNode.remove(); | |
| } | |
| // Show visual error on input | |
| els.input.classList.add('error'); | |
| const errorMsg = document.createElement('div'); | |
| errorMsg.className = 'error-message'; | |
| errorMsg.textContent = `API Error: ${error.message}`; | |
| els.input.parentNode.appendChild(errorMsg); | |
| ChatState.save(); | |
| } finally { | |
| 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); | |
| // Stop button | |
| els.stopBtn.addEventListener('click', () => { | |
| PoeAPI.stop(); | |
| els.stopBtn.style.display = 'none'; | |
| els.sendBtn.disabled = false; | |
| }); | |
| // 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 API calls) | |
| document.addEventListener('visibilitychange', () => { | |
| if (document.hidden) { | |
| PoeAPI.stop(); | |
| els.stopBtn.style.display = 'none'; | |
| els.sendBtn.disabled = false; | |
| } | |
| }); | |
| // Initialize | |
| renderAll(); | |
| autoResizeTextarea(); | |
| // Preload a welcome message if empty | |
| if (ChatState.isEmpty) { | |
| const welcome = { | |
| id: uid(), | |
| role: 'assistant', | |
| content: | |
| `Hello! I'm connected to the Poe.com API using the claude-haiku-cheap model. | |
| I can help you with: | |
| - Code explanations and programming help | |
| - Writing and editing text | |
| - Brainstorming ideas | |
| - Answering questions | |
| - And much more! | |
| Just type your message and I'll respond in real-time. Use Markdown for formatting.` | |
| }; | |
| ChatState.data.push(welcome); | |
| ChatState.save(); | |
| renderAll(); | |
| } | |
| </script> | |
| </body> | |
| </html> |