📜
chat_copy2.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// chat.js - Chat Component for AppOverlay System window.App = window.App || {}; window.AppItems = window.AppItems || []; (() => { // Initialize App.state if it doesn't exist if (!App.state) { App.state = { messages: [], 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: 'Provide complete, working code.', snippetsPrompt: 'Provide focused code snippets.', chunkedPrompt: 'Provide code in logical chunks.' }, stitcher: { chunks: [] }, stats: { totalMessages: 0, modelUsage: {} } }; } // Create App.saveState if it doesn't exist if (!App.saveState) { App.saveState = function(state) { try { localStorage.setItem('chatState', JSON.stringify(state)); } catch (e) { console.error('Failed to save state:', e); } }; } // Create App.trackUsage if it doesn't exist if (!App.trackUsage) { App.trackUsage = function(model, usage) { if (!usage) return; const stats = App.state.stats; if (!stats.modelUsage[model]) { stats.modelUsage[model] = { input: 0, output: 0, calls: 0 }; } stats.modelUsage[model].input += usage.prompt_tokens || 0; stats.modelUsage[model].output += usage.completion_tokens || 0; stats.modelUsage[model].calls += 1; stats.totalMessages += 1; App.saveState(App.state); }; } // Load from localStorage try { const saved = localStorage.getItem('chatState'); if (saved) { const parsed = JSON.parse(saved); if (parsed.messages) App.state.messages = parsed.messages; if (parsed.stats) App.state.stats = parsed.stats; if (parsed.settings) Object.assign(App.state.settings, parsed.settings); } } catch (e) { console.error('Failed to load chat state:', e); } // Utilities 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'; 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 updateDebugInfo(message) { const debugEl = document.getElementById('chatDebug'); if (debugEl) { const timestamp = new Date().toLocaleTimeString(); const existing = debugEl.textContent || ''; debugEl.textContent = `[${timestamp}] ${message}\n${existing}`.slice(0, 2000); } } function createCodeBlock(code, language = 'text', isUserMessage = false) { const container = document.createElement('div'); container.style.cssText = 'margin: 1rem 0; border: 1px solid #374151; border-radius: 0.5rem; overflow: hidden; background: #111827;'; const header = document.createElement('div'); header.style.cssText = 'display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 1rem; background: #1f2937; border-bottom: 1px solid #374151;'; const headerLeft = document.createElement('div'); headerLeft.style.cssText = 'display: flex; align-items: center; gap: 0.5rem; cursor: pointer; user-select: none;'; const collapseIcon = document.createElement('span'); collapseIcon.textContent = '▼'; collapseIcon.style.cssText = 'color: #9ca3af; font-size: 0.75rem;'; const langLabel = document.createElement('span'); langLabel.textContent = (language || 'text').toUpperCase(); langLabel.style.cssText = 'color: #d1d5db; font-size: 0.75rem; font-weight: 600;'; headerLeft.append(collapseIcon, langLabel); const headerRight = document.createElement('div'); headerRight.style.cssText = 'display: flex; gap: 0.5rem;'; const copyBtn = document.createElement('button'); copyBtn.textContent = 'Copy'; copyBtn.style.cssText = 'padding: 0.25rem 0.75rem; font-size: 0.75rem; background: #374151; color: #f3f4f6; border: none; border-radius: 0.25rem; cursor: pointer; transition: background 0.2s;'; copyBtn.onmouseover = () => copyBtn.style.background = '#4b5563'; copyBtn.onmouseout = () => copyBtn.style.background = '#374151'; headerRight.appendChild(copyBtn); header.append(headerLeft, headerRight); const content = document.createElement('pre'); content.style.cssText = 'margin: 0; padding: 1rem; background: #111827; color: #f3f4f6; overflow-x: auto; font-family: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, monospace; font-size: 0.8125rem; line-height: 1.6; -webkit-overflow-scrolling: touch;'; content.textContent = code; container.append(header, content); copyBtn.addEventListener('click', (e) => { e.stopPropagation(); 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); } }); headerLeft.addEventListener('click', () => { const isCollapsed = content.style.display === 'none'; content.style.display = isCollapsed ? 'block' : 'none'; collapseIcon.textContent = isCollapsed ? '▼' : '►'; }); return container; } function parseUserMessageForCodeBlocks(text) { const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g; const parts = []; let lastIndex = 0; let match; while ((match = codeBlockRegex.exec(text)) !== null) { if (match.index > lastIndex) { parts.push({ type: 'text', content: text.slice(lastIndex, match.index) }); } parts.push({ type: 'code', language: match[1] || 'text', content: match[2] }); lastIndex = match.index + match[0].length; } if (lastIndex < text.length) { parts.push({ type: 'text', content: text.slice(lastIndex) }); } return parts.length > 0 ? parts : [{ type: 'text', content: text }]; } function renderMessage(msg, index, renderCallback) { const wrapper = document.createElement('div'); wrapper.style.cssText = 'margin-bottom: 1.5rem; position: relative;'; const isUser = msg.role === 'user'; const deleteBtn = document.createElement('button'); deleteBtn.textContent = '×'; deleteBtn.title = 'Delete message'; deleteBtn.style.cssText = `position: absolute; top: -8px; ${isUser ? 'right: -8px' : 'left: -8px'}; width: 24px; height: 24px; border-radius: 50%; background: #ef4444; color: white; border: 2px solid #1f2937; font-size: 1.25rem; line-height: 1; cursor: pointer; opacity: 0; transition: opacity 0.2s; display: flex; align-items: center; justify-content: center; z-index: 10; font-weight: 700;`; wrapper.onmouseenter = () => deleteBtn.style.opacity = '1'; wrapper.onmouseleave = () => deleteBtn.style.opacity = '0'; if ('ontouchstart' in window || navigator.maxTouchPoints > 0) { deleteBtn.style.opacity = '0.7'; } deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); if (confirm('Delete this message?')) { App.state.messages.splice(index, 1); App.saveState(App.state); updateDebugInfo(`Deleted message ${index + 1}`); if (typeof renderCallback === 'function') renderCallback(); } }); const bubble = document.createElement('div'); bubble.style.cssText = `padding: 1rem; border-radius: 0.75rem; position: relative; ${isUser ? 'background: linear-gradient(135deg, #4f46e5, #6366f1); color: white; margin-left: 15%; box-shadow: 0 4px 6px -1px rgba(79, 70, 229, 0.3);' : 'background: #1f2937; color: #f3f4f6; margin-right: 15%; border: 1px solid #374151; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);'}`; const header = document.createElement('div'); header.style.cssText = `font-size: 0.75rem; opacity: 0.8; margin-bottom: 0.5rem; font-weight: 600; ${isUser ? 'color: rgba(255,255,255,0.9)' : 'color: #9ca3af'}`; const dt = new Date(msg.ts || Date.now()); header.textContent = `${isUser ? 'You' : 'Assistant'} • ${dt.toLocaleTimeString()}`; bubble.appendChild(header); const body = document.createElement('div'); body.style.cssText = 'font-size: 0.875rem; line-height: 1.6;'; if (!isUser && typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') { try { const raw = marked.parse(msg.content || ''); const safeHtml = DOMPurify.sanitize(raw); const tempDiv = document.createElement('div'); tempDiv.innerHTML = safeHtml; tempDiv.querySelectorAll('p, ul, ol, blockquote, h1, h2, h3, h4, h5, h6').forEach(el => { el.style.color = '#f3f4f6'; if (el.tagName === 'BLOCKQUOTE') { el.style.borderLeft = '3px solid #4f46e5'; el.style.paddingLeft = '1rem'; el.style.fontStyle = 'italic'; el.style.opacity = '0.9'; } }); 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, false); codeEl.closest('pre').replaceWith(codeBlock); }); body.replaceChildren(...tempDiv.childNodes); } catch (e) { console.error('render error', e); body.textContent = msg.content || ''; } } else { const parts = parseUserMessageForCodeBlocks(msg.content || ''); parts.forEach(part => { if (part.type === 'text') { const textNode = document.createElement('div'); textNode.style.whiteSpace = 'pre-wrap'; textNode.style.wordBreak = 'break-word'; textNode.textContent = part.content; body.appendChild(textNode); } else if (part.type === 'code') { body.appendChild(createCodeBlock(part.content, part.language, isUser)); } }); } bubble.appendChild(body); if (!isUser) { // Check if response indicates continuation needed const needsContinuation = msg.content.toLowerCase().includes('[continued in next response]') || msg.content.toLowerCase().includes('[to be continued]') || msg.content.toLowerCase().includes('[more to follow]') || msg.content.toLowerCase().includes('[continue]') || msg.content.trim().endsWith('...') || (App.state?.settings?.codeStyle === 'chunked' && msg.content.length > 1500); if (needsContinuation) { const action = document.createElement('button'); action.style.cssText = 'margin-top: 0.75rem; padding: 0.625rem 1.25rem; background: linear-gradient(135deg, #10b981, #059669); color: white; border: none; border-radius: 0.5rem; font-size: 0.875rem; font-weight: 600; cursor: pointer; transition: all 0.2s; box-shadow: 0 2px 4px rgba(16, 185, 129, 0.3);'; action.textContent = '▶ Continue'; action.onmouseover = () => { action.style.transform = 'translateY(-1px)'; action.style.boxShadow = '0 4px 6px rgba(16, 185, 129, 0.4)'; }; action.onmouseout = () => { action.style.transform = 'translateY(0)'; action.style.boxShadow = '0 2px 4px rgba(16, 185, 129, 0.3)'; }; action.onclick = () => { const codes = bubble.querySelectorAll('.code-container .code-content'); const questionEl = document.querySelector('#chatQuestion'); const sendBtn = document.querySelector('#chatSend'); if (!questionEl) return; let continuePrompt = ''; if (codes.length > 0) { const last = codes[codes.length - 1].textContent.split('\n'); const context = last.slice(-8).join('\n'); const langLabel = codes[codes.length - 1].closest('.code-container').querySelector('.code-header span'); const language = langLabel ? langLabel.textContent.toLowerCase() : 'code'; continuePrompt = `Continue from this point:\n\n\`\`\`${language}\n${context}\n\`\`\`\n\nPlease continue.`; } else { continuePrompt = 'Please continue from where you left off.'; } // Set value and auto-submit questionEl.value = continuePrompt; if (sendBtn) sendBtn.click(); }; bubble.appendChild(action); } } wrapper.appendChild(deleteBtn); wrapper.appendChild(bubble); return wrapper; } function generateChatHTML() { return ` <div style="display: flex; flex-direction: column; height: 100%; background: #0f172a; overflow: hidden;"> <div style="flex-shrink: 0; padding: 1rem; background: #1e293b; border-bottom: 1px solid #334155;"> <div style="display: flex; gap: 0.75rem; align-items: flex-start;"> <textarea id="chatQuestion" placeholder="Type your message..." style="flex: 1; min-height: 60px; max-height: 120px; padding: 0.875rem; border: 1px solid #334155; border-radius: 0.5rem; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 0.9375rem; resize: vertical; background: #0f172a; color: #f1f5f9; line-height: 1.5; box-sizing: border-box;" ></textarea> <div style="display: flex; flex-direction: column; gap: 0.5rem;"> <button id="chatSend" title="Send message" style="width: 44px; height: 44px; padding: 0; background: linear-gradient(135deg, #4f46e5, #6366f1); color: white; border: none; border-radius: 0.5rem; cursor: pointer; font-size: 1.25rem; box-shadow: 0 4px 6px -1px rgba(79, 70, 229, 0.4); transition: transform 0.1s, box-shadow 0.2s; display: flex; align-items: center; justify-content: center;" onmouseover="this.style.transform='translateY(-1px)'; this.style.boxShadow='0 6px 8px -1px rgba(79, 70, 229, 0.5)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 4px 6px -1px rgba(79, 70, 229, 0.4)'" > ↑ </button> <details style="position: relative;"> <summary style="cursor: pointer; color: #94a3b8; font-size: 1.25rem; user-select: none; list-style: none; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; border-radius: 0.5rem; background: #374151;" onmouseover="this.style.background='#4b5563'" onmouseout="this.style.background='#374151'">⋮</summary> <div style="position: absolute; right: 0; top: 100%; margin-top: 0.5rem; background: #1e293b; border: 1px solid #374151; border-radius: 0.5rem; min-width: 300px; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3); z-index: 100;"> <div style="padding: 0.5rem; border-bottom: 1px solid #374151; color: #94a3b8; font-size: 0.75rem; font-weight: 600;">Debug Info</div> <pre id="chatDebug" style="margin: 0; padding: 0.75rem; background: #0f172a; color: #94a3b8; font-size: 0.6875rem; overflow-x: auto; max-height: 300px; font-family: 'SF Mono', Monaco, monospace; line-height: 1.4; white-space: pre-wrap; word-break: break-word;"></pre> </div> </details> </div> </div> <div style="margin-top: 0.5rem; padding-left: 0.25rem;"> <span id="chatStatus" style="color: #94a3b8; font-size: 0.8125rem;"></span> </div> </div> <div id="chatTranscript" style="flex: 1; overflow-y: auto; padding: 1.5rem; background: #0f172a; -webkit-overflow-scrolling: touch;"></div> </div> `; } function setupChatHandlers(container) { const transcript = container.querySelector('#chatTranscript'); const question = container.querySelector('#chatQuestion'); const send = container.querySelector('#chatSend'); const status = container.querySelector('#chatStatus'); const debug = container.querySelector('#chatDebug'); updateDebugInfo(`Chat loaded with ${App.state.messages.length} messages`); function renderTranscript() { transcript.innerHTML = ''; const reversed = [...App.state.messages].reverse(); reversed.forEach((msg, idx) => { const originalIndex = App.state.messages.length - 1 - idx; transcript.appendChild(renderMessage(msg, originalIndex, renderTranscript)); }); transcript.scrollTop = 0; } async function submitMessage() { const q = question.value.trim(); if (!q) return; const timestamp = Date.now(); 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: q, 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' }; App.state.messages.push({ role: 'user', content: q, ts: Date.now() }); App.saveState(App.state); updateDebugInfo(`Sent: ${s.model} (${s.maxTokens} tokens, temp ${s.temperature})`); renderTranscript(); question.value = ''; send.disabled = true; 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' }, body: JSON.stringify(payload) }); resJSON = await res.json(); } catch (err) { resJSON = { error: 'Network error', debug: String(err) }; } send.disabled = false; debug.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); updateDebugInfo(`Error: ${msg}`); renderTranscript(); 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(' · ')}*`; if (usage && model) { App.trackUsage(model, usage); updateDebugInfo(`Tracked: ${usage.total_tokens ?? 0} tokens for ${model}`); } App.state.messages.push({ role: 'assistant', content, ts: Date.now() }); App.saveState(App.state); renderTranscript(); status.textContent = 'Done'; setTimeout(() => status.textContent = '', 1200); } send.addEventListener('click', submitMessage); question.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey && !('ontouchstart' in window)) { e.preventDefault(); submitMessage(); } }); setTimeout(() => question.focus(), 100); renderTranscript(); } // Register with AppItems window.AppItems.push({ title: 'Chat', html: generateChatHTML(), onRender: setupChatHandlers }); // Export to App namespace App.Chat = { getMessages: () => App.state.messages, clearMessages: () => { App.state.messages = []; App.saveState(App.state); if (window.AppOverlay) AppOverlay.close(); } }; console.log('[Chat] Component registered with', App.state.messages.length, 'messages'); })();