📜
chat.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
window.App = window.App || {}; (() => { const els = { transcript: document.getElementById('transcript'), question: document.getElementById('question'), send: document.getElementById('send'), status: document.getElementById('status'), debugWrap: document.getElementById('debugWrap'), debugArea: document.getElementById('debugArea'), }; // Utilities 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 detectLanguage(code) { if (code.includes('<!doctype html>') || code.includes('<html')) return 'html'; if (code.includes('<?php') || code.includes('$_')) return 'php'; 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'; if (code.includes('import ') && code.includes('from ')) return 'javascript'; return 'text'; } function fallbackCopy(text, button) { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px'; ta.style.top = '-9999px'; document.body.appendChild(ta); ta.focus(); ta.select(); try { document.execCommand('copy'); button.textContent = 'Copied!'; setTimeout(() => button.textContent = 'Copy', 2000); } catch { button.textContent = 'Failed'; setTimeout(() => button.textContent = 'Copy', 2000); } document.body.removeChild(ta); } 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 || 'text').toUpperCase(); headerLeft.append(collapseIcon, 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.append(copyBtn, stitchBtn); header.append(headerLeft, headerRight); const content = document.createElement('pre'); content.className = 'code-content'; content.textContent = code; container.append(header, content); // Listeners setTimeout(() => { copyBtn.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); if (navigator.clipboard?.writeText) { navigator.clipboard.writeText(code).then(() => { copyBtn.textContent = 'Copied!'; setTimeout(() => copyBtn.textContent = 'Copy', 2000); }).catch(() => fallbackCopy(code, copyBtn)); } else { fallbackCopy(code, copyBtn); } }); stitchBtn.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); const added = App.addToStitcher(code, language); stitchBtn.textContent = added ? 'Remove from Stitcher' : 'Add to Stitcher'; stitchBtn.classList.toggle('added', added); }); // Set initial stitch state const isIn = App.state.stitcher.chunks.some(c => c.code === code); if (isIn) { stitchBtn.textContent = 'Remove from Stitcher'; stitchBtn.classList.add('added'); } headerLeft.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); const collapsed = content.classList.contains('collapsed'); content.classList.toggle('collapsed', !collapsed); collapseIcon.classList.toggle('collapsed', !collapsed); collapseIcon.textContent = collapsed ? '▼' : '►'; }); }, 0); return container; } function renderMessage(msg, index) { const wrapper = document.createElement('div'); const isUser = msg.role === 'user'; wrapper.className = `message-wrapper ${isUser ? 'justify-end' : ''}`; const bubble = document.createElement('div'); bubble.className = `bubble ${isUser ? 'bg-indigo-600 text-white border-indigo-700' : 'bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800'}`; // delete button const del = document.createElement('button'); del.className = 'delete-btn message-actions'; del.textContent = '×'; del.title = 'Delete message'; del.onclick = (e) => { e.stopPropagation(); if (confirm('Delete this message?')) { App.state.messages.splice(index, 1); App.saveState(App.state); App.renderTranscript(); } }; wrapper.appendChild(del); // header 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); // body const body = document.createElement('div'); body.className = 'prose prose-zinc dark:prose-invert max-w-none text-sm mobile-text'; if (!isUser) { try { const raw = marked.parse(msg.content || ''); const safeHtml = DOMPurify.sanitize(raw, { ADD_TAGS: ['div','pre','button','span'], ADD_ATTR: ['class','onclick'] }); const tempDiv = document.createElement('div'); tempDiv.innerHTML = safeHtml; 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); }); body.replaceChildren(...tempDiv.childNodes); } catch (e) { console.error('render error', e); body.textContent = msg.content || ''; } } else { body.textContent = msg.content || ''; } bubble.appendChild(body); if (!isUser) { const action = document.createElement('button'); const chunked = App.state.settings.codeStyle === 'chunked'; action.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'; action.textContent = chunked ? 'Next Chunk' : 'Continue'; action.onclick = () => { const codes = bubble.querySelectorAll('.code-container .code-content'); if (codes.length > 0) { const last = codes[codes.length - 1].textContent.split('\n'); const context = last.slice(chunked ? -8 : -15).join('\n'); const langLabel = codes[codes.length - 1].closest('.code-container').querySelector('.code-header span'); const language = langLabel ? langLabel.textContent.toLowerCase() : 'code'; els.question.value = `${chunked ? 'Continue with the next chunk from this point:' : 'Continue from this point:'}\n\n\`\`\`${language}\n${context}\n\`\`\`\n\n${chunked ? 'Provide the next logical chunk/section.' : 'Continue from here.'}`; } else { els.question.value = chunked ? 'Please provide the next chunk/section.' : 'Continue from where you left off.'; } els.question.focus(); els.question.scrollIntoView({ behavior: 'smooth', block: 'center' }); }; bubble.appendChild(action); } wrapper.appendChild(bubble); return wrapper; } function renderTranscript() { els.transcript.innerHTML = ''; App.state.messages.forEach((m, i) => els.transcript.appendChild(renderMessage(m, i))); els.transcript.scrollTop = els.transcript.scrollHeight; } App.renderTranscript = renderTranscript; async function submitMessage() { const question = els.question.value.trim(); if (!question) return; const timestamp = Date.now(); // Build system prompt based on code style const s = App.state.settings; let systemPrompt = s.system || ''; if (s.codeStyle === 'fullCode' && s.fullCodePrompt) systemPrompt += '\n\nCODE RESPONSE STYLE: ' + s.fullCodePrompt; else if (s.codeStyle === 'snippets' && s.snippetsPrompt) systemPrompt += '\n\nCODE RESPONSE STYLE: ' + s.snippetsPrompt; else if (s.codeStyle === 'chunked' && s.chunkedPrompt) systemPrompt += '\n\nCODE RESPONSE STYLE: ' + s.chunkedPrompt; const payload = { question, model: s.model, maxTokens: s.maxTokens, temperature: s.temperature, system: systemPrompt || undefined, includeArtifacts: s.includeArtifacts, _t: timestamp }; if (s.forceTemperature) payload.forceTemperature = true; if (s.jsonFormat) payload.response_format = { type: 'json_object' }; const userMsg = { role: 'user', content: question, ts: Date.now() }; App.state.messages.push(userMsg); App.saveState(App.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)}` : ''; App.state.messages.push({ role: 'assistant', content: `❌ ${msg}${dbg}`, ts: Date.now() }); App.saveState(App.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(' · ')}*`; App.state.messages.push({ role: 'assistant', content, ts: Date.now() }); App.saveState(App.state); renderTranscript(); els.status.textContent = 'Done'; setTimeout(() => els.status.textContent = '', 1200); } // Wire send + keyboard behavior els.send.addEventListener('click', submitMessage); els.question.addEventListener('keydown', (e) => { if (e.key === 'Enter') { if ('ontouchstart' in window || navigator.maxTouchPoints > 0) return; // mobile: allow newline if (!e.shiftKey) { e.preventDefault(); submitMessage(); } } }); // Initial render + stitcher badge sync (stitcher.js will call render too) renderTranscript(); })();