'use strict'; // ══ State ════════════════════════════════════════════════════════════════════ let currentSessionId = genId(); let currentProvider = 'groq'; let currentModel = 'llama-3.3-70b-versatile'; let currentModelLabel = 'Groq · Llama 3.3'; let jurisdictionOverride = null; let isStreaming = false; let allModels = {}; // full catalog from /api/models let settings = {}; // from /api/settings // ══ Bootstrap ════════════════════════════════════════════════════════════════ document.addEventListener('DOMContentLoaded', async () => { try { const [modelsData, settingsData] = await Promise.all([ api('/api/models'), api('/api/settings') ]); allModels = modelsData; settings = settingsData; currentProvider = settings.provider || 'groq'; currentModel = settings.model || firstModel(currentProvider); jurisdictionOverride = settings.jurisdiction_override || null; syncModelLabel(); renderModelDropdown(); await refreshSessions(); } catch (e) { console.error('Init failed:', e); } }); // ══ Helpers ══════════════════════════════════════════════════════════════════ async function api(url, opts = {}) { const resp = await fetch(url, opts); if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`); return resp.json(); } function genId() { return 'sess_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); } function firstModel(provider) { const active = (settings.active_models || {})[provider] || []; if (active.length) return active[0]; return (allModels[provider] || [])[0]?.id || null; } function providerLabel(p) { return { groq: 'Groq', openrouter: 'OpenRouter', ollama: 'Ollama' }[p] || p; } function shortModelName(id, name) { if (name) return name.split(' ').slice(0, 3).join(' '); return id.split('/').pop().split(':')[0]; } function getActiveModels() { const active = settings.active_models || {}; const result = {}; for (const [p, catalog] of Object.entries(allModels)) { const allowed = new Set(active[p] || catalog.map(m => m.id)); result[p] = catalog.filter(m => allowed.has(m.id)); } return result; } function syncModelLabel() { const catalog = (allModels[currentProvider] || []); const m = catalog.find(m => m.id === currentModel); currentModelLabel = providerLabel(currentProvider) + ' · ' + shortModelName(currentModel, m?.name); const el = document.getElementById('model-label'); if (el) el.textContent = currentModelLabel; } // ══ Model Dropdown ═══════════════════════════════════════════════════════════ function renderModelDropdown() { const dd = document.getElementById('model-dropdown'); if (!dd) return; dd.innerHTML = ''; const activeModels = getActiveModels(); let hasAny = false; for (const [prov, models] of Object.entries(activeModels)) { if (!models.length) continue; hasAny = true; const lbl = document.createElement('div'); lbl.className = 'model-group-label'; lbl.textContent = providerLabel(prov); dd.appendChild(lbl); models.forEach(m => { const btn = document.createElement('button'); btn.className = 'model-option' + (prov === currentProvider && m.id === currentModel ? ' selected' : ''); btn.textContent = m.name; btn.onclick = e => { e.stopPropagation(); selectModel(prov, m.id, m.name); }; dd.appendChild(btn); }); const sep = document.createElement('hr'); sep.className = 'model-separator'; dd.appendChild(sep); } if (!hasAny) { dd.innerHTML = '
No active models. Enable some in Settings → Models.
'; } } function selectModel(provider, modelId, modelName) { currentProvider = provider; currentModel = modelId; currentModelLabel = providerLabel(provider) + ' · ' + shortModelName(modelId, modelName); document.getElementById('model-label').textContent = currentModelLabel; renderModelDropdown(); document.getElementById('model-dropdown').classList.add('hidden'); } function toggleModelDropdown(e) { e.stopPropagation(); document.getElementById('model-dropdown').classList.toggle('hidden'); } document.addEventListener('click', () => { document.getElementById('model-dropdown')?.classList.add('hidden'); }); // ══ Sessions ═════════════════════════════════════════════════════════════════ async function refreshSessions() { try { const sessions = await api('/api/sessions'); renderSessions(sessions); } catch (e) { /* silent */ } } function renderSessions(sessions) { const el = document.getElementById('session-list'); if (!el) return; el.innerHTML = ''; if (!sessions || !sessions.length) { el.innerHTML = '
No conversations yet
'; return; } sessions.forEach(s => { const item = document.createElement('div'); item.className = 'session-item' + (s.session_id === currentSessionId ? ' active' : ''); item.dataset.id = s.session_id; const name = document.createElement('span'); name.className = 'session-name'; name.textContent = s.name || s.preview || s.session_id.slice(0, 10); name.title = name.textContent; const del = document.createElement('button'); del.className = 'session-del'; del.textContent = '×'; del.title = 'Delete'; del.onclick = async ex => { ex.stopPropagation(); await fetch(`/api/sessions/${s.session_id}`, { method: 'DELETE' }); await refreshSessions(); if (s.session_id === currentSessionId) newChat(); }; item.onclick = () => loadSession(s.session_id); item.appendChild(name); item.appendChild(del); el.appendChild(item); }); } async function loadSession(id) { currentSessionId = id; document.querySelectorAll('.session-item').forEach(el => el.classList.toggle('active', el.dataset.id === id)); clearMessages(); hideEmpty(); try { const data = await api(`/api/sessions/${id}`); const msgs = data.messages || []; let lastUserQuestion = ''; msgs.forEach(msg => { if (msg.role === 'user') { appendUserBubble(msg.content); lastUserQuestion = msg.content; } else { const wrap = appendAIBubble(); const contentEl = wrap.querySelector('.msg-ai-content'); // Remove cursor before setting text contentEl.querySelector('.cursor')?.remove(); // Parse JURISDICTION tag if present in history message let content = msg.content || ''; let msgJur = 'Both'; const jurMatch = content.match(/JURISDICTION:\s*(India|UAE|Both|General)/i); if (jurMatch) { msgJur = jurMatch[1]; content = content.replace(/JURISDICTION:\s*(India|UAE|Both|General)/i, '').trim(); } if (typeof marked !== 'undefined') { contentEl.innerHTML = marked.parse(content); } else { contentEl.textContent = content; } const outer = wrap.querySelector('.msg-ai'); if (outer) outer.dataset.question = lastUserQuestion; const sources = Array.isArray(msg.sources) ? msg.sources : []; const confidence = sources.length ? 'GROUNDED' : 'SYNTHESIZED'; renderMetaBar(outer, { sources, confidence, jurisdiction: msgJur }); } }); scrollBottom(); } catch (e) { console.error('Load session error:', e); } } function newChat() { currentSessionId = genId(); clearMessages(); showEmpty(); document.querySelectorAll('.session-item').forEach(el => el.classList.remove('active')); } // ══ Messaging ════════════════════════════════════════════════════════════════ function handleKey(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } } function useSuggestion(btn) { document.getElementById('query-input').value = btn.textContent; sendMessage(); } async function sendMessage() { if (isStreaming) return; const input = document.getElementById('query-input'); const question = input.value.trim(); if (!question) return; input.value = ''; autoResize(input); hideEmpty(); isStreaming = true; setDisabled(true); // Append user bubble appendUserBubble(question); // Append AI bubble with thinking indicator const aiWrap = appendAIBubble(); const outerEl = aiWrap.querySelector('.msg-ai'); if (outerEl) outerEl.dataset.question = question; const contentEl = aiWrap.querySelector('.msg-ai-content'); const thinkEl = appendThinking(contentEl); let thinkGone = false; const removeThink = () => { if (!thinkGone) { thinkEl?.remove(); thinkGone = true; } }; const updateStatus = (text) => { const s = contentEl.querySelector('.status-text'); if (s) s.textContent = text; }; // absolute safety watchdog (100s) const watchdog = setTimeout(() => { if (isStreaming) { removeThink(); if (!fullAnswer) { contentEl.querySelector('.cursor')?.remove(); contentEl.textContent = '⚠ Interaction timed out (100s). This could be a connection issue or high server load.'; } finishStreaming(); } }, 100000); let meta = null; let fullAnswer = ''; let sessionName = question.slice(0, 60); try { const resp = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question, session_id: currentSessionId, provider: currentProvider, model: currentModel, jurisdiction_override: jurisdictionOverride }) }); if (!resp.ok) { clearTimeout(watchdog); removeThink(); const err = await resp.text(); contentEl.querySelector('.cursor')?.remove(); contentEl.textContent = '⚠ Server error: ' + resp.status + ' - ' + err; return finishStreaming(); } const reader = resp.body.getReader(); const decoder = new TextDecoder(); let buf = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); // SSE lines are separated by \n or \r\n let lines = buf.split(/\r?\n/); buf = lines.pop(); // Keep partial line let evType = null; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith(':')) continue; // Ignore empty lines and pings if (trimmed.startsWith('event:')) { evType = trimmed.slice(6).trim(); } else if (trimmed.startsWith('data:')) { const dataStr = trimmed.slice(5).trim(); let data; try { data = JSON.parse(dataStr); } catch(e) { continue; } switch (evType) { case 'sources': meta = data; updateStatus('Synthesizing expert legal response...'); break; case 'token': removeThink(); if (data.content) { appendToken(contentEl, data.content); fullAnswer += data.content; scrollBottom(); } break; case 'done': removeThink(); sessionName = data.session_name || sessionName; if (meta) Object.assign(meta, { session_name: sessionName }); break; case 'error': removeThink(); if (!fullAnswer) { contentEl.querySelector('.cursor')?.remove(); contentEl.textContent = '⚠ ' + (data.content || 'Unknown error'); } break; } evType = null; // reset for next data block } } } } catch (e) { removeThink(); if (!fullAnswer) { contentEl.querySelector('.cursor')?.remove(); contentEl.textContent = '⚠ Network error: ' + e.message; } } // Finalize bubble clearTimeout(watchdog); contentEl.querySelector('.cursor')?.remove(); finalizeAI(aiWrap, meta); await refreshSessions(); finishStreaming(); input.focus(); } function finishStreaming() { isStreaming = false; setDisabled(false); } // ══ DOM Helpers ══════════════════════════════════════════════════════════════ function appendUserBubble(text) { const wrap = document.createElement('div'); wrap.className = 'message-wrap'; const outer = document.createElement('div'); outer.className = 'msg-user'; const bub = document.createElement('div'); bub.className = 'msg-user-bubble'; bub.textContent = text; outer.appendChild(bub); const actions = document.createElement('div'); actions.className = 'msg-user-actions'; const editBtn = document.createElement('button'); editBtn.className = 'icon-action-btn'; editBtn.title = 'Edit'; editBtn.innerHTML = ``; editBtn.onclick = () => { const input = document.getElementById('query-input'); input.value = text; input.focus(); autoResize(input); }; actions.appendChild(editBtn); outer.appendChild(actions); wrap.appendChild(outer); document.getElementById('messages').appendChild(wrap); scrollBottom(); return wrap; } function appendAIBubble() { const wrap = document.createElement('div'); wrap.className = 'message-wrap'; const outer = document.createElement('div'); outer.className = 'msg-ai'; const content = document.createElement('div'); content.className = 'msg-ai-content'; const cursor = document.createElement('span'); cursor.className = 'cursor'; content.appendChild(cursor); outer.appendChild(content); wrap.appendChild(outer); document.getElementById('messages').appendChild(wrap); return wrap; } function appendThinking(parent) { const el = document.createElement('div'); el.className = 'thinking'; el.innerHTML = `
Searching legal archives...
`; parent.prepend(el); return el; } function appendToken(contentEl, token) { const cursor = contentEl.querySelector('.cursor'); const node = document.createTextNode(token); cursor ? contentEl.insertBefore(node, cursor) : contentEl.appendChild(node); } function finalizeAI(wrap, meta) { const contentEl = wrap.querySelector('.msg-ai-content'); let content = contentEl ? contentEl.textContent : ''; let msgJur = meta?.jurisdiction || 'Both'; // Parse JURISDICTION tag if present in final text const jurMatch = content.match(/JURISDICTION:\s*(India|UAE|Both|General)/i); if (jurMatch) { msgJur = jurMatch[1]; content = content.replace(/JURISDICTION:\s*(India|UAE|Both|General)/i, '').trim(); } if (contentEl) { if (typeof marked !== 'undefined') { contentEl.innerHTML = marked.parse(content); } else { contentEl.textContent = content; } } if (!meta) return; const outer = wrap.querySelector('.msg-ai'); if (outer) { meta.jurisdiction = msgJur; renderMetaBar(outer, meta); } } function renderMetaBar(outer, meta) { const confidence = meta.confidence || 'GROUNDED'; const jurisdiction = meta.jurisdiction || 'Both'; const sources = meta.sources || []; const isGeneral = (confidence === 'GENERAL' || confidence === 'SYNTHESIZED'); const bar = document.createElement('div'); bar.className = 'msg-meta'; // Confidence badge const confMap = { GROUNDED: ['badge-grounded', '✓ Verified Sources'], PARTIAL: ['badge-partial', '✦ Assisted Research'], SYNTHESIZED: ['badge-general', '🗎 Independent Analysis'], GENERAL: ['badge-general', '🗎 Independent Analysis'] }; const [cls, lbl] = confMap[confidence] || confMap.PARTIAL; const confBadge = document.createElement('span'); confBadge.className = `badge ${cls}`; confBadge.textContent = lbl; bar.appendChild(confBadge); // Jurisdiction badge const jurIcon = { India: '🇮🇳', UAE: '🇦🇪', Both: '🌐', General: '🌐' }[jurisdiction] || '🌐'; const jurBadge = document.createElement('span'); jurBadge.className = 'badge-jur'; jurBadge.textContent = `${jurIcon} ${jurisdiction}`; bar.appendChild(jurBadge); // Action buttons group (Copy + Retry) — icon-only ghost const actionGroup = document.createElement('div'); actionGroup.className = 'msg-ai-actions'; const copyBtn = document.createElement('button'); copyBtn.className = 'icon-action-btn'; copyBtn.title = 'Copy'; copyBtn.innerHTML = ``; copyBtn.onclick = () => { const textEl = outer.querySelector('.msg-ai-content'); const textToCopy = textEl ? textEl.innerText : ''; navigator.clipboard.writeText(textToCopy).then(() => { copyBtn.innerHTML = ``; copyBtn.style.color = 'var(--green)'; setTimeout(() => { copyBtn.innerHTML = ``; copyBtn.style.color = ''; }, 2000); }); }; actionGroup.appendChild(copyBtn); const retryBtn = document.createElement('button'); retryBtn.className = 'icon-action-btn'; retryBtn.title = 'Retry'; retryBtn.innerHTML = ``; retryBtn.onclick = () => { const question = outer.dataset.question || ''; if (question) { document.getElementById('query-input').value = question; sendMessage(); } }; actionGroup.appendChild(retryBtn); bar.appendChild(actionGroup); outer.appendChild(bar); // Disclaimer (Synthesized only) if (isGeneral) { const btn = document.createElement('button'); btn.className = 'disclaimer-btn'; btn.title = 'Disclaimer'; btn.textContent = 'ⓘ'; bar.appendChild(btn); const box = document.createElement('div'); box.className = 'disclaimer-box'; box.textContent = 'This response is synthesized from the AI model\'s general knowledge, rather than being retrieved from LexRAG\'s verified document base. It may not reflect the most current statutory provisions. Always consult a qualified legal professional before acting.'; btn.onclick = () => box.classList.toggle('visible'); outer.appendChild(box); } // Sources toggle if (sources.length) { const toggle = document.createElement('button'); toggle.className = 'sources-toggle'; toggle.textContent = `${sources.length} source${sources.length > 1 ? 's' : ''} ↓`; bar.appendChild(toggle); const panel = document.createElement('div'); panel.className = 'sources-panel'; sources.forEach(s => { const item = document.createElement('div'); item.className = 'source-item'; item.innerHTML = ` ${s.title || s.source || 'Source'} ${s.jurisdiction || ''} ${s.score != null ? s.score.toFixed(2) : ''} ${s.url ? `` : ''} `; panel.appendChild(item); }); toggle.onclick = () => { const open = panel.classList.toggle('visible'); toggle.textContent = `${sources.length} source${sources.length > 1 ? 's' : ''} ${open ? '↑' : '↓'}`; }; outer.appendChild(panel); } } // ══ Settings ═════════════════════════════════════════════════════════════════ function openSettings() { document.getElementById('settings-overlay').classList.remove('hidden'); const panel = document.getElementById('settings-panel'); panel.classList.remove('hidden'); requestAnimationFrame(() => panel.classList.add('visible')); populateSettings(); } function closeSettings() { const panel = document.getElementById('settings-panel'); panel.classList.remove('visible'); setTimeout(() => { panel.classList.add('hidden'); document.getElementById('settings-overlay').classList.add('hidden'); }, 300); } function switchSettingsTab(tab, btn) { document.querySelectorAll('.stab-content').forEach(el => el.classList.add('hidden')); document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active')); document.getElementById(`stab-${tab}`).classList.remove('hidden'); btn.classList.add('active'); } // ── General Tab ─────────────────────────────────────────────────────────── function populateSettings() { // Provider pills const pills = document.getElementById('s-provider-pills'); if (pills) { pills.innerHTML = ''; ['groq', 'openrouter', 'ollama'].forEach(p => { const btn = document.createElement('button'); btn.className = 'pill-btn' + (p === (settings.provider || currentProvider) ? ' active' : ''); btn.textContent = providerLabel(p); btn.onclick = () => { document.querySelectorAll('#s-provider-pills .pill-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); populateSettingsModelSelect(p); }; pills.appendChild(btn); }); } populateSettingsModelSelect(settings.provider || currentProvider); // Jurisdiction document.querySelectorAll('#s-jur-pills button[data-jur]').forEach(btn => { const v = btn.dataset.jur === 'null' ? null : btn.dataset.jur; btn.classList.toggle('active', v === jurisdictionOverride); }); // Models tab populateModelsTab(); // Custom tab populateCustomTab(); } function populateSettingsModelSelect(provider) { const sel = document.getElementById('s-model-select'); if (!sel) return; sel.innerHTML = ''; (allModels[provider] || []).forEach(m => { const opt = document.createElement('option'); opt.value = m.id; opt.textContent = m.name; opt.selected = m.id === (settings.model || currentModel); sel.appendChild(opt); }); } function setJurSetting(btn) { document.querySelectorAll('#s-jur-pills button[data-jur]').forEach(b => b.classList.remove('active')); btn.classList.add('active'); jurisdictionOverride = btn.dataset.jur === 'null' ? null : btn.dataset.jur; } // ── Models Tab ──────────────────────────────────────────────────────────── function populateModelsTab() { const list = document.getElementById('s-models-list'); if (!list) return; list.innerHTML = ''; const activeModels = settings.active_models || {}; for (const [prov, models] of Object.entries(allModels)) { if (!models.length) continue; const group = document.createElement('div'); group.className = 'model-provider-group'; const title = document.createElement('div'); title.className = 'model-provider-title'; title.textContent = providerLabel(prov); group.appendChild(title); const activeSet = new Set(activeModels[prov] || models.map(m => m.id)); models.forEach(m => { const row = document.createElement('div'); row.className = 'model-toggle-row'; const nameEl = document.createElement('span'); nameEl.className = 'model-toggle-name'; nameEl.textContent = m.name; const idEl = document.createElement('span'); idEl.className = 'model-toggle-id'; idEl.textContent = m.id.split('/').pop().split(':')[0]; const sw = document.createElement('label'); sw.className = 'toggle-switch'; sw.title = activeSet.has(m.id) ? 'Active — click to deactivate' : 'Inactive — click to activate'; const inp = document.createElement('input'); inp.type = 'checkbox'; inp.checked = activeSet.has(m.id); inp.dataset.provider = prov; inp.dataset.modelId = m.id; const slider = document.createElement('span'); slider.className = 'toggle-slider'; sw.appendChild(inp); sw.appendChild(slider); const delBtn = document.createElement('button'); delBtn.className = 'model-del-btn'; delBtn.textContent = '×'; delBtn.title = 'Remove model'; delBtn.onclick = () => removeModelFromCatalog(prov, m.id); row.appendChild(nameEl); row.appendChild(idEl); row.appendChild(sw); row.appendChild(delBtn); group.appendChild(row); }); list.appendChild(group); } } function removeModelFromCatalog(provider, modelId) { // Remove from allModels if (allModels[provider]) { allModels[provider] = allModels[provider].filter(m => m.id !== modelId); } // Remove from custom_models if present const custom = settings.custom_models || {}; if (custom[provider]) { custom[provider] = custom[provider].filter(m => m.id !== modelId); } // Remove from active_models const active = settings.active_models || {}; if (active[provider]) { active[provider] = active[provider].filter(id => id !== modelId); } settings.custom_models = custom; settings.active_models = active; // If removed model was the currently selected one, switch to first available if (currentModel === modelId) { const remaining = allModels[currentProvider] || []; currentModel = remaining[0]?.id || ''; syncModelLabel(); renderModelDropdown(); } saveSettings(true); populateSettings(); } // ── Custom Tab ──────────────────────────────────────────────────────────── function populateCustomTab() { const list = document.getElementById('s-custom-list'); if (!list) return; list.innerHTML = ''; const custom = settings.custom_models || {}; let any = false; for (const [prov, models] of Object.entries(custom)) { models.forEach(m => { any = true; const item = document.createElement('div'); item.className = 'custom-model-item'; item.innerHTML = `${m.name} (${providerLabel(prov)})`; const del = document.createElement('button'); del.className = 'custom-del-btn'; del.textContent = '×'; del.onclick = () => removeCustomModel(prov, m.id); item.appendChild(del); list.appendChild(item); }); } if (!any) { list.innerHTML = '
No custom models added yet.
'; } } async function addCustomModel() { const provider = document.getElementById('c-provider').value; const id = document.getElementById('c-model-id').value.trim(); const name = document.getElementById('c-model-name').value.trim(); if (!id || !name) { alert('Please enter both Model ID and display name.'); return; } const custom = settings.custom_models || {}; if (!custom[provider]) custom[provider] = []; // Prevent duplicates if (custom[provider].find(m => m.id === id)) { alert('Model already added.'); return; } custom[provider].push({ id, name }); // Also add to allModels catalog client-side if (!allModels[provider]) allModels[provider] = []; if (!allModels[provider].find(m => m.id === id)) allModels[provider].push({ id, name }); // Activate by default const active = settings.active_models || {}; if (!active[provider]) active[provider] = []; if (!active[provider].includes(id)) active[provider].push(id); settings.custom_models = custom; settings.active_models = active; document.getElementById('c-model-id').value = ''; document.getElementById('c-model-name').value = ''; // Auto-save and refresh everything await saveSettings(true); // pass true to skip closing the panel populateSettings(); // Refresh all tabs } async function removeCustomModel(provider, modelId) { const custom = settings.custom_models || {}; if (custom[provider]) { custom[provider] = custom[provider].filter(m => m.id !== modelId); } const active = settings.active_models || {}; if (active[provider]) { active[provider] = active[provider].filter(id => id !== modelId); } if (allModels[provider]) { allModels[provider] = allModels[provider].filter(m => m.id !== modelId); } settings.custom_models = custom; settings.active_models = active; if (allModels[provider]) { allModels[provider] = allModels[provider].filter(m => m.id !== modelId); } await saveSettings(true); populateSettings(); } async function saveSettings(skipClose = false) { // Read active provider from pills const activePill = document.querySelector('#s-provider-pills .pill-btn.active'); const provLabels = ['groq', 'openrouter', 'ollama']; let selProvider = settings.provider || currentProvider; if (activePill) { const idx = [...document.querySelectorAll('#s-provider-pills .pill-btn')].indexOf(activePill); if (idx >= 0) selProvider = provLabels[idx]; } const selModel = document.getElementById('s-model-select')?.value || currentModel; // Collect active models from toggles const activeModels = {}; document.querySelectorAll('.toggle-switch input[type="checkbox"]').forEach(inp => { const prov = inp.dataset.provider; const id = inp.dataset.modelId; if (!activeModels[prov]) activeModels[prov] = []; if (inp.checked) activeModels[prov].push(id); }); const toSave = { provider: selProvider, model: selModel, jurisdiction_override: jurisdictionOverride, active_models: activeModels, custom_models: settings.custom_models || {} }; try { const updated = await api('/api/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(toSave) }); settings = updated; currentProvider = selProvider; currentModel = selModel; syncModelLabel(); renderModelDropdown(); if (!skipClose) closeSettings(); } catch (e) { alert('Failed to save settings: ' + e.message); } } // ══ Utils ═════════════════════════════════════════════════════════════════════ function clearMessages() { document.getElementById('messages').innerHTML = ''; } function showEmpty() { document.getElementById('empty-state').classList.remove('hidden'); } function hideEmpty() { document.getElementById('empty-state').classList.add('hidden'); } function scrollBottom() { const el = document.getElementById('messages'); el.scrollTop = el.scrollHeight; } function setDisabled(v) { document.getElementById('send-btn').disabled = v; document.getElementById('query-input').disabled = v; } function autoResize(el) { el.style.height = 'auto'; el.style.height = Math.min(el.scrollHeight, 180) + 'px'; }