/* barnevernet.js — page-scoped UI for /barnevernet.php */ (function () { 'use strict'; const els = {}; let lang = window.DBN_TOOLS_LANG || localStorage.getItem('dbn-ui-lang') || 'en'; let uploadFiles = []; let lastResult = null; let branchContext = null; const SLICE_DEFS = [ { id: 'child_welfare', label: 'Child Welfare' }, { id: 'echr', label: 'ECHR' }, { id: 'family_core', label: 'Family Law Core' }, { id: 'bufdir_guidance', label: 'Bufdir Guidance' }, { id: 'norwegian_courts', label: 'Norwegian Courts' }, { id: 'hague', label: 'Hague Convention' }, { id: 'broader_legal', label: 'Broader Legal Support' }, { id: 'dbn_resources', label: 'DBN Resources' }, ]; const STEP_LABELS = [ 'Document classification', 'Party extraction', 'Timeline extraction', 'Sub-question generation', 'Corpus retrieval', 'Synthesis', 'Citation confidence', ]; const stepKeyToIndex = { doc_classify: 0, party_extract: 1, timeline_extract: 2, sub_question_gen: 3, slice_resolution: 3, // shown under sub-question gen phase upload_indexing: 4, retrieval: 4, synthesis: 5, confidence: 6, }; document.addEventListener('DOMContentLoaded', () => { if (!document.body.dataset.activeTool || document.body.dataset.activeTool !== 'barnevernet') return; Object.assign(els, { form: document.getElementById('bvjForm'), notes: document.getElementById('bvjNotes'), status: document.getElementById('bvjStatus'), runButton: document.getElementById('bvjRunButton'), results: document.getElementById('bvjResults'), traceList: document.getElementById('traceList'), roleSelect: document.getElementById('bvjRoleSelect'), roleCustom: document.getElementById('bvjRoleCustom'), slices: Array.from(document.querySelectorAll('.adv-slice')), langButtons: Array.from(document.querySelectorAll('#bvjLangSwitcher .lang-btn')), engineRadios: Array.from(document.querySelectorAll('input[name="bvjEngine"]')), subQ: document.getElementById('bvjSubQ'), subQVal: document.getElementById('bvjSubQValue'), chunkLimit: document.getElementById('bvjChunkLimit'), chunkLimitVal: document.getElementById('bvjChunkLimitValue'), sim: document.getElementById('bvjSim'), simVal: document.getElementById('bvjSimValue'), topK: document.getElementById('bvjTopK'), topKVal: document.getElementById('bvjTopKValue'), temp: document.getElementById('bvjTemp'), tempVal: document.getElementById('bvjTempValue'), uploadZone: document.getElementById('bvjUploadZone'), uploadInput: document.getElementById('bvjUploadInput'), uploadPrompt: document.getElementById('bvjUploadPrompt'), uploadFileInfo: document.getElementById('bvjUploadFileInfo'), uploadFileList: document.getElementById('bvjUploadFileList'), uploadClear: document.getElementById('bvjUploadClear'), modal: document.getElementById('bvjSourceModal'), modalClose: document.getElementById('bvjSourceModalClose'), modalTitle: document.getElementById('bvjSourceModalTitle'), modalEyebrow: document.getElementById('bvjSourceModalEyebrow'), modalMeta: document.getElementById('bvjSourceModalMeta'), modalText: document.getElementById('bvjSourceModalText'), branchPanel: document.getElementById('bvjBranchPanel'), branchClear: document.getElementById('bvjBranchClear'), branchOrigin: document.getElementById('bvjBranchOrigin'), branchSummary: document.getElementById('bvjBranchSummary'), branchNotes: document.getElementById('bvjBranchNotes'), }); if (!els.form) return; bindRole(); bindSlices(); bindLang(); bindRanges(); bindUpload(); bindModal(); bindBranch(); els.form.addEventListener('submit', onSubmit); els.results.addEventListener('click', (e) => { const btn = e.target.closest('.dr-branch-btn'); if (btn) branchFromSubQ(btn.dataset.question || ''); }); renderTrace(STEP_LABELS.map((label) => ({ label, detail: 'Waiting…', status: 'idle' }))); }); // ── Role binding ─────────────────────────────────────────────────────────── function bindRole() { if (!els.roleSelect) return; els.roleSelect.addEventListener('change', () => { const isOther = els.roleSelect.value === '__other__'; els.roleCustom.classList.toggle('is-hidden', !isOther); if (isOther) els.roleCustom.focus(); }); } function getAdvocateRole() { if (!els.roleSelect) return ''; if (els.roleSelect.value === '__other__') { return (els.roleCustom ? els.roleCustom.value.trim() : ''); } return els.roleSelect.value; } // ── Corpus slice toggles ─────────────────────────────────────────────────── function bindSlices() { els.slices.forEach((btn) => { btn.addEventListener('click', () => { const isOn = btn.classList.toggle('is-on'); btn.setAttribute('aria-pressed', isOn ? 'true' : 'false'); const badge = btn.querySelector('.dr-slice__badge'); if (badge) badge.textContent = isOn ? 'on' : 'off'; }); }); } function getSelectedSlices() { const out = {}; SLICE_DEFS.forEach((s) => { const btn = els.slices.find((b) => b.dataset.slice === s.id); out[s.id] = !!(btn && btn.classList.contains('is-on')); }); return out; } // ── Language ─────────────────────────────────────────────────────────────── function bindLang() { els.langButtons.forEach((b) => { b.classList.toggle('is-active', b.dataset.lang === lang); b.addEventListener('click', () => { els.langButtons.forEach((x) => x.classList.remove('is-active')); b.classList.add('is-active'); lang = b.dataset.lang || 'en'; localStorage.setItem('dbn-ui-lang', lang); }); }); } // ── Range controls ───────────────────────────────────────────────────────── function bindRanges() { const pairs = [ [els.subQ, els.subQVal, (v) => v], [els.chunkLimit, els.chunkLimitVal, (v) => v], [els.sim, els.simVal, (v) => Number(v).toFixed(2)], [els.topK, els.topKVal, (v) => v], [els.temp, els.tempVal, (v) => Number(v).toFixed(2)], ]; pairs.forEach(([range, label, fmt]) => { if (!range || !label) return; const sync = () => { label.textContent = fmt(range.value); }; range.addEventListener('input', sync); sync(); }); } function getControls() { return { sub_q_count: parseInt(els.subQ.value, 10), chunk_limit: parseInt(els.chunkLimit.value, 10), similarity_threshold: parseFloat(els.sim.value), reranker_top_k: parseInt(els.topK.value, 10), temperature: parseFloat(els.temp.value), }; } function getEngine() { const checked = els.engineRadios.find((r) => r.checked); return checked ? checked.value : 'azure_mini'; } // ── File upload ──────────────────────────────────────────────────────────── function bindUpload() { if (!els.uploadZone) return; const onFiles = (fileList) => { const files = Array.from(fileList || []).slice(0, 5); if (uploadFiles.length + files.length > 5) { setStatus('At most 5 files can be uploaded per request.', 'error'); return; } files.forEach((f) => { if (f.size > 8 * 1024 * 1024) { setStatus(`${f.name} exceeds the 8 MB limit.`, 'error'); return; } const ext = (f.name.split('.').pop() || '').toLowerCase(); if (!['pdf', 'docx', 'txt'].includes(ext)) { setStatus(`${f.name} is not a supported file type (PDF, DOCX, TXT).`, 'error'); return; } uploadFiles.push(f); }); renderUploadList(); }; els.uploadInput.addEventListener('change', (e) => onFiles(e.target.files)); els.uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); els.uploadZone.classList.add('is-drop'); }); els.uploadZone.addEventListener('dragleave', () => els.uploadZone.classList.remove('is-drop')); els.uploadZone.addEventListener('drop', (e) => { e.preventDefault(); els.uploadZone.classList.remove('is-drop'); onFiles(e.dataTransfer?.files); }); els.uploadClear?.addEventListener('click', () => { uploadFiles = []; els.uploadInput.value = ''; renderUploadList(); }); } function renderUploadList() { if (!uploadFiles.length) { els.uploadFileInfo.classList.add('is-hidden'); els.uploadPrompt.classList.remove('is-hidden'); return; } els.uploadPrompt.classList.add('is-hidden'); els.uploadFileInfo.classList.remove('is-hidden'); els.uploadFileList.innerHTML = uploadFiles.map((f) => { const kb = (f.size / 1024).toFixed(0); return `
  • ${escapeHtml(f.name)}${kb} KB
  • `; }).join(''); } // ── Source modal ─────────────────────────────────────────────────────────── function bindModal() { els.modalClose?.addEventListener('click', closeModal); els.modal?.addEventListener('click', (e) => { if (e.target === els.modal) closeModal(); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && els.modal && !els.modal.classList.contains('is-hidden')) closeModal(); }); } function closeModal() { els.modal?.classList.add('is-hidden'); } function openModal(source) { if (!source) return; els.modalEyebrow.textContent = source.source_origin === 'upload' ? 'Uploaded file' : 'Corpus source'; els.modalTitle.textContent = source.title || 'Source'; const metaRows = [ ['Number', `[${source.n}]`], source.section ? ['Section', source.section] : null, ['Corpus / package', source.package_or_corpus || '—'], source.authority_type ? ['Authority', source.authority_type] : null, source.jurisdiction ? ['Jurisdiction', source.jurisdiction] : null, source.similarity != null ? ['Similarity', String(source.similarity)] : null, source.reranker_score != null ? ['Rerank score', String(source.reranker_score)] : null, source.matched_sub_questions?.length ? ['Matched sub-Q', source.matched_sub_questions.join(', ')] : null, ].filter(Boolean); els.modalMeta.innerHTML = '
    ' + metaRows.map(([k, v]) => `
    ${escapeHtml(k)}
    ${escapeHtml(String(v))}
    `).join('') + '
    '; const summary = source.summary || ''; const chunkText = source.chunk_text || source.excerpt || ''; const isUpload = source.source_origin === 'upload'; const hasDocId = source.document_id != null; let html = summary ? `
    ${escapeHtml(summary)}
    ` : `
    Summary not yet generated — showing raw chunk below.
    `; if (chunkText) { html += ``; html += ``; } if (!isUpload && hasDocId) { html += ``; html += `
    `; } els.modalText.innerHTML = html; const chunkToggle = els.modalText.querySelector('.dr-modal-chunk-toggle'); const chunkDiv = els.modalText.querySelector('.dr-modal-chunk-text'); chunkToggle?.addEventListener('click', () => { const isHidden = chunkDiv.classList.toggle('is-hidden'); chunkToggle.textContent = isHidden ? 'Show matching chunk ▼' : 'Hide matching chunk ▲'; }); const allChunksBtn = els.modalText.querySelector('.dr-modal-all-chunks'); const chunksListDiv = els.modalText.querySelector('.dr-modal-chunks-list'); if (allChunksBtn && chunksListDiv) { allChunksBtn.addEventListener('click', async () => { allChunksBtn.disabled = true; allChunksBtn.textContent = 'Loading…'; try { const res = await fetch(`api/document-chunks.php?document_id=${source.document_id}`, { credentials: 'same-origin' }); const data = await res.json(); if (data.ok && data.chunks) { chunksListDiv.innerHTML = `
    ${escapeHtml(data.document?.title || '')} · ${data.chunks.length} chunks
    ` + data.chunks.map((c) => `
    #${c.chunk_index + 1}${c.section_title ? ' · ' + escapeHtml(c.section_title) : ''}

    ${escapeHtml(truncate(c.content, 300))}

    `).join(''); allChunksBtn.remove(); } else { allChunksBtn.textContent = 'Could not load chunks.'; allChunksBtn.disabled = false; } } catch (_) { allChunksBtn.textContent = 'Error loading chunks.'; allChunksBtn.disabled = false; } }); } els.modal.classList.remove('is-hidden'); } // ── Branch context ───────────────────────────────────────────────────────── function bindBranch() { if (!els.branchClear) return; els.branchClear.addEventListener('click', clearBranch); } function clearBranch() { branchContext = null; if (els.branchPanel) els.branchPanel.classList.add('is-hidden'); if (els.branchNotes) els.branchNotes.value = ''; } function branchFromSubQ(question) { if (!lastResult || !question) return; branchContext = { original_query: lastResult.query || '', brief_summary: (lastResult.advocacy_brief || '').slice(0, 600), what_we_found: lastResult.what_we_found || '', top_sources: (lastResult.sources || []).slice(0, 5).map((s) => ({ n: s.n, title: s.title, excerpt: (s.excerpt || '').slice(0, 200), })), }; // Pre-fill notes textarea (branch uses notes field, not a query textarea) if (els.notes) els.notes.value = question; if (els.branchOrigin) els.branchOrigin.textContent = 'Original query: ' + branchContext.original_query; if (els.branchSummary) els.branchSummary.textContent = branchContext.brief_summary; if (els.branchPanel) els.branchPanel.classList.remove('is-hidden'); els.form.scrollIntoView({ behavior: 'smooth', block: 'start' }); } // ── Form submission ──────────────────────────────────────────────────────── async function onSubmit(e) { e.preventDefault(); const advocateRole = getAdvocateRole(); if (!advocateRole) { setStatus('Select who you are representing before running.', 'error'); return; } if (!uploadFiles.length) { setStatus('Upload at least one BVJ document before running.', 'error'); return; } const slices = getSelectedSlices(); if (!Object.values(slices).some(Boolean)) { setStatus('Enable at least one corpus slice.', 'error'); return; } const engine = getEngine(); const additionalNotes = (els.notes ? els.notes.value : '').trim(); const expectedDuration = engine === 'azure_full' ? '90–180 seconds with Azure gpt-4o' : (engine === 'gpu' ? '45–90 seconds on GPU' : (engine === 'dbn_legal' ? '60–120 seconds with Norwegian specialist' : '30–60 seconds with Azure gpt-4o-mini')); setStatus(`Analysing document for ${advocateRole}… (${expectedDuration})`, 'busy'); els.runButton.disabled = true; // Clear results area but leave room for progressive renders els.results.innerHTML = `

    Analysing…

    Document classification, party extraction, and timeline are running. Legal corpus retrieval and advocacy synthesis follow. Expect ${expectedDuration}.

    `; const stepState = STEP_LABELS.map((label) => ({ label, detail: 'Queued', status: 'idle' })); renderTrace(stepState); const payload = { advocate_role: advocateRole, engine, language: lang, slices, controls: getControls(), additional_notes: additionalNotes, }; if (branchContext) { payload.prior_context = branchContext; payload.branch_notes = (els.branchNotes ? els.branchNotes.value : '').trim(); } // Always multipart — files are required const form = new FormData(); form.append('payload', JSON.stringify(payload)); uploadFiles.forEach((f) => form.append('files[]', f)); let response; try { response = await fetch('api/barnevernet.php', { method: 'POST', body: form, credentials: 'same-origin' }); } catch (err) { setStatus(`Network error: ${err.message || err}`, 'error'); els.runButton.disabled = false; stepState[0] = { ...stepState[0], status: 'error', detail: String(err) }; renderTrace(stepState); return; } if (!response.ok || !response.body) { setStatus(`Request failed (${response.status}).`, 'error'); els.runButton.disabled = false; return; } const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = ''; let finalResult = null; let errorEvent = null; // Track whether progressive sections have been rendered let docMetaRendered = false; let partiesRendered = false; let timelineRendered = false; function handleStreamEvent(evt) { if (!evt || !evt.event) return; if (evt.event === 'progress') { if (evt.detail) setStatus(evt.detail, 'busy'); return; } if (evt.event === 'start') { setStatus(`Running… engine=${evt.engine}, files=${evt.file_count || 0}`, 'busy'); return; } if (evt.event === 'step') { const idx = stepKeyToIndex[evt.step]; if (idx !== undefined) { if (evt.status === 'running' && stepState[idx].status !== 'running') { stepState[idx] = { label: evt.label || stepState[idx].label, detail: evt.detail || 'Running…', status: 'running' }; } else if (evt.status !== 'running') { stepState[idx] = { label: evt.label || stepState[idx].label, detail: evt.detail || stepState[idx].detail, status: evt.status || stepState[idx].status, }; } renderTrace(stepState); } return; } if (evt.event === 'doc_meta') { if (!docMetaRendered) { renderDocMetaIntoResults(evt.result || {}); docMetaRendered = true; } return; } if (evt.event === 'parties') { if (!partiesRendered && Array.isArray(evt.parties)) { renderPartiesIntoResults(evt.parties); partiesRendered = true; } return; } if (evt.event === 'timeline') { if (!timelineRendered && Array.isArray(evt.events)) { renderTimelineIntoResults(evt.events); timelineRendered = true; } return; } if (evt.event === 'subq') { setStatus(`Retrieving sub-question ${evt.index}/${evt.total}: ${String(evt.question || '').slice(0, 80)}${String(evt.question || '').length > 80 ? '…' : ''}`, 'busy'); return; } if (evt.event === 'final') { finalResult = evt.result; return; } if (evt.event === 'error') { errorEvent = evt; return; } } while (true) { let chunk; try { chunk = await reader.read(); } catch (err) { setStatus(`Stream error: ${err.message || err}`, 'error'); els.runButton.disabled = false; return; } const { done, value } = chunk; if (value) { buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; let evt; try { evt = JSON.parse(trimmed); } catch (_) { continue; } handleStreamEvent(evt); } } if (done) break; } if (errorEvent) { setStatus(`${errorEvent.code}: ${errorEvent.message}`, 'error'); els.runButton.disabled = false; const runningIdx = stepState.findIndex((s) => s.status === 'running'); if (runningIdx >= 0) { stepState[runningIdx] = { ...stepState[runningIdx], status: 'error', detail: errorEvent.message }; renderTrace(stepState); } return; } if (!finalResult) { setStatus('Stream ended without a final result.', 'error'); els.runButton.disabled = false; return; } lastResult = finalResult; const meta = finalResult.trace_metadata || {}; setStatus( `Done in ${Math.round((finalResult.latency_ms || 0) / 1000)} s · ${meta.source_count || 0} sources · confidence ${meta.citation_confidence || '?'}`, 'ok' ); els.runButton.disabled = false; renderTrace(finalResult.trace || []); renderFinalResults(finalResult); } // ── Progressive rendering (renders as stream events arrive) ──────────────── function ensureResultsReady() { // If the empty-state is still shown, clear it for progressive inserts const emptyState = els.results.querySelector('.empty-state'); if (emptyState) emptyState.remove(); } function renderDocMetaIntoResults(meta) { ensureResultsReady(); const existing = els.results.querySelector('#bvjDocMetaSection'); if (existing) existing.remove(); const docType = meta.doc_type || 'BVJ Document'; const docDate = meta.doc_date || ''; const authority = meta.issuing_authority || ''; const refNo = meta.reference_number || ''; const childInfo = meta.child_info || ''; const fields = [ docDate ? ['Date', docDate] : null, authority ? ['Issuing authority', authority] : null, refNo ? ['Reference', refNo] : null, childInfo ? ['Child', childInfo] : null, ].filter(Boolean); const section = document.createElement('div'); section.id = 'bvjDocMetaSection'; section.className = 'bvj-doc-meta'; section.innerHTML = `
    ${escapeHtml(authority || docType)} ${escapeHtml(docType)}
    ${fields.length ? `
    ${fields.map(([k, v]) => `${escapeHtml(k)}: ${escapeHtml(String(v))}`).join('')}
    ` : ''} `; els.results.insertBefore(section, els.results.firstChild); } function renderPartiesIntoResults(parties) { ensureResultsReady(); const existing = els.results.querySelector('#bvjPartiesSection'); if (existing) existing.remove(); if (!parties.length) return; const roleClass = (role) => { const r = (role || '').toLowerCase(); if (r.includes('bvv') || r.includes('barnevern') || r.includes('saksbehandler') || r.includes('casework') || r.includes('melder')) return 'bvj-party-role--bvv'; if (r.includes('mother') || r.includes('mor') || r.includes('father') || r.includes('far') || r.includes('parent') || r.includes('foreldre') || r.includes('foster')) return 'bvj-party-role--parent'; if (r.includes('child') || r.includes('barn')) return 'bvj-party-role--child'; if (r.includes('third') || r.includes('tredje') || r.includes('politi') || r.includes('police')) return 'bvj-party-role--third'; return 'bvj-party-role--other'; }; const section = document.createElement('div'); section.id = 'bvjPartiesSection'; section.className = 'dr-result-block'; section.innerHTML = `

    Parties identified (${parties.length})

    ${parties.map((p) => `
    ${escapeHtml(p.role || 'Unknown')}
    ${escapeHtml(p.name || '—')}
    ${p.organization ? `
    ${escapeHtml(p.organization)}
    ` : ''} ${p.relationship_to_child ? `
    ${escapeHtml(p.relationship_to_child)}
    ` : ''}
    `).join('')}
    `; // Insert after doc meta const docMeta = els.results.querySelector('#bvjDocMetaSection'); if (docMeta && docMeta.nextSibling) { els.results.insertBefore(section, docMeta.nextSibling); } else { els.results.appendChild(section); } } function renderTimelineIntoResults(events) { ensureResultsReady(); const existing = els.results.querySelector('#bvjTimelineSection'); if (existing) existing.remove(); if (!events.length) return; const sigClass = (sig) => `bvj-timeline-event--${sig === 'high' ? 'high' : (sig === 'medium' ? 'medium' : 'low')}`; const section = document.createElement('div'); section.id = 'bvjTimelineSection'; section.className = 'dr-result-block'; section.innerHTML = `

    Timeline (${events.length} events)

    ${events.map((ev) => { const sig = ev.significance || 'low'; const timeStr = ev.time_of_day ? `
    ${escapeHtml(ev.time_of_day)}` : ''; return `
    ${escapeHtml(ev.date || '?')}${timeStr}
    ${escapeHtml(ev.actor || '')}
    ${escapeHtml(ev.action || '')}
    `; }).join('')}
    `; // Insert after parties section (or doc meta if no parties) const parties = els.results.querySelector('#bvjPartiesSection'); const docMeta = els.results.querySelector('#bvjDocMetaSection'); const anchor = parties || docMeta; if (anchor && anchor.nextSibling) { els.results.insertBefore(section, anchor.nextSibling); } else { els.results.appendChild(section); } } // ── Final render (after stream completes) ────────────────────────────────── function renderFinalResults(data) { const sources = data.sources || []; const subs = data.sub_questions || []; const role = data.advocate_role || ''; const redFlags = Array.isArray(data.procedural_red_flags) ? data.procedural_red_flags : []; const strengths = Array.isArray(data.client_strengths) ? data.client_strengths : []; const weaknesses = Array.isArray(data.opposing_weaknesses) ? data.opposing_weaknesses : []; // Remove any previously rendered progressive sections (will be re-inserted in order below) const toRemove = ['#bvjDocMetaSection', '#bvjPartiesSection', '#bvjTimelineSection']; toRemove.forEach((sel) => els.results.querySelector(sel)?.remove()); // Rebuild progressive sections from final data (authoritative) const docMeta = data.doc_meta || {}; const parties = data.parties || []; const timeline = data.timeline || {}; // Re-render progressive sections now that we have final data renderDocMetaIntoResults(docMeta); if (parties.length) renderPartiesIntoResults(parties); if ((timeline.events || []).length) renderTimelineIntoResults(timeline.events); // 4. Advocate banner const bannerHtml = role ? `
    Representing ${escapeHtml(role)}
    ` : ''; // 5. Client strengths const strengthsHtml = strengths.length ? `

    Your strongest arguments

    ` : ''; // 6. Advocacy brief const briefHtml = renderBrief(data.advocacy_brief || '', sources); // 7. Procedural red flags const redFlagsHtml = redFlags.length ? `

    Procedural red flags (${redFlags.length})

    ${redFlags.map((f) => renderRedFlag(f, sources)).join('')}
    ` : ''; // 8. Opposing weaknesses const weaknessesHtml = weaknesses.length ? `

    Gaps in the opposing position

    ` : ''; // 9. Sub-Q cards const subQReportsHtml = subs.length ? `

    What each sub-question researched

    ${subs.length} sub-question${subs.length === 1 ? '' : 's'} framed for ${escapeHtml(role || 'your client')}
    ${subs.map((sq, i) => renderSubQReport(sq, i)).join('')}
    ` : ''; // 10. Sources const sourcesHtml = sources.length ? `

    All sources (${sources.length})

    Click a card to see the full source · external link opens the original article
    ${sources.map((s) => renderSourceCard(s)).join('')}
    ` : ''; // 11. Uncertainty + next step const uncertHtml = (data.what_remains_uncertain || []).length ? `

    What remains uncertain

    ` : ''; const nextHtml = data.next_practical_step ? `

    Next practical step

    ${escapeHtml(data.next_practical_step)}

    ` : ''; // Append final sections after the progressive sections const finalHtml = ` ${bannerHtml} ${strengthsHtml}

    Advocacy brief

    ${briefHtml}
    ${redFlagsHtml} ${weaknessesHtml} ${subQReportsHtml} ${sourcesHtml} ${uncertHtml} ${nextHtml} `; // Append to results (after the progressive sections already in place) const finalContainer = document.createElement('div'); finalContainer.innerHTML = finalHtml; while (finalContainer.firstChild) { els.results.appendChild(finalContainer.firstChild); } // Bind source card clicks els.results.querySelectorAll('.dr-source-card[data-source-n]').forEach((node) => { node.addEventListener('click', (e) => { if (e.target.closest('a')) return; const n = parseInt(node.dataset.sourceN, 10); const src = sources.find((s) => s.n === n); if (src) { openModal(src); flashSource(n); } }); }); els.results.querySelectorAll('.dr-cite[data-source-n]').forEach((node) => { node.addEventListener('click', (e) => { if (e.target.closest('a')) return; flashSource(parseInt(node.dataset.sourceN, 10)); }); }); } // ── Component renderers ──────────────────────────────────────────────────── function renderRedFlag(flag, sources) { const severity = flag.severity || 'low'; const sevClass = `bvj-severity-${severity}`; const legal = flag.legal_basis || ''; const what = flag.what_to_check || ''; return `
    ${renderInlineCitations(escapeHtml(flag.description || ''), sources)}
    ${escapeHtml(severity)}
    ${legal ? `${escapeHtml(legal)}` : ''} ${what ? `
    What to verify

    ${escapeHtml(what)}

    ` : ''}
    `; } function renderSubQReport(sq, idx) { const top = sq.top_sources || []; const sourceItems = top.length ? top.map((s) => { const link = s.deep_link || s.source_url; const titleHtml = link ? `${escapeHtml(s.title || 'Untitled')} ` : `${escapeHtml(s.title || 'Untitled')}`; const meta = []; if (s.section) meta.push(escapeHtml(s.section)); if (s.authority_label) meta.push(escapeHtml(s.authority_label)); if (s.source_origin === 'upload') meta.push('your upload'); return `
  • [${s.n ?? '?'}]
    ${titleHtml} ${meta.length ? `
    ${meta.join(' · ')}
    ` : ''}
    ${escapeHtml(truncate(s.excerpt || '', 180))}
  • `; }).join('') : `
  • No sources retrieved for this sub-question.
  • `; return `
    ${escapeHtml(sq.id || ('q' + (idx + 1)))}
    ${escapeHtml(sq.question || '')}
    ${sq.rationale ? `
    ${escapeHtml(sq.rationale)}
    ` : ''}
    `; } function renderSourceCard(s) { const score = s.reranker_score != null ? s.reranker_score : s.similarity; const originTagClass = s.source_origin === 'upload' ? 'dr-source-tag dr-source-tag--upload' : 'dr-source-tag'; const originLabel = s.source_origin === 'upload' ? 'upload' : 'corpus'; const link = s.deep_link || s.source_url; const titleHtml = link ? `${escapeHtml(s.title || 'Untitled')} ` : `${escapeHtml(s.title || 'Untitled')}`; return `
    ${s.n}
    ${titleHtml}
    ${s.section ? `
    ${escapeHtml(s.section)}
    ` : ''}
    ${originLabel} ${s.authority_label ? `${escapeHtml(s.authority_label)}` : ''} ${escapeHtml(s.package_or_corpus || '—')} ${(s.matched_sub_questions || []).map((q) => `${escapeHtml(q)}`).join('')}

    ${escapeHtml(truncate(s.excerpt || '', 240))}

    score
    ${score != null ? Number(score).toFixed(2) : '—'}
    ${s.reranker_score != null && s.similarity != null ? `sim
    ${Number(s.similarity).toFixed(2)}
    ` : ''}
    `; } // ── Trace rendering ──────────────────────────────────────────────────────── function renderTrace(steps) { if (!els.traceList) return; els.traceList.classList.add('is-rich'); els.traceList.innerHTML = steps.map((step, i) => { const statusClass = step.status === 'running' ? 'is-running' : step.status === 'complete' ? 'is-done' : step.status === 'warning' ? 'is-warning' : step.status === 'error' ? 'is-error' : ''; const marker = step.status === 'complete' ? '✓' : step.status === 'warning' ? '!' : step.status === 'error' ? '×' : (i + 1); return `
  • ${marker}
    ${escapeHtml(step.label || '')} ${escapeHtml(step.detail || '')}
  • `; }).join(''); } // ── Utility ──────────────────────────────────────────────────────────────── function setStatus(message, kind) { els.status.textContent = message; els.status.style.color = kind === 'error' ? '#b41e1e' : (kind === 'ok' ? 'var(--teal-dark)' : 'var(--muted)'); } function flashSource(n) { document.querySelectorAll('.dr-source-card.is-highlight').forEach((c) => c.classList.remove('is-highlight')); const target = document.querySelector(`.dr-source-card[data-source-n="${n}"]`); if (target) { target.classList.add('is-highlight'); target.scrollIntoView({ behavior: 'smooth', block: 'center' }); setTimeout(() => target.classList.remove('is-highlight'), 1800); } } function renderBrief(markdown, sources) { if (!markdown) return '

    No brief was returned.

    '; const escaped = escapeHtml(markdown); const withCites = escaped.replace(/\[(\d+(?:\s*[-,]\s*\d+)*)\]/g, (_, group) => { const nums = expandCiteGroup(group); return nums.map((n) => `${n}`).join(''); }); // Also mark [DOC] references const withDoc = withCites.replace(/\[DOC\]/g, 'DOC'); const withBold = withDoc .replace(/\*\*([^*]+)\*\*/g, '$1') .replace(/(^|[^*])\*([^*]+)\*(?!\*)/g, '$1$2') .replace(/`([^`]+)`/g, '$1'); const paragraphs = withBold.split(/\n{2,}/).map((p) => { const t = p.trim(); if (!t) return ''; if (/^## /.test(t)) return `

    ${t.replace(/^## /, '')}

    `; if (/^### /.test(t)) return `

    ${t.replace(/^### /, '')}

    `; return `

    ${t.replace(/\n/g, '
    ')}

    `; }).join(''); return paragraphs; } function renderInlineCitations(escapedHtml, sources) { return escapedHtml.replace(/\[(\d+(?:\s*[-,]\s*\d+)*)\]/g, (_, group) => { const nums = expandCiteGroup(group); return nums.map((n) => `${n}`).join(''); }); } function expandCiteGroup(group) { const out = []; group.split(',').forEach((part) => { const range = part.trim().match(/^(\d+)\s*-\s*(\d+)$/); if (range) { const a = parseInt(range[1], 10); const b = parseInt(range[2], 10); for (let i = a; i <= b; i++) out.push(i); } else { const n = parseInt(part.trim(), 10); if (!Number.isNaN(n)) out.push(n); } }); return Array.from(new Set(out)); } function escapeHtml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function truncate(s, n) { if (!s) return ''; if (s.length <= n) return s; return s.slice(0, n - 1) + '…'; } })();