/** * legal-analysis.js — Custom handler for the Legal Analysis tool. * * Two-pass flow: extract distinct legal issues (Azure) → answer each issue * with dbn-legal-agent-v3 fine-tune on ocelot → synthesise overall assessment. * Streams NDJSON so the UI fills in as each issue completes. */ (function () { 'use strict'; // ── Element refs ────────────────────────────────────────────────────────── var form = document.getElementById('laForm'); var runBtn = document.getElementById('laRunButton'); var statusEl = document.getElementById('laStatus'); var resultsEl = document.getElementById('laResults'); var textarea = document.getElementById('laInput'); var uploadZone = document.getElementById('laUploadZone'); var uploadInput = document.getElementById('laUploadInput'); var uploadPrompt = document.getElementById('laUploadPrompt'); var uploadFileInfo = document.getElementById('laUploadFileInfo'); var uploadFileList = document.getElementById('laUploadFileList'); var uploadClear = document.getElementById('laUploadClear'); // ── State ───────────────────────────────────────────────────────────────── var _extractedFiles = []; var _currentLang = 'no'; var _lastPayload = null; var _issueCards = {}; // id -> DOM element // ── Lang switcher ───────────────────────────────────────────────────────── document.querySelectorAll('.la-lang-btn').forEach(function (btn) { btn.addEventListener('click', function () { document.querySelectorAll('.la-lang-btn').forEach(function (b) { b.classList.toggle('is-active', b === btn); }); _currentLang = btn.dataset.lang || 'no'; }); }); // ── File upload (same pattern as summarize) ─────────────────────────────── if (uploadZone) { uploadZone.addEventListener('dragover', function (e) { e.preventDefault(); uploadZone.classList.add('is-drag-over'); }); uploadZone.addEventListener('dragleave', function (e) { if (!uploadZone.contains(e.relatedTarget)) { uploadZone.classList.remove('is-drag-over'); } }); uploadZone.addEventListener('drop', function (e) { e.preventDefault(); uploadZone.classList.remove('is-drag-over'); if (e.dataTransfer && e.dataTransfer.files.length) { handleFiles(e.dataTransfer.files); } }); uploadZone.addEventListener('click', function (e) { if (e.target === uploadClear || (uploadClear && uploadClear.contains(e.target))) return; if (e.target.tagName === 'LABEL') return; if (uploadInput) uploadInput.click(); }); } if (uploadInput) { uploadInput.addEventListener('change', function () { if (uploadInput.files && uploadInput.files.length) handleFiles(uploadInput.files); }); } if (uploadClear) { uploadClear.addEventListener('click', function (e) { e.stopPropagation(); resetUpload(); }); } function resetUpload() { _extractedFiles = []; if (uploadInput) uploadInput.value = ''; if (uploadPrompt) uploadPrompt.classList.remove('is-hidden'); if (uploadFileInfo) uploadFileInfo.classList.add('is-hidden'); if (uploadFileList) uploadFileList.innerHTML = ''; } function handleFiles(fileList) { var files = Array.from(fileList).slice(0, 5); if (!files.length) return; setStatus('Extracting text from ' + files.length + ' file(s)…'); setBusy(true); var promises = files.map(function (file) { var fd = new FormData(); fd.append('file', file); return fetch('api/extract.php', { method: 'POST', credentials: 'same-origin', body: fd }) .then(function (r) { return r.json(); }) .then(function (data) { if (!data.ok) throw new Error(data.error || 'Extraction failed for ' + file.name); return { name: file.name, text: data.text || '', chars: data.chars || 0 }; }); }); Promise.all(promises) .then(function (results) { _extractedFiles = results; renderFileList(results); setStatus(''); setBusy(false); }) .catch(function (err) { setStatus('Error: ' + err.message); setBusy(false); }); } function renderFileList(files) { if (!uploadFileList) return; uploadFileList.innerHTML = files.map(function (f) { return '
  • ' + esc(f.name) + '' + ' ' + f.chars.toLocaleString() + ' chars
  • '; }).join(''); if (uploadPrompt) uploadPrompt.classList.add('is-hidden'); if (uploadFileInfo) uploadFileInfo.classList.remove('is-hidden'); } // ── Form submission ─────────────────────────────────────────────────────── if (form) { form.addEventListener('submit', function (e) { e.preventDefault(); runAnalysis(); }); } async function runAnalysis() { var pastedText = textarea ? textarea.value.trim() : ''; var fileText = _extractedFiles.map(function (f) { return f.text; }).join('\n\n---\n\n'); var combined = [fileText, pastedText].filter(Boolean).join('\n\n---\n\n'); var docIdsEl = document.getElementById('docPickerIds'); var rawDocIds = docIdsEl ? docIdsEl.value.trim() : ''; var docIds = rawDocIds ? rawDocIds.split(',').map(Number).filter(Boolean) : []; if (!combined && !docIds.length) { setStatus('Paste text, upload a file, or select a document before running.'); return; } var docType = (document.querySelector('input[name="laDocType"]:checked') || {}).value || 'auto'; var payload = { text: combined, language: _currentLang, doc_type: docType, }; if (docIds.length) payload.doc_ids = docIds; _lastPayload = payload; _issueCards = {}; setBusy(true); setStatus('Running…'); renderInitial(); try { var resp = await fetch('api/legal-analysis.php', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!resp.ok || !resp.body) { throw new Error('Server returned ' + resp.status); } var reader = resp.body.getReader(); var decoder = new TextDecoder(); var buffer = ''; while (true) { var _ref = await reader.read(); if (_ref.done) break; buffer += decoder.decode(_ref.value, { stream: true }); var lines = buffer.split('\n'); buffer = lines.pop(); for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); if (!line) continue; var data; try { data = JSON.parse(line); } catch (_) { continue; } handleEvent(data); } } } catch (err) { showError(err.message || 'Request failed.'); } finally { setBusy(false); setStatus(''); } } function handleEvent(data) { if (data.event === 'progress') { setStatus(data.detail || ''); if (data.step === 'issue_searching_corpus' || data.step === 'issue_answering') { markIssueRunning(data.issue_id, data.step); } } else if (data.event === 'issues_extracted') { renderIssueList(data.issues || []); } else if (data.event === 'issue_answered') { fillIssueCard(data.issue); } else if (data.event === 'final') { renderFinal(data.result || {}); if (typeof window.dbnShowSaveResultButton === 'function') { window.dbnShowSaveResultButton( 'legal-analysis', _lastPayload || {}, data.result || {}, { model: (data.result && data.result.model) || 'dbn-legal-agent-v3', latency_ms: (data.result && data.result.latency_ms) || 0, }, resultsEl ); } } else if (data.event === 'error') { showError(data.message || data.error || 'An error occurred.'); } } // ── Result rendering ────────────────────────────────────────────────────── function renderInitial() { if (!resultsEl) return; resultsEl.innerHTML = '
    ' + '
    Pass 1 — Extracting legal issues from your document…
    ' + '
    ' + '
      '; } function renderIssueList(issues) { var list = document.getElementById('laIssueList'); if (!list) return; list.innerHTML = ''; _issueCards = {}; issues.forEach(function (issue) { var li = document.createElement('li'); li.className = 'la-issue pending'; li.dataset.issueId = String(issue.id); li.innerHTML = '
      ' + '#' + issue.id + '' + '' + esc((issue.severity_hint || 'medium').toUpperCase()) + '' + '
      ' + '

      ' + esc(issue.question) + '

      ' + (issue.brief_context ? '

      ' + esc(issue.brief_context) + '

      ' : '') + '
      Waiting…
      '; list.appendChild(li); _issueCards[issue.id] = li; }); // Mark Pass 1 complete var pipeline = resultsEl.querySelector('.la-pipeline'); if (pipeline) { pipeline.innerHTML = '
      Pass 1 — Found ' + issues.length + ' legal issue(s)
      ' + '
      Pass 2 — Asking dbn-legal-agent-v3 about each issue…
      '; } } function markIssueRunning(issueId, step) { var card = _issueCards[issueId]; if (!card) return; var status = card.querySelector('.la-issue__status'); if (!status) return; if (step === 'issue_searching_corpus') { status.textContent = 'Searching legal corpus…'; card.classList.add('running'); } else if (step === 'issue_answering') { status.textContent = 'Asking dbn-legal-agent-v3…'; } } function fillIssueCard(issue) { if (!issue || !issue.id) return; var card = _issueCards[issue.id]; if (!card) return; card.classList.remove('pending', 'running'); card.classList.add('answered'); // Refresh severity from real answer var sevEl = card.querySelector('.la-severity'); if (sevEl) { sevEl.className = 'la-severity la-severity-' + esc(issue.severity || 'medium'); sevEl.textContent = esc((issue.severity || 'medium').toUpperCase()); } var statusBlock = card.querySelector('.la-issue__status'); if (statusBlock) statusBlock.remove(); var answerHtml = '
      Svar

      ' + esc(issue.answer || '').replace(/\n/g, '
      ') + '

      '; var basisHtml = ''; if (issue.legal_basis) { basisHtml = '
      Lovgrunnlag: ' + esc(issue.legal_basis) + '
      '; } var checkHtml = ''; if (issue.what_to_check) { checkHtml = '

      ' + esc(issue.what_to_check) + '

      '; } card.insertAdjacentHTML('beforeend', answerHtml + basisHtml + checkHtml); } function renderFinal(result) { // Add Pass 3 synthesis at the top var topHtml = ''; if (result.overall_assessment) { topHtml = '
      ' + '

      Overall assessment

      ' + '

      ' + esc(result.overall_assessment) + '

      '; if (Array.isArray(result.next_steps) && result.next_steps.length) { topHtml += '

      Next steps

      '; } if (result.disclaimer) { topHtml += '

      ' + esc(result.disclaimer) + '

      '; } topHtml += '
      '; } // Mark Pass 2/3 complete in the pipeline strip var pipeline = resultsEl.querySelector('.la-pipeline'); if (pipeline) { pipeline.innerHTML = '
      Pass 1 — Issues extracted
      ' + '
      Pass 2 — Specialist answered ' + (result.issues || []).length + ' issue(s)
      ' + '
      Pass 3 — Synthesis complete
      '; } // Insert synthesis at top of resultsEl, after pipeline if (topHtml && pipeline) { pipeline.insertAdjacentHTML('afterend', topHtml); } else if (topHtml) { resultsEl.insertAdjacentHTML('afterbegin', topHtml); } // If issues weren't already rendered (e.g. empty result), put the message in if ((!result.issues || !result.issues.length) && resultsEl.querySelector('#laIssueList')) { resultsEl.querySelector('#laIssueList').innerHTML = '
    1. No discrete legal issues were identified.

    2. '; } } function showError(msg) { if (resultsEl) { resultsEl.innerHTML = '

      Error

      ' + esc(msg) + '

      '; } setStatus(''); } // ── Helpers ─────────────────────────────────────────────────────────────── function setBusy(on) { if (runBtn) runBtn.disabled = on; if (runBtn) runBtn.textContent = on ? 'Running…' : 'Run legal analysis'; } function setStatus(msg) { if (statusEl) statusEl.textContent = msg; } function esc(s) { return String(s == null ? '' : s) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } }());