šŸ“œ
chat_copy9.js
← Back
šŸ“ Javascript ⚔ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// chat.js - Simple Chat Component (history kept, prune on delete, new session on trash) // Default: plain text; Snippets: single fenced block; Chunked/Full: raw code; Debug Mode; Prompt Preview; z-index + wrapping fixes. window.App = window.App || {}; window.AppItems = window.AppItems || []; (() => { // ---------- Defaults ---------- const defaultSettings = { model: 'grok-code-fast-1', maxTokens: 800, temperature: 0.7, system: 'You are a helpful assistant.', codeStyle: 'default', fullCodePrompt: '', snippetsPrompt: "You are a coding assistant. Output the entire response inside a single triple-backtick code block. Do not explain anything outside the code block. Do not break across multiple responses. Complete the entire document inside the code block.", chunkedPrompt: "you are a coding assistant. Output only raw code. Do not explain, do not comment, do not use backticks. If the code is too long for one reply, stop exactly where you must and wait. When I say 'continue,' you will resume exactly where you left off, without repeating or skipping. At the very end of the entire document, output a single line containing exactly (END) and nothing else. You must always finish with (END). Never stop before (END) unless you are waiting for me to say 'continue'.", debugMode: false }; // ---------- Utilities ---------- function uuid() { if (crypto?.randomUUID) return crypto.randomUUID(); return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } function newSessionId() { return `sess_${uuid()}`; } function ensureState() { if (!App.state) { App.state = { sessionId: newSessionId(), messages: [], settings: { ...defaultSettings } }; return; } if (!App.state.sessionId) App.state.sessionId = newSessionId(); if (!App.state.messages) App.state.messages = []; if (!App.state.settings) App.state.settings = { ...defaultSettings }; else App.state.settings = { ...defaultSettings, ...App.state.settings }; } // ---------- Persist ---------- if (!App.saveState) { App.saveState = function (state) { try { localStorage.setItem('chatState', JSON.stringify(state)); } catch (e) { console.error('Save failed:', e); } }; } try { const saved = localStorage.getItem('chatState'); if (saved) App.state = JSON.parse(saved); } catch (e) { console.error('Load failed:', e); } ensureState(); // ---------- Styles ---------- const styleId = 'chat-component-styles'; if (!document.getElementById(styleId)) { const style = document.createElement('style'); style.id = styleId; style.textContent = ` .chat-message { margin: 1rem 0; position: relative; } .chat-card { border: 1px solid #3f3f46; background: #1f2937; border-radius: 0.5rem; padding: 1rem; position: relative; } .chat-message--user .chat-card { background: linear-gradient(135deg, #4f46e5, #6366f1); border-color: #4338ca; color: white; } .chat-message--assistant .chat-card { background: #1f2937; border-color: #3f3f46; } .chat-card__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.75rem; font-size: 0.875rem; font-weight: 600; opacity: 0.9; } .chat-card__content { white-space: pre-wrap; word-break: break-word; line-height: 1.6; } .chat-code-output { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Mono', Monaco, Consolas, monospace; font-size: 0.9rem; } .chat-fenced { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Mono', Monaco, Consolas, monospace; font-size: 0.9rem; background: #0b1220; border: 1px solid #1f2a44; border-radius: 0.5rem; padding: 0.75rem 1rem; color: #e5e7eb; } .chat-btn { font-size: 0.75rem; padding: 0.25rem 0.5rem; border-radius: 0.375rem; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.1); color: currentColor; cursor: pointer; transition: all 0.2s; } .chat-btn:hover { background: rgba(255,255,255,0.2); } .chat-btn--danger { background: #dc2626; color: white; border-color: #b91c1c; } .chat-btn--danger:hover { background: #b91c1c; } .chat-btn[disabled] { opacity: 0.5; cursor: not-allowed; } .chat-textarea { width: 100%; min-height: 60px; max-height: 200px; resize: vertical; padding: 0.875rem; border-radius: 0.5rem; border: 1px solid #3f3f46; background: #0f172a; color: #e5e7eb; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 0.9375rem; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; } .chat-send-btn { width: 44px; height: 44px; background: linear-gradient(135deg, #4f46e5, #6366f1); color: white; border: none; border-radius: 0.5rem; font-size: 1.25rem; cursor: pointer; transition: transform 0.1s, box-shadow 0.2s; box-shadow: 0 4px 6px -1px rgba(79, 70, 229, 0.4); } .chat-send-btn:hover { transform: translateY(-1px); box-shadow: 0 6px 8px -1px rgba(79, 70, 229, 0.5); } .chat-send-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } .chat-modal__backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(2px); display: flex; align-items: center; justify-content: center; z-index: 999999; } .chat-modal { width: min(900px, 92vw); max-height: 80vh; background: #0f172a; border: 1px solid #334155; border-radius: 0.75rem; box-shadow: 0 20px 50px rgba(0,0,0,0.4); display: flex; flex-direction: column; overflow: hidden; z-index: 1000000; } .chat-modal__header { padding: 0.875rem 1rem; background: #111827; border-bottom: 1px solid #334155; display: flex; gap: 0.5rem; align-items: center; justify-content: space-between; } .chat-modal__title { font-weight: 700; font-size: 0.95rem; color: #e5e7eb; } .chat-modal__actions { display: flex; gap: 0.5rem; } .chat-modal__body { padding: 1rem; overflow: auto; } .chat-modal__pre { margin: 0; white-space: pre-wrap; word-break: break-word; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.85rem; line-height: 1.5; color: #e5e7eb; background: #0b1220; border: 1px solid #1f2a44; border-radius: 0.5rem; padding: 1rem; } #menu-dropdown { z-index: 500000; } .chat-message, #transcript, .chat-card, .chat-card__content { min-width: 0; max-width: 100%; box-sizing: border-box; } .chat-card__content, .chat-fenced, .chat-code-output, .chat-modal__pre { white-space: pre-wrap; word-break: break-word; overflow-wrap: anywhere; line-break: anywhere; overflow: auto; } .chat-modal, .chat-modal__body { min-width: 0; max-width: 100%; } .chat-message--assistant .chat-card { max-width: 100%; overflow: hidden; } .chat-debug-pre { background:#0b1220; border:1px solid #334155; border-radius:0.5rem; padding:1rem; max-height:420px; overflow:auto; white-space:pre-wrap; word-break:break-word; font-size:0.8125rem; color:#f1f5f9; } `; document.head.appendChild(style); } // ---------- Helpers ---------- function buildSystemPromptFromSettings(s) { const baseSystem = (s.system ?? '').trim() || 'You are a helpful assistant.'; let codeStylePrompt = ''; if (s.codeStyle === 'fullCode' && s.fullCodePrompt) codeStylePrompt = s.fullCodePrompt; else if (s.codeStyle === 'snippets' && s.snippetsPrompt) codeStylePrompt = s.snippetsPrompt; else if (s.codeStyle === 'chunked' && s.chunkedPrompt) codeStylePrompt = s.chunkedPrompt; return codeStylePrompt ? `${baseSystem}\n\nCODE RESPONSE STYLE: ${codeStylePrompt}` : baseSystem; } function openModal({ title = 'Details', content = '' }) { const existing = document.querySelector('.chat-modal__backdrop'); if (existing) existing.remove(); const backdrop = document.createElement('div'); backdrop.className = 'chat-modal__backdrop'; backdrop.addEventListener('click', (e) => { if (e.target === backdrop) backdrop.remove(); }); const modal = document.createElement('div'); modal.className = 'chat-modal'; modal.addEventListener('click', e => e.stopPropagation()); const header = document.createElement('div'); header.className = 'chat-modal__header'; const hTitle = document.createElement('div'); hTitle.className = 'chat-modal__title'; hTitle.textContent = title; const actions = document.createElement('div'); actions.className = 'chat-modal__actions'; const copyBtn = document.createElement('button'); copyBtn.className = 'chat-btn'; copyBtn.textContent = 'Copy'; copyBtn.onclick = async () => { try { await navigator.clipboard.writeText(content.replace(/\u00a0/g, ' ')); copyBtn.textContent = 'Copied āœ“'; setTimeout(() => (copyBtn.textContent = 'Copy'), 1200); } catch { const ta = document.createElement('textarea'); ta.value = content; ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); copyBtn.textContent = 'Copied āœ“'; setTimeout(() => (copyBtn.textContent = 'Copy'), 1200); } }; const closeBtn = document.createElement('button'); closeBtn.className = 'chat-btn chat-btn--danger'; closeBtn.textContent = 'Close'; closeBtn.onclick = () => backdrop.remove(); actions.appendChild(copyBtn); actions.appendChild(closeBtn); header.appendChild(hTitle); header.appendChild(actions); const body = document.createElement('div'); body.className = 'chat-modal__body'; const pre = document.createElement('pre'); pre.className = 'chat-modal__pre'; pre.textContent = content; body.appendChild(pre); modal.appendChild(header); modal.appendChild(body); backdrop.appendChild(modal); document.body.appendChild(backdrop); } function computeHistoryOverride() { return (App.state.messages || []) .filter(m => m.role === 'user' || m.role === 'assistant') .map(m => ({ role: m.role, content: String(m.content || '') })); } // ---------- Rendering ---------- function renderMessage(msg, index, onDelete) { const wrapper = document.createElement('div'); wrapper.className = `chat-message chat-message--${msg.role}`; const card = document.createElement('div'); card.className = 'chat-card'; const s = App.state.settings; const isRawCodeOutput = msg.role === 'assistant' && (s.codeStyle === 'chunked' || s.codeStyle === 'fullCode'); const isSnippets = msg.role === 'assistant' && (s.codeStyle === 'snippets'); const header = document.createElement('div'); header.className = 'chat-card__header'; const title = document.createElement('div'); title.textContent = msg.role === 'user' ? 'You' : (s.debugMode ? 'Assistant (debug)' : isRawCodeOutput ? 'Code Output' : isSnippets ? 'Assistant (snippets)' : 'Assistant'); const headerBtns = document.createElement('div'); headerBtns.style.cssText = 'display:flex; gap:0.5rem;'; const deleteBtn = document.createElement('button'); deleteBtn.className = 'chat-btn chat-btn--danger'; deleteBtn.textContent = 'Ɨ'; deleteBtn.onclick = () => { if (confirm('Delete this message?')) { App.state.messages.splice(index, 1); App.saveState(App.state); onDelete(); } }; headerBtns.appendChild(deleteBtn); header.appendChild(title); header.appendChild(headerBtns); // DEBUG MODE if (s.debugMode && msg.role === 'assistant') { const pre = document.createElement('pre'); pre.className = 'chat-debug-pre'; const debugObj = { sent: msg.meta?.sent ?? null, response: msg.raw ?? msg.content ?? null }; pre.textContent = JSON.stringify(debugObj, null, 2); card.appendChild(header); card.appendChild(pre); wrapper.appendChild(card); return wrapper; } const content = document.createElement('div'); content.className = 'chat-card__content'; const raw = (msg.content || '').replace(/END\s*$/i, ''); // USER messages: collapsible if (msg.role === 'user') { const lines = raw.split('\n'); const charCount = raw.length; const shouldCollapse = lines.length > 5 || charCount > 300; if (!shouldCollapse) { const node = document.createElement('div'); node.textContent = raw.trim(); content.appendChild(node); } else { const preview = document.createElement('div'); const full = document.createElement('div'); full.style.display = 'none'; preview.textContent = lines.slice(0, 5).join('\n') + (lines.length > 5 ? '\n...' : ''); full.textContent = raw.trim(); const expandBtn = document.createElement('button'); expandBtn.className = 'chat-btn'; expandBtn.textContent = 'ā–¼ Show more'; expandBtn.style.cssText = 'margin-top: 0.5rem; font-size: 0.75rem;'; expandBtn.onclick = () => { const isExpanded = full.style.display !== 'none'; preview.style.display = isExpanded ? 'block' : 'none'; full.style.display = isExpanded ? 'none' : 'block'; expandBtn.textContent = isExpanded ? 'ā–¼ Show more' : 'ā–² Show less'; }; content.appendChild(preview); content.appendChild(full); content.appendChild(expandBtn); } } // DEFAULT: plain text else if (msg.role === 'assistant' && s.codeStyle === 'default') { const node = document.createElement('div'); node.textContent = raw.trim(); content.appendChild(node); } // SNIPPETS: interleaved text and code else if (isSnippets) { const parts = []; let lastIndex = 0; const fenceRegex = /```[\w]*\n([\s\S]*?)```/g; let match; while ((match = fenceRegex.exec(raw)) !== null) { if (match.index > lastIndex) { const textBefore = raw.substring(lastIndex, match.index).trim(); if (textBefore) parts.push({ type: 'text', content: textBefore }); } parts.push({ type: 'code', content: match[1].trim() }); lastIndex = fenceRegex.lastIndex; } if (lastIndex < raw.length) { const textAfter = raw.substring(lastIndex).trim(); if (textAfter) parts.push({ type: 'text', content: textAfter }); } const totalLines = parts.reduce((sum, part) => sum + part.content.split('\n').length, 0); const shouldCollapse = totalLines > 10; if (!shouldCollapse) { parts.forEach((part) => { if (part.type === 'text') { const textDiv = document.createElement('div'); textDiv.style.cssText = 'margin-bottom: 0.75rem; line-height: 1.6;'; textDiv.textContent = part.content; content.appendChild(textDiv); } else { const pre = document.createElement('pre'); pre.className = 'chat-fenced'; pre.style.cssText = 'margin-bottom: 0.75rem;'; pre.textContent = part.content; content.appendChild(pre); } }); } else { const previewContainer = document.createElement('div'); const fullContainer = document.createElement('div'); fullContainer.style.display = 'none'; let previewLines = 0; let previewIndex = 0; for (let i = 0; i < parts.length; i++) { const lineCount = parts[i].content.split('\n').length; if (previewLines + lineCount <= 5) { previewLines += lineCount; previewIndex = i + 1; } else break; } parts.slice(0, Math.max(1, previewIndex)).forEach((part) => { if (part.type === 'text') { const textDiv = document.createElement('div'); textDiv.style.cssText = 'margin-bottom: 0.75rem; line-height: 1.6;'; textDiv.textContent = part.content; previewContainer.appendChild(textDiv); } else { const pre = document.createElement('pre'); pre.className = 'chat-fenced'; pre.style.cssText = 'margin-bottom: 0.75rem;'; const lines = part.content.split('\n'); pre.textContent = lines.slice(0, 3).join('\n') + (lines.length > 3 ? '\n...' : ''); previewContainer.appendChild(pre); } }); parts.forEach((part) => { if (part.type === 'text') { const textDiv = document.createElement('div'); textDiv.style.cssText = 'margin-bottom: 0.75rem; line-height: 1.6;'; textDiv.textContent = part.content; fullContainer.appendChild(textDiv); } else { const pre = document.createElement('pre'); pre.className = 'chat-fenced'; pre.style.cssText = 'margin-bottom: 0.75rem;'; pre.textContent = part.content; fullContainer.appendChild(pre); } }); content.appendChild(previewContainer); content.appendChild(fullContainer); const expandBtn = document.createElement('button'); expandBtn.className = 'chat-btn'; expandBtn.textContent = 'ā–¼ Show more'; expandBtn.style.cssText = 'margin-top: 0.5rem; font-size: 0.75rem;'; expandBtn.onclick = () => { const isExpanded = fullContainer.style.display !== 'none'; previewContainer.style.display = isExpanded ? 'block' : 'none'; fullContainer.style.display = isExpanded ? 'none' : 'block'; expandBtn.textContent = isExpanded ? 'ā–¼ Show more' : 'ā–² Show less'; }; content.appendChild(expandBtn); } } // CHUNKED / FULLCODE else if (isRawCodeOutput) { const lines = raw.split('\n'); const shouldCollapse = lines.length > 5; if (!shouldCollapse) { const codeDiv = document.createElement('div'); codeDiv.className = 'chat-code-output'; codeDiv.textContent = raw.trim(); content.appendChild(codeDiv); } else { const preview = document.createElement('div'); preview.className = 'chat-code-output'; const full = document.createElement('div'); full.className = 'chat-code-output'; full.style.display = 'none'; preview.textContent = lines.slice(0, 5).join('\n'); full.textContent = lines.slice(5).join('\n'); const expandBtn = document.createElement('button'); expandBtn.className = 'chat-btn'; expandBtn.textContent = 'ā–¼ Show more'; expandBtn.style.cssText = 'margin-top: 0.5rem; font-size: 0.75rem;'; expandBtn.onclick = () => { const isExpanded = full.style.display !== 'none'; full.style.display = isExpanded ? 'none' : 'block'; expandBtn.textContent = isExpanded ? 'ā–¼ Show more' : 'ā–² Show less'; }; content.appendChild(preview); content.appendChild(full); content.appendChild(expandBtn); } } card.appendChild(header); card.appendChild(content); // Continue button for chunked if ( App.state.settings.codeStyle === 'chunked' && msg.role === 'assistant' && index === App.state.messages.length - 1 && !/END\s*$/i.test(msg.content || '') ) { const continueBtn = document.createElement('button'); continueBtn.className = 'chat-btn'; continueBtn.textContent = 'ā–¶ Continue'; continueBtn.style.cssText = 'margin-top: 0.75rem; padding: 0.5rem 1rem; background: linear-gradient(135deg, #10b981, #059669); color: white; border: none; font-weight: 600;'; continueBtn.onclick = async () => { const s2 = App.state.settings; const sendBtn = document.querySelector('#send'); const statusEl = document.querySelector('#status'); if (!sendBtn) return; continueBtn.disabled = true; continueBtn.textContent = 'Continuing...'; statusEl.textContent = 'Thinking...'; sendBtn.disabled = true; const systemToSend = buildSystemPromptFromSettings(s2); const payload = { question: 'Continue outputting the code from where you left off.', model: s2.model, maxTokens: s2.maxTokens, temperature: s2.temperature, system: systemToSend, includeHistory: true, sessionId: App.state.sessionId, history: computeHistoryOverride() }; if (s2.forceTemperature) payload.forceTemperature = true; if (s2.jsonFormat) payload.response_format = { type: 'json_object' }; if (s2.includeArtifacts) payload.includeArtifacts = true; try { const res = await fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Session-Id': App.state.sessionId }, body: JSON.stringify(payload) }); const data = await res.json(); if (data.error) { statusEl.textContent = 'Error: ' + data.error; continueBtn.disabled = false; continueBtn.textContent = 'ā–¶ Continue'; } else { const newContent = data.answer || ''; const oldLength = App.state.messages[index].content.length; App.state.messages[index].content += '\n' + newContent; App.saveState(App.state); statusEl.textContent = `Added ${newContent.length} chars (was ${oldLength}, now ${App.state.messages[index].content.length})`; if (newContent.includes('(END)')) { continueBtn.remove(); statusEl.textContent = 'Complete!'; } else { continueBtn.disabled = false; continueBtn.textContent = 'ā–¶ Continue'; } const transcript = document.querySelector('#transcript'); if (transcript && transcript._renderFunction) { transcript._renderFunction(); transcript.scrollTop = transcript.scrollHeight; } } } catch (err) { alert('Network error: ' + err.message); } sendBtn.disabled = false; statusEl.textContent = ''; }; card.appendChild(continueBtn); } wrapper.appendChild(card); return wrapper; } // ---------- HTML ---------- function generateHTML() { return ` <div style="display: flex; flex-direction: column; height: 100%; background: #0f172a; position: relative;"> <div id="transcript" style="flex: 1; overflow-y: auto; padding: 1rem; padding-bottom: 180px;"></div> <div style="position: fixed; bottom: 40px; left: 0; right: 0; padding: 1rem; background: #1e293b; border-top: 1px solid #334155; z-index: 100;"> <div style="display: flex; gap: 0.5rem;"> <textarea id="question" class="chat-textarea" placeholder="Type your message..."></textarea> <div style="display: flex; flex-direction: column; gap: 0.5rem;"> <button id="send" class="chat-send-btn">↑</button> <button id="clear" class="chat-btn chat-btn--danger" style="width: 44px; height: 44px; font-size: 0.875rem;" title="New conversation (new session)">šŸ—‘</button> <button id="menu" class="chat-btn" style="width: 44px; height: 44px; font-size: 1rem; background: #6b7280; border-color: #4b5563;" title="Menu">ā‹®</button> </div> </div> <div id="menu-dropdown" style="display: none; position: absolute; bottom: 100%; right: 1rem; margin-bottom: 0.5rem; background: #1e293b; border: 1px solid #334155; border-radius: 0.5rem; min-width: 260px; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.3); z-index: 1200;"> <div style="padding: 0.75rem; border-bottom: 1px solid #334155;"> <label style="display: block; font-size: 0.75rem; color: #94a3b8; margin-bottom: 0.25rem;">Model</label> <select id="quickModel" style="width: 100%; padding: 0.375rem; border-radius: 0.375rem; border: 1px solid #334155; background: #0f172a; color: #f1f5f9; font-size: 0.8125rem;"> <option value="grok-code-fast-1">grok-code-fast-1</option> <option value="grok-3">grok-3</option> <option value="grok-3-mini">grok-3-mini</option> <option value="deepseek-chat">deepseek-chat</option> <option value="deepseek-reasoner">deepseek-reasoner</option> <option value="gpt-4o">gpt-4o</option> <option value="gpt-4o-mini">gpt-4o-mini</option> <option value="gpt-5">gpt-5</option> <option value="gpt-5-mini">gpt-5-mini</option> <option value="gpt-5-turbo">gpt-5-turbo</option> </select> </div> <div style="padding: 0.75rem; border-bottom: 1px solid #334155;"> <label style="display: block; font-size: 0.75rem; color: #94a3b8; margin-bottom: 0.25rem;">Response Style</label> <select id="quickCodeStyle" style="width: 100%; padding: 0.375rem; border-radius: 0.375rem; border: 1px solid #334155; background: #0f172a; color: #f1f5f9; font-size: 0.8125rem;"> <option value="default">Default</option> <option value="fullCode">Full Code</option> <option value="snippets">Snippets</option> <option value="chunked">Chunked</option> </select> </div> <button id="newChat" style="width: 100%; padding: 0.75rem; text-align: left; background: none; border: none; color: #34d399; font-size: 0.875rem; cursor: pointer; border-top: 1px solid #334155;">New Chat (Reset Session)</button> <button id="showPrompt" style="width: 100%; padding: 0.75rem; text-align: left; background: none; border: none; color: #60a5fa; font-size: 0.875rem; cursor: pointer;">Show Full Prompt</button> <button id="clearMemory" style="width: 100%; padding: 0.75rem; text-align: left; background: none; border: none; color: #f87171; font-size: 0.875rem; cursor: pointer; border-radius: 0 0 0.5rem 0.5rem;">Clear Memory</button> </div> <div id="status" style="margin-top: 0.5rem; color: #94a3b8; font-size: 0.875rem;"></div> </div> </div> `; } // ---------- Handlers ---------- function setupHandlers(container) { const transcript = container.querySelector('#transcript'); const question = container.querySelector('#question'); const send = container.querySelector('#send'); const clear = container.querySelector('#clear'); const menu = container.querySelector('#menu'); const menuDropdown = container.querySelector('#menu-dropdown'); const quickModel = container.querySelector('#quickModel'); const quickCodeStyle = container.querySelector('#quickCodeStyle'); const clearMemory = container.querySelector('#clearMemory'); const showPrompt = container.querySelector('#showPrompt'); const newChat = container.querySelector('#newChat'); const status = container.querySelector('#status'); quickModel.value = App.state.settings.model; quickCodeStyle.value = App.state.settings.codeStyle; menu.onclick = (e) => { e.stopPropagation(); menuDropdown.style.display = menuDropdown.style.display === 'none' ? 'block' : 'none'; }; menuDropdown.addEventListener('click', (e) => e.stopPropagation()); document.addEventListener('click', () => { menuDropdown.style.display = 'none'; }); quickModel.addEventListener('change', () => { App.state.settings.model = quickModel.value; App.saveState(App.state); status.textContent = `Model: ${quickModel.value}`; setTimeout(() => (status.textContent = ''), 1200); }); quickCodeStyle.addEventListener('change', () => { App.state.settings.codeStyle = quickCodeStyle.value; App.saveState(App.state); status.textContent = `Style: ${quickCodeStyle.value}`; setTimeout(() => (status.textContent = ''), 1200); if (transcript && transcript._renderFunction) transcript._renderFunction(); }); clearMemory.onclick = () => { if (confirm('Clear all stored memory and reset? This resets settings and messages.')) { localStorage.clear(); location.reload(); } }; newChat.onclick = () => { if (!confirm('Start a new chat? This clears messages and resets the session context.')) return; App.state.messages = []; App.state.sessionId = newSessionId(); App.saveState(App.state); if (transcript && transcript._renderFunction) transcript._renderFunction(); status.textContent = `New session: ${App.state.sessionId}`; setTimeout(() => (status.textContent = ''), 2000); }; showPrompt.onclick = (e) => { e.stopPropagation(); const s = App.state.settings; let codeStylePrompt = ''; if (s.codeStyle === 'fullCode' && s.fullCodePrompt) codeStylePrompt = s.fullCodePrompt; else if (s.codeStyle === 'snippets' && s.snippetsPrompt) codeStylePrompt = s.snippetsPrompt; else if (s.codeStyle === 'chunked' && s.chunkedPrompt) codeStylePrompt = s.chunkedPrompt; const baseSystem = (s.system ?? '').trim() || 'You are a helpful assistant.'; const systemPrompt = codeStylePrompt ? `${baseSystem}\n\nCODE RESPONSE STYLE: ${codeStylePrompt}` : baseSystem; const activeSettingsSlim = { model: s.model, maxTokens: s.maxTokens, temperature: s.temperature, system: s.system, codeStyle: s.codeStyle, selectedPrompt: codeStylePrompt || '(none)', debugMode: !!s.debugMode }; const nextPayload = { question: '(your next message)', model: s.model, maxTokens: s.maxTokens, temperature: s.temperature, system: systemPrompt, includeHistory: true, sessionId: App.state.sessionId, history: computeHistoryOverride() }; const content = '— SYSTEM PROMPT —\n' + systemPrompt + '\n\n— ACTIVE SETTINGS (selected only) —\n' + JSON.stringify(activeSettingsSlim, null, 2) + `\n\n— SESSION —\n${App.state.sessionId}` + '\n\n— NEXT REQUEST PAYLOAD (Preview) —\n' + JSON.stringify(nextPayload, null, 2); openModal({ title: 'Full Prompt Preview', content }); }; function render() { transcript.innerHTML = ''; App.state.messages.forEach((msg, i) => { transcript.appendChild(renderMessage(msg, i, render)); }); transcript.scrollTop = transcript.scrollHeight; } transcript._renderFunction = render; async function submit() { const q = question.value.trim(); if (!q) return; const s = App.state.settings; const systemToSend = buildSystemPromptFromSettings(s); App.state.messages.push({ role: 'user', content: q, ts: Date.now() }); App.saveState(App.state); render(); question.value = ''; send.disabled = true; status.textContent = 'Thinking...'; const payload = { question: q, model: s.model, maxTokens: s.maxTokens, temperature: s.temperature, system: systemToSend, includeHistory: true, sessionId: App.state.sessionId, history: computeHistoryOverride() }; if (s.forceTemperature) payload.forceTemperature = true; if (s.jsonFormat) payload.response_format = { type: 'json_object' }; if (s.includeArtifacts) payload.includeArtifacts = true; try { const res = await fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Session-Id': App.state.sessionId }, body: JSON.stringify(payload) }); const data = await res.json(); if (data.error) { App.state.messages.push({ role: 'assistant', content: 'āŒ ' + data.error, raw: data, meta: { sent: { system: systemToSend, payload } }, ts: Date.now() }); } else { App.state.messages.push({ role: 'assistant', content: data.answer || '(no response)', raw: data, meta: { sent: { system: systemToSend, payload } }, ts: Date.now() }); } } catch (err) { App.state.messages.push({ role: 'assistant', content: 'āŒ Network error: ' + err.message, raw: { error: String(err) }, meta: { sent: { system: systemToSend, payload } }, ts: Date.now() }); } App.saveState(App.state); render(); send.disabled = false; status.textContent = ''; } send.onclick = submit; question.onkeydown = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submit(); } }; clear.onclick = () => { if (confirm('Start a new conversation? This clears messages and creates a new session.')) { App.state.messages = []; App.state.sessionId = newSessionId(); App.saveState(App.state); transcript._renderFunction && transcript._renderFunction(); } }; render(); question.focus(); } window.AppItems.push({ title: 'Chat', html: generateHTML(), onRender: setupHandlers }); App.Chat = { getMessages: () => App.state.messages, clearMessages: () => { App.state.messages = []; App.saveState(App.state); }, newSession: () => { App.state.sessionId = newSessionId(); App.saveState(App.state); } }; console.log('[Chat] Loaded', { messages: App.state.messages.length, sessionId: App.state.sessionId }); })();