/* deep-research.js — page-scoped UI for /deep-research.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 persona = ''; 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 !== 'deep-research') return; Object.assign(els, { form: document.getElementById('deepResearchForm'), input: document.getElementById('drInput'), status: document.getElementById('drStatus'), runButton: document.getElementById('drRunButton'), results: document.getElementById('drResults'), traceList: document.getElementById('traceList'), slices: Array.from(document.querySelectorAll('.dr-slice')), langButtons: Array.from(document.querySelectorAll('#drLangSwitcher .lang-btn')), tierRadios: Array.from(document.querySelectorAll('input[name="drTier"]')), personaControl: document.getElementById('drPersonaControl'), personaSelect: document.getElementById('drPersonaSelect'), subQ: document.getElementById('drSubQ'), subQVal: document.getElementById('drSubQValue'), chunkLimit: document.getElementById('drChunkLimit'), chunkLimitVal: document.getElementById('drChunkLimitValue'), sim: document.getElementById('drSim'), simVal: document.getElementById('drSimValue'), topK: document.getElementById('drTopK'), topKVal: document.getElementById('drTopKValue'), temp: document.getElementById('drTemp'), tempVal: document.getElementById('drTempValue'), uploadZone: document.getElementById('drUploadZone'), uploadInput: document.getElementById('drUploadInput'), uploadPrompt: document.getElementById('drUploadPrompt'), uploadFileInfo: document.getElementById('drUploadFileInfo'), uploadFileList: document.getElementById('drUploadFileList'), uploadClear: document.getElementById('drUploadClear'), modal: document.getElementById('drSourceModal'), modalClose: document.getElementById('drSourceModalClose'), modalTitle: document.getElementById('drSourceModalTitle'), modalEyebrow: document.getElementById('drSourceModalEyebrow'), modalMeta: document.getElementById('drSourceModalMeta'), modalText: document.getElementById('drSourceModalText'), branchPanel: document.getElementById('drBranchPanel'), branchClear: document.getElementById('drBranchClear'), branchOrigin: document.getElementById('drBranchOrigin'), branchSummary: document.getElementById('drBranchSummary'), branchNotes: document.getElementById('drBranchNotes'), }); if (!els.form) return; bindSlices(); bindLang(); bindRanges(); bindUpload(); bindModal(); bindBranch(); loadPersonas(); els.form.addEventListener('submit', onSubmit); els.results.addEventListener('click', (e) => { const btn = e.target.closest('.dr-branch-btn'); if (btn) branchFromSubQ(btn.dataset.question || ''); }); // Pre-render placeholder trace renderTrace(STEP_LABELS.map((label) => ({ label, detail: 'Waiting…', status: 'idle' }))); }); 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, i) => { 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 query = (els.input.value || '').trim(); if (!query && uploadFiles.length === 0) { setStatus('Type a question or upload a file before running deep research.', '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' ? '60–180 seconds with Claude Sonnet' : '15–45 seconds with Claude Haiku'; setStatus(`Running deep research… (${expectedDuration})`, 'busy'); els.runButton.disabled = true; els.results.innerHTML = `

    Working…

    The agent is expanding your question and researching the corpus. Live progress in the right-hand panel. Expect ${expectedDuration}.

    `; // Initialise the trace with all 7 steps as 'idle' const stepState = STEP_LABELS.map((label) => ({ label, detail: 'Queued', status: 'idle' })); renderTrace(stepState); const payload = { query, paste_text: '', slices, tier, language: lang, controls: getControls(), }; if (persona) payload.profile = persona; const _drDocIds = (document.getElementById('docPickerIds')?.value || '').split(',').map(Number).filter(Boolean); if (_drDocIds.length) payload.doc_ids = _drDocIds; if (branchContext) { payload.prior_context = branchContext; payload.branch_notes = (els.branchNotes ? els.branchNotes.value : '').trim(); } 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) { if (response.status === 402 || response.status === 429) { const d = await response.json().catch(() => ({})); if (typeof window.dbnFreeTierError === 'function') window.dbnFreeTierError(response.status, d); } else { setStatus(`Request failed (${response.status}).`, 'error'); } els.runButton.disabled = false; return; } const creditsRemaining = response.headers.get('X-Credits-Remaining'); if (creditsRemaining !== null && typeof window.dbnUpdateCredits === 'function') { window.dbnUpdateCredits(parseInt(creditsRemaining, 10)); } // Read the NDJSON stream const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = ''; let finalResult = null; let errorEvent = null; let progressDetail = ''; 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; // Mark the currently-running step as error 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; if (typeof finalResult.balance === 'number' && typeof window.dbnUpdateCredits === 'function') { window.dbnUpdateCredits(finalResult.balance); } 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); function handleStreamEvent(evt) { if (!evt || !evt.event) return; if (evt.event === 'progress') { progressDetail = evt.detail || ''; if (progressDetail) setStatus(progressDetail, '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; 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) { const sources = data.sources || []; const subs = data.sub_questions || []; const briefHtml = renderBrief(data.brief_markdown || '', sources); // Per-sub-question report cards — the "what each agent researched" view const subQReportsHtml = subs.length ? `

    What each sub-question agent researched

    ${subs.length} sub-question${subs.length === 1 ? '' : 's'}, top 3 sources each
    ${subs.map((sq, i) => renderSubQReport(sq, i)).join('')}
    ` : ''; const sourcesHtml = `

    All sources (${sources.length})

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

    What remains uncertain

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

    Next practical step

    ${escapeHtml(data.next_practical_step)}

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

    Synthesised brief

    ${briefHtml}
    ${sourcesHtml} ${uncertHtml} ${nextHtml} `; // Save-to-corpus button (inject after brief block) const briefEl = els.results.querySelector('.dr-brief'); if (briefEl) { briefEl.id = 'drBriefText'; const saveBtn = document.createElement('button'); saveBtn.type = 'button'; saveBtn.className = 'js-save-corpus secondary-button'; saveBtn.dataset.tool = 'deep-research'; saveBtn.dataset.contentId = 'drBriefText'; saveBtn.dataset.suggestedTitle = 'Research: ' + (document.getElementById('drQuery')?.value?.slice(0, 80) ?? 'Report'); saveBtn.textContent = 'Save to corpus'; saveBtn.style.marginTop = '12px'; briefEl.insertAdjacentElement('afterend', saveBtn); } // Bind source-card click handlers (open modal) — but ignore clicks on inner els.results.querySelectorAll('.dr-source-card[data-source-n]').forEach((node) => { node.addEventListener('click', (e) => { if (e.target.closest('a')) return; // let anchor handle its own click 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 in brief → flash + open modal els.results.querySelectorAll('.dr-cite[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) { flashSource(n); } }); }); } 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)}
    ` : ''}
    `; } async function loadPersonas() { if (!els.personaSelect) return; try { const r = await fetch('api/personas.php', { credentials: 'same-origin', headers: { Accept: 'application/json' } }); const data = await r.json().catch(() => ({})); if (!r.ok || data.ok !== true || !Array.isArray(data.personas) || !data.personas.length) return; const fallback = data.default_persona || 'family'; els.personaSelect.innerHTML = ''; data.personas.forEach((p) => { const opt = document.createElement('option'); opt.value = p.slug; opt.textContent = p.name || p.slug; els.personaSelect.appendChild(opt); }); const saved = sessionStorage.getItem('dbnPersona'); const initial = (saved && data.personas.some((p) => p.slug === saved)) ? saved : (data.personas.some((p) => p.slug === fallback) ? fallback : data.personas[0].slug); persona = initial; els.personaSelect.value = initial; els.personaSelect.addEventListener('change', () => { persona = els.personaSelect.value; sessionStorage.setItem('dbnPersona', persona); }); els.personaControl?.classList.remove('is-hidden'); } catch (_) { /* personas are optional UI sugar; ignore failures */ } } 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 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 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)}
    ` : ''}
    `; } // Markdown renderer — minimal: paragraphs, bold/italic, code, [n] citation badges function renderBrief(markdown, sources) { if (!markdown) return '

    No brief was returned.

    '; const sourceSet = new Set((sources || []).map((s) => s.n)); const escaped = escapeHtml(markdown); // Citation markers [1], [1,2], [1-3] const withCites = escaped.replace(/\[(\d+(?:\s*[-,]\s*\d+)*)\]/g, (_, group) => { const nums = expandCiteGroup(group); return nums.map((n) => { const known = sourceSet.has(n); const cls = known ? 'dr-cite' : 'dr-cite'; return `${n}`; }).join(''); }); // Bold/italic const withBold = withCites .replace(/\*\*([^*]+)\*\*/g, '$1') .replace(/(^|[^*])\*([^*]+)\*(?!\*)/g, '$1$2') .replace(/`([^`]+)`/g, '$1'); // Paragraphs 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 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) + '…'; } })();