From bffc714541409bd826723b67e9a023d4abf2c4ee Mon Sep 17 00:00:00 2001 From: davegilligan Date: Sun, 24 May 2026 13:04:45 +0200 Subject: [PATCH] Add My Docs picker to deep-research, advocate, barnevernet, korrespond, citations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PHP: Add docPickerSection (button + chips + hidden input) to all 5 tool pages - JS: Send doc_ids in payload for deep-research, advocate, barnevernet, korrespond - Backend: Inject selected corpus doc content into paste_text/narrative/notes via dbnToolsInjectDocContent - Citations: Add upload zone (file → api/extract.php → textarea) + paste textarea with live Norwegian legal reference extraction (regex) + ref chips → title search; doc picker populates titleInput via MutationObserver Co-Authored-By: Claude Sonnet 4.6 --- advocate.php | 9 +++ api/barnevernet.php | 10 +-- api/deep-research.php | 15 +++-- api/korrespond.php | 7 ++- assets/js/advocate.js | 2 + assets/js/barnevernet.js | 2 + assets/js/citations.js | 122 +++++++++++++++++++++++++++++++++++++ assets/js/deep-research.js | 2 + assets/js/korrespond.js | 5 +- barnevernet.php | 9 +++ citations.php | 26 ++++++++ deep-research.php | 9 +++ korrespond.php | 9 +++ 13 files changed, 214 insertions(+), 13 deletions(-) diff --git a/advocate.php b/advocate.php index 44ef6d0..ae59b27 100644 --- a/advocate.php +++ b/advocate.php @@ -153,6 +153,15 @@ require_once __DIR__ . '/includes/layout.php'; +
+ +
+ +
+
diff --git a/api/barnevernet.php b/api/barnevernet.php index 4099ffc..9a8316e 100644 --- a/api/barnevernet.php +++ b/api/barnevernet.php @@ -8,7 +8,6 @@ require_once __DIR__ . '/../includes/ToolModels.php'; dbnToolsRequireMethod('POST'); dbnToolsRequireAuth(); $ftUid = dbnToolsFreeTierCheck('barnevernet'); -$ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'barnevernet'); @ini_set('output_buffering', '0'); @ini_set('zlib.output_compression', '0'); @@ -19,7 +18,6 @@ ob_implicit_flush(true); header('Content-Type: application/x-ndjson; charset=utf-8'); header('Cache-Control: no-store'); header('X-Accel-Buffering: no'); -if ($ftRemaining >= 0) { header('X-Credits-Remaining: ' . $ftRemaining); } $language = 'en'; $startTime = microtime(true); @@ -58,12 +56,12 @@ try { $engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? 'azure_mini')); $sliceInput = $input['slices'] ?? []; $controls = is_array($input['controls'] ?? null) ? $input['controls'] : []; - $additionalNotes = mb_substr(trim((string)($input['additional_notes'] ?? '')), 0, 2000, 'UTF-8'); + $additionalNotes = mb_substr(dbnToolsInjectDocContent($input, trim((string)($input['additional_notes'] ?? ''))), 0, 8000, 'UTF-8'); if (mb_strlen($advocateRole, 'UTF-8') > 200) { throw new DbnToolsHttpException('advocate_role is too long.', 422, 'advocate_role_too_long'); } - if (mb_strlen($additionalNotes, 'UTF-8') > 2000) { + if (mb_strlen($additionalNotes, 'UTF-8') > 8000) { throw new DbnToolsHttpException('additional_notes is too long.', 422, 'notes_too_long'); } @@ -150,6 +148,10 @@ try { 'bvj_doc_type' => $result['doc_meta']['doc_type'] ?? null, ]); + $ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'barnevernet'); + if ($ftRemaining >= 0) { + $result['balance'] = $ftRemaining; + } $emit('final', ['result' => $result]); diff --git a/api/deep-research.php b/api/deep-research.php index 135bd82..001dfaa 100644 --- a/api/deep-research.php +++ b/api/deep-research.php @@ -7,8 +7,7 @@ require_once __DIR__ . '/../includes/ToolModels.php'; dbnToolsRequireMethod('POST'); dbnToolsRequireAuth(); -$ftUid = dbnToolsFreeTierCheck('deep-research'); -$ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'deep-research'); +$ftUid = 0; // Stream-friendly response — defeat output buffering so the user's browser // receives progress events while the agent runs (can take 60-180s for @@ -22,9 +21,9 @@ ob_implicit_flush(true); header('Content-Type: application/x-ndjson; charset=utf-8'); header('Cache-Control: no-store'); header('X-Accel-Buffering: no'); -if ($ftRemaining >= 0) { header('X-Credits-Remaining: ' . $ftRemaining); } $language = 'en'; +$chargeTool = 'deep-research'; $startTime = microtime(true); $emit = function (string $event, array $payload = []) use ($startTime): void { @@ -58,14 +57,16 @@ try { $language = dbnToolsNormalizeLanguage($input['language'] ?? 'en'); $seedQuery = trim((string)($input['query'] ?? '')); - $pastedText = trim((string)($input['paste_text'] ?? '')); + $pastedText = dbnToolsInjectDocContent($input, trim((string)($input['paste_text'] ?? ''))); $sliceInput = $input['slices'] ?? []; - $engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? 'azure_mini')); $controls = is_array($input['controls'] ?? null) ? $input['controls'] : []; $advocateRole = trim((string)($input['advocate_role'] ?? '')); if (mb_strlen($advocateRole, 'UTF-8') > 200) { throw new DbnToolsHttpException('advocate_role is too long.', 422, 'advocate_role_too_long'); } + $chargeTool = $advocateRole !== '' ? 'advocate' : 'deep-research'; + $ftUid = dbnToolsFreeTierCheck($chargeTool); + $engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? 'azure_mini')); $priorContext = is_array($input['prior_context'] ?? null) ? $input['prior_context'] : null; $branchNotes = mb_substr(trim((string)($input['branch_notes'] ?? '')), 0, 1000, 'UTF-8'); $subQsOverride = is_array($input['sub_questions_override'] ?? null) ? $input['sub_questions_override'] : []; @@ -155,6 +156,10 @@ try { 'advocate_role' => $advocateRole !== '' ? $advocateRole : null, ]); + $ftRemaining = dbnToolsFreeTierDeduct($ftUid, $chargeTool); + if ($ftRemaining >= 0) { + $result['balance'] = $ftRemaining; + } $emit('final', ['result' => $result]); diff --git a/api/korrespond.php b/api/korrespond.php index c85df0b..1b91d6c 100644 --- a/api/korrespond.php +++ b/api/korrespond.php @@ -80,6 +80,7 @@ try { 'clarifications' => is_array($input['clarifications'] ?? null) ? $input['clarifications'] : [], 'use_my_case' => !empty($input['use_my_case']), ]; + $intake['narrative'] = dbnToolsInjectDocContent($input, $intake['narrative']); $forceDraft = !empty($input['force_draft']); $hasClarif = !empty($intake['clarifications']); @@ -166,14 +167,14 @@ try { $ftUid = dbnToolsFreeTierCheck('korrespond'); $ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'korrespond'); $creditDeducted = true; - if ($ftRemaining >= 0) { - header('X-Credits-Remaining: ' . $ftRemaining); - } // ── Pass 2: retrieve law → draft → self-check → translate ────────────────── $result = $agent->generate($intake, $classify, $emit); $result['ok'] = true; $result['latency_ms'] = (int)round((microtime(true) - $startTime) * 1000); + if ($ftRemaining >= 0) { + $result['balance'] = $ftRemaining; + } dbnToolsLogMetadata([ 'tool' => 'korrespond', diff --git a/assets/js/advocate.js b/assets/js/advocate.js index 6073b5f..22048f4 100644 --- a/assets/js/advocate.js +++ b/assets/js/advocate.js @@ -393,6 +393,8 @@ 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(); diff --git a/assets/js/barnevernet.js b/assets/js/barnevernet.js index d92a3d3..076f357 100644 --- a/assets/js/barnevernet.js +++ b/assets/js/barnevernet.js @@ -410,6 +410,8 @@ additional_notes: additionalNotes, use_my_case: (typeof window.dbnGetUseMyCase === 'function') ? window.dbnGetUseMyCase() : false, }; + const _bvjDocIds = (document.getElementById('docPickerIds')?.value || '').split(',').map(Number).filter(Boolean); + if (_bvjDocIds.length) payload.doc_ids = _bvjDocIds; if (branchContext) { payload.prior_context = branchContext; diff --git a/assets/js/citations.js b/assets/js/citations.js index 20f69d5..aa59be6 100644 --- a/assets/js/citations.js +++ b/assets/js/citations.js @@ -115,6 +115,128 @@ 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 => { diff --git a/assets/js/deep-research.js b/assets/js/deep-research.js index 9036b91..f40ec9a 100644 --- a/assets/js/deep-research.js +++ b/assets/js/deep-research.js @@ -325,6 +325,8 @@ language: lang, controls: getControls(), }; + 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(); diff --git a/assets/js/korrespond.js b/assets/js/korrespond.js index 8be7e94..6712abb 100644 --- a/assets/js/korrespond.js +++ b/assets/js/korrespond.js @@ -260,7 +260,8 @@ function buildPayload(forceDraft) { const deadlines = []; if (els.deadline.value.trim()) deadlines.push(els.deadline.value.trim()); - return { + const korrDocIds = (document.getElementById('docPickerIds')?.value || '').split(',').map(Number).filter(Boolean); + const payload = { mode: getRadio(els.modeRadios) || 'initiate', recipient_body: els.bodySelect.value || 'other', output_type: getRadio(els.outputRadios) || 'email', @@ -276,6 +277,8 @@ force_draft: !!forceDraft, use_my_case: (typeof window.dbnGetUseMyCase === 'function') ? window.dbnGetUseMyCase() : false, }; + if (korrDocIds.length) payload.doc_ids = korrDocIds; + return payload; } async function runRequest(forceDraft) { diff --git a/barnevernet.php b/barnevernet.php index 610d35f..a1a0988 100644 --- a/barnevernet.php +++ b/barnevernet.php @@ -139,6 +139,15 @@ require_once __DIR__ . '/includes/layout.php';
    +
    + +
    + +
    +
    diff --git a/citations.php b/citations.php index 35b17de..ef2e100 100644 --- a/citations.php +++ b/citations.php @@ -99,6 +99,32 @@ require_once __DIR__ . '/includes/layout.php';
    +
    + +
    + +
    + +
    + +
    + +

    Upload a document to extract legal references, or

    +

    PDF, DOCX, TXT — text extracted locally, never stored

    +
    + +
    + + + +
    +
    +
    + +
    + +
    +
    diff --git a/korrespond.php b/korrespond.php index d3017db..41bcd69 100644 --- a/korrespond.php +++ b/korrespond.php @@ -104,6 +104,15 @@ require_once __DIR__ . '/includes/layout.php';
    +
    + +
    + +
    +