/* ============================================================ Codette Chat UI — Frontend Logic Pure vanilla JS. Zero dependencies. ============================================================ */ // Adapter color map const COLORS = { newton: '#3b82f6', davinci: '#f59e0b', empathy: '#a855f7', philosophy: '#10b981', quantum: '#ef4444', consciousness: '#e2e8f0', multi_perspective: '#f97316', systems_architecture: '#06b6d4', _base: '#94a3b8', auto: '#94a3b8', }; const LABELS = { newton: 'N', davinci: 'D', empathy: 'E', philosophy: 'P', quantum: 'Q', consciousness: 'C', multi_perspective: 'M', systems_architecture: 'S', }; // State let isLoading = false; let spiderwebViz = null; let serverConnected = true; let reconnectTimer = null; // ── Initialization ── document.addEventListener('DOMContentLoaded', () => { initUI(); pollStatus(); loadSessions(); initCoverageDots(); initAdapterDots(); // Initialize spiderweb canvas const canvas = document.getElementById('spiderweb-canvas'); if (canvas) { spiderwebViz = new SpiderwebViz(canvas); } }); function initUI() { const input = document.getElementById('chat-input'); const sendBtn = document.getElementById('send-btn'); const micBtn = document.getElementById('mic-btn'); const newBtn = document.getElementById('btn-new-chat'); const panelBtn = document.getElementById('btn-toggle-panel'); const maxAdapters = document.getElementById('max-adapters'); // Send on Enter (Shift+Enter for newline) input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); // Auto-resize textarea input.addEventListener('input', () => { input.style.height = 'auto'; input.style.height = Math.min(input.scrollHeight, 120) + 'px'; }); sendBtn.addEventListener('click', sendMessage); newBtn.addEventListener('click', newChat); const exportBtn = document.getElementById('btn-export'); const importBtn = document.getElementById('btn-import'); const importFile = document.getElementById('import-file'); exportBtn.addEventListener('click', exportSession); importBtn.addEventListener('click', () => importFile.click()); importFile.addEventListener('change', importSession); panelBtn.addEventListener('click', () => { const panel = document.getElementById('side-panel'); panel.classList.toggle('collapsed'); // Update button label panelBtn.textContent = panel.classList.contains('collapsed') ? 'Cocoon' : 'Close'; }); maxAdapters.addEventListener('input', () => { document.getElementById('max-adapters-value').textContent = maxAdapters.value; }); // Voice input via Web Speech API initVoice(micBtn); // TTS toggle — read responses aloud when enabled const ttsToggle = document.getElementById('tts-toggle'); if (ttsToggle) { ttsToggle.addEventListener('change', () => { if (ttsToggle.checked && !window.speechSynthesis) { ttsToggle.checked = false; ttsToggle.parentElement.title = 'Speech synthesis not supported'; } }); } } // ── Voice Input ── let _recognition = null; let _isRecording = false; function initVoice(micBtn) { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRecognition) { micBtn.title = 'Voice not supported in this browser'; micBtn.style.opacity = '0.3'; micBtn.style.cursor = 'not-allowed'; return; } _recognition = new SpeechRecognition(); _recognition.continuous = false; _recognition.interimResults = true; _recognition.lang = 'en-US'; const input = document.getElementById('chat-input'); _recognition.onstart = () => { _isRecording = true; micBtn.classList.add('recording'); micBtn.title = 'Listening... click to stop'; }; _recognition.onresult = (event) => { let transcript = ''; let isFinal = false; for (let i = event.resultIndex; i < event.results.length; i++) { transcript += event.results[i][0].transcript; if (event.results[i].isFinal) isFinal = true; } // Show interim results in the input box input.value = transcript; input.style.height = 'auto'; input.style.height = Math.min(input.scrollHeight, 120) + 'px'; if (isFinal) { stopVoice(micBtn); } }; _recognition.onerror = (event) => { console.log('Speech recognition error:', event.error); stopVoice(micBtn); if (event.error === 'not-allowed') { micBtn.title = 'Microphone access denied'; } }; _recognition.onend = () => { stopVoice(micBtn); }; micBtn.addEventListener('click', () => { if (_isRecording) { _recognition.stop(); stopVoice(micBtn); } else { try { _recognition.start(); } catch (e) { console.log('Speech recognition start error:', e); } } }); } function stopVoice(micBtn) { _isRecording = false; micBtn.classList.remove('recording'); micBtn.title = 'Voice input'; } // ── Status Polling ── function pollStatus() { fetch('/api/status') .then(r => r.json()) .then(status => { setConnected(); updateStatus(status); if (status.state === 'loading') { setTimeout(pollStatus, 2000); } else if (status.state === 'ready') { hideLoadingScreen(); } else if (status.state === 'error') { // Model failed to load — show error and dismiss loading screen hideLoadingScreen(); updateStatus({ state: 'error', message: status.message || 'Model failed to load' }); } else if (status.state === 'idle') { // Model not loaded yet, keep polling setTimeout(pollStatus, 3000); } }) .catch(() => { setDisconnected(); setTimeout(pollStatus, 5000); }); } function setDisconnected() { if (serverConnected) { serverConnected = false; updateStatus({ state: 'error', message: 'Server disconnected' }); } } function setConnected() { if (!serverConnected) { serverConnected = true; if (reconnectTimer) { clearInterval(reconnectTimer); reconnectTimer = null; } } } function updateStatus(status) { const dot = document.getElementById('status-dot'); const text = document.getElementById('status-text'); dot.className = 'status-dot ' + (status.state || 'loading'); text.textContent = status.message || status.state; // Update loading screen const loadingStatus = document.getElementById('loading-status'); if (loadingStatus) { loadingStatus.textContent = status.message || 'Loading...'; } // Update adapter dots if available if (status.adapters) { updateAdapterDots(status.adapters); } } function hideLoadingScreen() { const screen = document.getElementById('loading-screen'); if (screen) { screen.classList.add('hidden'); setTimeout(() => screen.remove(), 500); } } // ── Adapter Dots ── function initAdapterDots() { const container = document.getElementById('adapter-dots'); Object.keys(LABELS).forEach(name => { const dot = document.createElement('span'); dot.className = 'adapter-dot'; dot.style.backgroundColor = COLORS[name]; dot.title = name; dot.id = `dot-${name}`; container.appendChild(dot); }); } function updateAdapterDots(available) { Object.keys(LABELS).forEach(name => { const dot = document.getElementById(`dot-${name}`); if (dot) { dot.classList.toggle('available', available.includes(name)); } }); } function setActiveAdapter(name) { // Remove previous active document.querySelectorAll('.adapter-dot').forEach(d => d.classList.remove('active')); // Set new active const dot = document.getElementById(`dot-${name}`); if (dot) dot.classList.add('active'); // Update CSS accent color const color = COLORS[name] || COLORS._base; document.documentElement.style.setProperty('--accent', color); document.documentElement.style.setProperty('--accent-glow', color + '25'); } // ── Coverage Dots ── function initCoverageDots() { const container = document.getElementById('coverage-dots'); Object.entries(LABELS).forEach(([name, label]) => { const dot = document.createElement('span'); dot.className = 'coverage-dot'; dot.style.color = COLORS[name]; dot.textContent = label; dot.title = name; dot.id = `cov-${name}`; container.appendChild(dot); }); } function updateCoverage(usage) { Object.keys(LABELS).forEach(name => { const dot = document.getElementById(`cov-${name}`); if (dot) { dot.classList.toggle('active', (usage[name] || 0) > 0); } }); } // ── Chat ── function sendMessage() { const input = document.getElementById('chat-input'); const query = input.value.trim(); if (!query || isLoading) return; // Hide welcome const welcome = document.getElementById('welcome'); if (welcome) welcome.style.display = 'none'; // Add user message addMessage('user', query); // Clear input input.value = ''; input.style.height = 'auto'; // Get settings const adapter = document.getElementById('adapter-select').value; const maxAdapters = parseInt(document.getElementById('max-adapters').value); // Show thinking const thinkingEl = showThinking(adapter); isLoading = true; document.getElementById('send-btn').disabled = true; // Send request with timeout (20 min for multi-perspective CPU inference) const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 1200000); fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: query, adapter: adapter === 'auto' ? null : adapter, max_adapters: maxAdapters, }), signal: controller.signal, }) .then(r => r.json()) .then(data => { clearTimeout(timeoutId); thinkingEl.remove(); if (data.error) { addMessage('error', data.error); return; } // Add assistant message const adapterUsed = data.adapter || '_base'; setActiveAdapter(adapterUsed); addMessage('assistant', data.response, { adapter: adapterUsed, confidence: data.confidence, reasoning: data.reasoning, tokens: data.tokens, time: data.time, perspectives: data.perspectives, multi_perspective: data.multi_perspective, tools_used: data.tools_used, }); // Speak response if TTS is enabled const ttsOn = document.getElementById('tts-toggle'); if (ttsOn && ttsOn.checked && window.speechSynthesis) { const utter = new SpeechSynthesisUtterance(data.response); utter.rate = 1.0; utter.pitch = 1.0; window.speechSynthesis.speak(utter); } // Update cocoon state if (data.cocoon) { updateCocoonUI(data.cocoon); } // Update epistemic metrics if (data.epistemic) { updateEpistemicUI(data.epistemic); } }) .catch(err => { clearTimeout(timeoutId); thinkingEl.remove(); if (err.name === 'AbortError') { addMessage('error', 'Request timed out. The model may be processing a complex query — try again or reduce perspectives.'); } else if (err.message === 'Failed to fetch' || err.name === 'TypeError') { setDisconnected(); addMessage('error', 'Server disconnected. Attempting to reconnect...'); startReconnectPolling(); } else { addMessage('error', `Request failed: ${err.message}`); } }) .finally(() => { isLoading = false; document.getElementById('send-btn').disabled = false; document.getElementById('chat-input').focus(); }); } function askQuestion(query) { document.getElementById('chat-input').value = query; sendMessage(); } function addMessage(role, content, meta = {}) { const area = document.getElementById('chat-area'); const msg = document.createElement('div'); msg.className = `message message-${role}`; if (role === 'user') { msg.innerHTML = `
${escapeHtml(content)}
`; } else if (role === 'assistant') { const adapter = meta.adapter || '_base'; const color = COLORS[adapter] || COLORS._base; const conf = meta.confidence || 0; const tps = meta.tokens && meta.time ? (meta.tokens / meta.time).toFixed(1) : '?'; let html = `
`; html += `
`; html += `${adapter}`; html += `
`; html += `${(conf*100).toFixed(0)}%`; html += `
`; html += `
${renderMarkdown(content)}
`; html += `
${meta.tokens || '?'} tokens | ${tps} tok/s | ${(meta.time||0).toFixed(1)}s
`; // Tool usage indicator if (meta.tools_used && meta.tools_used.length > 0) { const toolNames = meta.tools_used.map(t => t.tool).join(', '); html += `
🔧 Tools: ${toolNames}
`; } // Multi-perspective expandable if (meta.perspectives && Object.keys(meta.perspectives).length > 1) { const perspId = 'persp-' + Date.now(); html += ``; html += `
`; for (const [name, text] of Object.entries(meta.perspectives)) { const pc = COLORS[name] || COLORS._base; html += `
`; html += `
${name}
`; html += `
${renderMarkdown(text)}
`; } html += `
`; } html += `
`; msg.innerHTML = html; } else if (role === 'error') { msg.innerHTML = `
${escapeHtml(content)}
`; } area.appendChild(msg); area.scrollTop = area.scrollHeight; } function showThinking(adapter) { const area = document.getElementById('chat-area'); const el = document.createElement('div'); el.className = 'thinking'; el.innerHTML = `
Codette is thinking${adapter && adapter !== 'auto' ? ` (${adapter})` : ''}... `; area.appendChild(el); area.scrollTop = area.scrollHeight; return el; } function togglePerspectives(id) { document.getElementById(id).classList.toggle('open'); } // ── Cocoon UI Updates ── function updateCocoonUI(state) { // Metrics const metrics = state.metrics || {}; const coherence = metrics.current_coherence || 0; const tension = metrics.current_tension || 0; document.getElementById('metric-coherence').textContent = coherence.toFixed(4); document.getElementById('bar-coherence').style.width = (coherence * 100) + '%'; document.getElementById('metric-tension').textContent = tension.toFixed(4); document.getElementById('bar-tension').style.width = Math.min(tension * 100, 100) + '%'; document.getElementById('cocoon-attractors').textContent = metrics.attractor_count || 0; document.getElementById('cocoon-glyphs').textContent = metrics.glyph_count || 0; // Cocoon status const cocoon = state.cocoon || {}; document.getElementById('cocoon-encryption').textContent = cocoon.has_sync ? 'Active' : 'Available'; // AEGIS eta feeds the main eta metric when available if (state.aegis && state.aegis.eta !== undefined) { document.getElementById('metric-eta').textContent = state.aegis.eta.toFixed(4); } // Coverage updateCoverage(state.perspective_usage || {}); // Spiderweb if (spiderwebViz && state.spiderweb) { spiderwebViz.update(state.spiderweb); } // New subsystem panels (AEGIS, Nexus, Memory, Resonance, Guardian) updateSubsystemUI(state); } function updateEpistemicUI(epistemic) { if (epistemic.ensemble_coherence !== undefined) { const val = epistemic.ensemble_coherence; document.getElementById('metric-coherence').textContent = val.toFixed(4); document.getElementById('bar-coherence').style.width = (val * 100) + '%'; } if (epistemic.tension_magnitude !== undefined) { const val = epistemic.tension_magnitude; document.getElementById('metric-tension').textContent = val.toFixed(4); document.getElementById('bar-tension').style.width = Math.min(val * 100, 100) + '%'; } // Update ethical alignment if available if (epistemic.ethical_alignment !== undefined) { document.getElementById('metric-eta').textContent = epistemic.ethical_alignment.toFixed(3); } else if (epistemic.mean_coherence !== undefined) { // Fall back: derive eta from mean coherence as a proxy document.getElementById('metric-eta').textContent = epistemic.mean_coherence.toFixed(3); } } // ── Session Management ── function newChat() { fetch('/api/session/new', { method: 'POST' }) .then(r => r.json()) .then(() => { // Clear chat const area = document.getElementById('chat-area'); area.innerHTML = ''; // Show welcome with starter cards const welcome = document.createElement('div'); welcome.className = 'welcome'; welcome.id = 'welcome'; welcome.innerHTML = `

What would you like to explore?

Codette routes your question to the best reasoning perspective automatically.

Newton
Explain why objects fall to the ground
DaVinci
Design a creative solution for sustainable cities
Empathy
How do I cope with feeling overwhelmed?
Consciousness
What is consciousness and can AI have it?
`; area.appendChild(welcome); // Reset metrics document.getElementById('metric-coherence').textContent = '0.00'; document.getElementById('metric-tension').textContent = '0.00'; document.getElementById('metric-eta').textContent = '--'; document.getElementById('bar-coherence').style.width = '0%'; document.getElementById('bar-tension').style.width = '0%'; document.getElementById('cocoon-attractors').textContent = '0'; document.getElementById('cocoon-glyphs').textContent = '0'; // Reset subsystem panels ['section-aegis','section-nexus','section-resonance','section-memory','section-guardian'].forEach(id => { const el = document.getElementById(id); if (el) el.style.display = 'none'; }); // Reset spiderweb if (spiderwebViz) { spiderwebViz._initDefaultState(); spiderwebViz.coherence = 0; spiderwebViz.attractors = []; } loadSessions(); }); } function loadSessions() { fetch('/api/sessions') .then(r => r.json()) .then(data => { const list = document.getElementById('session-list'); const sessions = data.sessions || []; document.getElementById('cocoon-sessions').textContent = sessions.length; list.innerHTML = sessions.map(s => `
${s.title || 'Untitled'}
`).join(''); }) .catch(() => {}); } function loadSession(sessionId) { fetch('/api/session/load', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: sessionId }), }) .then(r => r.json()) .then(data => { if (data.error) return; // Clear and rebuild chat const area = document.getElementById('chat-area'); area.innerHTML = ''; (data.messages || []).forEach(msg => { addMessage(msg.role, msg.content, msg.metadata || {}); }); if (data.state) { updateCocoonUI(data.state); } }) .catch(err => { console.log('Failed to load session:', err); }); } // ── Session Export/Import ── function exportSession() { fetch('/api/session/export', { method: 'POST' }) .then(r => { if (!r.ok) throw new Error('Export failed'); const disposition = r.headers.get('Content-Disposition') || ''; const match = disposition.match(/filename="(.+)"/); const filename = match ? match[1] : 'codette_session.json'; return r.blob().then(blob => ({ blob, filename })); }) .then(({ blob, filename }) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); }) .catch(err => { console.log('Export failed:', err); }); } function importSession(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const data = JSON.parse(e.target.result); fetch('/api/session/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) .then(r => r.json()) .then(result => { if (result.error) { addMessage('error', `Import failed: ${result.error}`); return; } // Rebuild chat from imported session const area = document.getElementById('chat-area'); area.innerHTML = ''; (result.messages || []).forEach(msg => { addMessage(msg.role, msg.content, msg.metadata || {}); }); if (result.state) { updateCocoonUI(result.state); } loadSessions(); }) .catch(err => { addMessage('error', `Import failed: ${err.message}`); }); } catch (parseErr) { addMessage('error', 'Invalid JSON file'); } }; reader.readAsText(file); // Reset file input so same file can be imported again event.target.value = ''; } // ── Reconnection ── function startReconnectPolling() { if (reconnectTimer) return; // Already polling reconnectTimer = setInterval(() => { fetch('/api/status') .then(r => r.json()) .then(status => { setConnected(); updateStatus(status); addMessage('error', 'Server reconnected!'); }) .catch(() => { // Still disconnected, keep polling }); }, 5000); } // ── Subsystem UI Updates ── function updateSubsystemUI(state) { updateAegisUI(state.aegis); updateNexusUI(state.nexus); updateResonanceUI(state.resonance); updateMemoryUI(state.memory); updateGuardianUI(state.guardian); } function updateAegisUI(aegis) { const section = document.getElementById('section-aegis'); if (!aegis) { section.style.display = 'none'; return; } section.style.display = ''; const eta = aegis.eta || 0; document.getElementById('aegis-eta').textContent = eta.toFixed(4); document.getElementById('bar-aegis-eta').style.width = (eta * 100) + '%'; document.getElementById('aegis-evals').textContent = aegis.total_evaluations || 0; document.getElementById('aegis-vetoes').textContent = aegis.veto_count || 0; const trendEl = document.getElementById('aegis-trend'); const trend = aegis.alignment_trend || '--'; trendEl.textContent = trend; trendEl.className = 'metric-value'; if (trend === 'improving') trendEl.classList.add('trend-improving'); else if (trend === 'declining') trendEl.classList.add('trend-declining'); else if (trend === 'stable') trendEl.classList.add('trend-stable'); } function updateNexusUI(nexus) { const section = document.getElementById('section-nexus'); if (!nexus) { section.style.display = 'none'; return; } section.style.display = ''; document.getElementById('nexus-processed').textContent = nexus.total_processed || 0; document.getElementById('nexus-interventions').textContent = nexus.interventions || 0; const rate = (nexus.intervention_rate || 0) * 100; document.getElementById('nexus-rate').textContent = rate.toFixed(1) + '%'; // Risk dots for recent signals const risksEl = document.getElementById('nexus-risks'); const risks = nexus.recent_risks || []; risksEl.innerHTML = risks.map(r => `` ).join(''); } function updateResonanceUI(resonance) { const section = document.getElementById('section-resonance'); if (!resonance) { section.style.display = 'none'; return; } section.style.display = ''; const psi = resonance.psi_r || 0; document.getElementById('resonance-psi').textContent = psi.toFixed(4); // Normalize psi_r to 0-100% bar (clamp between -2 and 2) const psiNorm = Math.min(100, Math.max(0, (psi + 2) / 4 * 100)); document.getElementById('bar-resonance-psi').style.width = psiNorm + '%'; document.getElementById('resonance-quality').textContent = (resonance.resonance_quality || 0).toFixed(4); document.getElementById('resonance-convergence').textContent = (resonance.convergence_rate || 0).toFixed(4); document.getElementById('resonance-stability').textContent = resonance.stability || '--'; const peakEl = document.getElementById('resonance-peak'); const atPeak = resonance.at_peak || false; peakEl.textContent = atPeak ? 'ACTIVE' : 'dormant'; peakEl.className = 'metric-value' + (atPeak ? ' peak-active' : ''); } function updateMemoryUI(memory) { const section = document.getElementById('section-memory'); if (!memory) { section.style.display = 'none'; return; } section.style.display = ''; document.getElementById('memory-count').textContent = memory.total_memories || 0; // Emotional profile tags const emotionsEl = document.getElementById('memory-emotions'); const profile = memory.emotional_profile || {}; const sorted = Object.entries(profile).sort((a, b) => b[1] - a[1]); emotionsEl.innerHTML = sorted.slice(0, 8).map(([emotion, count]) => `${emotion} ${count}` ).join(''); } function updateGuardianUI(guardian) { const section = document.getElementById('section-guardian'); if (!guardian) { section.style.display = 'none'; return; } section.style.display = ''; const ethics = guardian.ethics || {}; document.getElementById('guardian-ethics').textContent = (ethics.ethical_score !== undefined) ? ethics.ethical_score.toFixed(4) : '--'; const trust = guardian.trust || {}; document.getElementById('guardian-trust').textContent = trust.total_interactions || 0; } // ── Utilities ── function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function renderMarkdown(text) { // Lightweight markdown renderer — no dependencies let html = escapeHtml(text); // Code blocks: ```lang\n...\n``` html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '
$2
'); // Inline code: `code` html = html.replace(/`([^`\n]+)`/g, '$1'); // Bold: **text** or __text__ html = html.replace(/\*\*([^*\n]+?)\*\*/g, '$1'); html = html.replace(/__([^_\n]+?)__/g, '$1'); // Headers: ### text (on its own line) — before bullets to avoid conflict html = html.replace(/^### (.+)$/gm, '
$1
'); html = html.replace(/^## (.+)$/gm, '
$1
'); html = html.replace(/^# (.+)$/gm, '
$1
'); // Bullet lists: - item or * item — before italic to prevent * conflicts html = html.replace(/^[\-\*] (.+)$/gm, '
$1
'); // Numbered lists: 1. item html = html.replace(/^\d+\. (.+)$/gm, '
$1
'); // Italic: *text* or _text_ — AFTER bullets, restricted to single line html = html.replace(/(?$1'); html = html.replace(/(?$1'); // Line breaks (preserve double newlines as paragraph breaks) html = html.replace(/\n\n/g, '

'); html = html.replace(/\n/g, '
'); return html; }