// 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');
})();