๐Ÿ“œ
chat_copy6.js
โ† Back
๐Ÿ“ Javascript โšก Executable Ctrl+S: Save โ€ข Ctrl+R: Run โ€ข Ctrl+F: Find
// chat.js - Simple Chat Component (history kept by default, prune on delete, new session on trash) // Includes Debug Mode, Prompt Preview (selected style only), high z-index modal, 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', // default | fullCode | snippets | chunked fullCodePrompt: '', snippetsPrompt: '', 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; } /* Modal (z-index boosted) */ .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 z-index just in case */ #menu-dropdown { z-index: 500000; } /* Wrapping & layout guards */ .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; } /* Debug pre style */ .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); } // Build a history override array from whatโ€™s visible in the UI 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 isFencedBlock = msg.role === 'assistant' && (s.codeStyle === 'default' || 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' : isFencedBlock ? 'Assistant (fenced)' : '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: dump meta + raw response if (s.debugMode && msg.role === 'assistant') { const pre = document.createElement('pre'); pre.className = 'chat-debug-pre'; const debugObj = { sent: msg.meta?.sent ?? null, // system & payload we sent response: msg.raw ?? msg.content ?? null // full API JSON or string }; pre.textContent = JSON.stringify(debugObj, null, 2); card.appendChild(header); card.appendChild(pre); wrapper.appendChild(card); return wrapper; } // Normal content rendering const content = document.createElement('div'); content.className = 'chat-card__content'; // For default/snippets, visually show triple-backtick blocks const raw = (msg.content || '').replace(/\(END\)\s*$/i, ''); const visibleText = isFencedBlock ? '```\n' + raw + '\n```' : raw; const lines = visibleText.split('\n'); const shouldCollapse = lines.length > 5; if (shouldCollapse) { const previewLines = lines.slice(0, 5).join('\n'); const restLines = lines.slice(5).join('\n'); const preview = document.createElement(isFencedBlock ? 'pre' : 'div'); if (isFencedBlock) preview.className = 'chat-fenced'; preview.textContent = previewLines; const fullContent = document.createElement(isFencedBlock ? 'pre' : 'div'); if (isFencedBlock) fullContent.className = 'chat-fenced'; fullContent.style.display = 'none'; fullContent.textContent = '\n' + restLines; 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 = fullContent.style.display !== 'none'; fullContent.style.display = isExpanded ? 'none' : 'block'; expandBtn.textContent = isExpanded ? 'โ–ผ Show more' : 'โ–ฒ Show less'; }; content.appendChild(preview); content.appendChild(fullContent); content.appendChild(expandBtn); } else { const node = document.createElement(isFencedBlock ? 'pre' : 'div'); if (isFencedBlock) node.className = 'chat-fenced'; node.textContent = visibleText.trim(); content.appendChild(node); } card.appendChild(header); card.appendChild(content); // Continue button for chunked output (if not ended) 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, // include past turns sessionId: App.state.sessionId, history: computeHistoryOverride() // prune based on visible messages }; 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); }; // Prompt preview โ€” only show selected prompt 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); // push user message 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, // keep convo by default sessionId: App.state.sessionId, history: computeHistoryOverride() // prune server history to match UI }; 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, // full API response for debug meta: { sent: { system: systemToSend, payload } }, // what we sent 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(); } }; // TRASH = brand-new conversation (new sessionId) 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(); } // ---------- Register ---------- 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 }); })();