(function () { 'use strict'; if (document.body.dataset.activeTool !== 'citations') return; const form = document.getElementById('citationsForm'); const titleInput = document.getElementById('titleInput'); const docIdInput = document.getElementById('docIdHidden'); const acList = document.getElementById('citAutocomplete'); const depthRow = document.getElementById('depthRow'); const depthRange = document.getElementById('depthRange'); const depthValue = document.getElementById('depthValue'); const runBtn = document.getElementById('citeRunBtn'); const statusEl = document.getElementById('citStatus'); const resultsDiv = document.getElementById('citationResults'); const sourceCard = document.getElementById('citSourceCard'); const resultsArea = document.getElementById('citResultsArea'); const trailList = document.getElementById('trailList'); const trail = []; // ── Utilities ───────────────────────────────────────────────────────────── function esc(s) { return String(s ?? '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function setStatus(msg, kind) { statusEl.textContent = msg; statusEl.style.color = kind === 'error' ? '#b41e1e' : kind === 'ok' ? 'var(--teal-dark)' : 'var(--muted)'; } function debounce(fn, ms) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; } function authorityLabel(atype) { const map = { case_law: 'Case law', guidance: 'Guidance', report: 'Report', law: 'Statute', treaty: 'Treaty', ombudsman: 'Ombudsman', tribunal: 'Tribunal', regulatory: 'Regulatory', }; return map[atype] || atype || ''; } function relChipClass(action, relType) { if (action === 'chain') return 'rel-chip--reachable'; if (action === 'cited_by') return 'rel-chip--cited-by'; if (action === 'cites') return 'rel-chip--cites'; const rt = (relType || '').toUpperCase(); if (rt === 'REPEALS') return 'rel-chip--repeals'; return 'rel-chip--implements'; } function relChipLabel(action, relType) { if (action === 'chain') return 'Reachable'; if (action === 'cited_by') return 'Cited by'; if (action === 'cites') return 'Cites'; if (relType) return relType.replace(/_/g, ' '); return 'Implements'; } function yearFrom(dateStr) { if (!dateStr) return ''; return String(dateStr).slice(0, 4); } // ── Autocomplete ────────────────────────────────────────────────────────── function hideAc() { acList.hidden = true; } function renderAc(docs) { if (!docs.length) { hideAc(); return; } acList.innerHTML = docs.map(d => { const al = authorityLabel(d.authority_type); return `
  • ` + esc(d.title || '(Untitled)') + (al ? `${esc(al)}` : '') + `
  • `; }).join(''); acList.hidden = false; } const fetchAc = debounce(async (q) => { if (!q || q.length < 2) { hideAc(); return; } try { const res = await fetch('api/corpus-documents.php?' + new URLSearchParams({ title: q, limit: 15 }), { credentials: 'same-origin' }); if (!res.ok) { hideAc(); return; } const data = await res.json(); renderAc(data.documents || []); } catch { hideAc(); } }, 300); titleInput.addEventListener('input', (e) => { const val = e.target.value.trim(); if (/^\d+$/.test(val)) { docIdInput.value = val; hideAc(); } else { docIdInput.value = ''; fetchAc(val); } }); acList.addEventListener('mousedown', (e) => { const li = e.target.closest('li[data-id]'); if (!li) return; e.preventDefault(); titleInput.value = li.dataset.title; docIdInput.value = li.dataset.id; hideAc(); }); titleInput.addEventListener('blur', () => setTimeout(hideAc, 150)); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') hideAc(); }); // ── Legal reference extraction ───────────────────────────────────────────── const REF_PATTERNS = [ /\bLOV-\d{4}-\d{2}-\d{2}-\d+\b/g, /\bFOR-\d{4}-\d{2}-\d{2}-\d+\b/g, /\bHR-\d{4}-\d{3,5}-[AUS]\b/g, /\bLB-\d{4}-\d+\b/g, /\bLA-\d{4}-\d+\b/g, /\bLE-\d{4}-\d+\b/g, /\bLF-\d{4}-\d+\b/g, /\bLG-\d{4}-\d+\b/g, /\bRt\.\s*\d{4}\s+\d+\b/g, ]; const refChipsEl = document.getElementById('citRefChips'); function extractRefs(text) { const found = new Set(); REF_PATTERNS.forEach((rx) => { const matches = text.match(new RegExp(rx.source, rx.flags)) || []; matches.forEach((m) => found.add(m.replace(/\s+/g, ' ').trim())); }); return Array.from(found).slice(0, 20); } function renderRefChips(refs) { if (!refChipsEl) return; refChipsEl.innerHTML = refs.map((ref) => { const safe = ref.replace(/"/g, '"'); return ``; }).join(''); refChipsEl.querySelectorAll('.doc-chip').forEach((btn) => { btn.addEventListener('click', () => { titleInput.value = btn.dataset.ref; docIdInput.value = ''; fetchAc(btn.dataset.ref); titleInput.focus(); }); }); } const citPasteInput = document.getElementById('citPasteInput'); if (citPasteInput) { citPasteInput.addEventListener('input', debounce(() => { const refs = extractRefs(citPasteInput.value); renderRefChips(refs); }, 400)); } // ── File upload → extract text → populate paste area ───────────────────── const citUploadZone = document.getElementById('citUploadZone'); const citUploadInput = document.getElementById('citUploadInput'); const citUploadPrompt = document.getElementById('citUploadPrompt'); const citUploadFileInfo = document.getElementById('citUploadFileInfo'); const citUploadFileList = document.getElementById('citUploadFileList'); const citUploadClear = document.getElementById('citUploadClear'); function handleCitFile(file) { if (!file) return; citUploadFileList.innerHTML = `
  • ${esc(file.name)} Extracting…
  • `; citUploadPrompt.classList.add('is-hidden'); citUploadFileInfo.classList.remove('is-hidden'); const fd = new FormData(); fd.append('file', file); fetch('api/extract.php', { method: 'POST', body: fd, credentials: 'same-origin' }) .then((r) => r.json()) .then((data) => { const text = data.text || ''; if (citPasteInput) { citPasteInput.value = text.slice(0, 8000); citPasteInput.dispatchEvent(new Event('input')); } const chars = data.chars || text.length; citUploadFileList.innerHTML = `
  • ${esc(file.name)} ${chars.toLocaleString()} chars extracted
  • `; }) .catch(() => { citUploadFileList.innerHTML = `
  • ${esc(file.name)} Extraction failed
  • `; }); } if (citUploadInput) { citUploadInput.addEventListener('change', () => handleCitFile(citUploadInput.files[0])); } if (citUploadZone) { citUploadZone.addEventListener('dragover', (e) => { e.preventDefault(); citUploadZone.classList.add('is-dragover'); }); citUploadZone.addEventListener('dragleave', () => citUploadZone.classList.remove('is-dragover')); citUploadZone.addEventListener('drop', (e) => { e.preventDefault(); citUploadZone.classList.remove('is-dragover'); const file = e.dataTransfer?.files?.[0]; if (file) handleCitFile(file); }); } if (citUploadClear) { citUploadClear.addEventListener('click', () => { if (citUploadInput) citUploadInput.value = ''; if (citUploadFileInfo) citUploadFileInfo.classList.add('is-hidden'); if (citUploadPrompt) citUploadPrompt.classList.remove('is-hidden'); if (citPasteInput) { citPasteInput.value = ''; renderRefChips([]); } }); } // ── Doc picker → populate title input ───────────────────────────────────── // doc-picker.js handles the modal; we hook the confirm callback by listening // for changes to the hidden docPickerIds input and reading the chip labels. const docPickerChipsEl = document.getElementById('docPickerChips'); if (docPickerChipsEl) { const observer = new MutationObserver(() => { const firstChip = docPickerChipsEl.querySelector('.doc-chip__label'); if (firstChip && firstChip.textContent.trim()) { titleInput.value = firstChip.textContent.trim(); docIdInput.value = ''; fetchAc(titleInput.value); } }); observer.observe(docPickerChipsEl, { childList: true, subtree: true }); } // ── Action radios + depth slider ────────────────────────────────────────── document.querySelectorAll('input[name="citAction"]').forEach(r => { r.addEventListener('change', () => { depthRow.hidden = r.value !== 'chain'; }); }); depthRange.addEventListener('input', () => { depthValue.textContent = depthRange.value; }); // ── Form submit ─────────────────────────────────────────────────────────── form.addEventListener('submit', async (e) => { e.preventDefault(); let docId = parseInt(docIdInput.value, 10) || parseInt(titleInput.value.trim(), 10); if (!docId || docId <= 0) { setStatus('Enter a document title or numeric ID.', 'error'); titleInput.focus(); return; } const action = document.querySelector('input[name="citAction"]:checked').value; const params = { action, doc_id: docId, limit: 50 }; if (action === 'chain') params.depth = parseInt(depthRange.value, 10); setStatus('Querying citation graph…', 'busy'); runBtn.disabled = true; try { const url = 'https://ai.bluenotelogic.com/api/graph-search.php?' + new URLSearchParams(params); const res = await fetch(url, { credentials: 'omit' }); if (res.status === 404) { setStatus('', ''); renderNotFound(docId); return; } if (!res.ok) { setStatus('Graph error (' + res.status + ').', 'error'); return; } const data = await res.json(); if (data.error) { setStatus(data.error, 'error'); return; } setStatus('', ''); renderResults(data, action); addToTrail(data.source); } catch (err) { setStatus('Network error: ' + err.message, 'error'); } finally { runBtn.disabled = false; } }); // ── Render ──────────────────────────────────────────────────────────────── function renderNotFound(docId) { resultsDiv.hidden = false; sourceCard.innerHTML = ''; resultsArea.innerHTML = '

    Document ' + esc(String(docId)) + ' is not in the citation graph yet. The graph grows as new documents are scraped and resolved.

    '; } function renderResults(data, action) { const src = data.source; const atLabel = authorityLabel(src.atype); const year = yearFrom(src.valid_from); sourceCard.innerHTML = '

    ' + esc(src.title || '(Untitled)') + '

    ' + '
    ' + (atLabel ? '' + esc(atLabel) + '' : '') + (year ? '' + esc(year) + '' : '') + (src.jurisdiction ? '' + esc(src.jurisdiction) + '' : '') + 'ID ' + esc(String(src.doc_id)) + '' + (src.source_url ? 'Source ↗' : '') + '
    '; const results = data.results || []; const count = data.count ?? results.length; const actionLabels = { cites: 'Documents cited', cited_by: 'Documents citing this', implements: 'Documents implementing / amending', chain: 'Reachable documents (citation chain)', }; let html = '
    ' + '

    ' + esc(actionLabels[action] || action) + '

    ' + '' + esc(String(count)) + ' document' + (count !== 1 ? 's' : '') + '' + '
    '; if (results.length === 0) { html += '

    No connected documents found for this action.

    '; } else { html += '
    '; for (const r of results) { const chipCls = relChipClass(action, r.rel_type); const chipLbl = relChipLabel(action, r.rel_type); const unresolved = r.doc_id === null || r.doc_id === undefined; const cardTitle = r.title || ''; const atl = authorityLabel(r.atype); const yr = yearFrom(r.valid_from); html += '
    ' + '' + esc(chipLbl) + '' + (cardTitle ? '' + esc(cardTitle) + '' : '') + (!cardTitle && r.ref ? '' + esc(r.ref) + '' : '') + '
    ' + (atl ? '' + esc(atl) + '' : '') + (yr ? '' + esc(yr) + '' : '') + '
    ' + (!unresolved ? '' : '') + '
    '; } html += '
    '; } resultsArea.innerHTML = html; resultsDiv.hidden = false; resultsArea.querySelectorAll('.cit-explore-btn').forEach(btn => { btn.addEventListener('click', () => exploreDoc(parseInt(btn.dataset.exploreId, 10), btn.dataset.exploreTitle)); }); } // ── Trail ───────────────────────────────────────────────────────────────── function addToTrail(source) { if (trail.length && trail[trail.length - 1].doc_id === source.doc_id) return; trail.push({ doc_id: source.doc_id, title: source.title || '(Untitled)', atype: source.atype }); renderTrail(); } function renderTrail() { const placeholder = document.getElementById('trailPlaceholder'); if (placeholder) placeholder.remove(); trailList.innerHTML = ''; for (let i = trail.length - 1; i >= 0; i--) { const entry = trail[i]; const isCurrent = i === trail.length - 1; const li = document.createElement('li'); li.className = 'cit-trail-item' + (isCurrent ? ' is-active' : ''); li.innerHTML = '' + '
    ' + '' + esc(entry.title) + '' + '

    ' + esc(authorityLabel(entry.atype) || 'Document') + ' · ID ' + esc(String(entry.doc_id)) + '

    ' + '
    '; if (!isCurrent) { li.addEventListener('click', () => exploreDoc(entry.doc_id, entry.title)); } trailList.appendChild(li); } } // ── Navigation ──────────────────────────────────────────────────────────── function exploreDoc(docId, title) { titleInput.value = title || String(docId); docIdInput.value = String(docId); hideAc(); form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); } })();