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