const COLORS = ['#60a5fa','#a78bfa','#f472b6','#34d399','#fb923c','#38bdf8','#e879f9','#a3e635']; const speakerColor = (id) => COLORS[(id || 0) % COLORS.length]; const speakerInitial = (name) => (name || '?')[0].toUpperCase(); const fmtTime = (iso) => new Date(iso).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'}); const fmtDate = (iso) => new Date(iso).toLocaleDateString([], {weekday:'long',month:'long',day:'numeric',year:'numeric'}); const today = () => new Date().toISOString().slice(0,10); let _currentDate = today(); function showPanel(name, el) { document.querySelectorAll('.panel').forEach(p => p.classList.remove('active')); document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); document.getElementById('panel-' + name).classList.add('active'); if (el) el.classList.add('active'); if (name === 'log') loadLog(); if (name === 'search') renderSearch(); if (name === 'speakers') loadSpeakers(); if (name === 'rag') renderRag(); if (name === 'rooms') loadRooms(); } function escHtml(s) { return String(s).replace(/&/g,'&').replace(//g,'>'); } // --- Daily Log --- async function loadLog(date) { if (date) _currentDate = date; const panel = document.getElementById('panel-log'); const utterances = await fetch(`/api/utterances?date=${_currentDate}`).then(r => r.json()).catch(() => []); const nav = `
${fmtDate(_currentDate + 'T12:00:00')}
`; if (!utterances.length) { panel.innerHTML = nav + '
No utterances recorded for this day.
'; return; } const byHour = {}; for (const u of [...utterances].reverse()) { const key = new Date(u.start_time).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}); if (!byHour[key]) byHour[key] = []; byHour[key].push(u); } let html = nav; for (const [timeKey, utts] of Object.entries(byHour)) { html += `
${timeKey}
`; for (const u of utts) { const color = speakerColor(u.speaker_id); const name = u.speaker_name || 'Unknown'; const badge = u.match_status === 'unknown' ? 'unknown' : u.match_status === 'ambiguous' ? 'ambiguous' : ''; const actions = !u.speaker_id ? `
` : ''; html += `
${speakerInitial(name)}
${escHtml(name)}${badge} ${escHtml(u.room_name)} ${fmtTime(u.start_time)}
${escHtml(u.transcript)}
${actions}
`; } html += '
'; } panel.innerHTML = html; } function shiftDate(days) { const d = new Date(_currentDate + 'T12:00:00'); d.setDate(d.getDate() + days); return d.toISOString().slice(0,10); } async function playClip(id) { const a = new Audio(`/api/utterances/${id}/clip`); a.play(); } async function tagSpeaker(utteranceId) { const speakers = await fetch('/api/speakers').then(r => r.json()); const name = prompt('Speaker name:\n' + speakers.map(s => '- ' + s.name).join('\n')); if (!name) return; const existing = speakers.find(s => s.name.toLowerCase() === name.trim().toLowerCase()); const body = existing ? { speaker_id: existing.id } : { name: name.trim(), create_new: true }; await fetch(`/api/utterances/${utteranceId}/tag`, { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body) }); loadLog(); } async function dismiss(id) { await fetch(`/api/utterances/${id}/dismiss`, { method: 'POST' }); loadLog(); } // --- Search --- function renderSearch() { const panel = document.getElementById('panel-search'); panel.innerHTML = `

Search Transcripts

`; } async function doSearch() { const q = document.getElementById('searchQ').value.trim(); if (!q) return; const results = await fetch(`/api/search?q=${encodeURIComponent(q)}`).then(r => r.json()).catch(() => []); const container = document.getElementById('searchResults'); if (!results.length) { container.innerHTML = '
No results.
'; return; } const re = new RegExp(`(${q.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')})`, 'gi'); container.innerHTML = results.map(r => `
${escHtml(r.speaker_name || 'Unknown')} · ${escHtml(r.room_name)} · ${fmtDate(r.start_time)} ${fmtTime(r.start_time)}
${escHtml(r.transcript).replace(re, '$1')}
`).join(''); } // --- Speakers --- async function loadSpeakers() { const panel = document.getElementById('panel-speakers'); const [speakers, unreviewed] = await Promise.all([ fetch('/api/speakers').then(r => r.json()).catch(() => []), fetch('/api/utterances?match_status=unreviewed').then(r => r.json()).catch(() => []), ]); let html = ''; if (unreviewed.length) { html += `
${unreviewed.length} utterances need speaker tags
`; } html += `

Speakers

`; for (const s of speakers) { const color = speakerColor(s.id); html += `
${speakerInitial(s.name)}
${escHtml(s.name)}
${s.utterance_count} utterances
`; } html += `
+ Add speaker
`; panel.innerHTML = html; } async function createSpeaker() { const name = prompt('Speaker name:'); if (!name) return; await fetch('/api/speakers', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({name}) }); loadSpeakers(); } // --- Ask AI --- const _ragHistory = []; function renderRag() { const panel = document.getElementById('panel-rag'); panel.innerHTML = `

Ask AI

`; renderRagMessages(); } function renderRagMessages() { const el = document.getElementById('ragMessages'); if (!el) return; if (!_ragHistory.length) { el.innerHTML = '
Ask a question about any conversation.
'; return; } el.innerHTML = _ragHistory.map(m => `
${m.role === 'user' ? 'You' : 'LinkedStorm AI'}
${escHtml(m.content)}
`).join(''); el.scrollTop = el.scrollHeight; } async function askRag() { const q = document.getElementById('ragQ')?.value.trim(); if (!q) return; document.getElementById('ragQ').value = ''; _ragHistory.push({ role: 'user', content: q }); renderRagMessages(); const resp = await fetch('/api/ask', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify({ question: q, date: _currentDate }), }).then(r => r.json()).catch(() => ({ answer: 'Error contacting AI.' })); _ragHistory.push({ role: 'ai', content: resp.answer || 'No response.' }); renderRagMessages(); } // --- Rooms --- async function loadRooms() { const panel = document.getElementById('panel-rooms'); const rooms = await fetch('/api/rooms').then(r => r.json()).catch(() => []); let html = `

Capture Rooms

`; for (const r of rooms) { const active = r.is_active; const dotColor = active ? '#4ade80' : '#475569'; const statusText = active ? 'Streaming' : 'Disconnected'; const cls = active ? 'streaming' : ''; html += `
${escHtml(r.name)}
${statusText}
${r.last_seen ? `
Last: ${fmtTime(r.last_seen)}
` : ''} Open capture page
`; } html += `
`; panel.innerHTML = html; } async function muteRoom(roomId) { await fetch(`/api/rooms/${roomId}/mute`, { method: 'POST' }); loadRooms(); } // Initial load loadLog(); setInterval(loadLog, 30000);