/* advocate.js — page-scoped UI for /advocate.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; let customSubQuestions = null; const CACHE_KEY = 'dbn-advocate-last'; const ROLE_FLIP = { 'Biological mother': 'Child welfare services (Barnevernet)', 'Biological father': 'Child welfare services (Barnevernet)', 'Both biological parents': 'Child welfare services (Barnevernet)', 'Foster carer / long-term placement': 'Biological mother', 'Adoptive parent': 'Biological mother', 'Child (via representative)': 'Child welfare services (Barnevernet)', 'Extended family (grandparent, sibling, aunt/uncle)': 'Child welfare services (Barnevernet)', 'Child welfare services (Barnevernet)': 'Both biological parents', }; const PARENT_ROLES = new Set([ 'Biological mother', 'Biological father', 'Both biological parents', 'Foster carer / long-term placement', 'Adoptive parent', 'Child (via representative)', 'Extended family (grandparent, sibling, aunt/uncle)', ]); let sliceHintShown = false; let synthTimer = null; let synthStartMs = null; const SLICE_DEFS = [ { id: 'family_core', label: 'Family Law Core' }, { id: 'child_welfare', label: 'Child Welfare' }, { id: 'echr', label: 'ECHR' }, { id: 'hague', label: 'Hague Convention' }, { id: 'norwegian_courts', label: 'Norwegian Courts' }, { id: 'bufdir_guidance', label: 'Bufdir Guidance' }, { id: 'broader_legal', label: 'Broader Legal Support' }, { id: 'dbn_resources', label: 'DBN Resources' }, ]; const STEP_LABELS = [ 'Query interpretation', 'Query expansion', 'Slice resolution', 'Upload indexing', 'Retrieval', 'Synthesis', 'Citation confidence', ]; document.addEventListener('DOMContentLoaded', () => { if (!document.body.dataset.activeTool || document.body.dataset.activeTool !== 'advocate') return; Object.assign(els, { form: document.getElementById('advocateForm'), input: document.getElementById('advInput'), status: document.getElementById('advStatus'), runButton: document.getElementById('advRunButton'), results: document.getElementById('advResults'), traceList: document.getElementById('traceList'), roleSelect: document.getElementById('advRoleSelect'), roleCustom: document.getElementById('advRoleCustom'), slices: Array.from(document.querySelectorAll('.adv-slice')), langButtons: Array.from(document.querySelectorAll('#advLangSwitcher .lang-btn')), tierRadios: Array.from(document.querySelectorAll('input[name="advTier"]')), subQ: document.getElementById('advSubQ'), subQVal: document.getElementById('advSubQValue'), chunkLimit: document.getElementById('advChunkLimit'), chunkLimitVal: document.getElementById('advChunkLimitValue'), sim: document.getElementById('advSim'), simVal: document.getElementById('advSimValue'), topK: document.getElementById('advTopK'), topKVal: document.getElementById('advTopKValue'), temp: document.getElementById('advTemp'), tempVal: document.getElementById('advTempValue'), uploadZone: document.getElementById('advUploadZone'), uploadInput: document.getElementById('advUploadInput'), uploadPrompt: document.getElementById('advUploadPrompt'), uploadFileInfo: document.getElementById('advUploadFileInfo'), uploadFileList: document.getElementById('advUploadFileList'), uploadClear: document.getElementById('advUploadClear'), modal: document.getElementById('advSourceModal'), modalClose: document.getElementById('advSourceModalClose'), modalTitle: document.getElementById('advSourceModalTitle'), modalEyebrow: document.getElementById('advSourceModalEyebrow'), modalMeta: document.getElementById('advSourceModalMeta'), modalText: document.getElementById('advSourceModalText'), branchPanel: document.getElementById('advBranchPanel'), branchClear: document.getElementById('advBranchClear'), branchOrigin: document.getElementById('advBranchOrigin'), branchSummary: document.getElementById('advBranchSummary'), branchNotes: document.getElementById('advBranchNotes'), inputCount: document.getElementById('advInputCount'), previewAngles: document.getElementById('advPreviewAngles'), subQPreview: document.getElementById('advSubQPreview'), subQPreviewList: document.getElementById('advSubQPreviewList'), runWithAngles: document.getElementById('advRunWithAngles'), discardAngles: document.getElementById('advDiscardAngles'), }); if (!els.form) return; bindRole(); bindSlices(); bindLang(); bindRanges(); bindUpload(); bindModal(); bindBranch(); bindPreviewAngles(); els.form.addEventListener('submit', onSubmit); els.results.addEventListener('click', (e) => { const btn = e.target.closest('.dr-branch-btn, .dr-strength-branch-btn'); if (btn) branchFromSubQ(btn.dataset.question || ''); }); renderTrace(STEP_LABELS.map((label) => ({ label, detail: 'Waiting…', status: 'idle' }))); els.input.addEventListener('input', updateCharCount); els.input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); els.form.requestSubmit(); } }); updateCharCount(); const cached = loadFromCache(); if (cached) showRestoreBanner(cached); }); 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(); if (!sliceHintShown && PARENT_ROLES.has(els.roleSelect.value)) { const echrBtn = els.slices.find((b) => b.dataset.slice === 'echr'); const ncBtn = els.slices.find((b) => b.dataset.slice === 'norwegian_courts'); const echrOff = echrBtn && !echrBtn.classList.contains('is-on'); const ncOff = ncBtn && !ncBtn.classList.contains('is-on'); if (echrOff || ncOff) { showSliceHint(echrBtn, ncBtn, echrOff, ncOff); sliceHintShown = true; } } }); } function getAdvocateRole() { if (!els.roleSelect) return ''; if (els.roleSelect.value === '__other__') { return (els.roleCustom ? els.roleCustom.value.trim() : ''); } return els.roleSelect.value; } 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 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); }); }); } 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 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.`, '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(''); } 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'); } 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; } function getTier() { const checked = els.tierRadios.find((r) => r.checked); return checked ? checked.value : 'quick'; } 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), }; } async function onSubmit(e) { e.preventDefault(); const advocateRole = getAdvocateRole(); if (!advocateRole) { setStatus('Select who you are representing before running.', 'error'); return; } const query = (els.input.value || '').trim(); if (!query && uploadFiles.length === 0) { setStatus('Describe the case situation or upload a file before running.', 'error'); return; } const slices = getSelectedSlices(); if (!Object.values(slices).some(Boolean)) { setStatus('Enable at least one corpus slice.', 'error'); return; } const tier = getTier(); const expectedDuration = tier === 'pro' ? '3–5 minutes with Claude Sonnet' : '2–4 minutes with Claude Haiku'; setStatus(`Building advocate brief for ${advocateRole}… (${expectedDuration})`, 'busy'); els.runButton.disabled = true; els.results.innerHTML = `

    Researching your case…

    The agent is generating adversarial sub-questions and retrieving from the legal corpus on behalf of ${escapeHtml(advocateRole)}. Live progress in the right-hand panel. Expect ${expectedDuration}.

    `; const stepState = STEP_LABELS.map((label) => ({ label, detail: 'Queued', status: 'idle' })); renderTrace(stepState); const payload = { query, paste_text: '', slices, tier, language: lang, controls: getControls(), advocate_role: advocateRole, use_my_case: (typeof window.dbnGetUseMyCase === 'function') ? window.dbnGetUseMyCase() : false, }; const _advDocIds = (document.getElementById('docPickerIds')?.value || '').split(',').map(Number).filter(Boolean); if (_advDocIds.length) payload.doc_ids = _advDocIds; if (branchContext) { payload.prior_context = branchContext; payload.branch_notes = (els.branchNotes ? els.branchNotes.value : '').trim(); } if (customSubQuestions) { payload.sub_questions_override = customSubQuestions; customSubQuestions = null; } const stepKeyToIndex = { interpretation: 0, expansion: 1, slice_resolution: 2, upload_indexing: 3, retrieval: 4, synthesis: 5, confidence: 6, }; let response; try { if (uploadFiles.length > 0) { const form = new FormData(); form.append('payload', JSON.stringify(payload)); uploadFiles.forEach((f) => form.append('files[]', f)); response = await fetch('api/deep-research.php', { method: 'POST', body: form, credentials: 'same-origin' }); } else { response = await fetch('api/deep-research.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), 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; 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; } finalResult.query = query; lastResult = finalResult; const meta = finalResult.trace_metadata || {}; const rc = meta.retrieval_counts || {}; const countSummary = (rc.post_filter_corpus != null) ? `${rc.post_filter_corpus} corpus${rc.filtered_website ? ` (${rc.filtered_website} website filtered)` : ''}${rc.raw_upload ? ` + ${rc.raw_upload} upload` : ''}` : `${meta.source_count || 0} sources`; setStatus( `Done in ${Math.round((finalResult.latency_ms || 0) / 1000)} s · ${countSummary} · confidence ${meta.citation_confidence || '?'}`, 'ok' ); els.runButton.disabled = false; renderTrace(finalResult.trace || []); renderResults(finalResult); saveToCache(finalResult, { query, role: advocateRole, tier, slices, lang }); function handleStreamEvent(evt) { if (!evt || !evt.event) return; if (evt.event === 'progress') { const d = evt.detail || ''; if (d) setStatus(d, 'busy'); return; } if (evt.event === 'start') { setStatus(`Running… engine=${evt.engine}, uploads=${evt.upload_count || 0}`, 'busy'); return; } if (evt.event === 'step') { const idx = stepKeyToIndex[evt.step]; if (idx === undefined) return; if (evt.step === 'synthesis') { if (evt.status === 'running') { synthStartMs = Date.now(); synthTimer = setInterval(() => { const elapsed = Math.round((Date.now() - synthStartMs) / 1000); stepState[5] = { ...stepState[5], detail: `Synthesising… (${elapsed}s)` }; renderTrace(stepState); }, 1000); } else if (synthTimer) { clearInterval(synthTimer); synthTimer = null; } } 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 === 'subq') { setStatus(`Retrieving sub-question ${evt.index}/${evt.total}: ${evt.question.slice(0, 80)}${evt.question.length > 80 ? '…' : ''}`, 'busy'); return; } if (evt.event === 'final') { finalResult = evt.result; return; } if (evt.event === 'error') { errorEvent = evt; return; } } } function setStatus(message, kind) { els.status.textContent = message; els.status.style.color = kind === 'error' ? '#b41e1e' : (kind === 'ok' ? 'var(--teal-dark)' : 'var(--muted)'); } 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(''); } function renderResults(data, restoreMode = false) { const sources = data.sources || []; const subs = data.sub_questions || []; const role = data.advocate_role || ''; const strengths = Array.isArray(data.client_strengths) ? data.client_strengths : []; const weaknesses = Array.isArray(data.opposing_weaknesses) ? data.opposing_weaknesses : []; // 1. Advocate banner + flip bar const flipRole = ROLE_FLIP[role] || ''; const bannerHtml = role ? `
    Representing ${escapeHtml(role)} Brief argues for this party · grounded in Norwegian law and ECHR authorities
    ${flipRole ? `
    See the other side? ${escapeHtml(flipRole)}
    ` : ''}` : ''; // 2. Client strengths — with ↗ deep-dive branch button per item const strengthsHtml = strengths.length ? `

    Your strongest arguments

    ` : ''; // 3. Brief const briefHtml = renderBrief(data.brief_markdown || '', sources); // 4. Opposing weaknesses const weaknessesHtml = weaknesses.length ? `

    Gaps in the opposing position

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

    What each sub-question agent researched

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

    All sources (${sources.length})

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

    What remains uncertain

    ` : ''; // 8. Next step const nextHtml = data.next_practical_step ? `

    Next practical step

    ${escapeHtml(data.next_practical_step)}

    ` : ''; els.results.innerHTML = `
    ${bannerHtml} ${strengthsHtml}

    Advocate brief

    ↓ .md
    ${briefHtml}
    ${weaknessesHtml} ${subQReportsHtml} ${sourcesHtml} ${uncertHtml} ${nextHtml} `; // Bind new-query button const nq = document.getElementById('advNewQuery'); if (nq) nq.addEventListener('click', () => { els.input.scrollIntoView({ behavior: 'smooth', block: 'center' }); setTimeout(() => els.input.focus(), 300); }); // Bind copy-brief const copyBtn = document.getElementById('advCopyBrief'); if (copyBtn) { copyBtn.addEventListener('click', () => { copyToClipboard(data.brief_markdown || '', copyBtn, 'Copy brief'); }); } // Bind copy-arguments const copyArgsBtn = document.getElementById('advCopyArgs'); if (copyArgsBtn) { copyArgsBtn.addEventListener('click', () => { const text = (data.client_strengths || []).map((s) => `- ${s}`).join('\n'); copyToClipboard(text, copyArgsBtn, 'Copy arguments'); }); } // Bind download .md const dlLink = document.getElementById('advDownloadMd'); if (dlLink) { const md = buildExportMarkdown(data); const blob = new Blob([md], { type: 'text/markdown; charset=utf-8' }); dlLink.href = URL.createObjectURL(blob); dlLink.setAttribute('download', `advocate-brief-${Date.now()}.md`); } // Bind flip-brief button els.results.querySelector('.adv-flip-btn')?.addEventListener('click', (e) => { const toRole = e.currentTarget.dataset.flipRole; if (toRole) flipBrief(toRole); }); // 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); } }); }); // Bind inline citation markers 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)); }); }); } // Convert [n] markers already present in escaped HTML into clickable spans function renderInlineCitations(escapedHtml, sources) { const sourceSet = new Set((sources || []).map((s) => s.n)); return escapedHtml.replace(/\[(\d+(?:\s*[-,]\s*\d+)*)\]/g, (_, group) => { const nums = expandCiteGroup(group); return nums.map((n) => `${n}`).join(''); }); } 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 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 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.brief_markdown || '').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), })), }; els.input.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' }); } 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 graphExpanded = s.graph_expanded === true; 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} ${graphExpanded ? `via citation graph` : ''} ${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)}
    ` : ''}
    `; } function renderBrief(markdown, sources) { if (!markdown) return '

    No brief was returned.

    '; const sourceSet = new Set((sources || []).map((s) => s.n)); const escaped = escapeHtml(markdown); const withCites = escaped.replace(/\[(\d+(?:\s*[-,]\s*\d+)*)\]/g, (_, group) => { const nums = expandCiteGroup(group); return nums.map((n) => `${n}`).join(''); }); const withBold = withCites .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(/^### /, '')}

    `; return `

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

    `; }).join(''); return paragraphs; } 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 updateCharCount() { if (!els.inputCount || !els.input) return; const len = els.input.value.length; els.inputCount.textContent = `${len.toLocaleString()} / 4,000`; els.inputCount.classList.toggle('is-warn', len > 3500 && len <= 3900); els.inputCount.classList.toggle('is-crit', len > 3900); } function saveToCache(result, formState) { try { localStorage.setItem(CACHE_KEY, JSON.stringify({ result, formState, ts: Date.now() })); } catch (e) { /* quota — silently ignore */ } } function loadFromCache() { try { const raw = localStorage.getItem(CACHE_KEY); if (!raw) return null; const obj = JSON.parse(raw); if (!obj || !obj.result || !obj.ts) return null; if (Date.now() - obj.ts > 86400000) { localStorage.removeItem(CACHE_KEY); return null; } return obj; } catch (e) { return null; } } function showRestoreBanner({ result, formState, ts }) { const age = Math.round((Date.now() - ts) / 60000); const ageStr = age < 60 ? `${age}m ago` : `${Math.round(age / 60)}h ago`; const banner = document.createElement('div'); banner.id = 'advRestoreBanner'; banner.className = 'adv-restore-banner'; banner.innerHTML = ` Restore last session (${ageStr}) — ${escapeHtml(formState.role || '?')} · “${escapeHtml((formState.query || '').slice(0, 60))}${(formState.query || '').length > 60 ? '…' : ''}”
    `; els.results.parentNode.insertBefore(banner, els.results); document.getElementById('advRestoreYes').addEventListener('click', () => { banner.remove(); restoreSession(result, formState); }); document.getElementById('advRestoreNo').addEventListener('click', () => { banner.remove(); localStorage.removeItem(CACHE_KEY); }); } function restoreSession(result, formState) { els.input.value = formState.query || ''; updateCharCount(); if (formState.role) els.roleSelect.value = formState.role; const radio = els.tierRadios.find((r) => r.value === formState.tier); if (radio) radio.checked = true; if (formState.slices) { els.slices.forEach((btn) => { const on = !!formState.slices[btn.dataset.slice]; btn.classList.toggle('is-on', on); btn.setAttribute('aria-pressed', on ? 'true' : 'false'); btn.querySelector('.dr-slice__badge').textContent = on ? 'on' : 'off'; }); } lastResult = result; result.query = formState.query || ''; renderResults(result, true); const meta = result.trace_metadata || {}; setStatus(`Restored · confidence ${meta.citation_confidence || '?'}`, 'ok'); } function showSliceHint(echrBtn, ncBtn, echrOff, ncOff) { document.getElementById('advSliceHint')?.remove(); const names = [echrOff && 'ECHR', ncOff && 'Norwegian Courts'].filter(Boolean).join(' & '); const hint = document.createElement('p'); hint.id = 'advSliceHint'; hint.className = 'adv-slice-hint'; hint.innerHTML = `${escapeHtml(names)} ${names.includes('&') ? 'are' : 'is'} often valuable for parent/family cases. `; document.querySelector('.dr-slice-section')?.after(hint); document.getElementById('advSliceHintEnable').addEventListener('click', () => { if (echrOff) activateSlice(echrBtn); if (ncOff) activateSlice(ncBtn); hint.remove(); }); document.getElementById('advSliceHintDismiss').addEventListener('click', () => hint.remove()); } function activateSlice(btn) { if (!btn) return; btn.classList.add('is-on'); btn.setAttribute('aria-pressed', 'true'); btn.querySelector('.dr-slice__badge').textContent = 'on'; } function copyToClipboard(text, btn, originalLabel) { navigator.clipboard.writeText(text).then(() => { btn.textContent = 'Copied!'; setTimeout(() => { btn.textContent = originalLabel; }, 1500); }).catch(() => { const ta = document.createElement('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;opacity:0;top:0;left:0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); btn.textContent = 'Copied!'; setTimeout(() => { btn.textContent = originalLabel; }, 1500); }); } function buildExportMarkdown(data) { const role = data.advocate_role || 'Unknown party'; const date = new Date().toISOString().slice(0, 10); const strengths = (data.client_strengths || []).map((s) => `- ${s}`).join('\n'); const weaknesses = (data.opposing_weaknesses || []).map((w) => `- ${w}`).join('\n'); const uncertainty = (data.what_remains_uncertain || []).map((u) => `- ${u}`).join('\n'); return [ `# Advocate Brief — ${role}`, `**Generated:** ${date}`, '', strengths ? '## Strongest Arguments\n' + strengths : '', '', '## Brief', data.brief_markdown || '', weaknesses ? '\n## Gaps in Opposing Position\n' + weaknesses : '', uncertainty ? '\n## What Remains Uncertain\n' + uncertainty : '', data.next_practical_step ? `\n## Next Practical Step\n${data.next_practical_step}` : '', '', '---', '_Generated by Do Better Norge — Case Advocate_', ].filter((s) => s !== null && s !== '').join('\n'); } function flipBrief(toRole) { const optionExists = Array.from(els.roleSelect.options).some((o) => o.value === toRole); if (optionExists) { els.roleSelect.value = toRole; if (els.roleCustom) els.roleCustom.classList.add('is-hidden'); } else { els.roleSelect.value = '__other__'; if (els.roleCustom) { els.roleCustom.classList.remove('is-hidden'); els.roleCustom.value = toRole; } } els.form.scrollIntoView({ behavior: 'smooth', block: 'start' }); setTimeout(() => els.form.requestSubmit(), 200); } function bindPreviewAngles() { if (!els.previewAngles) return; els.previewAngles.addEventListener('click', async () => { const advocateRole = getAdvocateRole(); if (!advocateRole) { setStatus('Select who you are representing first.', 'error'); return; } const query = (els.input.value || '').trim(); if (!query) { setStatus('Describe the case situation first.', 'error'); return; } els.previewAngles.disabled = true; els.previewAngles.textContent = 'Generating angles…'; setStatus('Generating research angles (steps 1–2 only, ~10s)…', 'busy'); try { const res = await fetch('api/generate-subq.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, language: lang, engine: getTier() === 'pro' ? 'claude_sonnet' : 'claude_haiku', controls: getControls(), advocate_role: advocateRole, }), credentials: 'same-origin', }); const json = await res.json(); if (!res.ok || !json.ok) { setStatus(json.message || `Error ${res.status}`, 'error'); return; } const sqs = json.sub_questions || []; if (els.subQPreviewList) { els.subQPreviewList.innerHTML = sqs.map((sq, i) => `
    ${sq.rationale ? `

    ${escapeHtml(sq.rationale)}

    ` : ''}
    `).join(''); } if (els.subQPreview) { els.subQPreview.classList.remove('is-hidden'); els.subQPreview.scrollIntoView({ behavior: 'smooth', block: 'start' }); } setStatus(`${sqs.length} research angles generated — review and edit, then run.`, 'ok'); } catch (err) { setStatus(`Error generating angles: ${err.message || err}`, 'error'); } finally { els.previewAngles.disabled = false; els.previewAngles.textContent = 'Preview research angles first'; } }); els.runWithAngles?.addEventListener('click', () => { const textareas = els.subQPreviewList?.querySelectorAll('.adv-subq-edit') || []; customSubQuestions = Array.from(textareas).map((ta, i) => ({ id: ta.dataset.sqId || `q${i + 1}`, question: ta.value.trim(), rationale: 'User-edited angle.', })).filter((sq) => sq.question); if (!customSubQuestions.length) { setStatus('All angles are empty — edit at least one before running.', 'error'); customSubQuestions = null; return; } if (els.subQPreview) els.subQPreview.classList.add('is-hidden'); els.form.requestSubmit(); }); els.discardAngles?.addEventListener('click', () => { customSubQuestions = null; if (els.subQPreview) els.subQPreview.classList.add('is-hidden'); }); } 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) + '…'; } })();