🌐
index_copy.html
Back
📝 Html ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> <meta http-equiv="Pragma" content="no-cache" /> <meta http-equiv="Expires" content="0" /> <title>Unified Chat (DeepSeek · Grok · OpenAI)</title> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js"></script> <script> const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; document.documentElement.classList.toggle('dark', prefersDark); </script> <script src="https://cdn.tailwindcss.com"></script> <style> .scrollbar-thin::-webkit-scrollbar { height: 8px; width: 8px; } .scrollbar-thin::-webkit-scrollbar-thumb { background: #9ca3af; border-radius: 8px; } .scrollbar-thin::-webkit-scrollbar-track { background: transparent; } /* Mobile improvements */ @media (max-width: 640px) { .mobile-padding { padding-left: 0.75rem; padding-right: 0.75rem; } .mobile-text { font-size: 0.875rem; } .mobile-compact { gap: 0.5rem; } } /* Code block improvements */ .code-container { position: relative; background: #1f2937; border-radius: 0.75rem; overflow: hidden; margin: 1rem 0; } .code-header { background: #374151; padding: 0.5rem 1rem; font-size: 0.75rem; color: #d1d5db; border-bottom: 1px solid #4b5563; display: flex; justify-content: space-between; align-items: center; } .copy-btn { background: #6b7280; color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; cursor: pointer; transition: background-color 0.2s; } .copy-btn:hover { background: #9ca3af; } .code-content { padding: 1rem; overflow-x: auto; font-family: 'Courier New', monospace; font-size: 0.875rem; line-height: 1.5; color: #e5e7eb; } </style> </head> <body class="bg-zinc-50 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100"> <div class="min-h-screen grid grid-rows-[auto,1fr]"> <!-- Header --> <header class="border-b border-zinc-200/80 dark:border-zinc-800/80 bg-white/80 dark:bg-zinc-950/80 backdrop-blur sticky top-0 z-10"> <div class="max-w-5xl mx-auto mobile-padding px-4 py-3 flex items-center justify-between mobile-compact gap-3"> <div class="flex items-center mobile-compact gap-3"> <div class="h-6 w-6 sm:h-8 sm:w-8 rounded-xl bg-gradient-to-br from-indigo-500 via-sky-500 to-emerald-500"></div> <h1 class="text-base sm:text-lg font-semibold">Unified Chat</h1> <span class="hidden sm:inline text-xs text-zinc-500">DeepSeek · xAI Grok · OpenAI</span> </div> <div class="flex items-center gap-2"> <button id="openSettings" class="text-xs px-2 sm:px-3 py-1.5 rounded-lg border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-800"> Settings </button> </div> </div> </header> <!-- Main --> <main class="max-w-5xl mx-auto w-full mobile-padding px-4 py-3 sm:py-6"> <!-- Chat column only --> <section class="flex flex-col min-h-[70vh]"> <!-- Transcript --> <div id="transcript" class="flex-1 space-y-3 sm:space-y-4 overflow-y-auto pr-1 scrollbar-thin"> <!-- messages will render here --> </div> <!-- Debug --> <details id="debugWrap" class="mt-4 hidden"> <summary class="cursor-pointer text-sm text-zinc-600 dark:text-zinc-300">Debug (request / response)</summary> <pre id="debugArea" class="mt-2 p-3 rounded-xl bg-zinc-100 dark:bg-zinc-900 text-xs overflow-x-auto"></pre> </details> <!-- Composer --> <div class="mt-3 sm:mt-4"> <div class="rounded-2xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 p-2 sm:p-3 shadow-sm"> <label class="sr-only" for="question">Your message</label> <div class="flex items-end gap-2"> <textarea id="question" rows="3" class="flex-1 min-h-[60px] sm:min-h-[72px] rounded-xl border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-3 py-2 mobile-text" placeholder="Ask me anything…"></textarea> <div class="flex flex-col items-stretch gap-2"> <button id="send" class="h-8 sm:h-10 px-3 sm:px-4 rounded-xl bg-indigo-600 text-white hover:bg-indigo-500 disabled:opacity-50 text-sm"> Send </button> <div id="status" class="text-[10px] sm:text-[11px] text-zinc-500 text-center"></div> </div> </div> </div> </div> </section> </main> </div> <!-- Settings Overlay --> <div id="settingsOverlay" class="fixed inset-0 z-40 hidden" aria-hidden="true"> <div id="overlayBackdrop" class="absolute inset-0 bg-black/40 backdrop-blur-sm"></div> <div class="absolute inset-0 flex items-start justify-center p-2 sm:p-4"> <div role="dialog" aria-modal="true" aria-labelledby="settingsTitle" class="w-full max-w-md mt-4 sm:mt-10 rounded-2xl border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 shadow-xl max-h-[90vh] overflow-hidden"> <div class="p-3 sm:p-4 border-b border-zinc-200 dark:border-zinc-800 flex items-center justify-between"> <h2 id="settingsTitle" class="font-semibold text-sm sm:text-base">Settings</h2> <div class="flex items-center gap-2"> <button id="clearChat" class="text-xs px-2 py-1 rounded-md border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-800">Clear</button> <button id="closeSettings" class="text-xs px-2 py-1 rounded-md border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-800">Close</button> </div> </div> <div class="p-3 sm:p-4 space-y-3 overflow-y-auto" style="max-height: calc(90vh - 60px);"> <label class="text-sm block">Model</label> <select id="model" class="w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-3 py-2 text-sm"> <optgroup label="OpenAI"> <option>gpt-5</option> <option>gpt-5-mini</option> <option>gpt-5-nano</option> <option>gpt-5-thinking</option> <option>gpt-5-pro</option> <option>gpt-4o</option> <option>gpt-4o-mini</option> </optgroup> <optgroup label="DeepSeek"> <option selected>deepseek-chat</option> <option>deepseek-reasoner</option> </optgroup> <optgroup label="xAI (Grok)"> <option>grok-3</option> <option>grok-3-mini</option> <option>grok-code-fast-1</option> <option>grok-4-0709</option> </optgroup> </select> <div> <label class="text-sm block">Max tokens: <span id="maxTokensVal" class="font-mono">800</span></label> <input id="maxTokens" type="range" min="64" max="4096" step="32" value="800" class="w-full"> </div> <div> <label class="text-sm block">Temperature: <span id="tempVal" class="font-mono">0.7</span></label> <input id="temperature" type="range" min="0" max="2" step="0.1" value="0.7" class="w-full"> <label class="flex items-center gap-2 mt-1 text-xs"><input id="forceTemperature" type="checkbox" class="accent-indigo-600"> Force temperature (for GPT-5)</label> </div> <div> <label class="text-sm block mb-1">System prompt</label> <textarea id="system" rows="3" class="w-full rounded-lg border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-3 py-2 text-sm" placeholder="You are a helpful, accurate assistant. Be concise and clear. Use markdown when it helps readability."></textarea> </div> <div> <label class="text-sm block mb-2">Code Response Style</label> <div class="flex border border-zinc-300 dark:border-zinc-700 rounded-lg overflow-hidden"> <button id="tabDefault" class="flex-1 px-3 py-2 text-xs bg-indigo-600 text-white">Default</button> <button id="tabFullCode" class="flex-1 px-3 py-2 text-xs bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700">Full Code</button> <button id="tabSnippets" class="flex-1 px-3 py-2 text-xs bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700">Snippets</button> </div> <div id="promptDefault" class="mt-2 p-3 rounded-lg bg-zinc-50 dark:bg-zinc-800 text-xs"> <strong>Default:</strong> Normal responses with code in markdown blocks when helpful. </div> <div id="promptFullCode" class="mt-2 p-3 rounded-lg bg-zinc-50 dark:bg-zinc-800 text-xs hidden"> <strong>Full Code:</strong> Always provide complete, working code examples. <textarea id="fullCodePrompt" rows="3" class="w-full mt-2 rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-xs" placeholder="Additional instructions for full code responses...">When providing code examples, always include complete, runnable code with all necessary imports, setup, and context. Provide full file contents rather than partial snippets.</textarea> </div> <div id="promptSnippets" class="mt-2 p-3 rounded-lg bg-zinc-50 dark:bg-zinc-800 text-xs hidden"> <strong>Snippets:</strong> Focus on concise code snippets and key changes only. <textarea id="snippetsPrompt" rows="3" class="w-full mt-2 rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-xs" placeholder="Additional instructions for snippet responses...">Focus on providing concise code snippets that show only the relevant changes or key parts. Explain what each snippet does and where it should be used.</textarea> </div> </div> <div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2"> <label class="flex items-center gap-2 text-sm"><input id="includeArtifacts" type="checkbox" class="accent-indigo-600"> Include artifacts from session</label> <label class="flex items-center gap-2 text-sm"><input id="jsonFormat" type="checkbox" class="accent-indigo-600"> Response JSON</label> </div> <details class="text-xs text-zinc-500"> <summary class="cursor-pointer mb-1">Session & CORS notes</summary> <p class="mt-1">Serve this file and <code>api.php</code> from the same origin to keep PHP session history. If cross-origin, enable credentials and set a specific <code>Access-Control-Allow-Origin</code> instead of <code>*</code>.</p> </details> </div> </div> </div> </div> <script> const els = { // dynamic settings (bound when overlay opens) model: null, maxTokens: null, maxTokensVal: document.getElementById('maxTokensVal'), temperature: null, tempVal: document.getElementById('tempVal'), forceTemperature: null, system: null, includeArtifacts: null, jsonFormat: null, clearChat: null, // chat transcript: document.getElementById('transcript'), question: document.getElementById('question'), send: document.getElementById('send'), status: document.getElementById('status'), debugWrap: document.getElementById('debugWrap'), debugArea: document.getElementById('debugArea'), // overlay chrome overlay: document.getElementById('settingsOverlay'), openSettings: document.getElementById('openSettings'), closeSettings: document.getElementById('closeSettings'), overlayBackdrop: document.getElementById('overlayBackdrop'), }; // --- Local persistence --- const STORAGE_KEY = 'unified-chat-state-v2'; const defaultState = () => ({ settings: { model: 'deepseek-chat', maxTokens: 800, temperature: 0.7, forceTemperature: false, includeArtifacts: false, jsonFormat: false, system: 'You are a helpful, accurate assistant. Be concise and clear. Use markdown when it helps readability.', codeStyle: 'default', fullCodePrompt: 'When providing code examples, always include complete, runnable code with all necessary imports, setup, and context. Provide full file contents rather than partial snippets.', snippetsPrompt: 'Focus on providing concise code snippets that show only the relevant changes or key parts. Explain what each snippet does and where it should be used.' }, messages: [] }); function loadState(){ try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || defaultState(); } catch { return defaultState(); } } function saveState(s){ localStorage.setItem(STORAGE_KEY, JSON.stringify(s)); } let state = loadState(); // --- Improved code detection and rendering --- function detectLanguage(code) { // Simple language detection based on patterns if (code.includes('<!doctype html>') || code.includes('<html')) return 'html'; if (code.includes('<?php') || code.includes('$_')) return 'php'; if (code.includes('function') && code.includes('{')) return 'javascript'; if (code.includes('import ') && code.includes('from ')) return 'javascript'; if (code.includes('def ') && code.includes(':')) return 'python'; if (code.includes('class ') && code.includes('public')) return 'java'; if (code.includes('#include') || code.includes('int main')) return 'c'; if (code.includes('console.log') || code.includes('document.')) return 'javascript'; return 'text'; } function isLikelyCode(text) { const codeMarkers = /(<!doctype html>|<html\b|<script\b|<\?php|^\s*#include\b|^\s*import\b|^\s*from\b|function\s+\w+\s*\(|class\s+\w+|console\.log\(|=>|^\s*const\s|^\s*let\s|^\s*var\s|document\.querySelector|React\.createElement)/mi; const lines = (text || '').split(/\n/); const codeish = lines.filter(l => /[;{}=<>()$]/.test(l) || codeMarkers.test(l)).length; return codeish >= Math.max(3, Math.ceil(lines.length * 0.35)); } function createCodeBlock(code, language = 'text') { const container = document.createElement('div'); container.className = 'code-container'; const header = document.createElement('div'); header.className = 'code-header'; const langLabel = document.createElement('span'); langLabel.textContent = language.toUpperCase(); const copyBtn = document.createElement('button'); copyBtn.className = 'copy-btn'; copyBtn.textContent = 'Copy'; copyBtn.onclick = () => { navigator.clipboard.writeText(code).then(() => { copyBtn.textContent = 'Copied!'; setTimeout(() => copyBtn.textContent = 'Copy', 2000); }); }; header.appendChild(langLabel); header.appendChild(copyBtn); const content = document.createElement('pre'); content.className = 'code-content'; content.textContent = code; container.appendChild(header); container.appendChild(content); return container; } function renderMessage(msg) { const wrapper = document.createElement('div'); const isUser = msg.role === 'user'; wrapper.className = `flex gap-2 sm:gap-3 ${isUser ? 'justify-end' : ''}`; const bubble = document.createElement('div'); bubble.className = `max-w-[90%] sm:max-w-[85%] rounded-2xl px-3 sm:px-4 py-2 sm:py-3 shadow-sm border ${isUser ? 'bg-indigo-600 text-white border-indigo-700' : 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800'}`; const header = document.createElement('div'); header.className = 'text-xs opacity-70 mb-1'; const dt = new Date(msg.ts || Date.now()); header.textContent = `${isUser ? 'You' : 'Assistant'} • ${dt.toLocaleTimeString()}`; bubble.appendChild(header); const body = document.createElement('div'); if (!isUser) { try { let content = msg.content || ''; // First parse with marked to get HTML const html = marked.parse(content); // Create a temporary div to work with the HTML const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; // Find all code blocks and replace them const codeBlocks = tempDiv.querySelectorAll('pre code'); codeBlocks.forEach(codeEl => { const code = codeEl.textContent; const classList = Array.from(codeEl.classList); let language = 'text'; // Extract language from class (language-html, language-js, etc.) for (const cls of classList) { if (cls.startsWith('language-')) { language = cls.replace('language-', ''); break; } } // If no language found, try to detect it if (language === 'text') { language = detectLanguage(code); } const codeBlock = createCodeBlock(code, language); codeEl.closest('pre').replaceWith(codeBlock); }); // Now sanitize the final HTML const sanitizedHTML = DOMPurify.sanitize(tempDiv.innerHTML, { ADD_TAGS: ['div', 'pre', 'button', 'span'], ADD_ATTR: ['class', 'onclick'] }); body.innerHTML = sanitizedHTML; } catch (e) { console.error('Error rendering message:', e); body.textContent = msg.content; } } else { body.textContent = msg.content; } body.className = 'prose prose-zinc dark:prose-invert max-w-none text-sm mobile-text'; bubble.appendChild(body); wrapper.appendChild(bubble); return wrapper; } function renderTranscript() { els.transcript.innerHTML = ''; state.messages.forEach(m => els.transcript.appendChild(renderMessage(m))); els.transcript.scrollTop = els.transcript.scrollHeight; } // --- Bind settings elements (overlay content exists after open) --- function bindSettingsElements() { els.model = document.getElementById('model'); els.maxTokens = document.getElementById('maxTokens'); els.temperature = document.getElementById('temperature'); els.forceTemperature = document.getElementById('forceTemperature'); els.system = document.getElementById('system'); els.includeArtifacts = document.getElementById('includeArtifacts'); els.jsonFormat = document.getElementById('jsonFormat'); els.clearChat = document.getElementById('clearChat'); // Code style tabs and prompts els.tabDefault = document.getElementById('tabDefault'); els.tabFullCode = document.getElementById('tabFullCode'); els.tabSnippets = document.getElementById('tabSnippets'); els.fullCodePrompt = document.getElementById('fullCodePrompt'); els.snippetsPrompt = document.getElementById('snippetsPrompt'); } function hydrateSettingsUI() { bindSettingsElements(); els.model.value = state.settings.model; els.maxTokens.value = state.settings.maxTokens; document.getElementById('maxTokensVal').textContent = state.settings.maxTokens; els.temperature.value = state.settings.temperature; document.getElementById('tempVal').textContent = state.settings.temperature; els.forceTemperature.checked = !!state.settings.forceTemperature; els.includeArtifacts.checked = !!state.settings.includeArtifacts; els.jsonFormat.checked = !!state.settings.jsonFormat; els.system.value = state.settings.system || ''; els.fullCodePrompt.value = state.settings.fullCodePrompt || ''; els.snippetsPrompt.value = state.settings.snippetsPrompt || ''; // Set active tab setActiveTab(state.settings.codeStyle || 'default'); } function setActiveTab(tabName) { // Reset all tabs document.getElementById('tabDefault').className = 'flex-1 px-3 py-2 text-xs bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700'; document.getElementById('tabFullCode').className = 'flex-1 px-3 py-2 text-xs bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700'; document.getElementById('tabSnippets').className = 'flex-1 px-3 py-2 text-xs bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700'; // Hide all content document.getElementById('promptDefault').classList.add('hidden'); document.getElementById('promptFullCode').classList.add('hidden'); document.getElementById('promptSnippets').classList.add('hidden'); // Set active tab and show content if (tabName === 'fullCode') { document.getElementById('tabFullCode').className = 'flex-1 px-3 py-2 text-xs bg-indigo-600 text-white'; document.getElementById('promptFullCode').classList.remove('hidden'); } else if (tabName === 'snippets') { document.getElementById('tabSnippets').className = 'flex-1 px-3 py-2 text-xs bg-indigo-600 text-white'; document.getElementById('promptSnippets').classList.remove('hidden'); } else { document.getElementById('tabDefault').className = 'flex-1 px-3 py-2 text-xs bg-indigo-600 text-white'; document.getElementById('promptDefault').classList.remove('hidden'); } state.settings.codeStyle = tabName; saveState(state); } function wireSettingsHandlers() { els.maxTokens.addEventListener('input', () => { document.getElementById('maxTokensVal').textContent = els.maxTokens.value; state.settings.maxTokens = parseInt(els.maxTokens.value, 10); saveState(state); }); els.temperature.addEventListener('input', () => { document.getElementById('tempVal').textContent = els.temperature.value; state.settings.temperature = parseFloat(els.temperature.value); saveState(state); }); els.forceTemperature.addEventListener('change', () => { state.settings.forceTemperature = els.forceTemperature.checked; saveState(state); }); els.includeArtifacts.addEventListener('change', () => { state.settings.includeArtifacts = els.includeArtifacts.checked; saveState(state); }); els.jsonFormat.addEventListener('change', () => { state.settings.jsonFormat = els.jsonFormat.checked; saveState(state); }); els.model.addEventListener('change', () => { state.settings.model = els.model.value; saveState(state); }); els.system.addEventListener('input', () => { state.settings.system = els.system.value; saveState(state); }); els.fullCodePrompt.addEventListener('input', () => { state.settings.fullCodePrompt = els.fullCodePrompt.value; saveState(state); }); els.snippetsPrompt.addEventListener('input', () => { state.settings.snippetsPrompt = els.snippetsPrompt.value; saveState(state); }); // Tab handlers els.tabDefault.addEventListener('click', () => setActiveTab('default')); els.tabFullCode.addEventListener('click', () => setActiveTab('fullCode')); els.tabSnippets.addEventListener('click', () => setActiveTab('snippets')); els.clearChat.addEventListener('click', () => { state.messages = []; saveState(state); renderTranscript(); els.status.textContent = 'Cleared local transcript.'; setTimeout(() => els.status.textContent = '', 1500); closeSettings(); }); } // --- Overlay behavior --- function openSettings() { els.overlay.classList.remove('hidden'); hydrateSettingsUI(); wireSettingsHandlers(); setTimeout(() => { try { els.model.focus(); } catch {} }, 0); } function closeSettings() { els.overlay.classList.add('hidden'); } els.openSettings.addEventListener('click', openSettings); document.getElementById('closeSettings').addEventListener('click', closeSettings); els.overlayBackdrop.addEventListener('click', closeSettings); window.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeSettings(); }); // --- Mobile-friendly submit behavior --- els.send.addEventListener('click', async () => { await submitMessage(); }); // Enter key handling - submit on desktop, new line on mobile els.question.addEventListener('keydown', (e) => { if (e.key === 'Enter') { // On mobile (detected by touch capability), allow Enter for new lines if ('ontouchstart' in window || navigator.maxTouchPoints > 0) { // Allow default behavior (new line) on mobile return; } else { // On desktop, submit with Enter (unless Shift is held) if (!e.shiftKey) { e.preventDefault(); submitMessage(); } } } }); async function submitMessage() { const question = els.question.value.trim(); if (!question) return; // Add cache busting to API requests const timestamp = Date.now(); // Build system prompt based on code style let systemPrompt = state.settings.system || ''; if (state.settings.codeStyle === 'fullCode' && state.settings.fullCodePrompt) { systemPrompt += '\n\nCODE RESPONSE STYLE: ' + state.settings.fullCodePrompt; } else if (state.settings.codeStyle === 'snippets' && state.settings.snippetsPrompt) { systemPrompt += '\n\nCODE RESPONSE STYLE: ' + state.settings.snippetsPrompt; } const payload = { question, model: state.settings.model, maxTokens: state.settings.maxTokens, temperature: state.settings.temperature, system: systemPrompt || undefined, includeArtifacts: state.settings.includeArtifacts, _t: timestamp // Cache buster }; if (state.settings.forceTemperature) payload.forceTemperature = true; if (state.settings.jsonFormat) payload.response_format = { type: 'json_object' }; const userMsg = { role: 'user', content: question, ts: Date.now() }; state.messages.push(userMsg); saveState(state); renderTranscript(); els.question.value = ''; els.send.disabled = true; els.status.textContent = 'Thinking…'; let resJSON = null; try { const res = await fetch(`api.php?_t=${timestamp}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache' }, body: JSON.stringify(payload), }); resJSON = await res.json(); } catch (err) { resJSON = { error: 'Network error', debug: String(err) }; } els.send.disabled = false; els.debugWrap.classList.remove('hidden'); els.debugArea.textContent = JSON.stringify({ request: payload, response: resJSON }, null, 2); if (!resJSON || resJSON.error) { const msg = resJSON?.error || 'Unknown error'; const dbg = resJSON?.debug ? `\n\nDebug: ${JSON.stringify(resJSON.debug)}` : ''; state.messages.push({ role: 'assistant', content: `❌ ${msg}${dbg}`, ts: Date.now() }); saveState(state); renderTranscript(); els.status.textContent = 'Error'; return; } const { answer, usage, model, provider, warning } = resJSON; let content = answer || '(no content)'; if (warning) content = `> ⚠️ ${warning}\n\n` + content; const meta = []; if (provider) meta.push(`provider: ${provider}`); if (model) meta.push(`model: ${model}`); if (usage) meta.push(`tokens – prompt: ${usage.prompt_tokens ?? 0}, completion: ${usage.completion_tokens ?? 0}, total: ${usage.total_tokens ?? 0}`); if (meta.length) content += `\n\n---\n*${meta.join(' · ')}*`; state.messages.push({ role: 'assistant', content, ts: Date.now() }); saveState(state); renderTranscript(); els.status.textContent = 'Done'; setTimeout(() => els.status.textContent = '', 1200); } // Handle viewport changes for mobile function handleViewportChange() { // Adjust textarea height on mobile for better UX if (window.innerWidth < 640) { els.question.rows = 2; } else { els.question.rows = 3; } } window.addEventListener('resize', handleViewportChange); handleViewportChange(); // Call on load // initial render renderTranscript(); </script> </body> </html>