🌐
index_copy4.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, maximum-scale=1, user-scalable=no" /> <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/[email protected]/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.5rem; padding-right: 0.5rem; } .mobile-text { font-size: 0.875rem; } .mobile-compact { gap: 0.25rem; } .mobile-tabs { flex-direction: column; } .mobile-tab-btn { flex: none; width: 100%; text-align: center; } /* Ensure no horizontal overflow */ body { overflow-x: hidden; } /* Smaller code blocks on mobile */ .code-container { margin: 0.5rem 0; } .code-header { padding: 0.25rem 0.5rem; font-size: 0.625rem; } .code-content { padding: 0.5rem; font-size: 0.75rem; } .copy-btn { padding: 0.125rem 0.25rem; font-size: 0.625rem; } .collapse-icon { font-size: 0.625rem; min-width: 0.75rem; } .delete-btn { font-size: 0.625rem; padding: 0.125rem 0.25rem; } /* Better mobile touch targets and animations */ .code-header { padding: 0.5rem; min-height: 2rem; } .code-content { padding: 0.5rem; font-size: 0.75rem; min-height: 1.5rem; } .copy-btn { min-height: 1.5rem; min-width: 2rem; } .delete-btn { min-height: 1.5rem; min-width: 1.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; user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; } .code-header:hover { background: #4b5563; } .code-header-left { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; flex: 1; } .code-header-right { display: flex; align-items: center; gap: 0.5rem; } .collapse-icon { transition: transform 0.2s; font-size: 0.75rem; min-width: 1rem; text-align: center; } .collapse-icon.collapsed { transform: rotate(-90deg); } .copy-btn, .stitch-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; margin-left: 0.25rem; } .copy-btn:hover, .stitch-btn:hover { background: #9ca3af; } .stitch-btn { background: #059669; } .stitch-btn:hover { background: #047857; } .stitch-btn.added { background: #dc2626; } .stitch-btn.added:hover { background: #b91c1c; } .code-content { padding: 1rem; overflow-x: auto; font-family: 'Courier New', monospace; font-size: 0.875rem; line-height: 1.5; color: #e5e7eb; transition: max-height 0.3s ease, opacity 0.2s ease; min-height: 2rem; } .code-content.collapsed { max-height: 0 !important; padding-top: 0; padding-bottom: 0; opacity: 0; overflow: hidden; } .message-actions { position: absolute; top: 0.25rem; right: 0.25rem; opacity: 0; transition: opacity 0.2s; z-index: 10; } .message-wrapper:hover .message-actions { opacity: 1; } /* Show delete button on mobile with tap */ @media (max-width: 640px) { .message-actions { opacity: 0.7; } .message-wrapper:active .message-actions { opacity: 1; } } .delete-btn { background: #ef4444; color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; cursor: pointer; transition: background-color 0.2s; } .delete-btn:hover { background: #dc2626; } </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 px-2 sm:px-4 py-3 flex items-center justify-between gap-2 sm:gap-3"> <div class="flex items-center gap-2 sm: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-sm 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="openStitcher" class="text-xs px-2 py-1 sm:px-3 sm:py-1.5 rounded-lg border border-emerald-300 dark:border-emerald-700 bg-emerald-50 dark:bg-emerald-900 hover:bg-emerald-100 dark:hover:bg-emerald-800 text-emerald-700 dark:text-emerald-300"> Stitcher <span id="stitcherCount" class="hidden ml-1 px-1 bg-emerald-600 text-white rounded-full text-xs">0</span> </button> <button id="openSettings" class="text-xs px-2 py-1 sm:px-3 sm: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 px-2 sm: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 shadow-sm"> <label class="sr-only" for="question">Your message</label> <div class="flex items-end gap-2"> <textarea id="question" rows="2" class="flex-1 min-h-[48px] sm:min-h-[72px] rounded-xl border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 sm:px-3 py-2 text-sm" placeholder="Ask me anything…"></textarea> <div class="flex flex-col items-stretch gap-1"> <button id="send" class="h-8 sm:h-10 px-2 sm:px-4 rounded-xl bg-indigo-600 text-white hover:bg-indigo-500 disabled:opacity-50 text-xs sm:text-sm"> Send </button> <div id="status" class="text-[9px] sm:text-[11px] text-zinc-500 text-center"></div> </div> </div> </div> </div> </section> </main> </div> <!-- Stitcher Overlay --> <div id="stitcherOverlay" class="fixed inset-0 z-40 hidden" aria-hidden="true"> <div id="stitcherBackdrop" 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="stitcherTitle" class="w-full max-w-4xl 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="stitcherTitle" class="font-semibold text-sm sm:text-base">Code Stitcher</h2> <div class="flex items-center gap-2"> <button id="clearStitcher" 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 All</button> <button id="downloadStitched" class="text-xs px-2 py-1 rounded-md bg-emerald-600 text-white hover:bg-emerald-700">Download</button> <button id="copyStitched" class="text-xs px-2 py-1 rounded-md bg-indigo-600 text-white hover:bg-indigo-700">Copy All</button> <button id="closeStitcher" 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 overflow-y-auto" style="max-height: calc(90vh - 60px);"> <div id="stitcherContent" class="space-y-3"> <div class="text-center text-zinc-500 text-sm" id="stitcherEmpty"> No code chunks added yet. Use the "Add to Stitcher" button on code blocks to start building your complete file. </div> </div> <div class="mt-4 p-3 rounded-lg bg-zinc-50 dark:bg-zinc-800"> <label class="text-sm block mb-2">Filename for download:</label> <input id="stitcherFilename" type="text" class="w-full rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-2 py-1 text-sm" placeholder="my-code.html" value="stitched-code.txt"> </div> </div> </div> </div> </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 sm:flex-row flex-col mobile-tabs border border-zinc-300 dark:border-zinc-700 rounded-lg overflow-hidden"> <button id="tabDefault" class="mobile-tab-btn px-3 py-2 text-xs sm:text-xs bg-indigo-600 text-white border-b sm:border-b-0 sm:border-r border-zinc-300 dark:border-zinc-700 last:border-b-0 last:border-r-0">Default</button> <button id="tabFullCode" class="mobile-tab-btn px-3 py-2 text-xs sm:text-xs bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 border-b sm:border-b-0 sm:border-r border-zinc-300 dark:border-zinc-700 last:border-b-0 last:border-r-0">Full Code</button> <button id="tabSnippets" class="mobile-tab-btn px-3 py-2 text-xs sm:text-xs bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 border-b sm:border-b-0 sm:border-r border-zinc-300 dark:border-zinc-700 last:border-b-0 last:border-r-0">Snippets</button> <button id="tabChunked" class="mobile-tab-btn px-3 py-2 text-xs sm:text-xs bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 last:border-b-0 last:border-r-0">Chunked</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 id="promptChunked" class="mt-2 p-3 rounded-lg bg-zinc-50 dark:bg-zinc-800 text-xs hidden"> <strong>Chunked:</strong> Break code into manageable chunks to fit token limits. <textarea id="chunkedPrompt" 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 chunked responses...">When providing large code files or long responses, break them into logical chunks. End each chunk with a clear indication of what comes next. If approaching token limits, stop at a logical break point and indicate there's more to follow.</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'), // stitcher stitcherOverlay: document.getElementById('stitcherOverlay'), openStitcher: document.getElementById('openStitcher'), closeStitcher: document.getElementById('closeStitcher'), stitcherBackdrop: document.getElementById('stitcherBackdrop'), stitcherContent: document.getElementById('stitcherContent'), stitcherEmpty: document.getElementById('stitcherEmpty'), stitcherCount: document.getElementById('stitcherCount'), clearStitcher: document.getElementById('clearStitcher'), copyStitched: document.getElementById('copyStitched'), downloadStitched: document.getElementById('downloadStitched'), stitcherFilename: document.getElementById('stitcherFilename'), }; // --- 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.', chunkedPrompt: 'When providing large code files or long responses, break them into logical chunks. End each chunk with a clear indication of what comes next. If approaching token limits, stop at a logical break point and indicate there\'s more to follow.' }, messages: [], stitcher: { chunks: [], isOpen: false } }); 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 headerLeft = document.createElement('div'); headerLeft.className = 'code-header-left'; const collapseIcon = document.createElement('span'); collapseIcon.className = 'collapse-icon'; collapseIcon.textContent = '▼'; const langLabel = document.createElement('span'); langLabel.textContent = language.toUpperCase(); headerLeft.appendChild(collapseIcon); headerLeft.appendChild(langLabel); const headerRight = document.createElement('div'); headerRight.className = 'code-header-right'; const copyBtn = document.createElement('button'); copyBtn.className = 'copy-btn'; copyBtn.textContent = 'Copy'; const stitchBtn = document.createElement('button'); stitchBtn.className = 'stitch-btn'; stitchBtn.textContent = 'Add to Stitcher'; headerRight.appendChild(copyBtn); headerRight.appendChild(stitchBtn); header.appendChild(headerLeft); header.appendChild(headerRight); const content = document.createElement('pre'); content.className = 'code-content'; content.textContent = code; container.appendChild(header); container.appendChild(content); // Add event listeners after elements are created and added to DOM setTimeout(() => { // Copy button functionality copyBtn.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(code).then(() => { copyBtn.textContent = 'Copied!'; setTimeout(() => copyBtn.textContent = 'Copy', 2000); }).catch(() => { fallbackCopyTextToClipboard(code, copyBtn); }); } else { fallbackCopyTextToClipboard(code, copyBtn); } }); // Stitch button functionality stitchBtn.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); const chunkId = Date.now() + Math.random(); const isAlreadyAdded = state.stitcher.chunks.some(chunk => chunk.code === code); if (isAlreadyAdded) { // Remove from stitcher state.stitcher.chunks = state.stitcher.chunks.filter(chunk => chunk.code !== code); stitchBtn.textContent = 'Add to Stitcher'; stitchBtn.classList.remove('added'); } else { // Add to stitcher state.stitcher.chunks.push({ id: chunkId, code: code, language: language, timestamp: Date.now() }); stitchBtn.textContent = 'Remove from Stitcher'; stitchBtn.classList.add('added'); } saveState(state); updateStitcherUI(); }); // Check if this code is already in stitcher const isInStitcher = state.stitcher.chunks.some(chunk => chunk.code === code); if (isInStitcher) { stitchBtn.textContent = 'Remove from Stitcher'; stitchBtn.classList.add('added'); } // Collapse functionality - only on the header left side headerLeft.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); const isCollapsed = content.classList.contains('collapsed'); if (isCollapsed) { content.classList.remove('collapsed'); collapseIcon.classList.remove('collapsed'); collapseIcon.textContent = '▼'; } else { content.classList.add('collapsed'); collapseIcon.classList.add('collapsed'); collapseIcon.textContent = '►'; } }); }, 0); return container; } // Fallback copy function for older browsers function fallbackCopyTextToClipboard(text, button) { const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; textArea.style.top = '-999999px'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); button.textContent = 'Copied!'; setTimeout(() => button.textContent = 'Copy', 2000); } catch (err) { button.textContent = 'Failed'; setTimeout(() => button.textContent = 'Copy', 2000); } document.body.removeChild(textArea); } function renderMessage(msg, index) { const wrapper = document.createElement('div'); const isUser = msg.role === 'user'; wrapper.className = `message-wrapper flex gap-2 sm:gap-3 ${isUser ? 'justify-end' : ''} relative`; const bubble = document.createElement('div'); bubble.className = `max-w-[95%] sm:max-w-[85%] rounded-2xl px-2 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'}`; // Add delete button for all messages (both user and assistant) const deleteBtn = document.createElement('button'); deleteBtn.className = 'delete-btn message-actions'; deleteBtn.textContent = '×'; deleteBtn.title = 'Delete message'; deleteBtn.onclick = (e) => { e.stopPropagation(); if (confirm('Delete this message?')) { state.messages.splice(index, 1); saveState(state); renderTranscript(); } }; wrapper.appendChild(deleteBtn); 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 || ''; // 1) Get raw HTML from marked const raw = marked.parse(content); // 2) Sanitize the HTML string const safeHtml = DOMPurify.sanitize(raw, { ADD_TAGS: ['div', 'pre', 'button', 'span'], ADD_ATTR: ['class', 'onclick'] }); // 3) Build a working DOM from the sanitized HTML const tempDiv = document.createElement('div'); tempDiv.innerHTML = safeHtml; // 4) Now replace <pre><code> with interactive blocks (adds listeners) const codeBlocks = tempDiv.querySelectorAll('pre code'); codeBlocks.forEach(codeEl => { const code = codeEl.textContent; let language = 'text'; for (const cls of codeEl.classList) { if (cls.startsWith('language-')) { language = cls.slice(9); break; } } if (language === 'text') language = detectLanguage(code); const codeBlock = createCodeBlock(code, language); codeEl.closest('pre').replaceWith(codeBlock); }); // 5) Finally inject the DOM (not innerHTML) so listeners stay intact body.replaceChildren(...tempDiv.childNodes); } 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); // Add continue/next chunk button for assistant messages if (!isUser) { const actionBtn = document.createElement('button'); const isChunkedMode = state.settings.codeStyle === 'chunked'; actionBtn.className = 'mt-2 px-3 py-1 text-xs rounded-lg border border-zinc-300 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors'; actionBtn.textContent = isChunkedMode ? 'Next Chunk' : 'Continue'; actionBtn.onclick = () => { if (isChunkedMode) { // Find the last code block in this message const codeContainers = bubble.querySelectorAll('.code-container .code-content'); let lastCode = ''; if (codeContainers.length > 0) { // Get the last code block const lastCodeContainer = codeContainers[codeContainers.length - 1]; lastCode = lastCodeContainer.textContent; // Take the last 8-10 lines for context in chunked mode const lines = lastCode.split('\n'); const contextLines = lines.slice(-8); // Last 8 lines for chunked mode const contextCode = contextLines.join('\n'); // Detect language from the code container const codeContainer = lastCodeContainer.closest('.code-container'); const langLabel = codeContainer.querySelector('.code-header span'); const language = langLabel ? langLabel.textContent.toLowerCase() : 'code'; // Create the next chunk message const nextChunkMessage = `Continue with the next chunk from this point:\n\n\`\`\`${language}\n${contextCode}\n\`\`\`\n\nProvide the next logical chunk/section.`; els.question.value = nextChunkMessage; } else { // No code found, ask for next chunk of the explanation/content els.question.value = 'Please provide the next chunk/section.'; } } else { // Regular continue mode const codeContainers = bubble.querySelectorAll('.code-container .code-content'); let lastCode = ''; if (codeContainers.length > 0) { const lastCodeContainer = codeContainers[codeContainers.length - 1]; lastCode = lastCodeContainer.textContent; const lines = lastCode.split('\n'); const contextLines = lines.slice(-15); // More context for regular continue const contextCode = contextLines.join('\n'); const codeContainer = lastCodeContainer.closest('.code-container'); const langLabel = codeContainer.querySelector('.code-header span'); const language = langLabel ? langLabel.textContent.toLowerCase() : 'code'; const continueMessage = `Continue from this point:\n\n\`\`\`${language}\n${contextCode}\n\`\`\`\n\nContinue from here.`; els.question.value = continueMessage; } else { els.question.value = 'Continue from where you left off.'; } } els.question.focus(); els.question.scrollIntoView({ behavior: 'smooth', block: 'center' }); }; bubble.appendChild(actionBtn); } wrapper.appendChild(bubble); return wrapper; } function renderTranscript() { els.transcript.innerHTML = ''; state.messages.forEach((m, index) => els.transcript.appendChild(renderMessage(m, index))); 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.tabChunked = document.getElementById('tabChunked'); els.fullCodePrompt = document.getElementById('fullCodePrompt'); els.snippetsPrompt = document.getElementById('snippetsPrompt'); els.chunkedPrompt = document.getElementById('chunkedPrompt'); } 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 || ''; els.chunkedPrompt.value = state.settings.chunkedPrompt || ''; // Set active tab setActiveTab(state.settings.codeStyle || 'default'); } function setActiveTab(tabName) { // Reset all tabs with mobile-friendly classes const baseClasses = 'mobile-tab-btn px-3 py-2 text-xs sm:text-xs border-b sm:border-b-0 sm:border-r border-zinc-300 dark:border-zinc-700 last:border-b-0 last:border-r-0'; const inactiveClasses = baseClasses + ' bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700'; const activeClasses = baseClasses + ' bg-indigo-600 text-white'; document.getElementById('tabDefault').className = inactiveClasses; document.getElementById('tabFullCode').className = inactiveClasses; document.getElementById('tabSnippets').className = inactiveClasses; document.getElementById('tabChunked').className = inactiveClasses; // Hide all content document.getElementById('promptDefault').classList.add('hidden'); document.getElementById('promptFullCode').classList.add('hidden'); document.getElementById('promptSnippets').classList.add('hidden'); document.getElementById('promptChunked').classList.add('hidden'); // Set active tab and show content if (tabName === 'fullCode') { document.getElementById('tabFullCode').className = activeClasses; document.getElementById('promptFullCode').classList.remove('hidden'); } else if (tabName === 'snippets') { document.getElementById('tabSnippets').className = activeClasses; document.getElementById('promptSnippets').classList.remove('hidden'); } else if (tabName === 'chunked') { document.getElementById('tabChunked').className = activeClasses; document.getElementById('promptChunked').classList.remove('hidden'); } else { document.getElementById('tabDefault').className = activeClasses; 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); }); els.chunkedPrompt.addEventListener('input', () => { state.settings.chunkedPrompt = els.chunkedPrompt.value; saveState(state); }); // Tab handlers els.tabDefault.addEventListener('click', () => setActiveTab('default')); els.tabFullCode.addEventListener('click', () => setActiveTab('fullCode')); els.tabSnippets.addEventListener('click', () => setActiveTab('snippets')); els.tabChunked.addEventListener('click', () => setActiveTab('chunked')); els.clearChat.addEventListener('click', () => { state.messages = []; saveState(state); renderTranscript(); els.status.textContent = 'Cleared local transcript.'; setTimeout(() => els.status.textContent = '', 1500); closeSettings(); }); } // --- Stitcher functions --- function updateStitcherUI() { const count = state.stitcher.chunks.length; // Update button badge if (count > 0) { els.stitcherCount.textContent = count; els.stitcherCount.classList.remove('hidden'); } else { els.stitcherCount.classList.add('hidden'); } // Update stitcher content if (count === 0) { els.stitcherEmpty.classList.remove('hidden'); els.stitcherContent.innerHTML = '<div class="text-center text-zinc-500 text-sm" id="stitcherEmpty">No code chunks added yet. Use the "Add to Stitcher" button on code blocks to start building your complete file.</div>'; } else { els.stitcherEmpty.classList.add('hidden'); els.stitcherContent.innerHTML = ''; state.stitcher.chunks.forEach((chunk, index) => { const chunkDiv = document.createElement('div'); chunkDiv.className = 'border border-zinc-200 dark:border-zinc-800 rounded-lg p-3 bg-zinc-50 dark:bg-zinc-800'; const header = document.createElement('div'); header.className = 'flex items-center justify-between mb-2'; const title = document.createElement('div'); title.className = 'text-sm font-medium'; title.textContent = `Chunk ${index + 1} (${chunk.language.toUpperCase()})`; const actions = document.createElement('div'); actions.className = 'flex gap-2'; const moveUp = document.createElement('button'); moveUp.className = 'text-xs px-2 py-1 rounded border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-700'; moveUp.textContent = '↑'; moveUp.disabled = index === 0; if (moveUp.disabled) moveUp.className += ' opacity-50 cursor-not-allowed'; const moveDown = document.createElement('button'); moveDown.className = 'text-xs px-2 py-1 rounded border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-700'; moveDown.textContent = '↓'; moveDown.disabled = index === count - 1; if (moveDown.disabled) moveDown.className += ' opacity-50 cursor-not-allowed'; const remove = document.createElement('button'); remove.className = 'text-xs px-2 py-1 rounded bg-red-600 text-white hover:bg-red-700'; remove.textContent = '×'; actions.appendChild(moveUp); actions.appendChild(moveDown); actions.appendChild(remove); header.appendChild(title); header.appendChild(actions); const codePreview = document.createElement('pre'); codePreview.className = 'text-xs bg-zinc-900 text-zinc-100 p-2 rounded overflow-x-auto max-h-32 overflow-y-auto'; codePreview.textContent = chunk.code.substring(0, 500) + (chunk.code.length > 500 ? '...' : ''); chunkDiv.appendChild(header); chunkDiv.appendChild(codePreview); els.stitcherContent.appendChild(chunkDiv); // Event listeners moveUp.addEventListener('click', () => { if (index > 0) { [state.stitcher.chunks[index], state.stitcher.chunks[index - 1]] = [state.stitcher.chunks[index - 1], state.stitcher.chunks[index]]; saveState(state); updateStitcherUI(); } }); moveDown.addEventListener('click', () => { if (index < count - 1) { [state.stitcher.chunks[index], state.stitcher.chunks[index + 1]] = [state.stitcher.chunks[index + 1], state.stitcher.chunks[index]]; saveState(state); updateStitcherUI(); } }); remove.addEventListener('click', () => { state.stitcher.chunks.splice(index, 1); saveState(state); updateStitcherUI(); renderTranscript(); // Update stitch button states }); }); } } function getStitchedCode() { return state.stitcher.chunks.map(chunk => chunk.code).join('\n\n'); } function downloadStitchedCode() { const code = getStitchedCode(); const filename = els.stitcherFilename.value || 'stitched-code.txt'; const blob = new Blob([code], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // --- 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'); } function openStitcher() { els.stitcherOverlay.classList.remove('hidden'); updateStitcherUI(); } function closeStitcher() { els.stitcherOverlay.classList.add('hidden'); } els.openSettings.addEventListener('click', openSettings); document.getElementById('closeSettings').addEventListener('click', closeSettings); els.overlayBackdrop.addEventListener('click', closeSettings); els.openStitcher.addEventListener('click', openStitcher); els.closeStitcher.addEventListener('click', closeStitcher); els.stitcherBackdrop.addEventListener('click', closeStitcher); els.clearStitcher.addEventListener('click', () => { if (confirm('Clear all chunks from stitcher?')) { state.stitcher.chunks = []; saveState(state); updateStitcherUI(); renderTranscript(); // Update button states } }); els.copyStitched.addEventListener('click', () => { const code = getStitchedCode(); if (code) { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(code).then(() => { els.copyStitched.textContent = 'Copied!'; setTimeout(() => els.copyStitched.textContent = 'Copy All', 2000); }); } else { fallbackCopyTextToClipboard(code, els.copyStitched); } } }); els.downloadStitched.addEventListener('click', downloadStitchedCode); window.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeSettings(); closeStitcher(); } }); // --- 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; } else if (state.settings.codeStyle === 'chunked' && state.settings.chunkedPrompt) { systemPrompt += '\n\nCODE RESPONSE STYLE: ' + state.settings.chunkedPrompt; } 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; // Ensure mobile-friendly max width document.body.style.maxWidth = '100vw'; } else { els.question.rows = 3; document.body.style.maxWidth = ''; } } window.addEventListener('resize', handleViewportChange); handleViewportChange(); // Call on load // initial render and stitcher setup renderTranscript(); updateStitcherUI(); </script> </body> </html>