šŸ“œ
chat_copy.js
← Back
šŸ“ Javascript ⚔ Executable Ctrl+S: Save • Ctrl+R: Run • Ctrl+F: Find
// chat.js - Simple Chat Component (with Prompt Preview modal + fenced blocks for default/snippets) 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.' // Optional: // fullCodePrompt: '...', // snippetsPrompt: '...' }; // Initialize state with proper merging if (!App.state) { App.state = { messages: [], settings: { ...defaultSettings } }; } else { if (!App.state.messages) App.state.messages = []; if (!App.state.settings) { App.state.settings = { ...defaultSettings }; } else { 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) { App.state.settings = { ...defaultSettings, ...parsed.settings }; } } } catch (e) { console.error('Load failed:', e); } // Inject CSS (component + modal + fenced blocks) 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; } `; document.head.appendChild(style); } // Helpers function escapeForClipboard(text) { return text.replace(/\u00a0/g, ' '); } function stopEvent(e) { e.preventDefault(); e.stopPropagation(); } function openModal({ title = 'Details', content = '' }) { // Ensure only one modal at a time 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.title = 'Copy to clipboard'; copyBtn.onclick = async (e) => { stopEvent(e); try { await navigator.clipboard.writeText(escapeForClipboard(content)); 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 = (e) => { stopEvent(e); 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 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; } // 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'; const s = App.state.settings || defaultSettings; // Determine presentation style: const isRawCodeOutput = msg.role === 'assistant' && (s.codeStyle === 'chunked' || s.codeStyle === 'fullCode'); const isFencedBlock = msg.role === 'assistant' && (s.codeStyle === 'default' || s.codeStyle === 'snippets'); // Special styling for raw code output 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;'; // Add copy button for assistant messages 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; contentToCopy = contentToCopy.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); // Build content area const content = document.createElement('div'); content.className = 'chat-card__content'; if (isRawCodeOutput) content.classList.add('chat-code-output'); const raw = msg.content.replace(/\(END\)\s*$/i, ''); const visibleText = isFencedBlock ? '```\n' + raw + '\n```' : raw; // Collapsing logic (based on visible lines) 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); // Add Continue button ONLY for chunked code that hasn't ended if ( s.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; 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 = buildSystemPromptFromSettings(s2, true); const payload = { question: continuePrompt, model: s2.model, maxTokens: s2.maxTokens, temperature: s2.temperature, system: systemPrompt, includeHistory: true }; 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' }, 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: 240px; 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="showPrompt" style="width: 100%; padding: 0.75rem; text-align: left; background: none; border: none; color: #60a5fa; font-size: 0.875rem; cursor: pointer; border-top: 1px solid #334155;" > 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> `; } // 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 showPrompt = container.querySelector('#showPrompt'); 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 = (e) => { e.stopPropagation(); menuDropdown.style.display = menuDropdown.style.display === 'none' ? 'block' : 'none'; }; // Keep menu open when clicking inside menuDropdown.addEventListener('click', (e) => e.stopPropagation()); // Close menu when clicking outside document.addEventListener('click', () => { menuDropdown.style.display = 'none'; }); // Quick model change 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 quickCodeStyle.addEventListener('change', () => { App.state.settings.codeStyle = quickCodeStyle.value; App.saveState(App.state); status.textContent = `Style: ${quickCodeStyle.value}`; setTimeout(() => (status.textContent = ''), 2000); // Re-render so the fence styling applies immediately if (transcript && transcript._renderFunction) transcript._renderFunction(); }); // Clear memory (localStorage) clearMemory.onclick = () => { if ( confirm( 'Clear all stored memory? This will reset settings and clear messages.' ) ) { localStorage.clear(); location.reload(); } }; // Show Full Prompt modal showPrompt.onclick = (e) => { e.stopPropagation(); const s = App.state.settings; const systemPrompt = buildSystemPromptFromSettings(s, true); const payloadPreview = { question: '(your next message)', model: s.model, maxTokens: s.maxTokens, temperature: s.temperature, system: systemPrompt, ...(s.forceTemperature ? { forceTemperature: true } : {}), ...(s.jsonFormat ? { response_format: { type: 'json_object' } } : {}), ...(s.includeArtifacts ? { includeArtifacts: true } : {}) }; const content = '— SYSTEM PROMPT SENT —\n' + systemPrompt + '\n\n— ACTIVE SETTINGS —\n' + JSON.stringify( { model: s.model, temperature: s.temperature, maxTokens: s.maxTokens, codeStyle: s.codeStyle }, null, 2 ) + '\n\n— NEXT REQUEST PAYLOAD (Preview) —\n' + JSON.stringify(payloadPreview, 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...'; let systemPrompt = buildSystemPromptFromSettings(s, true); 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'); })();