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

      - - + +
      -

      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.

      +

      +

      +

      + +