From 21c092e0d0a5a2e5d0c9b83afa0d119e5d1c562e Mon Sep 17 00:00:00 2001 From: davegilligan Date: Sun, 24 May 2026 08:43:15 +0200 Subject: [PATCH] Legal Analysis: full language follow-through (UI + LLM) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tool now respects the chosen UI language end-to-end — even if the source document is Norwegian, a user on EN/UK/PL gets the analysis in their language. Norwegian statute references (barnevernsloven § 4-25, EMK Art. 8) and case names (Strand Lobben mot Norge 37283/13) are kept verbatim because they are proper nouns. LLM (LegalAnalysisAgent.php): - extractIssues: prompt asks for question + brief_context in user's language; statute refs preserved - answerIssue: Norwegian core system prompt (keeps fine-tune precision) + language-coercion line for non-NO; localised context/source labels - synthesise: overall_assessment, next_steps, disclaimer in user's language; explicit per-language disclaimer text - runFullAnalysis empty-case fallback also localised - what_to_check translated per language UI: - 40 new la_* translation keys in i18n.php × 4 languages (NO/EN/UK/PL) - legal-analysis.php: 4-way lang switcher, dbnToolsT() for every label, emits window.DBN_LA_I18N for runtime JS strings - legal-analysis.js: t() helper reads from window.DBN_LA_I18N - layout_footer.php: emits window.DBN_CURRENT_LANG + window.DBN_ADDON_I18N so the legal-analysis add-on button works in the page's language no matter which tool it's invoked from - tools.js add-on: reads from DBN_ADDON_I18N, passes DBN_CURRENT_LANG to /api/legal-analysis.php so server responds in same language Co-Authored-By: Claude Sonnet 4.6 --- assets/js/legal-analysis.js | 80 +++++++++------- assets/js/tools.js | 51 +++++----- includes/LegalAnalysisAgent.php | 95 +++++++++++++++---- includes/i18n.php | 160 ++++++++++++++++++++++++++++++++ includes/layout_footer.php | 31 +++++++ legal-analysis.php | 69 ++++++++++---- 6 files changed, 397 insertions(+), 89 deletions(-) diff --git a/assets/js/legal-analysis.js b/assets/js/legal-analysis.js index ab8712e..c9c1121 100644 --- a/assets/js/legal-analysis.js +++ b/assets/js/legal-analysis.js @@ -4,10 +4,25 @@ * 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'); @@ -24,21 +39,23 @@ // ── State ───────────────────────────────────────────────────────────────── var _extractedFiles = []; - var _currentLang = 'no'; + var _currentLang = window.DBN_LA_LANG || 'no'; var _lastPayload = null; - var _issueCards = {}; // id -> DOM element + var _issueCards = {}; // ── 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'; + // 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 (same pattern as summarize) ─────────────────────────────── + // ── File upload ─────────────────────────────────────────────────────────── if (uploadZone) { uploadZone.addEventListener('dragover', function (e) { e.preventDefault(); @@ -68,7 +85,6 @@ uploadZone.addEventListener('click', function (e) { if (e.target === uploadClear || (uploadClear && uploadClear.contains(e.target))) return; if (e.target === uploadInput) return; - // Any label or descendant of a label-for=uploadInput already triggered the input var lbl = e.target.closest && e.target.closest('label'); if (lbl && lbl.getAttribute('for') === uploadInput.id) return; if (uploadInput) uploadInput.click(); @@ -98,7 +114,7 @@ var files = Array.from(fileList).slice(0, 5); if (!files.length) return; - setStatus('Extracting text from ' + files.length + ' file(s)…'); + setStatus(t('extractingFiles', { n: files.length })); setBusy(true); var promises = files.map(function (file) { @@ -120,7 +136,7 @@ setBusy(false); }) .catch(function (err) { - setStatus('Error: ' + err.message); + setStatus(t('errorPrefix') + ' ' + err.message); setBusy(false); }); } @@ -153,7 +169,7 @@ 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.'); + setStatus(t('needInput')); return; } @@ -169,7 +185,7 @@ _issueCards = {}; setBusy(true); - setStatus('Running…'); + setStatus(t('runButtonBusy')); renderInitial(); try { @@ -181,7 +197,7 @@ }); if (!resp.ok || !resp.body) { - throw new Error('Server returned ' + resp.status); + throw new Error(t('serverReturned') + ' ' + resp.status); } var reader = resp.body.getReader(); @@ -244,7 +260,7 @@ if (!resultsEl) return; resultsEl.innerHTML = '
' - + '
Pass 1 — Extracting legal issues from your document…
' + + '
' + esc(t('pass1')) + ' — ' + esc(t('pass1Extracting')) + '
' + '
' + '
    '; } @@ -267,15 +283,14 @@ + '' + '

    ' + esc(issue.question) + '

    ' + (issue.brief_context ? '

    ' + esc(issue.brief_context) + '

    ' : '') - + '
    Waiting…
    '; + + '
    ' + esc(t('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…
    '; + pipeline.innerHTML = '
    ' + esc(t('pass1')) + ' — ' + esc(t('pass1Found', { n: issues.length })) + '
    ' + + '
    ' + esc(t('pass2')) + ' — ' + esc(t('pass2Asking')) + '
    '; } } @@ -285,10 +300,10 @@ var status = card.querySelector('.la-issue__status'); if (!status) return; if (step === 'issue_searching_corpus') { - status.textContent = 'Searching legal corpus…'; + status.textContent = t('searchingCorpus'); card.classList.add('running'); } else if (step === 'issue_answering') { - status.textContent = 'Asking dbn-legal-agent-v3…'; + status.textContent = t('askingFinetune'); } } @@ -299,7 +314,6 @@ 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'); @@ -309,13 +323,13 @@ var statusBlock = card.querySelector('.la-issue__status'); if (statusBlock) statusBlock.remove(); - var answerHtml = '
    Svar

    ' + var answerHtml = '

    ' + esc(t('answerHeader')) + '

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

    '; var basisHtml = ''; if (issue.legal_basis) { - basisHtml = '
    Lovgrunnlag: ' + basisHtml = '
    ' + esc(t('legalBasis')) + ' ' + esc(issue.legal_basis) + '
    '; } var checkHtml = ''; @@ -326,14 +340,13 @@ } function renderFinal(result) { - // Add Pass 3 synthesis at the top var topHtml = ''; if (result.overall_assessment) { topHtml = '
    ' - + '

    Overall assessment

    ' + + '

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

    ' + '

    ' + esc(result.overall_assessment) + '

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

    Next steps

      ' + topHtml += '

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

        ' + result.next_steps.map(function (s) { return '
      • ' + esc(s) + '
      • '; }).join('') + '
      '; } @@ -343,32 +356,29 @@ 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
    '; + '
    ' + 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')) + '
    '; } - // 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. '; + '
  3. ' + esc(t('emptyIssues')) + '

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

    Error

    ' + esc(msg) + '

    '; + resultsEl.innerHTML = '

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

    ' + esc(msg) + '

    '; } setStatus(''); } @@ -376,7 +386,7 @@ // ── Helpers ─────────────────────────────────────────────────────────────── function setBusy(on) { if (runBtn) runBtn.disabled = on; - if (runBtn) runBtn.textContent = on ? 'Running…' : 'Run legal analysis'; + if (runBtn) runBtn.textContent = on ? t('runButtonBusy') : t('runButton'); } function setStatus(msg) { if (statusEl) statusEl.textContent = msg; diff --git a/assets/js/tools.js b/assets/js/tools.js index 1bb2b17..cd1633d 100644 --- a/assets/js/tools.js +++ b/assets/js/tools.js @@ -2579,6 +2579,14 @@ window.dbnShowSaveResultButton = showSaveResultButton; // Injects a button that, when clicked, streams the two-pass legal-analysis flow // (extract issues → ask dbn-legal-agent-v3 → synthesise) into a sub-section // below the host container. +function _laT(key, vars) { + const map = window.DBN_ADDON_I18N || {}; + let s = map[key] || key; + if (vars) { + Object.keys(vars).forEach((k) => { s = s.split('{' + k + '}').join(String(vars[k])); }); + } + return s; +} function dbnRunLegalAnalysisAddon(text, lang, sourceTool, containerEl) { if (!containerEl) containerEl = document.getElementById('results'); if (!containerEl || !text || text.length < 80) return; @@ -2595,9 +2603,9 @@ function dbnRunLegalAnalysisAddon(text, lang, sourceTool, containerEl) { section.style.borderRadius = '8px'; section.style.background = '#f0fdfa'; section.innerHTML = - '

    ⚖️🇳🇴 Deep Legal Analysis

    ' + '

    ⚖️🇳🇴 ' + escapeHtml(_laT('addonSection')) + '

    ' + '
    ' - + '
    Pass 1 — Extracting legal issues…
    ' + + '
    ' + escapeHtml(_laT('pass1')) + ' — ' + escapeHtml(_laT('pass1Extracting')) + '
    ' + '
    ' + '
      '; containerEl.parentNode.appendChild(section); @@ -2608,7 +2616,7 @@ function dbnRunLegalAnalysisAddon(text, lang, sourceTool, containerEl) { const payload = { text: text, - language: lang || 'no', + language: lang || window.DBN_CURRENT_LANG || 'no', doc_type: 'auto', source_tool: sourceTool || 'addon', }; @@ -2638,7 +2646,7 @@ function dbnRunLegalAnalysisAddon(text, lang, sourceTool, containerEl) { } } }).catch(function (err) { - section.innerHTML += '

      Error: ' + escapeHtml(err.message || String(err)) + '

      '; + section.innerHTML += '

      ' + escapeHtml(_laT('errorPrefix')) + ' ' + escapeHtml(err.message || String(err)) + '

      '; }); } @@ -2655,21 +2663,21 @@ function laAddonEvent(data, listEl, pipelineEl, cards) { + '' + escapeHtml(sev.toUpperCase()) + '
      ' + '

      ' + escapeHtml(issue.question || '') + '

      ' + (issue.brief_context ? '

      ' + escapeHtml(issue.brief_context) + '

      ' : '') - + '
      Waiting…
      '; + + '
      ' + escapeHtml(_laT('waiting')) + '
      '; listEl.appendChild(li); cards[issue.id] = li; }); pipelineEl.innerHTML = - '
      Pass 1 — Found ' + (data.issues || []).length + ' issue(s)
      ' - + '
      Pass 2 — Asking dbn-legal-agent-v3…
      '; + '
      ' + escapeHtml(_laT('pass1')) + ' — ' + escapeHtml(_laT('pass1Found', { n: (data.issues || []).length })) + '
      ' + + '
      ' + escapeHtml(_laT('pass2')) + ' — ' + escapeHtml(_laT('pass2Asking')) + '
      '; } else if (data.event === 'progress') { const card = cards[data.issue_id]; if (card) { const status = card.querySelector('.la-issue__status'); if (status) { status.textContent = data.step === 'issue_searching_corpus' - ? 'Searching legal corpus…' - : 'Asking dbn-legal-agent-v3…'; + ? _laT('searchingCorpus') + : _laT('askingFinetune'); } card.classList.add('running'); } @@ -2687,22 +2695,22 @@ function laAddonEvent(data, listEl, pipelineEl, cards) { } const statusEl = card.querySelector('.la-issue__status'); if (statusEl) statusEl.remove(); - let html = '
      Svar

      ' + escapeHtml(iss.answer || '').replace(/\n/g, '
      ') + '

      '; - if (iss.legal_basis) html += '
      Lovgrunnlag: ' + escapeHtml(iss.legal_basis) + '
      '; + let html = '
      ' + escapeHtml(_laT('answerHeader')) + '

      ' + escapeHtml(iss.answer || '').replace(/\n/g, '
      ') + '

      '; + if (iss.legal_basis) html += '
      ' + escapeHtml(_laT('legalBasis')) + ' ' + escapeHtml(iss.legal_basis) + '
      '; if (iss.what_to_check) html += '

      ' + escapeHtml(iss.what_to_check) + '

      '; card.insertAdjacentHTML('beforeend', html); } else if (data.event === 'final' && data.result) { const res = data.result; pipelineEl.innerHTML = - '
      Pass 1 — Issues extracted
      ' - + '
      Pass 2 — Specialist answered ' + (res.issues || []).length + '
      ' - + '
      Pass 3 — Synthesis complete
      '; + '
      ' + escapeHtml(_laT('pass1')) + ' — ' + escapeHtml(_laT('pass1Found', { n: (res.issues || []).length })) + '
      ' + + '
      ' + escapeHtml(_laT('pass2')) + ' — ' + escapeHtml(_laT('pass2Answered', { n: (res.issues || []).length })) + '
      ' + + '
      ' + escapeHtml(_laT('pass3')) + ' — ' + escapeHtml(_laT('pass3Synthesis')) + '
      '; if (res.overall_assessment) { let syn = '
      ' - + '

      Overall assessment

      ' + + '

      ' + escapeHtml(_laT('overall')) + '

      ' + '

      ' + escapeHtml(res.overall_assessment) + '

      '; if (Array.isArray(res.next_steps) && res.next_steps.length) { - syn += '

      Next steps

        ' + syn += '

        ' + escapeHtml(_laT('nextSteps')) + '

          ' + res.next_steps.map(function (s) { return '
        • ' + escapeHtml(s) + '
        • '; }).join('') + '
        '; } @@ -2717,25 +2725,24 @@ function laAddonEvent(data, listEl, pipelineEl, cards) { const containerSection = pipelineEl.parentNode; window.dbnShowSaveResultButton( 'legal-analysis', - { text: '', language: 'no', doc_type: 'auto', source_tool: 'addon' }, // input was the prior tool's text + { text: '', language: window.DBN_CURRENT_LANG || 'no', doc_type: 'auto', source_tool: 'addon' }, res, { model: res.model || 'dbn-legal-agent-v3', latency_ms: res.latency_ms || 0 }, containerSection ); } } else if (data.event === 'error') { - pipelineEl.innerHTML += '
        Error: ' + escapeHtml(data.message || data.error || 'unknown') + '
        '; + pipelineEl.innerHTML += '
        ' + escapeHtml(_laT('errorPrefix')) + ' ' + escapeHtml(data.message || data.error || 'unknown') + '
        '; } } function dbnInjectLegalAnalysisButton(text, lang, sourceTool, containerEl, options) { if (!containerEl || !text || text.length < 80) return; - // Avoid duplicate buttons if (containerEl.querySelector('.la-addon-btn')) return; const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'la-addon-btn'; - btn.textContent = (options && options.label) || '⚖️🇳🇴 Run deep legal analysis on this text'; + btn.textContent = (options && options.label) || _laT('addonButton'); btn.style.cssText = 'display:block;margin:1rem auto 0;padding:0.6rem 1.2rem;font-size:0.92rem;' + 'background:#0f766e;color:#fff;border:none;border-radius:6px;cursor:pointer;' + 'font-weight:600;letter-spacing:0.02em;'; @@ -2743,9 +2750,9 @@ function dbnInjectLegalAnalysisButton(text, lang, sourceTool, containerEl, optio btn.addEventListener('mouseleave', function () { btn.style.background = '#0f766e'; }); btn.addEventListener('click', function () { btn.disabled = true; - btn.textContent = 'Running deep legal analysis…'; + btn.textContent = _laT('addonButtonBusy'); btn.style.background = '#94a3b8'; - dbnRunLegalAnalysisAddon(text, lang, sourceTool, containerEl); + dbnRunLegalAnalysisAddon(text, lang || window.DBN_CURRENT_LANG || 'no', sourceTool, containerEl); }); containerEl.appendChild(btn); } diff --git a/includes/LegalAnalysisAgent.php b/includes/LegalAnalysisAgent.php index 22a48fd..0f9e3e5 100644 --- a/includes/LegalAnalysisAgent.php +++ b/includes/LegalAnalysisAgent.php @@ -48,15 +48,18 @@ process law). Each issue must be answerable as a SINGLE focused legal question (≤ 25 words), not a multi-part essay. Document type hint: {$docType} -Document language: {$locale} +User's response language: {$locale} -Return JSON only: +Return JSON only — every human-readable string ("question", "brief_context") MUST be +written in {$locale}. Keep Norwegian statute references (barnevernsloven § 4-25, +forvaltningsloven § 17, EMK Art. 8) and Norwegian/EMD case names (Strand Lobben mot +Norge 37283/13) in their original form even when the surrounding text is in {$locale}. { "issues": [ { "id": 1, - "question": "", - "brief_context": "<≤2 sentences from the document that triggered this question>", + "question": "", + "brief_context": "<≤2 sentences in {$locale} summarising what in the document triggered this question — paraphrase, do not quote in Norwegian unless quoting a statute>", "doc_type": "", "severity_hint": "" } @@ -69,6 +72,7 @@ Rules: named Høyesterett/EMD cases — NOT general advice. - If the document has fewer than 5 real legal issues, return fewer entries. - If NO real legal issue exists, return {"issues": []}. +- The source document may be in Norwegian — that is fine; still write your output in {$locale}. DOCUMENT: --- @@ -119,19 +123,48 @@ PROMPT; */ public function answerIssue(array $issue, string $corpusContext, string $language): array { + $locale = dbnToolsLanguageName($language); + + // The fine-tune was trained primarily in Norwegian; the Norwegian system + // prompt keeps its precision on barnevernsloven / EMD. We then add a + // language-coercion line so the prose comes back in the user's chosen + // language. Statute and case names stay in their original Norwegian form. $sysMsg = 'Du er en ekspert på norsk barnevernsloven og EMD-praksis. ' - . 'Svar alltid på norsk med korrekt juridisk terminologi. ' + . 'Bruk korrekt juridisk terminologi. ' . 'Bruk terskler fra barnevernsloven 2021: § 4-25 krever «klar nødvendighet». ' . 'Strand Lobben mot Norge (37283/13) setter krav om rehabiliteringsplan før adopsjon. ' . 'Aldri oppfinn paragrafnumre, saksnumre eller dommernavn. ' - . 'Avslutt med en «Kilder:»-seksjon som lister lovparagrafer og dommer du har sitert.'; + . 'Avslutt med en «Kilder:»-seksjon som lister lovparagrafer og dommer du har sitert. '; + + if ($language === 'no') { + $sysMsg .= 'Svar på norsk.'; + } else { + $sysMsg .= 'IMPORTANT: Write your answer in ' . $locale + . '. Keep all Norwegian statute references (e.g. "barnevernsloven § 4-25", ' + . '"forvaltningsloven § 17", "EMK Art. 8") and case names (e.g. "Strand Lobben ' + . 'mot Norge 37283/13") in their original Norwegian/Latin form. The "Kilder:" ' + . 'section heading stays as "Kilder:" but its contents (the cited authorities) ' + . 'are listed in their original Norwegian form.'; + } $userMsg = $issue['question']; if ($issue['brief_context'] !== '') { - $userMsg .= "\n\nKontekst fra saken: " . $issue['brief_context']; + $ctxLabel = match ($language) { + 'no' => 'Kontekst fra saken', + 'pl' => 'Kontekst sprawy', + 'uk' => 'Контекст справи', + default => 'Case context', + }; + $userMsg .= "\n\n" . $ctxLabel . ': ' . $issue['brief_context']; } if ($corpusContext !== '') { - $userMsg .= "\n\nRelevante kilder fra Do Better Norge-korpuset:\n" . $corpusContext; + $srcLabel = match ($language) { + 'no' => 'Relevante kilder fra Do Better Norge-korpuset', + 'pl' => 'Istotne źródła z korpusu Do Better Norge', + 'uk' => 'Релевантні джерела з корпусу Do Better Norge', + default => 'Relevant sources from the Do Better Norge corpus', + }; + $userMsg .= "\n\n" . $srcLabel . ":\n" . $corpusContext; } $answer = ''; @@ -164,6 +197,13 @@ PROMPT; $severity = $clean !== '' ? dbnToolsInferCheckSeverity($clean) : $issue['severity_hint']; $legalBasis = dbnToolsExtractCheckLegalBasis($clean); + $whatToCheck = match ($language) { + 'no' => 'Verifiser med norsk familieretsadvokat før handling.', + 'pl' => 'Zweryfikuj z norweskim adwokatem ds. rodzinnych przed podjęciem działań.', + 'uk' => 'Перевірте з норвезьким адвокатом із сімейного права перед діями.', + default => 'Verify with a qualified Norwegian family-law lawyer before acting.', + }; + return [ 'id' => $issue['id'], 'question' => $issue['question'], @@ -171,8 +211,8 @@ PROMPT; 'answer' => $clean, 'severity' => $severity, 'legal_basis' => $legalBasis, - 'citations_from_corpus' => [], // populated by orchestrator if it kept the chunks - 'what_to_check' => 'Verifiser med norsk familieretsadvokat før handling.', + 'citations_from_corpus' => [], + 'what_to_check' => $whatToCheck, ]; } @@ -194,18 +234,29 @@ PROMPT; } $issuesBlock = implode("\n", $bullets); + $disclaimerText = match ($language) { + 'no' => 'Dette er automatisert juridisk analyse, ikke juridisk rådgivning. Verifiser med en kvalifisert norsk advokat før du handler.', + 'pl' => 'To jest zautomatyzowana analiza prawna, a nie porada prawna. Zweryfikuj z wykwalifikowanym norweskim prawnikiem przed podjęciem działań.', + 'uk' => 'Це автоматизований юридичний аналіз, а не юридична консультація. Перевірте з кваліфікованим норвезьким юристом перед діями.', + default => 'This is automated legal analysis, not legal advice. Verify with a qualified Norwegian lawyer before acting.', + }; + $prompt = <<", - "next_steps": ["", "", ""], - "disclaimer": "This is automated legal analysis, not legal advice. Verify with a qualified Norwegian lawyer before acting." + "overall_assessment": "<3-5 sentences in {$locale} summarising the legal picture across all issues>", + "next_steps": ["", "", ""], + "disclaimer": "{$disclaimerText}" } PROMPT; @@ -250,12 +301,24 @@ PROMPT; $issues = $this->extractIssues($text, $language, $docType); if (empty($issues)) { + $emptyAssessment = match ($language) { + 'no' => 'Ingen distinkte juridiske spørsmål identifisert i dette dokumentet.', + 'pl' => 'Nie zidentyfikowano odrębnych kwestii prawnych w tym dokumencie.', + 'uk' => 'У цьому документі не виявлено окремих юридичних питань.', + default => 'No discrete legal issues identified in this document.', + }; + $emptyDisclaimer = match ($language) { + 'no' => 'Automatisert analyse — ikke juridisk rådgivning.', + 'pl' => 'Analiza zautomatyzowana — nie stanowi porady prawnej.', + 'uk' => 'Автоматизований аналіз — не є юридичною консультацією.', + default => 'Automated analysis — not legal advice.', + }; return [ 'ok' => true, 'issues' => [], - 'overall_assessment' => 'No discrete legal issues identified in this document.', + 'overall_assessment' => $emptyAssessment, 'next_steps' => [], - 'disclaimer' => 'Automated analysis — not legal advice.', + 'disclaimer' => $emptyDisclaimer, 'model' => self::LEGAL_MODEL, 'latency_ms' => (int)round(microtime(true) * 1000) - $startMs, ]; diff --git a/includes/i18n.php b/includes/i18n.php index b1af41b..2510d4a 100644 --- a/includes/i18n.php +++ b/includes/i18n.php @@ -395,6 +395,46 @@ function dbnToolsTranslations(): array 'js_field_tags' => 'Tags (comma-separated)', 'js_field_lang' => 'Language', 'js_field_author' => 'Author', + + // ── Legal Analysis tool ─────────────────────────────────────────── + 'la_doc_type_label' => 'Document type', + 'la_doc_type_auto' => 'Auto-detect', + 'la_doc_type_other' => 'Other', + 'la_engine_hint' => 'Engine: dbn-legal-agent-v3 (Norwegian legal fine-tune on GPU). Each issue answered separately; ~30-60s per issue, up to 5 issues per run.', + 'la_input_label' => 'Pasted text', + 'la_input_hint' => '(optional if file or doc selected)', + 'la_input_placeholder' => 'Paste a case note, court decision, vedtak, letter, or any legal document text. You can also upload a file or select from My Docs above — at least one source is required.', + 'la_run_button' => 'Run legal analysis', + 'la_run_button_busy' => 'Running…', + 'la_ready_title' => 'Ready', + 'la_ready_intro' => 'Upload a document or paste text — the tool will extract up to 5 distinct legal issues, then ask the Norwegian-law fine-tune to answer each one with citations.', + 'la_ready_pipeline' => 'Pass 1 uses Azure GPT-4o-mini to spot issues. Pass 2 calls the dbn-legal-agent-v3 fine-tune on ocelot for each one. Pass 3 synthesises the overall picture. A typical run takes 2-5 minutes.', + 'la_pipeline_pass1' => 'Pass 1', + 'la_pipeline_pass2' => 'Pass 2', + 'la_pipeline_pass3' => 'Pass 3', + 'la_pass1_extracting' => 'Extracting legal issues from your document…', + 'la_pass1_found' => 'Found {n} legal issue(s)', + 'la_pass2_asking' => 'Asking dbn-legal-agent-v3 about each issue…', + 'la_pass2_answered' => 'Specialist answered {n} issue(s)', + 'la_pass3_synthesis' => 'Synthesis complete', + 'la_waiting' => 'Waiting…', + 'la_searching_corpus' => 'Searching legal corpus…', + 'la_asking_finetune' => 'Asking dbn-legal-agent-v3…', + 'la_overall' => 'Overall assessment', + 'la_next_steps' => 'Next steps', + 'la_answer_header' => 'Answer', + 'la_legal_basis' => 'Legal basis:', + 'la_extracting_status' => 'Identifying distinct legal issues…', + 'la_synthesising_status' => 'Synthesising overall assessment…', + 'la_extracting_files' => 'Extracting text from {n} file(s)…', + 'la_need_input' => 'Paste text, upload a file, or select a document before running.', + 'la_error_prefix' => 'Error:', + 'la_server_returned' => 'Server returned', + 'la_empty_issues' => 'No discrete legal issues were identified.', + + 'la_addon_button' => '⚖️🇳🇴 Run deep legal analysis on this text', + 'la_addon_button_busy' => 'Running deep legal analysis…', + 'la_addon_section' => 'Deep Legal Analysis', ], 'no' => [ 'meta_title' => 'Do Better Norge - juridiske AI-verktøy', @@ -722,6 +762,46 @@ function dbnToolsTranslations(): array 'js_field_tags' => 'Tagger (komma-separert)', 'js_field_lang' => 'Språk', 'js_field_author' => 'Forfatter', + + // ── Juridisk analyse ────────────────────────────────────────────── + 'la_doc_type_label' => 'Dokumenttype', + 'la_doc_type_auto' => 'Auto-oppdaging', + 'la_doc_type_other' => 'Annet', + 'la_engine_hint' => 'Motor: dbn-legal-agent-v3 (norsk juridisk fine-tune på GPU). Hvert spørsmål besvares hver for seg; ~30-60s per spørsmål, opp til 5 spørsmål per kjøring.', + 'la_input_label' => 'Limt inn tekst', + 'la_input_hint' => '(valgfritt hvis fil eller dokument er valgt)', + 'la_input_placeholder' => 'Lim inn et saksnotat, rettsavgjørelse, vedtak, brev eller annen juridisk dokumenttekst. Du kan også laste opp en fil eller velge fra Mine dokumenter ovenfor — minst én kilde kreves.', + 'la_run_button' => 'Kjør juridisk analyse', + 'la_run_button_busy' => 'Kjører…', + 'la_ready_title' => 'Klar', + 'la_ready_intro' => 'Last opp et dokument eller lim inn tekst — verktøyet henter ut opptil 5 distinkte juridiske spørsmål og lar den norske fine-tunen svare på hvert enkelt med kilder.', + 'la_ready_pipeline' => 'Pass 1 bruker Azure GPT-4o-mini for å identifisere spørsmål. Pass 2 kaller dbn-legal-agent-v3 fine-tunen på ocelot for hvert spørsmål. Pass 3 sammenfatter helhetsbildet. En typisk kjøring tar 2-5 minutter.', + 'la_pipeline_pass1' => 'Pass 1', + 'la_pipeline_pass2' => 'Pass 2', + 'la_pipeline_pass3' => 'Pass 3', + 'la_pass1_extracting' => 'Henter ut juridiske spørsmål fra dokumentet…', + 'la_pass1_found' => 'Fant {n} juridisk(e) spørsmål', + 'la_pass2_asking' => 'Spør dbn-legal-agent-v3 om hvert spørsmål…', + 'la_pass2_answered' => 'Spesialisten besvarte {n} spørsmål', + 'la_pass3_synthesis' => 'Syntese fullført', + 'la_waiting' => 'Venter…', + 'la_searching_corpus' => 'Søker i juridisk korpus…', + 'la_asking_finetune' => 'Spør dbn-legal-agent-v3…', + 'la_overall' => 'Helhetsvurdering', + 'la_next_steps' => 'Neste skritt', + 'la_answer_header' => 'Svar', + 'la_legal_basis' => 'Lovgrunnlag:', + 'la_extracting_status' => 'Identifiserer distinkte juridiske spørsmål…', + 'la_synthesising_status' => 'Sammenfatter helhetsvurdering…', + 'la_extracting_files' => 'Henter ut tekst fra {n} fil(er)…', + 'la_need_input' => 'Lim inn tekst, last opp en fil eller velg et dokument før du kjører.', + 'la_error_prefix' => 'Feil:', + 'la_server_returned' => 'Server svarte', + 'la_empty_issues' => 'Ingen distinkte juridiske spørsmål ble identifisert.', + + 'la_addon_button' => '⚖️🇳🇴 Kjør dyp juridisk analyse på denne teksten', + 'la_addon_button_busy' => 'Kjører dyp juridisk analyse…', + 'la_addon_section' => 'Dyp juridisk analyse', ], 'uk' => [ 'meta_title' => 'Do Better Norge - юридичні AI інструменти', @@ -1049,6 +1129,46 @@ function dbnToolsTranslations(): array 'js_field_tags' => 'Теги (через кому)', 'js_field_lang' => 'Мова', 'js_field_author' => 'Автор', + + // ── Юридичний аналіз ────────────────────────────────────────────── + 'la_doc_type_label' => 'Тип документа', + 'la_doc_type_auto' => 'Авто-визначення', + 'la_doc_type_other' => 'Інше', + 'la_engine_hint' => 'Модель: dbn-legal-agent-v3 (норвезька юридична fine-tune на GPU). Кожне питання обробляється окремо; ~30-60с на питання, до 5 питань за запуск.', + 'la_input_label' => 'Вставлений текст', + 'la_input_hint' => '(необов’язково, якщо вибрано файл або документ)', + 'la_input_placeholder' => 'Вставте судову нотатку, рішення суду, vedtak, лист або будь-який юридичний текст. Можна також завантажити файл або вибрати з Моїх документів вище — потрібне принаймні одне джерело.', + 'la_run_button' => 'Запустити юридичний аналіз', + 'la_run_button_busy' => 'Виконання…', + 'la_ready_title' => 'Готово', + 'la_ready_intro' => 'Завантажте документ або вставте текст — інструмент виявить до 5 окремих юридичних питань і попросить норвезьку fine-tune відповісти на кожне з цитатами.', + 'la_ready_pipeline' => 'Прохід 1 використовує Azure GPT-4o-mini для виявлення питань. Прохід 2 викликає dbn-legal-agent-v3 для кожного. Прохід 3 синтезує загальну картину. Типовий запуск триває 2-5 хвилин.', + 'la_pipeline_pass1' => 'Прохід 1', + 'la_pipeline_pass2' => 'Прохід 2', + 'la_pipeline_pass3' => 'Прохід 3', + 'la_pass1_extracting' => 'Виявлення юридичних питань у документі…', + 'la_pass1_found' => 'Знайдено {n} юридичне(их) питання', + 'la_pass2_asking' => 'Запит до dbn-legal-agent-v3 для кожного питання…', + 'la_pass2_answered' => 'Спеціаліст відповів на {n} питання', + 'la_pass3_synthesis' => 'Синтез завершено', + 'la_waiting' => 'Очікування…', + 'la_searching_corpus' => 'Пошук у юридичному корпусі…', + 'la_asking_finetune' => 'Запит до dbn-legal-agent-v3…', + 'la_overall' => 'Загальна оцінка', + 'la_next_steps' => 'Наступні кроки', + 'la_answer_header' => 'Відповідь', + 'la_legal_basis' => 'Правова підстава:', + 'la_extracting_status' => 'Виявлення окремих юридичних питань…', + 'la_synthesising_status' => 'Синтез загальної оцінки…', + 'la_extracting_files' => 'Виділення тексту з {n} файл(ів)…', + 'la_need_input' => 'Вставте текст, завантажте файл або виберіть документ перед запуском.', + 'la_error_prefix' => 'Помилка:', + 'la_server_returned' => 'Сервер повернув', + 'la_empty_issues' => 'Окремих юридичних питань не виявлено.', + + 'la_addon_button' => '⚖️🇳🇴 Запустити глибокий юридичний аналіз цього тексту', + 'la_addon_button_busy' => 'Виконується глибокий юридичний аналіз…', + 'la_addon_section' => 'Глибокий юридичний аналіз', ], 'pl' => [ 'meta_title' => 'Do Better Norge - prawne narzędzia AI', @@ -1376,6 +1496,46 @@ function dbnToolsTranslations(): array 'js_field_tags' => 'Tagi (oddzielone przecinkami)', 'js_field_lang' => 'Język', 'js_field_author' => 'Autor', + + // ── Analiza prawna ──────────────────────────────────────────────── + 'la_doc_type_label' => 'Typ dokumentu', + 'la_doc_type_auto' => 'Auto-wykrywanie', + 'la_doc_type_other' => 'Inny', + 'la_engine_hint' => 'Silnik: dbn-legal-agent-v3 (norweski prawny fine-tune na GPU). Każda kwestia odpowiadana osobno; ~30-60s na kwestię, do 5 kwestii na uruchomienie.', + 'la_input_label' => 'Wklejony tekst', + 'la_input_hint' => '(opcjonalne, jeśli wybrano plik lub dokument)', + 'la_input_placeholder' => 'Wklej notatkę sprawy, orzeczenie sądu, vedtak, list lub dowolny tekst prawny. Możesz też przesłać plik lub wybrać z Moich dokumentów powyżej — wymagane jest co najmniej jedno źródło.', + 'la_run_button' => 'Uruchom analizę prawną', + 'la_run_button_busy' => 'Uruchamianie…', + 'la_ready_title' => 'Gotowe', + 'la_ready_intro' => 'Prześlij dokument lub wklej tekst — narzędzie wyodrębni do 5 odrębnych kwestii prawnych i poprosi norweski fine-tune o odpowiedź na każdą z cytatami.', + 'la_ready_pipeline' => 'Przebieg 1 używa Azure GPT-4o-mini do wykrycia kwestii. Przebieg 2 wywołuje dbn-legal-agent-v3 dla każdej. Przebieg 3 syntetyzuje obraz całości. Typowe uruchomienie trwa 2-5 minut.', + 'la_pipeline_pass1' => 'Przebieg 1', + 'la_pipeline_pass2' => 'Przebieg 2', + 'la_pipeline_pass3' => 'Przebieg 3', + 'la_pass1_extracting' => 'Wyodrębnianie kwestii prawnych z dokumentu…', + 'la_pass1_found' => 'Znaleziono {n} kwestii prawnych', + 'la_pass2_asking' => 'Pytanie dbn-legal-agent-v3 o każdą kwestię…', + 'la_pass2_answered' => 'Specjalista odpowiedział na {n} kwestii', + 'la_pass3_synthesis' => 'Synteza zakończona', + 'la_waiting' => 'Oczekiwanie…', + 'la_searching_corpus' => 'Przeszukiwanie korpusu prawnego…', + 'la_asking_finetune' => 'Pytanie dbn-legal-agent-v3…', + 'la_overall' => 'Ocena całościowa', + 'la_next_steps' => 'Następne kroki', + 'la_answer_header' => 'Odpowiedź', + 'la_legal_basis' => 'Podstawa prawna:', + 'la_extracting_status' => 'Identyfikowanie odrębnych kwestii prawnych…', + 'la_synthesising_status' => 'Syntetyzowanie oceny całościowej…', + 'la_extracting_files' => 'Wyodrębnianie tekstu z {n} plik(ów)…', + 'la_need_input' => 'Wklej tekst, prześlij plik lub wybierz dokument przed uruchomieniem.', + 'la_error_prefix' => 'Błąd:', + 'la_server_returned' => 'Serwer zwrócił', + 'la_empty_issues' => 'Nie zidentyfikowano odrębnych kwestii prawnych.', + + 'la_addon_button' => '⚖️🇳🇴 Uruchom głęboką analizę prawną tego tekstu', + 'la_addon_button_busy' => 'Trwa głęboka analiza prawna…', + 'la_addon_section' => 'Głęboka analiza prawna', ], ]; } diff --git a/includes/layout_footer.php b/includes/layout_footer.php index 404bd55..b18f9de 100644 --- a/includes/layout_footer.php +++ b/includes/layout_footer.php @@ -23,6 +23,37 @@ + dbnToolsT('la_addon_button', $_footerLang), + 'addonButtonBusy' => dbnToolsT('la_addon_button_busy', $_footerLang), + 'addonSection' => dbnToolsT('la_addon_section', $_footerLang), + 'pass1' => dbnToolsT('la_pipeline_pass1', $_footerLang), + 'pass2' => dbnToolsT('la_pipeline_pass2', $_footerLang), + 'pass3' => dbnToolsT('la_pipeline_pass3', $_footerLang), + 'pass1Extracting' => dbnToolsT('la_pass1_extracting', $_footerLang), + 'pass1Found' => dbnToolsT('la_pass1_found', $_footerLang), + 'pass2Asking' => dbnToolsT('la_pass2_asking', $_footerLang), + 'pass2Answered' => dbnToolsT('la_pass2_answered', $_footerLang), + 'pass3Synthesis' => dbnToolsT('la_pass3_synthesis', $_footerLang), + 'waiting' => dbnToolsT('la_waiting', $_footerLang), + 'searchingCorpus' => dbnToolsT('la_searching_corpus', $_footerLang), + 'askingFinetune' => dbnToolsT('la_asking_finetune', $_footerLang), + 'overall' => dbnToolsT('la_overall', $_footerLang), + 'nextSteps' => dbnToolsT('la_next_steps', $_footerLang), + 'answerHeader' => dbnToolsT('la_answer_header', $_footerLang), + 'legalBasis' => dbnToolsT('la_legal_basis', $_footerLang), + 'errorPrefix' => dbnToolsT('la_error_prefix', $_footerLang), + 'serverReturned' => dbnToolsT('la_server_returned', $_footerLang), +]; +?> + diff --git a/legal-analysis.php b/legal-analysis.php index 5fb6bf0..529fb4f 100644 --- a/legal-analysis.php +++ b/legal-analysis.php @@ -6,28 +6,58 @@ $toolKind = 'Deep Legal Q&A'; $toolBadge = 'Two-pass'; $extraScripts = ['assets/js/legal-analysis.js']; require_once __DIR__ . '/includes/layout.php'; + +$laLang = dbnToolsCurrentLanguage(); +$laT = static fn(string $k): string => dbnToolsT($k, $laLang); +$laI18n = [ + 'docTypeAuto' => $laT('la_doc_type_auto'), + 'runButton' => $laT('la_run_button'), + 'runButtonBusy' => $laT('la_run_button_busy'), + 'extractingFiles' => $laT('la_extracting_files'), + 'needInput' => $laT('la_need_input'), + 'errorPrefix' => $laT('la_error_prefix'), + 'serverReturned' => $laT('la_server_returned'), + 'pass1' => $laT('la_pipeline_pass1'), + 'pass2' => $laT('la_pipeline_pass2'), + 'pass3' => $laT('la_pipeline_pass3'), + 'pass1Extracting' => $laT('la_pass1_extracting'), + 'pass1Found' => $laT('la_pass1_found'), + 'pass2Asking' => $laT('la_pass2_asking'), + 'pass2Answered' => $laT('la_pass2_answered'), + 'pass3Synthesis' => $laT('la_pass3_synthesis'), + 'waiting' => $laT('la_waiting'), + 'searchingCorpus' => $laT('la_searching_corpus'), + 'askingFinetune' => $laT('la_asking_finetune'), + 'overall' => $laT('la_overall'), + 'nextSteps' => $laT('la_next_steps'), + 'answerHeader' => $laT('la_answer_header'), + 'legalBasis' => $laT('la_legal_basis'), + 'extractingStatus' => $laT('la_extracting_status'), + 'synthesisingStatus' => $laT('la_synthesising_status'), + 'emptyIssues' => $laT('la_empty_issues'), +]; ?>
        - - + + + +
        - Document type - + + - +
        -

        - Engine: dbn-legal-agent-v3 (Norwegian legal fine-tune on GPU). Expect ~30-60 seconds per issue, up to 5 issues per run. -

        +

      - - + +
      -

      Ready

      -

      Upload a document or paste text — the tool will extract up to 5 distinct legal issues, then ask the Norwegian-law fine-tune to answer each one with citations.

      -

      Pass 1 uses Azure GPT-4o-mini to spot issues. Pass 2 calls the dbn-legal-agent-v3 fine-tune on ocelot for each one. Pass 3 synthesises the overall picture. A typical run takes 2-5 minutes.

      +

      +

      +

      + +