Choose a single AI or compare all four side-by-side.
'
+ ''
+ '
'
+ '' + cfg.name + ''
+ '' + cfg.tag + ''
+ '
'
+ '
'
+ '
'
+ ''
+ ''
+ '
'
+ '
';
resp.appendChild(card);
cards[ai] = card.querySelector('.ai-bub');
});
chatEl.appendChild(block);
scroll();
return cards;
}
function setBub(bub, txt) { if (bub) bub.innerHTML = fmt(txt); }
// ── COPY / SPEAK ─────────────────────────────────────────────────
function copyBub(bubId) {
var el = document.getElementById(bubId); if (!el) return;
navigator.clipboard.writeText(el.innerText || el.textContent).then(function() {
toast('✓ Copied to clipboard');
var card = el.closest('.ai-card');
if (card) {
var btn = card.querySelector('.ma-btn');
if (btn) {
var o = btn.textContent; btn.textContent = '✓ Copied!'; btn.classList.add('copied');
setTimeout(function() { btn.textContent = o; btn.classList.remove('copied'); }, 1800);
}
}
});
}
function speakBub(bubId) {
var el = document.getElementById(bubId); if (!el || !window.speechSynthesis) return;
if (speechSynthesis.speaking) { speechSynthesis.cancel(); toast('⏹ Stopped speaking'); return; }
var utt = new SpeechSynthesisUtterance(el.innerText || el.textContent);
utt.rate = 1.05; utt.pitch = 1;
speechSynthesis.speak(utt);
toast('🔊 Speaking...', 3000);
}
// ── SEND / STOP / REGEN ──────────────────────────────────────────
async function sendSug(t) { if (input) { input.value = t; input.dispatchEvent(new Event('input')); await sendMessage(); } }
async function newChat() {
chatHistory = { claude:[], gpt:[], gemini:[], grok:[] };
totalTokens = 0; updateTokenDisplay();
currentSessionId = null;
if (chatEl) chatEl.innerHTML = '';
showWelcome();
var rb = document.getElementById('regenBtn'); if (rb) rb.style.display = 'none';
var cc = document.getElementById('charCounter'); if (cc) cc.textContent = '';
if (backendOnline) {
var sess = await api('POST', '/api/sessions', {});
if (sess && sess.id) currentSessionId = sess.id;
}
}
function setLoading(v) {
isLoading = v;
var sb = document.getElementById('stopBtn');
var rb = document.getElementById('regenBtn');
var sendB = document.getElementById('sendBtn');
if (sb) sb.classList.toggle('visible', v);
if (sendB) sendB.style.opacity = v ? '0.4' : '1';
if (!v && rb && lastUserMsg) rb.style.display = 'flex';
}
function stopGeneration() {
if (abortController) abortController.abort();
setLoading(false);
toast('⏹ Generation stopped');
}
async function regenerate() {
if (!lastUserMsg || isLoading) return;
if (input) { input.value = lastUserMsg; input.dispatchEvent(new Event('input')); }
await sendMessage();
}
async function sendMessage() {
if (!input) return;
var txt = input.value.trim();
if (!txt || isLoading) return;
lastUserMsg = txt;
abortController = new AbortController();
setLoading(true);
input.value = ''; input.style.height = 'auto';
var cc = document.getElementById('charCounter'); if (cc) cc.textContent = '';
addUserMsg(txt);
var ais = activeAI === 'all' ? ['claude','gpt','gemini','grok'] : [activeAI];
var bubs = addAIBlock(ais);
ais.forEach(function(ai) { chatHistory[ai].push({ role:'user', content:txt }); });
totalTokens += Math.ceil(txt.split(' ').length * 1.3);
if (backendOnline && !currentSessionId) {
var sess = await api('POST', '/api/sessions', {});
if (sess && sess.id) currentSessionId = sess.id;
}
// Capture bubIds now before async operations
var bubIdMap = {};
Object.keys(bubs).forEach(function(ai){ if(bubs[ai]&&bubs[ai].id) bubIdMap[ai]=bubs[ai].id; });
await Promise.all(ais.map(function(ai){ return callAI(ai, bubs[ai], txt, bubIdMap[ai]||''); }));
setLoading(false);
}
async function callAI(ai, bub, userTxt) {
try {
var reply;
if (ai === 'claude') reply = await callClaude(userTxt);
else if (ai === 'gpt') reply = await callGPT(userTxt);
else if (ai === 'grok') reply = await callGrok(userTxt);
else reply = await callGemini(userTxt);
chatHistory[ai].push({ role:'assistant', content:reply });
totalTokens += Math.ceil(reply.split(' ').length * 1.3);
updateTokenDisplay();
setBub(bub, reply);
if (backendOnline && currentSessionId) {
api('POST', '/api/sessions/' + currentSessionId + '/messages', {
messages: [
{ role:'user', content:userTxt },
{ role:'assistant', content:reply, ai:ai },
],
});
}
} catch(e) {
if (e.name === 'AbortError') setBub(bub, '⏹ Generation stopped.');
else setBub(bub, '⚠️ ' + e.message);
}
scroll();
}
// ── AI API CALLS ─────────────────────────────────────────────────
async function callClaude() {
if (backendOnline) {
var data = await api('POST', '/api/chat/claude', { messages:chatHistory.claude.slice(-12), system:getSYS(), sessionId:currentSessionId });
if (!data) throw new Error('Backend offline');
if (data.error) throw new Error(data.error);
return data.reply;
}
var r = await fetch('https://api.anthropic.com/v1/messages', {
signal: abortController && abortController.signal,
method: 'POST', headers: { 'Content-Type':'application/json' },
body: JSON.stringify({ model:'claude-sonnet-4-20250514', max_tokens:1000, system:getSYS(), messages:chatHistory.claude.slice(-12) }),
});
var d = await r.json();
if (d.error) throw new Error('Claude: ' + d.error.message);
return d.content[0].text;
}
async function callGPT() {
if (backendOnline) {
var data = await api('POST', '/api/chat/gpt', { messages:chatHistory.gpt.slice(-12), system:getSYS(), sessionId:currentSessionId });
if (!data) throw new Error('Backend offline');
if (data.error) throw new Error(data.error);
return data.reply;
}
var k = apiKeys.gpt || ((document.getElementById('gptKeyInput')||{}).value||'').trim();
if (!k) return '⚙️ Add your OpenAI API key in Settings → API Keys to enable ChatGPT.';
var r = await fetch('https://api.openai.com/v1/chat/completions', {
signal: abortController && abortController.signal,
method: 'POST', headers: { 'Content-Type':'application/json', 'Authorization':'Bearer '+k },
body: JSON.stringify({ model:'gpt-4o', max_tokens:1000, messages:[{role:'system',content:getSYS()},...chatHistory.gpt.slice(-12)] }),
});
var d = await r.json();
if (d.error) throw new Error('ChatGPT: ' + d.error.message);
return d.choices[0].message.content;
}
async function callGemini() {
if (backendOnline) {
var data = await api('POST', '/api/chat/gemini', { messages:chatHistory.gemini.slice(-12), system:getSYS(), sessionId:currentSessionId });
if (!data) throw new Error('Backend offline');
if (data.error) throw new Error(data.error);
return data.reply;
}
var k = apiKeys.gemini || ((document.getElementById('geminiKeyInput')||{}).value||'').trim();
if (!k) return '⚙️ Add your Google Gemini API key in Settings → API Keys to enable Gemini.';
var msgs = chatHistory.gemini.slice(-12).map(function(m) { return { role:m.role==='assistant'?'model':'user', parts:[{text:m.content}] }; });
var r = await fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key='+k, {
signal: abortController && abortController.signal,
method: 'POST', headers: { 'Content-Type':'application/json' },
body: JSON.stringify({ contents:msgs, systemInstruction:{parts:[{text:getSYS()}]} }),
});
var d = await r.json();
if (d.error) throw new Error('Gemini: ' + d.error.message);
return d.candidates[0].content.parts[0].text;
}
async function callGrok() {
if (backendOnline) {
var data = await api('POST', '/api/chat/grok', { messages:chatHistory.grok.slice(-12), system:getSYS(), sessionId:currentSessionId });
if (!data) throw new Error('Backend offline');
if (data.error) throw new Error(data.error);
return data.reply;
}
var k = apiKeys.grok || ((document.getElementById('grokKeyInput')||{}).value||'').trim();
if (!k) return '⚙️ Add your xAI Grok key in Settings → API Keys. Get one free at console.x.ai';
var r = await fetch('https://api.x.ai/v1/chat/completions', {
signal: abortController && abortController.signal,
method: 'POST', headers: { 'Content-Type':'application/json', 'Authorization':'Bearer '+k },
body: JSON.stringify({ model:'grok-4-1-fast-non-reasoning', max_tokens:1000, messages:[{role:'system',content:getSYS()},...chatHistory.grok.slice(-12)] }),
});
var d = await r.json();
if (d.error) throw new Error('Grok: ' + d.error.message);
return d.choices[0].message.content;
}
// ── TOOLBAR ──────────────────────────────────────────────────────
function toggleSysPrompt() {
var bar = document.getElementById('sysBar'); if (!bar) return;
bar.classList.toggle('visible');
var btn = document.getElementById('sysBtnLabel');
if (btn) btn.textContent = bar.classList.contains('visible') ? '✕ Close Prompt' : '⚙ System Prompt';
}
function clearChat() {
chatHistory = { claude:[], gpt:[], gemini:[], grok:[] };
totalTokens = 0; updateTokenDisplay();
currentSessionId = null;
if (chatEl) chatEl.innerHTML = '';
showWelcome();
var rb = document.getElementById('regenBtn'); if (rb) rb.style.display = 'none';
toast('✓ Chat cleared');
}
function exportChat() {
if (!chatEl) return;
var blocks = chatEl.querySelectorAll('.user-msg,.ai-card');
if (!blocks.length) { toast('Nothing to export yet'); return; }
var out = 'SOLAI Chat Export\n' + '='.repeat(40) + '\n\n';
blocks.forEach(function(b) {
if (b.classList.contains('user-msg')) {
out += 'You:\n' + ((b.querySelector('.user-bub')||{}).innerText||'') + '\n\n';
} else {
var name = ((b.querySelector('.ai-nm')||{}).textContent) || 'AI';
var txt = ((b.querySelector('.ai-bub')||{}).innerText) || '';
out += name + ':\n' + txt + '\n\n';
}
});
out += '='.repeat(40) + '\nExported: ' + new Date().toLocaleString();
var a = document.createElement('a');
a.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(out);
a.download = 'SOLAI-chat-' + Date.now() + '.txt';
a.click();
toast('✓ Chat exported');
}
function copyFullChat() {
if (!chatEl) return;
var blocks = chatEl.querySelectorAll('.user-msg,.ai-card');
if (!blocks.length) { toast('Nothing to copy yet'); return; }
var out = '';
blocks.forEach(function(b) {
if (b.classList.contains('user-msg')) {
out += 'You: ' + ((b.querySelector('.user-bub')||{}).innerText||'') + '\n\n';
} else {
var name = ((b.querySelector('.ai-nm')||{}).textContent)||'AI';
var txt = ((b.querySelector('.ai-bub')||{}).innerText)||'';
out += name + ': ' + txt + '\n\n';
}
});
navigator.clipboard.writeText(out.trim()).then(function() { toast('✓ Chat copied to clipboard'); });
}
function toggleTheme() {
isDarkMode = !isDarkMode;
document.body.classList.toggle('light-mode', !isDarkMode);
var knob = document.querySelector('.tt-knob');
if (knob) knob.style.left = isDarkMode ? '2px' : '18px';
toast(isDarkMode ? '🌙 Dark mode' : '☀ Light mode');
}
function showShortcuts() { var o = document.getElementById('shortcutsOverlay'); if (o) o.classList.add('visible'); }
function hideShortcuts() { var o = document.getElementById('shortcutsOverlay'); if (o) o.classList.remove('visible'); }
document.addEventListener('keydown', function(e) {
var meta = e.metaKey || e.ctrlKey;
if (e.key === 'Escape') { hideShortcuts(); return; }
if (!meta) return;
switch(e.key) {
case 'k': e.preventDefault(); newChat(); break;
case 'l': e.preventDefault(); clearChat(); break;
case 'p': e.preventDefault(); toggleSysPrompt(); break;
case 'e': e.preventDefault(); exportChat(); break;
case 'd': e.preventDefault(); toggleTheme(); break;
case '1': e.preventDefault(); selectAI('all'); break;
case '2': e.preventDefault(); selectAI('claude'); break;
case '3': e.preventDefault(); selectAI('gpt'); break;
case '4': e.preventDefault(); selectAI('gemini'); break;
case '5': e.preventDefault(); selectAI('grok'); break;
case '/': e.preventDefault(); if (input) input.focus(); break;
}
});
// ── SETTINGS ─────────────────────────────────────────────────────
function switchSettTab(btn, id) {
document.querySelectorAll('.sett-tab').forEach(function(t) { t.classList.remove('active'); });
btn.classList.add('active');
document.querySelectorAll('.sett-section').forEach(function(s) { s.style.display = 'none'; });
var sec = document.getElementById(id); if (sec) sec.style.display = 'block';
}
function updateKeyStatus(ai) {
var inp = document.getElementById(ai + 'KeyInput');
var dot = document.getElementById(ai + 'Status');
if (!inp || !dot) return;
dot.classList.toggle('set', inp.value.trim().length > 0);
dot.classList.toggle('unset', inp.value.trim().length === 0);
}
async function saveKeys() {
var gpt = ((document.getElementById('gptKeyInput')||{}).value||'').trim() || null;
var gemini = ((document.getElementById('geminiKeyInput')||{}).value||'').trim() || null;
var grok = ((document.getElementById('grokKeyInput')||{}).value||'').trim() || null;
apiKeys = { gpt:gpt, gemini:gemini, grok:grok };
var data = await api('POST', '/api/keys', { gpt:gpt, gemini:gemini, grok:grok });
if (data && data.ok) toast('✓ API keys saved securely to server');
else toast('✓ API keys saved locally' + (backendOnline ? '' : ' (backend offline)'));
var b = document.querySelector('#sett-api .sett-btn-primary');
if (b) { b.textContent = '✓ Saved!'; setTimeout(function(){ b.textContent = 'Save API Keys'; }, 2000); }
}
async function savePersona(btn) {
var personaTA = document.querySelector('#sett-persona .sett-textarea');
var personaName = document.querySelector('#sett-persona .sett-input');
var sysInput = document.getElementById('sysPromptInput');
var newPrompt = personaTA ? personaTA.value.trim() : '';
var newName = personaName ? personaName.value.trim() : '';
if (newPrompt && sysInput) sysInput.value = newPrompt;
var wt = document.getElementById('welcomeTitle');
if (wt && newName) wt.textContent = 'Ask ' + newName + ' anything.';
if (newPrompt) await api('POST', '/api/brand', { sysPrompt:newPrompt, name:newName || undefined });
toast('✓ Persona saved & applied');
if (btn) { btn.textContent = '✓ Saved!'; setTimeout(function(){ btn.textContent = 'Save Persona Settings'; }, 2000); }
}
async function saveAccount(btn) {
var name = ((document.getElementById('accName')||{}).value||'').trim();
var email = ((document.getElementById('accEmail')||{}).value||'').trim();
if (name && user) { user.name = name; user.email = email || user.email; }
var data = await api('PUT', '/api/auth/me', { name:name, email:email });
toast((data && !data.error) ? '✓ Account details saved' : '✓ Account details saved locally');
if (btn) { btn.textContent = '✓ Saved!'; setTimeout(function(){ btn.textContent = 'Save Changes'; }, 2000); }
}
function updateBrandPreview() {
var n = ((document.getElementById('brandNameInput')||{}).value) || 'SOLAI';
var ic = ((document.getElementById('brandIconInput')||{}).value) || '☀';
var pn = document.getElementById('prevName'); if (pn) pn.textContent = n;
var pd = document.getElementById('prevDot');
if (pd) { pd.textContent = ic; pd.style.background = 'linear-gradient(135deg,' + brandColor + ',' + brandColor2 + ')'; }
}
function setBrandColor(el, c1, c2) {
document.querySelectorAll('.color-swatch').forEach(function(s) { s.classList.remove('active'); });
el.classList.add('active'); brandColor = c1; brandColor2 = c2;
updateBrandPreview();
}
async function applyBrand() {
var n = ((document.getElementById('brandNameInput')||{}).value) || 'SOLAI';
var ic = ((document.getElementById('brandIconInput')||{}).value) || '☀';
document.documentElement.style.setProperty('--brand-color', brandColor);
document.documentElement.style.setProperty('--brand-color2', brandColor2);
var sbn = document.getElementById('sidebarBrandName'); if (sbn) sbn.textContent = n;
var tb = document.getElementById('topbarBrand'); if (tb) tb.textContent = ic + ' ' + n;
document.querySelectorAll('.welcome-sun').forEach(function(w) { w.textContent = ic; w.style.background = 'linear-gradient(135deg,' + brandColor + ',' + brandColor2 + ')'; });
var lnav = document.querySelector('.lnav-logo'); if (lnav) lnav.textContent = ic;
await api('POST', '/api/brand', { name:n, icon:ic, color1:brandColor, color2:brandColor2 });
toast('✓ Branding applied' + (backendOnline ? ' & saved' : ''));
var b = document.querySelector('#sett-brand .sett-btn-primary');
if (b) { b.textContent = '✓ Applied!'; setTimeout(function(){ b.textContent = 'Apply Branding'; }, 2000); }
}
function resetBrand() {
brandColor = '#f5a623'; brandColor2 = '#ff7c4a';
document.documentElement.style.setProperty('--brand-color', '#f5a623');
document.documentElement.style.setProperty('--brand-color2', '#ff7c4a');
var bi = document.getElementById('brandNameInput'); if (bi) bi.value = 'SOLAI';
var bic = document.getElementById('brandIconInput'); if (bic) bic.value = '☀';
var sbn = document.getElementById('sidebarBrandName'); if (sbn) sbn.textContent = 'SOLAI';
var tb = document.getElementById('topbarBrand'); if (tb) tb.textContent = '☀ SOLAI';
updateBrandPreview();
toast('↩ Brand reset to SOLAI');
}
function copyEmbed() {
var code = ((document.getElementById('embedSnippet')||{}).textContent) || '';
if (!code) return;
navigator.clipboard.writeText(code).then(function() {
toast('✓ Embed code copied');
var b = document.querySelector('#sett-embed .sett-btn-primary');
if (b) { b.textContent = '✓ Copied!'; setTimeout(function(){ b.textContent = 'Copy Code'; }, 2000); }
});
}
// ── REVENUE CHART ─────────────────────────────────────────────────
function buildRevChart(dailyData) {
var bars = document.getElementById('revBars');
var labels = document.getElementById('revLabels');
if (!bars || !labels) return;
bars.innerHTML = ''; labels.innerHTML = '';
var months, vals;
if (dailyData && dailyData.length > 0) {
var slice = dailyData.slice(-8);
months = slice.map(function(d) { return d.date.slice(5); });
vals = slice.map(function(d) { return d.count; });
} else {
months = ['Aug','Sep','Oct','Nov','Dec','Jan','Feb','Mar'];
vals = [220,310,480,620,750,890,1100,1247];
}
var max = Math.max.apply(null, vals.concat([1]));
vals.forEach(function(v, i) {
var b = document.createElement('div');
b.className = 'rev-bar'; b.style.height = (v / max * 100) + '%'; b.title = months[i] + ': ' + v;
bars.appendChild(b);
var l = document.createElement('div');
l.className = 'rev-label'; l.textContent = months[i];
labels.appendChild(l);
});
}
// ── MARKDOWN FORMATTER ────────────────────────────────────────────
function fmt(t) {
var s = t.replace(/&/g,'&').replace(//g,'>');
s = s.replace(/```(\w*)\n?([\s\S]*?)```/g, function(_, lang, code) {
return '