const state = { activeTool: 'ask', authenticated: Boolean(window.DBN_TOOLS_AUTHENTICATED), }; let lastTimelineEvents = []; let lastAudioFile = null; let lastTranscriptData = null; const tools = { ask: { kind: 'Source-grounded Legal Ask', title: 'Ask a legal question', label: 'Question', endpoint: 'api/ask.php', payloadKey: 'question', placeholder: 'Example: What evidence is needed before asking for changes in custody arrangements?', usesLanguage: true, badge: 'family-legal', }, search: { kind: 'Legal Source Search', title: 'Search legal sources', label: 'Search query', endpoint: 'api/search.php', payloadKey: 'query', placeholder: 'Example: barnets beste samvær foreldreansvar', usesLanguage: true, badge: 'family-legal', }, summarize: { kind: 'Document Summarizer', title: 'Summarize pasted text', label: 'Pasted text', endpoint: 'api/summarize.php', payloadKey: 'text', placeholder: 'Paste a case note, letter, or excerpt.', usesLanguage: true, badge: 'process-and-forget', }, timeline: { kind: 'Timeline Builder', title: 'Build a timeline', label: 'Pasted text', endpoint: 'api/timeline.php', payloadKey: 'text', placeholder: 'Paste case notes with dates, actors, and events.', usesLanguage: true, badge: 'process-and-forget', }, redact: { kind: 'Redaction Assistant', title: 'Redact sensitive details', label: 'Pasted text', endpoint: 'api/redact.php', payloadKey: 'text', placeholder: 'Paste text containing names, phone numbers, emails, addresses, or fødselsnummer-like values.', usesLanguage: false, badge: 'deterministic first', }, transcribe: { kind: 'Audio Transcription', title: 'Transcribe audio', label: 'Audio file', endpoint: 'api/transcribe.php', payloadKey: null, placeholder: '', usesLanguage: false, badge: 'Whisper / GPU', }, }; const els = {}; document.addEventListener('DOMContentLoaded', () => { Object.assign(els, { gate: document.querySelector('#publicLanding'), app: document.querySelector('#appShell'), passcodeForm: document.querySelector('#passcodeForm'), loginEmail: document.querySelector('#loginEmail'), loginPassword: document.querySelector('#loginPassword'), gateStatus: document.querySelector('#gateStatus'), tabs: Array.from(document.querySelectorAll('.tool-tab')), toolKind: document.querySelector('#toolKind'), toolTitle: document.querySelector('#toolTitle'), toolBadge: document.querySelector('#toolBadge'), form: document.querySelector('#toolForm'), inputLabel: document.querySelector('#inputLabel'), input: document.querySelector('#toolInput'), languageControl: document.querySelector('#languageControl'), redactionControl: document.querySelector('#redactionControl'), status: document.querySelector('#toolStatus'), results: document.querySelector('#results'), traceList: document.querySelector('#traceList'), healthButton: document.querySelector('#healthButton'), healthPill: document.querySelector('#healthPill'), uploadZone: document.querySelector('#uploadZone'), uploadInput: document.querySelector('#uploadInput'), uploadPrompt: document.querySelector('#uploadPrompt'), uploadFileInfo: document.querySelector('#uploadFileInfo'), uploadFileList: document.querySelector('#uploadFileList'), uploadClear: document.querySelector('#uploadClear'), aliasSection: document.querySelector('#aliasSection'), addAliasRow: document.querySelector('#addAliasRow'), aliasRows: document.querySelector('#aliasRows'), audioZone: document.querySelector('#audioZone'), audioInput: document.querySelector('#audioInput'), audioPrompt: document.querySelector('#audioPrompt'), audioFileInfo: document.querySelector('#audioFileInfo'), audioFileName: document.querySelector('#audioFileName'), audioFileSize: document.querySelector('#audioFileSize'), audioClear: document.querySelector('#audioClear'), diarizeControl: document.querySelector('#diarizeControl'), diarizeCheck: document.querySelector('#diarizeCheck'), numSpeakersInput: document.querySelector('#numSpeakersInput'), transcribeLangControl: document.querySelector('#transcribeLangControl'), }); els.tabs.forEach((button) => { button.addEventListener('click', () => setTool(button.dataset.tool)); }); els.form.addEventListener('submit', runTool); els.passcodeForm.addEventListener('submit', submitPasscode); els.healthButton.addEventListener('click', checkHealth); setupUpload(); setupAliases(); setupAudio(); els.results.addEventListener('click', (e) => { if (e.target.closest('#exportCsvBtn')) exportTimelineCSV(lastTimelineEvents); if (e.target.closest('#dlTxt')) downloadTranscriptTxt(); if (e.target.closest('#dlSrt')) downloadTranscriptSrt(); if (e.target.closest('#dlVtt')) downloadTranscriptVtt(); }); setTool(state.activeTool); if (state.authenticated) { checkHealth(); } else { els.loginEmail?.focus(); } }); function setTool(toolName) { state.activeTool = toolName; const tool = tools[toolName]; els.tabs.forEach((button) => { const active = button.dataset.tool === toolName; button.classList.toggle('is-active', active); button.setAttribute('aria-pressed', String(active)); }); els.toolKind.textContent = tool.kind; els.toolTitle.textContent = tool.title; els.toolBadge.textContent = tool.badge; els.inputLabel.textContent = tool.label; els.input.value = ''; els.input.placeholder = tool.placeholder; els.languageControl.classList.toggle('is-hidden', !tool.usesLanguage); els.redactionControl.classList.toggle('is-hidden', toolName !== 'redact'); els.uploadZone.classList.toggle('is-hidden', toolName !== 'redact' && toolName !== 'timeline'); els.aliasSection.classList.toggle('is-hidden', toolName !== 'redact'); els.audioZone.classList.toggle('is-hidden', toolName !== 'transcribe'); els.diarizeControl.classList.toggle('is-hidden', toolName !== 'transcribe'); els.transcribeLangControl.classList.toggle('is-hidden', toolName !== 'transcribe'); els.input.classList.toggle('is-hidden', toolName === 'transcribe'); els.inputLabel.classList.toggle('is-hidden', toolName === 'transcribe'); els.input.required = toolName !== 'transcribe'; resetUpload(); resetAliases(); resetAudio(); els.status.textContent = ''; renderTrace([]); } async function submitPasscode(event) { event.preventDefault(); els.gateStatus.textContent = 'Signing in…'; try { const data = await postJson('api/session.php', { email: els.loginEmail.value.trim(), password: els.loginPassword.value, }); if (!data.ok) { throw new Error(data.error?.message || 'Credentials were not accepted.'); } state.authenticated = true; els.gate.classList.add('is-hidden'); els.app.classList.remove('is-hidden'); els.loginPassword.value = ''; els.healthPill.textContent = 'Session active'; checkHealth(); els.input.focus(); } catch (error) { els.gateStatus.textContent = error.message; } } async function runTool(event) { event.preventDefault(); if (state.activeTool === 'transcribe') { await runTranscribe(); return; } const tool = tools[state.activeTool]; const text = els.input.value.trim(); if (!text) { els.status.textContent = 'Add text before running the tool.'; els.input.focus(); return; } const payload = { [tool.payloadKey]: text }; if (tool.usesLanguage) { payload.language = currentLanguage(); } if (state.activeTool === 'search') { payload.limit = 7; } if (state.activeTool === 'redact') { payload.mode = currentRedactionMode(); payload.region = currentRedactionRegion(); payload.aliases = getAliases(); } setBusy(true); renderTrace([ { label: 'Query interpretation', detail: 'Preparing request.', status: 'running' }, ]); try { const data = await postJson(tool.endpoint, payload); if (!data.ok) { throw new Error(data.error?.message || 'Tool request failed.'); } renderResults(data); renderTrace(data.trace || []); els.status.textContent = `Done in ${data.latency_ms || 0} ms.`; } catch (error) { els.status.textContent = error.message; renderTrace([ { label: 'Tool error', detail: error.message, status: 'warning' }, ]); } finally { setBusy(false); } } function resetUpload() { if (!els.uploadInput) return; els.uploadInput.value = ''; els.uploadPrompt.classList.remove('is-hidden'); els.uploadFileInfo.classList.add('is-hidden'); els.uploadFileList.innerHTML = ''; els.uploadZone.classList.remove('is-drag-over'); } function setupUpload() { els.uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); els.uploadZone.classList.add('is-drag-over'); }); els.uploadZone.addEventListener('dragleave', (e) => { if (!els.uploadZone.contains(e.relatedTarget)) { els.uploadZone.classList.remove('is-drag-over'); } }); els.uploadZone.addEventListener('drop', (e) => { e.preventDefault(); els.uploadZone.classList.remove('is-drag-over'); if (e.dataTransfer?.files?.length) handleFiles(e.dataTransfer.files); }); els.uploadZone.addEventListener('click', (e) => { if (e.target === els.uploadClear || els.uploadClear?.contains(e.target)) return; if (e.target.tagName === 'LABEL') return; els.uploadInput.click(); }); els.uploadInput.addEventListener('change', () => { if (els.uploadInput.files?.length) handleFiles(els.uploadInput.files); }); els.uploadClear.addEventListener('click', () => { resetUpload(); els.input.value = ''; els.status.textContent = ''; }); } async function handleFiles(fileList) { const allowed = ['pdf', 'docx', 'txt']; const files = Array.from(fileList).slice(0, 5); for (const file of files) { const ext = file.name.split('.').pop().toLowerCase(); if (!allowed.includes(ext)) { els.status.textContent = `Skipped ${file.name}: unsupported type. Use .pdf, .docx, or .txt.`; return; } } els.status.textContent = files.length === 1 ? `Extracting ${files[0].name}…` : `Extracting ${files.length} files…`; setBusy(true); const parts = []; let totalChars = 0; let anyTruncated = false; try { for (const file of files) { const formData = new FormData(); formData.append('file', file); const resp = await fetch('api/extract.php', { method: 'POST', credentials: 'same-origin', body: formData, }); const data = await resp.json().catch(() => ({})); if (!resp.ok || !data.ok) { throw new Error(data.error?.message || `Extraction failed for ${file.name} (HTTP ${resp.status}).`); } parts.push({ filename: file.name, chars: data.chars, truncated: data.truncated, text: data.text }); totalChars += data.chars; if (data.truncated) anyTruncated = true; } const combined = parts.length === 1 ? parts[0].text : parts.map((p) => `--- Document: ${p.filename} ---\n\n${p.text}`).join('\n\n'); const MAX_COMBINED = 128000; const combinedTruncated = combined.length > MAX_COMBINED; els.input.value = combinedTruncated ? combined.slice(0, MAX_COMBINED) : combined; els.uploadFileList.innerHTML = parts .map((p) => `
  • ${escapeHtml(p.filename)}${p.chars.toLocaleString()} chars${p.truncated ? ' • per-file limit reached' : ''}
  • `) .join(''); els.uploadPrompt.classList.add('is-hidden'); els.uploadFileInfo.classList.remove('is-hidden'); const truncNote = (anyTruncated || combinedTruncated) ? ' — truncated to 128 000 char limit' : ''; els.status.textContent = parts.length === 1 ? `Extracted ${totalChars.toLocaleString()} chars from ${parts[0].filename}${truncNote}.` : `Extracted ${totalChars.toLocaleString()} chars total from ${parts.length} files${truncNote}.`; } catch (err) { els.status.textContent = err.message; resetUpload(); } finally { setBusy(false); } } function setupAliases() { els.addAliasRow.addEventListener('click', () => { const row = document.createElement('div'); row.className = 'alias-row'; row.innerHTML = [ '', '', '', '', ].join(''); els.aliasRows.appendChild(row); row.querySelector('.alias-original').focus(); }); els.aliasRows.addEventListener('click', (e) => { const btn = e.target.closest('.alias-remove'); if (btn) btn.closest('.alias-row').remove(); }); } function getAliases() { return Array.from(els.aliasRows.querySelectorAll('.alias-row')).flatMap((row) => { const original = row.querySelector('.alias-original')?.value.trim() ?? ''; const alias = row.querySelector('.alias-label')?.value.trim() ?? ''; return original && alias ? [{ original, alias }] : []; }); } function resetAliases() { if (els.aliasRows) els.aliasRows.innerHTML = ''; } async function checkHealth() { els.healthPill.textContent = 'Checking...'; try { const response = await fetch('api/health.php', { method: 'GET', headers: { Accept: 'application/json' }, credentials: 'same-origin', }); const data = await response.json(); els.healthPill.textContent = data.ok ? 'Healthy' : 'Needs config'; els.healthPill.classList.toggle('is-warning', !data.ok); if (!data.ok && data.checks) { renderHealth(data); } } catch (error) { els.healthPill.textContent = 'Health failed'; els.healthPill.classList.add('is-warning'); } } async function postJson(url, payload) { const response = await fetch(url, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, credentials: 'same-origin', body: JSON.stringify(payload), }); const data = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(data.error?.message || `Request failed with HTTP ${response.status}.`); } return data; } function setBusy(isBusy) { const button = document.querySelector('#runButton'); button.disabled = isBusy; button.textContent = isBusy ? (state.activeTool === 'transcribe' ? 'Transcribing...' : 'Running...') : 'Run Tool'; } function currentLanguage() { return document.querySelector('input[name="language"]:checked')?.value || 'en'; } function currentRedactionMode() { return document.querySelector('input[name="redactionMode"]:checked')?.value || 'standard'; } function currentRedactionRegion() { return document.querySelector('input[name="redactionRegion"]:checked')?.value || 'nordic'; } function renderResults(data) { const sections = []; sections.push(sectionHtml('What We Found', renderMainFinding(data))); sections.push(sectionHtml('Evidence Trail', renderEvidence(data))); sections.push(sectionHtml('What Remains Uncertain', renderListish(data.what_remains_uncertain))); sections.push(sectionHtml('Next Practical Step', `

    ${escapeHtml(data.next_practical_step || 'Review the evidence trail.')}

    `)); if (data.disclaimer) { sections.push(`

    ${escapeHtml(data.disclaimer)}

    `); } els.results.innerHTML = sections.join(''); } function renderMainFinding(data) { if (data.tool === 'ask') { return `

    ${escapeHtml(data.answer || data.what_we_found || '')}

    `; } if (data.tool === 'redact') { return `
    ${escapeHtml(data.redacted_text || '')}
    ${renderEntityCounts(data.entity_counts)}`; } if (data.tool === 'timeline') { lastTimelineEvents = data.events || []; const csvBtn = lastTimelineEvents.length ? `
    ` : ''; return `

    ${escapeHtml(data.what_we_found || '')}

    ${renderTimeline(lastTimelineEvents)}${csvBtn}`; } if (data.tool === 'summarize') { return [ `

    ${escapeHtml(data.what_we_found || '')}

    `, detailList('Key Facts', data.key_facts), detailList('Dates', data.dates), detailList('Parties', data.parties), detailList('Legal References Detected', data.legal_references_detected), ].join(''); } if (data.tool === 'search') { return `

    ${escapeHtml(data.what_we_found || '')}

    `; } return `

    ${escapeHtml(data.what_we_found || '')}

    `; } function currentTranscribeLang() { return document.querySelector('input[name="transcribeLang"]:checked')?.value || 'auto'; } function renderEvidence(data) { const items = data.evidence_trail || data.sources || data.hits || []; if (!items.length) { return '

    No evidence trail was available for this request.

    '; } return `
    ${items.map(renderEvidenceItem).join('')}
    `; } function renderEvidenceItem(item) { const title = item.title || item.citation || 'Source'; const body = item.excerpt || item.why_it_matters || item.citation || ''; const chunkText = item.chunk_text || ''; const meta = [ item.package_or_corpus, item.section, item.score !== undefined && item.score !== null ? `score ${item.score}` : '', ].filter(Boolean).join(' · '); const chunkToggle = (chunkText && chunkText !== body) ? `
    View chunk
    ${escapeHtml(chunkText)}
    ` : ''; return `

    ${escapeHtml(title)}

    ${meta ? `

    ${escapeHtml(meta)}

    ` : ''}

    ${escapeHtml(body)}

    ${chunkToggle}
    `; } function renderTimeline(events) { if (!events.length) { return '

    No events were identified.

    '; } return `
      ${events.map((ev) => `
    1. ${escapeHtml(ev.date || 'unknown')} ${ev.date_type ? `${escapeHtml(ev.date_type)}` : ''} ${escapeHtml(ev.actor || 'unknown actor')}

      ${escapeHtml(ev.event || '')}

      ${ev.source_excerpt ? `${escapeHtml(ev.source_excerpt)}` : ''}
    2. `).join('')}
    `; } function exportTimelineCSV(events) { const header = ['Date', 'Date Type', 'Actor', 'Event', 'Source Excerpt', 'Confidence']; const rows = events.map((ev) => [ ev.date || '', ev.date_type || '', ev.actor || '', ev.event || '', ev.source_excerpt || '', ev.confidence || '', ]); const csv = [header, ...rows] .map((row) => row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(',')) .join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const url = URL.createObjectURL(blob); const a = Object.assign(document.createElement('a'), { href: url, download: 'timeline.csv' }); a.click(); URL.revokeObjectURL(url); } async function runTranscribe() { if (!lastAudioFile) { els.status.textContent = 'Choose an audio file before transcribing.'; return; } setBusy(true); const startTime = Date.now(); let elapsed = 0; updateTranscribeTrace(0); els.status.textContent = 'Transcribing…'; const timer = setInterval(() => { elapsed = Math.floor((Date.now() - startTime) / 1000); const m = Math.floor(elapsed / 60); const s = elapsed % 60; els.status.textContent = m > 0 ? `Transcribing… ${m}:${pad2(s)}` : `Transcribing… ${s}s`; updateTranscribeTrace(elapsed); }, 1000); try { const formData = new FormData(); formData.append('audio', lastAudioFile); formData.append('language', currentTranscribeLang()); if (els.diarizeCheck?.checked) { formData.append('diarize', '1'); const n = parseInt(els.numSpeakersInput?.value || '', 10); if (n >= 2) formData.append('num_speakers', String(n)); } const resp = await fetch('api/transcribe.php', { method: 'POST', credentials: 'same-origin', body: formData, }); const data = await resp.json().catch(() => ({})); if (!resp.ok || !data.ok) { throw new Error(data.error?.message || `Transcription failed (HTTP ${resp.status}).`); } lastTranscriptData = data; renderTranscriptResults(data); const dur = data.duration_sec ? ` · Audio: ${Math.round(data.duration_sec)}s` : ''; els.status.textContent = `Done in ${data.latency_ms || 0} ms${dur}.`; } catch (error) { els.status.textContent = error.message; renderTrace([{ label: 'Transcription error', detail: error.message, status: 'warning' }]); } finally { clearInterval(timer); setBusy(false); } } function updateTranscribeTrace(elapsed) { let label, detail; if (elapsed < 10) { label = 'Uploading to Whisper'; detail = 'Sending audio to cuttlefish GPU…'; } else if (elapsed < 60) { label = 'Processing on GPU'; detail = 'Whisper is transcribing. Large files take 1–3 minutes.'; } else if (elapsed < 120) { label = 'Still processing…'; detail = `${Math.floor(elapsed / 60)} min elapsed — Whisper is working through the audio.`; } else { label = 'Still processing…'; detail = `${Math.floor(elapsed / 60)} min ${pad2(elapsed % 60)}s — long recordings can take several minutes.`; } renderTrace([{ label, detail, status: 'running' }]); } function renderTranscriptResults(data) { const speakerRoles = data.speaker_roles || {}; const segments = data.segments || []; const hasSpeakers = segments.some((s) => s.speaker); const speakerOrder = [...new Set(segments.filter((s) => s.speaker).map((s) => s.speaker))]; const rolesHtml = speakerOrder.length ? `

    ${speakerOrder.map((id, i) => { const role = speakerRoles[id] || id; return `${escapeHtml(role)}${escapeHtml(id)}`; }).join('')}

    ` : ''; const segmentsHtml = hasSpeakers ? `
    Segments (${segments.length})
    ${segments.map((seg) => { const idx = speakerOrder.indexOf(seg.speaker); const roleLabel = seg.speaker && speakerRoles[seg.speaker] ? `${speakerRoles[seg.speaker]} (${seg.speaker})` : (seg.speaker || ''); return `
    ${fmtTime(seg.start)}–${fmtTime(seg.end)} ${seg.speaker ? `${escapeHtml(roleLabel)}` : ''} ${escapeHtml(seg.text)}
    `; }).join('')}
    ` : ''; const dlSrtVtt = segments.length ? ` ` : ''; els.results.innerHTML = `

    Transcript

    ${rolesHtml}
    ${escapeHtml(data.transcript)}
    ${segmentsHtml}
    ${dlSrtVtt}
    `; const traceMeta = []; if (data.duration_sec) traceMeta.push({ label: `Duration: ${Math.round(data.duration_sec)}s`, detail: '', status: 'complete' }); if (data.language) traceMeta.push({ label: `Language: ${data.language}`, detail: '', status: 'complete' }); if (data.num_speakers > 1) traceMeta.push({ label: `Speakers detected: ${data.num_speakers}`, detail: Object.entries(speakerRoles).map(([id, r]) => `${id}: ${r}`).join(', ') || '', status: 'complete' }); if (data.model) traceMeta.push({ label: `Model: ${data.model}`, detail: '', status: 'complete' }); renderTrace(traceMeta.length ? traceMeta : [{ label: 'Transcribed', detail: '', status: 'complete' }]); } function fmtTime(secs) { const h = Math.floor(secs / 3600); const m = Math.floor((secs % 3600) / 60); const s = Math.floor(secs % 60); const parts = h > 0 ? [pad2(h), pad2(m), pad2(s)] : [pad2(m), pad2(s)]; return parts.join(':'); } function pad2(n) { return String(n).padStart(2, '0'); } function toSrtTime(secs) { const h = Math.floor(secs / 3600); const m = Math.floor((secs % 3600) / 60); const s = Math.floor(secs % 60); const ms = Math.round((secs % 1) * 1000); return `${pad2(h)}:${pad2(m)}:${pad2(s)},${String(ms).padStart(3, '0')}`; } function toVttTime(secs) { return toSrtTime(secs).replace(',', '.'); } function downloadBlob(blob, filename) { const url = URL.createObjectURL(blob); const a = Object.assign(document.createElement('a'), { href: url, download: filename }); a.click(); URL.revokeObjectURL(url); } function downloadTranscriptTxt() { if (!lastTranscriptData) return; downloadBlob(new Blob([lastTranscriptData.transcript], { type: 'text/plain' }), 'transcript.txt'); } function downloadTranscriptSrt() { if (!lastTranscriptData?.segments?.length) return; const { segments, speaker_roles: roles = {} } = lastTranscriptData; const lines = segments.map((seg, i) => { const spk = seg.speaker ? `[${roles[seg.speaker] || seg.speaker}] ` : ''; return `${i + 1}\n${toSrtTime(seg.start)} --> ${toSrtTime(seg.end)}\n${spk}${seg.text}\n`; }); downloadBlob(new Blob([lines.join('\n')], { type: 'text/srt' }), 'transcript.srt'); } function downloadTranscriptVtt() { if (!lastTranscriptData?.segments?.length) return; const { segments, speaker_roles: roles = {} } = lastTranscriptData; const lines = ['WEBVTT\n']; segments.forEach((seg) => { const spk = seg.speaker ? `` : ''; lines.push(`${toVttTime(seg.start)} --> ${toVttTime(seg.end)}\n${spk}${seg.text}\n`); }); downloadBlob(new Blob([lines.join('\n')], { type: 'text/vtt' }), 'transcript.vtt'); } function resetAudio() { lastAudioFile = null; if (!els.audioInput) return; els.audioInput.value = ''; if (els.audioPrompt) els.audioPrompt.classList.remove('is-hidden'); if (els.audioFileInfo) els.audioFileInfo.classList.add('is-hidden'); if (els.audioFileName) els.audioFileName.textContent = ''; if (els.audioFileSize) els.audioFileSize.textContent = ''; } function setupAudio() { if (!els.audioZone) return; els.audioZone.addEventListener('dragover', (e) => { e.preventDefault(); els.audioZone.classList.add('is-drag-over'); }); els.audioZone.addEventListener('dragleave', (e) => { if (!els.audioZone.contains(e.relatedTarget)) { els.audioZone.classList.remove('is-drag-over'); } }); els.audioZone.addEventListener('drop', (e) => { e.preventDefault(); els.audioZone.classList.remove('is-drag-over'); const f = e.dataTransfer?.files?.[0]; if (f) handleAudio(f); }); els.audioZone.addEventListener('click', (e) => { if (e.target === els.audioClear || els.audioClear?.contains(e.target)) return; if (e.target === els.audioInput) return; if (e.target.tagName === 'LABEL') return; els.audioInput.click(); }); els.audioInput.addEventListener('change', () => { const f = els.audioInput.files?.[0]; if (f) handleAudio(f); }); els.audioClear.addEventListener('click', () => { resetAudio(); els.status.textContent = ''; }); } function handleAudio(file) { const allowedExts = ['mp3', 'wav', 'ogg', 'oga', 'm4a', 'mp4', 'flac', 'webm', 'aac']; const ext = file.name.split('.').pop().toLowerCase(); if (!allowedExts.includes(ext)) { els.status.textContent = `Unsupported format: .${ext}. Use MP3, WAV, OGG, M4A, FLAC, or WebM.`; return; } const sizeMB = file.size / 1024 / 1024; if (sizeMB > 200) { els.status.textContent = `File too large (${sizeMB.toFixed(1)} MB). Maximum 200 MB.`; return; } lastAudioFile = file; if (els.audioFileName) els.audioFileName.textContent = file.name; if (els.audioFileSize) els.audioFileSize.textContent = `${sizeMB.toFixed(1)} MB`; if (els.audioPrompt) els.audioPrompt.classList.add('is-hidden'); if (els.audioFileInfo) els.audioFileInfo.classList.remove('is-hidden'); els.status.textContent = `Ready: ${file.name} (${sizeMB.toFixed(1)} MB)`; } function renderEntityCounts(counts = {}) { const entries = Object.entries(counts).filter(([, count]) => Number(count) > 0); if (!entries.length) { return '

    No deterministic sensitive categories detected.

    '; } return ``; } function detailList(title, values = []) { if (!Array.isArray(values) || !values.length) { return ''; } return `

    ${escapeHtml(title)}

    `; } function renderListish(value) { if (Array.isArray(value)) { if (!value.length) { return '

    No uncertainty listed.

    '; } return ``; } return `

    ${escapeHtml(value || 'No uncertainty listed.')}

    `; } function sectionHtml(title, content) { return `

    ${escapeHtml(title)}

    ${content}
    `; } function renderTrace(trace) { if (!trace.length) { els.traceList.innerHTML = `
  • Waiting

    Run a tool to see interpretation, retrieval, confidence, uncertainty, and next step.

  • `; return; } els.traceList.innerHTML = trace.map((item) => `
  • ${escapeHtml(item.label || 'Step')}

    ${escapeHtml(item.detail || '')}

  • `).join(''); } function renderHealth(data) { const checks = Object.entries(data.checks || {}).map(([name, check]) => ({ label: name.replaceAll('_', ' '), detail: check.detail || '', status: check.ok ? 'complete' : 'warning', })); renderTrace(checks); } function escapeHtml(value) { return String(value ?? '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); }