/** * 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. * * UI strings come from window.DBN_LA_I18N (populated server-side from * dbnToolsT('la_*')), so the same JS works in EN/NO/UK/PL. */ (function () { 'use strict'; // ── i18n helper ─────────────────────────────────────────────────────────── var I18N = window.DBN_LA_I18N || {}; function t(key, vars) { var s = (I18N && I18N[key]) || key; if (vars) { Object.keys(vars).forEach(function (k) { s = s.split('{' + k + '}').join(String(vars[k])); }); } return s; } // ── 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 = window.DBN_LA_LANG || 'no'; var _lastPayload = null; var _issueCards = {}; // ── Lang switcher ───────────────────────────────────────────────────────── document.querySelectorAll('.la-lang-btn').forEach(function (btn) { btn.addEventListener('click', function () { // Switching UI language requires a page reload so all labels rerender. var lang = btn.dataset.lang || 'no'; if (lang === _currentLang) return; var url = new URL(window.location.href); url.searchParams.set('lang', lang); window.location.href = url.toString(); }); }); // ── File upload ─────────────────────────────────────────────────────────── 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); } }); // Stop label-for and the input itself from bubbling into the zone click // handler — otherwise the picker opens twice (native + programmatic). var browseLabel = uploadZone.querySelector('label[for="' + (uploadInput && uploadInput.id) + '"]'); if (browseLabel) { browseLabel.addEventListener('click', function (e) { e.stopPropagation(); }); } if (uploadInput) { uploadInput.addEventListener('click', function (e) { e.stopPropagation(); }); } uploadZone.addEventListener('click', function (e) { if (e.target === uploadClear || (uploadClear && uploadClear.contains(e.target))) return; if (e.target === uploadInput) return; var lbl = e.target.closest && e.target.closest('label'); if (lbl && lbl.getAttribute('for') === uploadInput.id) 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(t('extractingFiles', { n: files.length })); 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(t('errorPrefix') + ' ' + 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(t('needInput')); 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(t('runButtonBusy')); 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(t('serverReturned') + ' ' + 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 = '
    ' + '
    ' + esc(t('pass1')) + ' — ' + esc(t('pass1Extracting')) + '
    ' + '
    ' + '
      '; } 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) + '

      ' : '') + '
      ' + esc(t('waiting')) + '
      '; list.appendChild(li); _issueCards[issue.id] = li; }); var pipeline = resultsEl.querySelector('.la-pipeline'); if (pipeline) { pipeline.innerHTML = '
      ' + esc(t('pass1')) + ' — ' + esc(t('pass1Found', { n: issues.length })) + '
      ' + '
      ' + esc(t('pass2')) + ' — ' + esc(t('pass2Asking')) + '
      '; } } 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 = t('searchingCorpus'); card.classList.add('running'); } else if (step === 'issue_answering') { status.textContent = t('askingFinetune'); } } 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'); 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 = '
      ' + esc(t('answerHeader')) + '

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

      '; var basisHtml = ''; if (issue.legal_basis) { basisHtml = '
      ' + esc(t('legalBasis')) + ' ' + 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) { var topHtml = ''; if (result.overall_assessment) { topHtml = '
      ' + '

      ' + esc(t('overall')) + '

      ' + '

      ' + esc(result.overall_assessment) + '

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

      ' + esc(t('nextSteps')) + '

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

      ' + esc(result.disclaimer) + '

      '; } topHtml += '
      '; } var pipeline = resultsEl.querySelector('.la-pipeline'); if (pipeline) { pipeline.innerHTML = '
      ' + esc(t('pass1')) + ' — ' + esc(t('pass1Found', { n: (result.issues || []).length })) + '
      ' + '
      ' + esc(t('pass2')) + ' — ' + esc(t('pass2Answered', { n: (result.issues || []).length })) + '
      ' + '
      ' + esc(t('pass3')) + ' — ' + esc(t('pass3Synthesis')) + '
      '; } if (topHtml && pipeline) { pipeline.insertAdjacentHTML('afterend', topHtml); } else if (topHtml) { resultsEl.insertAdjacentHTML('afterbegin', topHtml); } if ((!result.issues || !result.issues.length) && resultsEl.querySelector('#laIssueList')) { resultsEl.querySelector('#laIssueList').innerHTML = '
    1. ' + esc(t('emptyIssues')) + '

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

      ' + esc(t('errorPrefix')) + '

      ' + esc(msg) + '

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