📜
chat_copy3.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 || []; (() => { // Initialize state if (!App.state) { App.state = { messages: [], settings: { 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.' } }; } else if (!App.state.settings) { // State exists but no settings - create default settings App.state.settings = { 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.' }; } // If App.state.settings already exists (from settings.js), use it as-is // 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) Object.assign(App.state.settings, 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 chunked code response const isChunkedCode = msg.role === 'assistant' && App.state.settings?.codeStyle === 'chunked'; // Special styling for chunked code if (isChunkedCode) { 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' : (isChunkedCode ? 'Code Output' : 'Assistant'); const headerBtns = document.createElement('div'); headerBtns.style.cssText = 'display: flex; gap: 0.5rem;'; // Add copy button for chunked code if (isChunkedCode) { const copyBtn = document.createElement('button'); copyBtn.className = 'chat-btn'; copyBtn.textContent = '📋'; copyBtn.title = 'Copy code'; copyBtn.onclick = (e) => { e.stopPropagation(); // Remove (END) marker if present before copying 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 { // Fallback 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); // Split content into lines const lines = msg.content.split('\n'); const shouldCollapse = lines.length > 5; const content = document.createElement('div'); content.className = 'chat-card__content'; if (isChunkedCode) content.classList.add('chat-code-output'); if (shouldCollapse) { // Show first 5 lines (hide (END) if it's in preview) const previewLines = lines.slice(0, 5).map(line => line.replace(/\(END\)\s*$/i, '')).join('\n'); const preview = document.createElement('div'); preview.textContent = previewLines; // Hidden rest of content (hide (END) if present) 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; // Expand/collapse button 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 { // Hide (END) marker from display const cleanContent = msg.content.replace(/\(END\)\s*$/i, '').trim(); content.textContent = cleanContent; } card.appendChild(header); card.appendChild(content); // Add Continue button for chunked code that hasn't ended if (isChunkedCode && 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; // Better continue prompt - tell it explicitly to continue outputting code const continuePrompt = 'Continue outputting the code from where you left off.'; // Disable continue button continueBtn.disabled = true; continueBtn.textContent = 'Continuing...'; statusEl.textContent = 'Thinking...'; sendBtn.disabled = true; // Build system prompt 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 // Make sure API includes session history }; 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) { // Show error but don't append statusEl.textContent = 'Error: ' + data.error; continueBtn.disabled = false; continueBtn.textContent = '▶ Continue'; } else { // APPEND to existing message instead of creating new one const newContent = data.answer || ''; if (!newContent) { statusEl.textContent = 'Warning: API returned empty response'; continueBtn.disabled = false; continueBtn.textContent = '▶ Continue'; return; } // Update the message in App.state 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})`; // Check if this chunk has (END) marker if (newContent.includes('(END)')) { // Remove the continue button since we're done continueBtn.remove(); statusEl.textContent = 'Complete!'; } else { // Re-enable continue button for next chunk continueBtn.disabled = false; continueBtn.textContent = '▶ Continue'; } // Force full re-render to show updated content const transcript = document.querySelector('#transcript'); if (transcript && transcript._renderFunction) { transcript._renderFunction(); // Scroll to bottom to show new content 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> </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 settings if (App.state.settings) { quickModel.value = App.state.settings.model || 'grok-code-fast-1'; quickCodeStyle.value = App.state.settings.codeStyle || 'default'; } // 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 quickModel.addEventListener('change', () => { App.state.settings.model = quickModel.value; App.saveState(App.state); status.textContent = `Model changed to ${quickModel.value}`; setTimeout(() => status.textContent = '', 2000); }); // Quick code style change quickCodeStyle.addEventListener('change', () => { App.state.settings.codeStyle = quickCodeStyle.value; App.saveState(App.state); status.textContent = `Response style changed to ${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; } // Store render function for continue button to access transcript._renderFunction = render; async function submit() { const q = question.value.trim(); if (!q) return; const s = App.state.settings; console.log('Full App.state.settings:', s); console.log('Model being used:', s.model); App.state.messages.push({ role: 'user', content: q, ts: Date.now() }); App.saveState(App.state); render(); question.value = ''; send.disabled = true; status.textContent = 'Thinking...'; // Build system prompt with code style 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 }; // Add optional settings if they exist if (s.forceTemperature) payload.forceTemperature = true; if (s.jsonFormat) payload.response_format = { type: 'json_object' }; if (s.includeArtifacts) payload.includeArtifacts = true; console.log('Payload being sent:', payload); try { const res = await fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await res.json(); console.log('Response from API:', data); 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'); })();