๐Ÿ“œ
chat_copy5.js
โ† Back
๐Ÿ“ Javascript โšก Executable Ctrl+S: Save โ€ข Ctrl+R: Run โ€ข Ctrl+F: Find
// chat.js - Simple Chat Component // Adds: Prompt Preview modal, fenced blocks for default/snippets, // proper session isolation (no old context), and "New Chat (Reset Session)" control. 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', // Optional: // fullCodePrompt: '...', // snippetsPrompt: '...', chunkedPrompt: 'You are a coding assistant. When I request a file or document, output ONLY the raw code with NO backticks, NO markdown formatting, NO explanations, and NO comments. Start outputting code immediately. If the document is too long to fit in one response, split it into chunks and continue until the entire document has been delivered. When the full document is complete, write (END) on a new line by itself.' }; // ---------- Utilities ---------- function uuid() { if (crypto?.randomUUID) return crypto.randomUUID(); // Fallback 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); } }; } // Load try { const saved = localStorage.getItem('chatState'); if (saved) { App.state = JSON.parse(saved); } } catch (e) { console.error('Load failed:', e); } ensureState(); // ---------- Styles (component + modal + fenced) ---------- 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; } .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 */ .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: 2000; } .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; } .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; } /* --- Wrapping Fixes --- */ .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-textarea { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; } .chat-modal, .chat-modal__body { min-width: 0; max-width: 100%; } .chat-message--assistant .chat-card { max-width: 100%; overflow: hidden; } `; document.head.appendChild(style); } // ---------- Helpers ---------- function buildSystemPromptFromSettings(s, includeCodeStyle = true) { let systemPrompt = s.system || 'You are a helpful assistant.'; if (includeCodeStyle) { 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; } } return systemPrompt; } 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'), 1500); } 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'), 1500); } }; 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); } // ---------- 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'); if (isRawCodeOutput) { card.style.background = '#0b1220'; card.style.borderColor = '#4f46e5'; card.style.borderWidth = '2px'; } const header = document.createElement('div'); header.className = 'chat-card__header'; const title = document.createElement('div'); title.textContent = msg.role === 'user' ? 'You' : isRawCodeOutput ? 'Code Output' : isFencedBlock ? 'Assistant (fenced)' : 'Assistant'; const headerBtns = document.createElement('div'); headerBtns.style.cssText = 'display: flex; gap: 0.5rem;'; if (msg.role === 'assistant') { const copyBtn = document.createElement('button'); copyBtn.className = 'chat-btn'; copyBtn.textContent = '๐Ÿ“‹'; copyBtn.title = 'Copy'; copyBtn.onclick = (e) => { e.stopPropagation(); let contentToCopy = msg.content.replace(/\(END\)\s*$/i, '').trim(); if (isFencedBlock) contentToCopy = '```\n' + contentToCopy + '\n```'; if (navigator.clipboard?.writeText) { navigator.clipboard.writeText(contentToCopy).then(() => { copyBtn.textContent = 'โœ“'; setTimeout(() => (copyBtn.textContent = '๐Ÿ“‹'), 2000); }); } else { const ta = document.createElement('textarea'); ta.value = contentToCopy; ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); copyBtn.textContent = 'โœ“'; setTimeout(() => (copyBtn.textContent = '๐Ÿ“‹'), 2000); } }; headerBtns.appendChild(copyBtn); } 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); const content = document.createElement('div'); content.className = 'chat-card__content'; if (isRawCodeOutput) content.classList.add('chat-code-output'); // 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 payload = { question: 'Continue outputting the code from where you left off.', model: s2.model, maxTokens: s2.maxTokens, temperature: s2.temperature, system: buildSystemPromptFromSettings(s2, true), includeHistory: true, // CHUNK CONTINUES WITH HISTORY sessionId: App.state.sessionId // <- Session isolation }; 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 || ''; if (!newContent) { statusEl.textContent = 'Warning: API returned empty response'; continueBtn.disabled = false; continueBtn.textContent = 'โ–ถ Continue'; return; } 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="Clear all messages">๐Ÿ—‘</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 = ''), 2000); }); quickCodeStyle.addEventListener('change', () => { App.state.settings.codeStyle = quickCodeStyle.value; App.saveState(App.state); status.textContent = `Style: ${quickCodeStyle.value}`; setTimeout(() => (status.textContent = ''), 2000); if (transcript && transcript._renderFunction) transcript._renderFunction(); }); // HARD reset (localStorage + reload). Will also get a fresh sessionId in init. clearMemory.onclick = () => { if (confirm('Clear all stored memory and reset? This resets settings and messages.')) { localStorage.clear(); location.reload(); } }; // NEW CHAT: clears messages and rotates sessionId WITHOUT touching settings/localStorage (except saving the new session). newChat.onclick = () => { if (!confirm('Start a new chat? This clears visible messages and resets the session context.')) return; App.state.messages = []; App.state.sessionId = newSessionId(); // ๐Ÿ”‘ NEW SESSION so backend wonโ€™t see old context App.saveState(App.state); if (transcript && transcript._renderFunction) transcript._renderFunction(); status.textContent = `New session: ${App.state.sessionId}`; setTimeout(() => (status.textContent = ''), 3000); }; // Prompt preview modal (shows exactly what we send + session info) showPrompt.onclick = (e) => { e.stopPropagation(); const s = App.state.settings; const systemPrompt = buildSystemPromptFromSettings(s, true); const nextPayload = { question: '(your next message)', model: s.model, maxTokens: s.maxTokens, temperature: s.temperature, system: systemPrompt, includeHistory: false, // โœ… we donโ€™t send history by default sessionId: App.state.sessionId }; if (s.forceTemperature) nextPayload.forceTemperature = true; if (s.jsonFormat) nextPayload.response_format = { type: 'json_object' }; if (s.includeArtifacts) nextPayload.includeArtifacts = true; const content = 'โ€” SYSTEM PROMPT โ€”\n' + systemPrompt + '\n\nโ€” ACTIVE SETTINGS โ€”\n' + JSON.stringify({ ...s }, 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; 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: buildSystemPromptFromSettings(s, true), includeHistory: false, // โœ… DO NOT send prior turns by default sessionId: App.state.sessionId // โœ… keep server context separated }; 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, ts: Date.now() }); } else { App.state.messages.push({ role: 'assistant', content: data.answer || '(no response)', ts: Date.now() }); } } catch (err) { App.state.messages.push({ role: 'assistant', content: 'โŒ Network error: ' + err.message, 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('Clear messages in this view? (Session stays the same)')) { App.state.messages = []; App.saveState(App.state); if (transcript && 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 }); })();