📜
chat_copy4.js
Back
📝 Javascript ⚡ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// chat.js - Simple Chat Component window.App = window.App || {}; window.AppItems = window.AppItems || []; (() => { // Define default settings const defaultSettings = { model: 'grok-code-fast-1', maxTokens: 800, temperature: 0.7, system: 'You are a helpful assistant.', codeStyle: 'default', 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.' }; // Initialize state with proper merging if (!App.state) { App.state = { messages: [], settings: { ...defaultSettings } }; } else { // State exists - ensure messages array exists if (!App.state.messages) { App.state.messages = []; } // Merge settings: keep existing settings, fill in missing defaults if (!App.state.settings) { App.state.settings = { ...defaultSettings }; } else { // Fill in any missing settings with defaults App.state.settings = { ...defaultSettings, ...App.state.settings }; } } // Save/load from localStorage 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) { const parsed = JSON.parse(saved); if (parsed.messages) App.state.messages = parsed.messages; if (parsed.settings) { // Merge saved settings with defaults (saved takes precedence) App.state.settings = { ...defaultSettings, ...parsed.settings }; } } } catch (e) { console.error('Load failed:', e); } // Inject CSS 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-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; } `; document.head.appendChild(style); } // Render a message 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'; // Check if this is a code response (chunked or fullCode) const isCodeOutput = msg.role === 'assistant' && (App.state.settings?.codeStyle === 'chunked' || App.state.settings?.codeStyle === 'fullCode'); // Special styling for code output if (isCodeOutput) { 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' : (isCodeOutput ? 'Code Output' : 'Assistant'); const headerBtns = document.createElement('div'); headerBtns.style.cssText = 'display: flex; gap: 0.5rem;'; // Add copy button for code output if (isCodeOutput) { const copyBtn = document.createElement('button'); copyBtn.className = 'chat-btn'; copyBtn.textContent = '📋'; copyBtn.title = 'Copy code'; copyBtn.onclick = (e) => { e.stopPropagation(); const cleanContent = msg.content.replace(/\(END\)\s*$/i, '').trim(); if (navigator.clipboard?.writeText) { navigator.clipboard.writeText(cleanContent).then(() => { copyBtn.textContent = '✓'; setTimeout(() => copyBtn.textContent = '📋', 2000); }); } else { const ta = document.createElement('textarea'); ta.value = cleanContent; 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 lines = msg.content.split('\n'); const shouldCollapse = lines.length > 5; const content = document.createElement('div'); content.className = 'chat-card__content'; if (isCodeOutput) content.classList.add('chat-code-output'); if (shouldCollapse) { const previewLines = lines.slice(0, 5).map(line => line.replace(/\(END\)\s*$/i, '')).join('\n'); const preview = document.createElement('div'); preview.textContent = previewLines; const restLines = lines.slice(5).map(line => line.replace(/\(END\)\s*$/i, '')).join('\n'); const fullContent = document.createElement('div'); 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 cleanContent = msg.content.replace(/\(END\)\s*$/i, '').trim(); content.textContent = cleanContent; } card.appendChild(header); card.appendChild(content); // Add Continue button ONLY for chunked code that hasn't ended if (App.state.settings?.codeStyle === 'chunked' && isCodeOutput && index === App.state.messages.length - 1) { 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 s = App.state.settings; const questionEl = document.querySelector('#question'); const sendBtn = document.querySelector('#send'); const statusEl = document.querySelector('#status'); if (!sendBtn) return; const continuePrompt = 'Continue outputting the code from where you left off.'; continueBtn.disabled = true; continueBtn.textContent = 'Continuing...'; statusEl.textContent = 'Thinking...'; sendBtn.disabled = true; let systemPrompt = s.system || 'You are a helpful assistant.'; if (s.codeStyle === 'chunked' && s.chunkedPrompt) { systemPrompt += '\n\nCODE RESPONSE STYLE: ' + s.chunkedPrompt; } const payload = { question: continuePrompt, model: s.model, maxTokens: s.maxTokens, temperature: s.temperature, system: systemPrompt, includeHistory: true }; 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' }, 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 template 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: 200px; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.3); z-index: 200;"> <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="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;" onmouseover="this.style.background='#374151'" onmouseout="this.style.background='none'"> Clear Memory </button> </div> <div id="status" style="margin-top: 0.5rem; color: #94a3b8; font-size: 0.875rem;"></div> </div> </div> `; } // Setup 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 status = container.querySelector('#status'); // Set initial values from current settings quickModel.value = App.state.settings.model; quickCodeStyle.value = App.state.settings.codeStyle; // Toggle menu menu.onclick = () => { menuDropdown.style.display = menuDropdown.style.display === 'none' ? 'block' : 'none'; }; // Close menu when clicking outside document.addEventListener('click', (e) => { if (!menu.contains(e.target) && !menuDropdown.contains(e.target)) { menuDropdown.style.display = 'none'; } }); // Quick model change - updates App.state which is shared quickModel.addEventListener('change', () => { App.state.settings.model = quickModel.value; App.saveState(App.state); status.textContent = `Model: ${quickModel.value}`; setTimeout(() => status.textContent = '', 2000); }); // Quick code style change - updates App.state which is shared quickCodeStyle.addEventListener('change', () => { App.state.settings.codeStyle = quickCodeStyle.value; App.saveState(App.state); status.textContent = `Style: ${quickCodeStyle.value}`; setTimeout(() => status.textContent = '', 2000); }); // Clear memory (localStorage) clearMemory.onclick = () => { if (confirm('Clear all stored memory? This will reset settings and clear messages.')) { localStorage.clear(); location.reload(); } }; 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...'; let systemPrompt = s.system || 'You are a helpful assistant.'; 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 }; 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' }, 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 all messages? This cannot be undone.')) { App.state.messages = []; App.saveState(App.state); render(); } }; render(); question.focus(); } // Register component window.AppItems.push({ title: 'Chat', html: generateHTML(), onRender: setupHandlers }); App.Chat = { getMessages: () => App.state.messages, clearMessages: () => { App.state.messages = []; App.saveState(App.state); } }; console.log('[Chat] Loaded with', App.state.messages.length, 'messages'); })();