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; | |
| } | |
| .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) ; | |
| background: rgba(255, 92, 124, 0.1) ; | |
| } | |
| .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, "&") | |
| .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'), | |
| 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(); |