Legal Analysis: full language follow-through (UI + LLM)
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 <noreply@anthropic.com>
This commit is contained in:
+45
-35
@@ -4,10 +4,25 @@
|
|||||||
* Two-pass flow: extract distinct legal issues (Azure) → answer each issue
|
* Two-pass flow: extract distinct legal issues (Azure) → answer each issue
|
||||||
* with dbn-legal-agent-v3 fine-tune on ocelot → synthesise overall assessment.
|
* with dbn-legal-agent-v3 fine-tune on ocelot → synthesise overall assessment.
|
||||||
* Streams NDJSON so the UI fills in as each issue completes.
|
* 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 () {
|
(function () {
|
||||||
'use strict';
|
'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 ──────────────────────────────────────────────────────────
|
// ── Element refs ──────────────────────────────────────────────────────────
|
||||||
var form = document.getElementById('laForm');
|
var form = document.getElementById('laForm');
|
||||||
var runBtn = document.getElementById('laRunButton');
|
var runBtn = document.getElementById('laRunButton');
|
||||||
@@ -24,21 +39,23 @@
|
|||||||
|
|
||||||
// ── State ─────────────────────────────────────────────────────────────────
|
// ── State ─────────────────────────────────────────────────────────────────
|
||||||
var _extractedFiles = [];
|
var _extractedFiles = [];
|
||||||
var _currentLang = 'no';
|
var _currentLang = window.DBN_LA_LANG || 'no';
|
||||||
var _lastPayload = null;
|
var _lastPayload = null;
|
||||||
var _issueCards = {}; // id -> DOM element
|
var _issueCards = {};
|
||||||
|
|
||||||
// ── Lang switcher ─────────────────────────────────────────────────────────
|
// ── Lang switcher ─────────────────────────────────────────────────────────
|
||||||
document.querySelectorAll('.la-lang-btn').forEach(function (btn) {
|
document.querySelectorAll('.la-lang-btn').forEach(function (btn) {
|
||||||
btn.addEventListener('click', function () {
|
btn.addEventListener('click', function () {
|
||||||
document.querySelectorAll('.la-lang-btn').forEach(function (b) {
|
// Switching UI language requires a page reload so all labels rerender.
|
||||||
b.classList.toggle('is-active', b === btn);
|
var lang = btn.dataset.lang || 'no';
|
||||||
});
|
if (lang === _currentLang) return;
|
||||||
_currentLang = btn.dataset.lang || 'no';
|
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) {
|
if (uploadZone) {
|
||||||
uploadZone.addEventListener('dragover', function (e) {
|
uploadZone.addEventListener('dragover', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -68,7 +85,6 @@
|
|||||||
uploadZone.addEventListener('click', function (e) {
|
uploadZone.addEventListener('click', function (e) {
|
||||||
if (e.target === uploadClear || (uploadClear && uploadClear.contains(e.target))) return;
|
if (e.target === uploadClear || (uploadClear && uploadClear.contains(e.target))) return;
|
||||||
if (e.target === uploadInput) 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');
|
var lbl = e.target.closest && e.target.closest('label');
|
||||||
if (lbl && lbl.getAttribute('for') === uploadInput.id) return;
|
if (lbl && lbl.getAttribute('for') === uploadInput.id) return;
|
||||||
if (uploadInput) uploadInput.click();
|
if (uploadInput) uploadInput.click();
|
||||||
@@ -98,7 +114,7 @@
|
|||||||
var files = Array.from(fileList).slice(0, 5);
|
var files = Array.from(fileList).slice(0, 5);
|
||||||
if (!files.length) return;
|
if (!files.length) return;
|
||||||
|
|
||||||
setStatus('Extracting text from ' + files.length + ' file(s)…');
|
setStatus(t('extractingFiles', { n: files.length }));
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
|
|
||||||
var promises = files.map(function (file) {
|
var promises = files.map(function (file) {
|
||||||
@@ -120,7 +136,7 @@
|
|||||||
setBusy(false);
|
setBusy(false);
|
||||||
})
|
})
|
||||||
.catch(function (err) {
|
.catch(function (err) {
|
||||||
setStatus('Error: ' + err.message);
|
setStatus(t('errorPrefix') + ' ' + err.message);
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -153,7 +169,7 @@
|
|||||||
var docIds = rawDocIds ? rawDocIds.split(',').map(Number).filter(Boolean) : [];
|
var docIds = rawDocIds ? rawDocIds.split(',').map(Number).filter(Boolean) : [];
|
||||||
|
|
||||||
if (!combined && !docIds.length) {
|
if (!combined && !docIds.length) {
|
||||||
setStatus('Paste text, upload a file, or select a document before running.');
|
setStatus(t('needInput'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +185,7 @@
|
|||||||
_issueCards = {};
|
_issueCards = {};
|
||||||
|
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setStatus('Running…');
|
setStatus(t('runButtonBusy'));
|
||||||
renderInitial();
|
renderInitial();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -181,7 +197,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!resp.ok || !resp.body) {
|
if (!resp.ok || !resp.body) {
|
||||||
throw new Error('Server returned ' + resp.status);
|
throw new Error(t('serverReturned') + ' ' + resp.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
var reader = resp.body.getReader();
|
var reader = resp.body.getReader();
|
||||||
@@ -244,7 +260,7 @@
|
|||||||
if (!resultsEl) return;
|
if (!resultsEl) return;
|
||||||
resultsEl.innerHTML =
|
resultsEl.innerHTML =
|
||||||
'<div class="la-pipeline">'
|
'<div class="la-pipeline">'
|
||||||
+ '<div class="la-step running"><strong>Pass 1</strong> — Extracting legal issues from your document…</div>'
|
+ '<div class="la-step running"><strong>' + esc(t('pass1')) + '</strong> — ' + esc(t('pass1Extracting')) + '</div>'
|
||||||
+ '</div>'
|
+ '</div>'
|
||||||
+ '<ol id="laIssueList" class="la-issues"></ol>';
|
+ '<ol id="laIssueList" class="la-issues"></ol>';
|
||||||
}
|
}
|
||||||
@@ -267,15 +283,14 @@
|
|||||||
+ '</div>'
|
+ '</div>'
|
||||||
+ '<h4 class="la-issue__q">' + esc(issue.question) + '</h4>'
|
+ '<h4 class="la-issue__q">' + esc(issue.question) + '</h4>'
|
||||||
+ (issue.brief_context ? '<p class="la-issue__ctx"><em>' + esc(issue.brief_context) + '</em></p>' : '')
|
+ (issue.brief_context ? '<p class="la-issue__ctx"><em>' + esc(issue.brief_context) + '</em></p>' : '')
|
||||||
+ '<div class="la-issue__status">Waiting…</div>';
|
+ '<div class="la-issue__status">' + esc(t('waiting')) + '</div>';
|
||||||
list.appendChild(li);
|
list.appendChild(li);
|
||||||
_issueCards[issue.id] = li;
|
_issueCards[issue.id] = li;
|
||||||
});
|
});
|
||||||
// Mark Pass 1 complete
|
|
||||||
var pipeline = resultsEl.querySelector('.la-pipeline');
|
var pipeline = resultsEl.querySelector('.la-pipeline');
|
||||||
if (pipeline) {
|
if (pipeline) {
|
||||||
pipeline.innerHTML = '<div class="la-step done"><strong>Pass 1</strong> — Found ' + issues.length + ' legal issue(s)</div>'
|
pipeline.innerHTML = '<div class="la-step done"><strong>' + esc(t('pass1')) + '</strong> — ' + esc(t('pass1Found', { n: issues.length })) + '</div>'
|
||||||
+ '<div class="la-step running"><strong>Pass 2</strong> — Asking dbn-legal-agent-v3 about each issue…</div>';
|
+ '<div class="la-step running"><strong>' + esc(t('pass2')) + '</strong> — ' + esc(t('pass2Asking')) + '</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,10 +300,10 @@
|
|||||||
var status = card.querySelector('.la-issue__status');
|
var status = card.querySelector('.la-issue__status');
|
||||||
if (!status) return;
|
if (!status) return;
|
||||||
if (step === 'issue_searching_corpus') {
|
if (step === 'issue_searching_corpus') {
|
||||||
status.textContent = 'Searching legal corpus…';
|
status.textContent = t('searchingCorpus');
|
||||||
card.classList.add('running');
|
card.classList.add('running');
|
||||||
} else if (step === 'issue_answering') {
|
} 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.remove('pending', 'running');
|
||||||
card.classList.add('answered');
|
card.classList.add('answered');
|
||||||
|
|
||||||
// Refresh severity from real answer
|
|
||||||
var sevEl = card.querySelector('.la-severity');
|
var sevEl = card.querySelector('.la-severity');
|
||||||
if (sevEl) {
|
if (sevEl) {
|
||||||
sevEl.className = 'la-severity la-severity-' + esc(issue.severity || 'medium');
|
sevEl.className = 'la-severity la-severity-' + esc(issue.severity || 'medium');
|
||||||
@@ -309,13 +323,13 @@
|
|||||||
var statusBlock = card.querySelector('.la-issue__status');
|
var statusBlock = card.querySelector('.la-issue__status');
|
||||||
if (statusBlock) statusBlock.remove();
|
if (statusBlock) statusBlock.remove();
|
||||||
|
|
||||||
var answerHtml = '<div class="la-issue__answer"><h5>Svar</h5><p>'
|
var answerHtml = '<div class="la-issue__answer"><h5>' + esc(t('answerHeader')) + '</h5><p>'
|
||||||
+ esc(issue.answer || '').replace(/\n/g, '<br>')
|
+ esc(issue.answer || '').replace(/\n/g, '<br>')
|
||||||
+ '</p></div>';
|
+ '</p></div>';
|
||||||
|
|
||||||
var basisHtml = '';
|
var basisHtml = '';
|
||||||
if (issue.legal_basis) {
|
if (issue.legal_basis) {
|
||||||
basisHtml = '<div class="la-issue__basis"><strong>Lovgrunnlag:</strong> '
|
basisHtml = '<div class="la-issue__basis"><strong>' + esc(t('legalBasis')) + '</strong> '
|
||||||
+ esc(issue.legal_basis) + '</div>';
|
+ esc(issue.legal_basis) + '</div>';
|
||||||
}
|
}
|
||||||
var checkHtml = '';
|
var checkHtml = '';
|
||||||
@@ -326,14 +340,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderFinal(result) {
|
function renderFinal(result) {
|
||||||
// Add Pass 3 synthesis at the top
|
|
||||||
var topHtml = '';
|
var topHtml = '';
|
||||||
if (result.overall_assessment) {
|
if (result.overall_assessment) {
|
||||||
topHtml = '<section class="la-synthesis result-section">'
|
topHtml = '<section class="la-synthesis result-section">'
|
||||||
+ '<h3>Overall assessment</h3>'
|
+ '<h3>' + esc(t('overall')) + '</h3>'
|
||||||
+ '<p>' + esc(result.overall_assessment) + '</p>';
|
+ '<p>' + esc(result.overall_assessment) + '</p>';
|
||||||
if (Array.isArray(result.next_steps) && result.next_steps.length) {
|
if (Array.isArray(result.next_steps) && result.next_steps.length) {
|
||||||
topHtml += '<h4>Next steps</h4><ul>'
|
topHtml += '<h4>' + esc(t('nextSteps')) + '</h4><ul>'
|
||||||
+ result.next_steps.map(function (s) { return '<li>' + esc(s) + '</li>'; }).join('')
|
+ result.next_steps.map(function (s) { return '<li>' + esc(s) + '</li>'; }).join('')
|
||||||
+ '</ul>';
|
+ '</ul>';
|
||||||
}
|
}
|
||||||
@@ -343,32 +356,29 @@
|
|||||||
topHtml += '</section>';
|
topHtml += '</section>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark Pass 2/3 complete in the pipeline strip
|
|
||||||
var pipeline = resultsEl.querySelector('.la-pipeline');
|
var pipeline = resultsEl.querySelector('.la-pipeline');
|
||||||
if (pipeline) {
|
if (pipeline) {
|
||||||
pipeline.innerHTML =
|
pipeline.innerHTML =
|
||||||
'<div class="la-step done"><strong>Pass 1</strong> — Issues extracted</div>'
|
'<div class="la-step done"><strong>' + esc(t('pass1')) + '</strong> — ' + esc(t('pass1Found', { n: (result.issues || []).length })) + '</div>'
|
||||||
+ '<div class="la-step done"><strong>Pass 2</strong> — Specialist answered ' + (result.issues || []).length + ' issue(s)</div>'
|
+ '<div class="la-step done"><strong>' + esc(t('pass2')) + '</strong> — ' + esc(t('pass2Answered', { n: (result.issues || []).length })) + '</div>'
|
||||||
+ '<div class="la-step done"><strong>Pass 3</strong> — Synthesis complete</div>';
|
+ '<div class="la-step done"><strong>' + esc(t('pass3')) + '</strong> — ' + esc(t('pass3Synthesis')) + '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert synthesis at top of resultsEl, after pipeline
|
|
||||||
if (topHtml && pipeline) {
|
if (topHtml && pipeline) {
|
||||||
pipeline.insertAdjacentHTML('afterend', topHtml);
|
pipeline.insertAdjacentHTML('afterend', topHtml);
|
||||||
} else if (topHtml) {
|
} else if (topHtml) {
|
||||||
resultsEl.insertAdjacentHTML('afterbegin', 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')) {
|
if ((!result.issues || !result.issues.length) && resultsEl.querySelector('#laIssueList')) {
|
||||||
resultsEl.querySelector('#laIssueList').innerHTML =
|
resultsEl.querySelector('#laIssueList').innerHTML =
|
||||||
'<li class="la-issue answered"><p><em>No discrete legal issues were identified.</em></p></li>';
|
'<li class="la-issue answered"><p><em>' + esc(t('emptyIssues')) + '</em></p></li>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(msg) {
|
function showError(msg) {
|
||||||
if (resultsEl) {
|
if (resultsEl) {
|
||||||
resultsEl.innerHTML = '<div class="empty-state"><h3>Error</h3><p>' + esc(msg) + '</p></div>';
|
resultsEl.innerHTML = '<div class="empty-state"><h3>' + esc(t('errorPrefix')) + '</h3><p>' + esc(msg) + '</p></div>';
|
||||||
}
|
}
|
||||||
setStatus('');
|
setStatus('');
|
||||||
}
|
}
|
||||||
@@ -376,7 +386,7 @@
|
|||||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
function setBusy(on) {
|
function setBusy(on) {
|
||||||
if (runBtn) runBtn.disabled = 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) {
|
function setStatus(msg) {
|
||||||
if (statusEl) statusEl.textContent = msg;
|
if (statusEl) statusEl.textContent = msg;
|
||||||
|
|||||||
+29
-22
@@ -2579,6 +2579,14 @@ window.dbnShowSaveResultButton = showSaveResultButton;
|
|||||||
// Injects a button that, when clicked, streams the two-pass legal-analysis flow
|
// 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
|
// (extract issues → ask dbn-legal-agent-v3 → synthesise) into a sub-section
|
||||||
// below the host container.
|
// 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) {
|
function dbnRunLegalAnalysisAddon(text, lang, sourceTool, containerEl) {
|
||||||
if (!containerEl) containerEl = document.getElementById('results');
|
if (!containerEl) containerEl = document.getElementById('results');
|
||||||
if (!containerEl || !text || text.length < 80) return;
|
if (!containerEl || !text || text.length < 80) return;
|
||||||
@@ -2595,9 +2603,9 @@ function dbnRunLegalAnalysisAddon(text, lang, sourceTool, containerEl) {
|
|||||||
section.style.borderRadius = '8px';
|
section.style.borderRadius = '8px';
|
||||||
section.style.background = '#f0fdfa';
|
section.style.background = '#f0fdfa';
|
||||||
section.innerHTML =
|
section.innerHTML =
|
||||||
'<h3 style="margin-top:0;color:#0e7490;">⚖️🇳🇴 Deep Legal Analysis</h3>'
|
'<h3 style="margin-top:0;color:#0e7490;">⚖️🇳🇴 ' + escapeHtml(_laT('addonSection')) + '</h3>'
|
||||||
+ '<div class="la-pipeline" style="margin-bottom:1rem;">'
|
+ '<div class="la-pipeline" style="margin-bottom:1rem;">'
|
||||||
+ '<div class="la-step running"><strong>Pass 1</strong> — Extracting legal issues…</div>'
|
+ '<div class="la-step running"><strong>' + escapeHtml(_laT('pass1')) + '</strong> — ' + escapeHtml(_laT('pass1Extracting')) + '</div>'
|
||||||
+ '</div>'
|
+ '</div>'
|
||||||
+ '<ol id="laAddonIssues" class="la-issues" style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:1rem;"></ol>';
|
+ '<ol id="laAddonIssues" class="la-issues" style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:1rem;"></ol>';
|
||||||
containerEl.parentNode.appendChild(section);
|
containerEl.parentNode.appendChild(section);
|
||||||
@@ -2608,7 +2616,7 @@ function dbnRunLegalAnalysisAddon(text, lang, sourceTool, containerEl) {
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
text: text,
|
text: text,
|
||||||
language: lang || 'no',
|
language: lang || window.DBN_CURRENT_LANG || 'no',
|
||||||
doc_type: 'auto',
|
doc_type: 'auto',
|
||||||
source_tool: sourceTool || 'addon',
|
source_tool: sourceTool || 'addon',
|
||||||
};
|
};
|
||||||
@@ -2638,7 +2646,7 @@ function dbnRunLegalAnalysisAddon(text, lang, sourceTool, containerEl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
section.innerHTML += '<p style="color:#b91c1c;margin-top:0.5rem;">Error: ' + escapeHtml(err.message || String(err)) + '</p>';
|
section.innerHTML += '<p style="color:#b91c1c;margin-top:0.5rem;">' + escapeHtml(_laT('errorPrefix')) + ' ' + escapeHtml(err.message || String(err)) + '</p>';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2655,21 +2663,21 @@ function laAddonEvent(data, listEl, pipelineEl, cards) {
|
|||||||
+ '<span class="la-severity la-severity-' + escapeHtml(sev) + '">' + escapeHtml(sev.toUpperCase()) + '</span></div>'
|
+ '<span class="la-severity la-severity-' + escapeHtml(sev) + '">' + escapeHtml(sev.toUpperCase()) + '</span></div>'
|
||||||
+ '<h4 class="la-issue__q">' + escapeHtml(issue.question || '') + '</h4>'
|
+ '<h4 class="la-issue__q">' + escapeHtml(issue.question || '') + '</h4>'
|
||||||
+ (issue.brief_context ? '<p class="la-issue__ctx"><em>' + escapeHtml(issue.brief_context) + '</em></p>' : '')
|
+ (issue.brief_context ? '<p class="la-issue__ctx"><em>' + escapeHtml(issue.brief_context) + '</em></p>' : '')
|
||||||
+ '<div class="la-issue__status">Waiting…</div>';
|
+ '<div class="la-issue__status">' + escapeHtml(_laT('waiting')) + '</div>';
|
||||||
listEl.appendChild(li);
|
listEl.appendChild(li);
|
||||||
cards[issue.id] = li;
|
cards[issue.id] = li;
|
||||||
});
|
});
|
||||||
pipelineEl.innerHTML =
|
pipelineEl.innerHTML =
|
||||||
'<div class="la-step done"><strong>Pass 1</strong> — Found ' + (data.issues || []).length + ' issue(s)</div>'
|
'<div class="la-step done"><strong>' + escapeHtml(_laT('pass1')) + '</strong> — ' + escapeHtml(_laT('pass1Found', { n: (data.issues || []).length })) + '</div>'
|
||||||
+ '<div class="la-step running"><strong>Pass 2</strong> — Asking dbn-legal-agent-v3…</div>';
|
+ '<div class="la-step running"><strong>' + escapeHtml(_laT('pass2')) + '</strong> — ' + escapeHtml(_laT('pass2Asking')) + '</div>';
|
||||||
} else if (data.event === 'progress') {
|
} else if (data.event === 'progress') {
|
||||||
const card = cards[data.issue_id];
|
const card = cards[data.issue_id];
|
||||||
if (card) {
|
if (card) {
|
||||||
const status = card.querySelector('.la-issue__status');
|
const status = card.querySelector('.la-issue__status');
|
||||||
if (status) {
|
if (status) {
|
||||||
status.textContent = data.step === 'issue_searching_corpus'
|
status.textContent = data.step === 'issue_searching_corpus'
|
||||||
? 'Searching legal corpus…'
|
? _laT('searchingCorpus')
|
||||||
: 'Asking dbn-legal-agent-v3…';
|
: _laT('askingFinetune');
|
||||||
}
|
}
|
||||||
card.classList.add('running');
|
card.classList.add('running');
|
||||||
}
|
}
|
||||||
@@ -2687,22 +2695,22 @@ function laAddonEvent(data, listEl, pipelineEl, cards) {
|
|||||||
}
|
}
|
||||||
const statusEl = card.querySelector('.la-issue__status');
|
const statusEl = card.querySelector('.la-issue__status');
|
||||||
if (statusEl) statusEl.remove();
|
if (statusEl) statusEl.remove();
|
||||||
let html = '<div class="la-issue__answer"><h5>Svar</h5><p>' + escapeHtml(iss.answer || '').replace(/\n/g, '<br>') + '</p></div>';
|
let html = '<div class="la-issue__answer"><h5>' + escapeHtml(_laT('answerHeader')) + '</h5><p>' + escapeHtml(iss.answer || '').replace(/\n/g, '<br>') + '</p></div>';
|
||||||
if (iss.legal_basis) html += '<div class="la-issue__basis"><strong>Lovgrunnlag:</strong> ' + escapeHtml(iss.legal_basis) + '</div>';
|
if (iss.legal_basis) html += '<div class="la-issue__basis"><strong>' + escapeHtml(_laT('legalBasis')) + '</strong> ' + escapeHtml(iss.legal_basis) + '</div>';
|
||||||
if (iss.what_to_check) html += '<p class="la-issue__check"><em>' + escapeHtml(iss.what_to_check) + '</em></p>';
|
if (iss.what_to_check) html += '<p class="la-issue__check"><em>' + escapeHtml(iss.what_to_check) + '</em></p>';
|
||||||
card.insertAdjacentHTML('beforeend', html);
|
card.insertAdjacentHTML('beforeend', html);
|
||||||
} else if (data.event === 'final' && data.result) {
|
} else if (data.event === 'final' && data.result) {
|
||||||
const res = data.result;
|
const res = data.result;
|
||||||
pipelineEl.innerHTML =
|
pipelineEl.innerHTML =
|
||||||
'<div class="la-step done"><strong>Pass 1</strong> — Issues extracted</div>'
|
'<div class="la-step done"><strong>' + escapeHtml(_laT('pass1')) + '</strong> — ' + escapeHtml(_laT('pass1Found', { n: (res.issues || []).length })) + '</div>'
|
||||||
+ '<div class="la-step done"><strong>Pass 2</strong> — Specialist answered ' + (res.issues || []).length + '</div>'
|
+ '<div class="la-step done"><strong>' + escapeHtml(_laT('pass2')) + '</strong> — ' + escapeHtml(_laT('pass2Answered', { n: (res.issues || []).length })) + '</div>'
|
||||||
+ '<div class="la-step done"><strong>Pass 3</strong> — Synthesis complete</div>';
|
+ '<div class="la-step done"><strong>' + escapeHtml(_laT('pass3')) + '</strong> — ' + escapeHtml(_laT('pass3Synthesis')) + '</div>';
|
||||||
if (res.overall_assessment) {
|
if (res.overall_assessment) {
|
||||||
let syn = '<section class="la-synthesis" style="margin-bottom:1rem;padding:0.85rem 1.1rem;border-left:4px solid #0f766e;background:#ecfeff;border-radius:6px;">'
|
let syn = '<section class="la-synthesis" style="margin-bottom:1rem;padding:0.85rem 1.1rem;border-left:4px solid #0f766e;background:#ecfeff;border-radius:6px;">'
|
||||||
+ '<h3 style="margin-top:0;color:#0e7490;">Overall assessment</h3>'
|
+ '<h3 style="margin-top:0;color:#0e7490;">' + escapeHtml(_laT('overall')) + '</h3>'
|
||||||
+ '<p>' + escapeHtml(res.overall_assessment) + '</p>';
|
+ '<p>' + escapeHtml(res.overall_assessment) + '</p>';
|
||||||
if (Array.isArray(res.next_steps) && res.next_steps.length) {
|
if (Array.isArray(res.next_steps) && res.next_steps.length) {
|
||||||
syn += '<h4 style="margin:0.7rem 0 0.3rem;color:#155e75;">Next steps</h4><ul>'
|
syn += '<h4 style="margin:0.7rem 0 0.3rem;color:#155e75;">' + escapeHtml(_laT('nextSteps')) + '</h4><ul>'
|
||||||
+ res.next_steps.map(function (s) { return '<li>' + escapeHtml(s) + '</li>'; }).join('')
|
+ res.next_steps.map(function (s) { return '<li>' + escapeHtml(s) + '</li>'; }).join('')
|
||||||
+ '</ul>';
|
+ '</ul>';
|
||||||
}
|
}
|
||||||
@@ -2717,25 +2725,24 @@ function laAddonEvent(data, listEl, pipelineEl, cards) {
|
|||||||
const containerSection = pipelineEl.parentNode;
|
const containerSection = pipelineEl.parentNode;
|
||||||
window.dbnShowSaveResultButton(
|
window.dbnShowSaveResultButton(
|
||||||
'legal-analysis',
|
'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,
|
res,
|
||||||
{ model: res.model || 'dbn-legal-agent-v3', latency_ms: res.latency_ms || 0 },
|
{ model: res.model || 'dbn-legal-agent-v3', latency_ms: res.latency_ms || 0 },
|
||||||
containerSection
|
containerSection
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (data.event === 'error') {
|
} else if (data.event === 'error') {
|
||||||
pipelineEl.innerHTML += '<div style="color:#b91c1c;margin-top:0.4rem;">Error: ' + escapeHtml(data.message || data.error || 'unknown') + '</div>';
|
pipelineEl.innerHTML += '<div style="color:#b91c1c;margin-top:0.4rem;">' + escapeHtml(_laT('errorPrefix')) + ' ' + escapeHtml(data.message || data.error || 'unknown') + '</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function dbnInjectLegalAnalysisButton(text, lang, sourceTool, containerEl, options) {
|
function dbnInjectLegalAnalysisButton(text, lang, sourceTool, containerEl, options) {
|
||||||
if (!containerEl || !text || text.length < 80) return;
|
if (!containerEl || !text || text.length < 80) return;
|
||||||
// Avoid duplicate buttons
|
|
||||||
if (containerEl.querySelector('.la-addon-btn')) return;
|
if (containerEl.querySelector('.la-addon-btn')) return;
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.type = 'button';
|
btn.type = 'button';
|
||||||
btn.className = 'la-addon-btn';
|
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;'
|
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;'
|
+ 'background:#0f766e;color:#fff;border:none;border-radius:6px;cursor:pointer;'
|
||||||
+ 'font-weight:600;letter-spacing:0.02em;';
|
+ '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('mouseleave', function () { btn.style.background = '#0f766e'; });
|
||||||
btn.addEventListener('click', function () {
|
btn.addEventListener('click', function () {
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = 'Running deep legal analysis…';
|
btn.textContent = _laT('addonButtonBusy');
|
||||||
btn.style.background = '#94a3b8';
|
btn.style.background = '#94a3b8';
|
||||||
dbnRunLegalAnalysisAddon(text, lang, sourceTool, containerEl);
|
dbnRunLegalAnalysisAddon(text, lang || window.DBN_CURRENT_LANG || 'no', sourceTool, containerEl);
|
||||||
});
|
});
|
||||||
containerEl.appendChild(btn);
|
containerEl.appendChild(btn);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,15 +48,18 @@ process law). Each issue must be answerable as a SINGLE focused legal question
|
|||||||
(≤ 25 words), not a multi-part essay.
|
(≤ 25 words), not a multi-part essay.
|
||||||
|
|
||||||
Document type hint: {$docType}
|
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": [
|
"issues": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"question": "<short Norwegian legal question, single issue>",
|
"question": "<short legal question in {$locale}, single issue, statute refs kept in original Norwegian/Latin>",
|
||||||
"brief_context": "<≤2 sentences from the document that triggered this 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": "<barnevernet|adopsjon|emergency|samvær|other>",
|
"doc_type": "<barnevernet|adopsjon|emergency|samvær|other>",
|
||||||
"severity_hint": "<high|medium|low>"
|
"severity_hint": "<high|medium|low>"
|
||||||
}
|
}
|
||||||
@@ -69,6 +72,7 @@ Rules:
|
|||||||
named Høyesterett/EMD cases — NOT general advice.
|
named Høyesterett/EMD cases — NOT general advice.
|
||||||
- If the document has fewer than 5 real legal issues, return fewer entries.
|
- If the document has fewer than 5 real legal issues, return fewer entries.
|
||||||
- If NO real legal issue exists, return {"issues": []}.
|
- 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:
|
DOCUMENT:
|
||||||
---
|
---
|
||||||
@@ -119,19 +123,48 @@ PROMPT;
|
|||||||
*/
|
*/
|
||||||
public function answerIssue(array $issue, string $corpusContext, string $language): array
|
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. '
|
$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». '
|
. 'Bruk terskler fra barnevernsloven 2021: § 4-25 krever «klar nødvendighet». '
|
||||||
. 'Strand Lobben mot Norge (37283/13) setter krav om rehabiliteringsplan før adopsjon. '
|
. 'Strand Lobben mot Norge (37283/13) setter krav om rehabiliteringsplan før adopsjon. '
|
||||||
. 'Aldri oppfinn paragrafnumre, saksnumre eller dommernavn. '
|
. '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'];
|
$userMsg = $issue['question'];
|
||||||
if ($issue['brief_context'] !== '') {
|
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 !== '') {
|
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 = '';
|
$answer = '';
|
||||||
@@ -164,6 +197,13 @@ PROMPT;
|
|||||||
$severity = $clean !== '' ? dbnToolsInferCheckSeverity($clean) : $issue['severity_hint'];
|
$severity = $clean !== '' ? dbnToolsInferCheckSeverity($clean) : $issue['severity_hint'];
|
||||||
$legalBasis = dbnToolsExtractCheckLegalBasis($clean);
|
$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 [
|
return [
|
||||||
'id' => $issue['id'],
|
'id' => $issue['id'],
|
||||||
'question' => $issue['question'],
|
'question' => $issue['question'],
|
||||||
@@ -171,8 +211,8 @@ PROMPT;
|
|||||||
'answer' => $clean,
|
'answer' => $clean,
|
||||||
'severity' => $severity,
|
'severity' => $severity,
|
||||||
'legal_basis' => $legalBasis,
|
'legal_basis' => $legalBasis,
|
||||||
'citations_from_corpus' => [], // populated by orchestrator if it kept the chunks
|
'citations_from_corpus' => [],
|
||||||
'what_to_check' => 'Verifiser med norsk familieretsadvokat før handling.',
|
'what_to_check' => $whatToCheck,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,18 +234,29 @@ PROMPT;
|
|||||||
}
|
}
|
||||||
$issuesBlock = implode("\n", $bullets);
|
$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 = <<<PROMPT
|
$prompt = <<<PROMPT
|
||||||
Below are 1-5 legal questions raised about a {$docType} document, each with an answer
|
Below are 1-5 legal questions raised about a {$docType} document, each with an answer
|
||||||
from a Norwegian-law specialist model. Write a concise overall assessment in {$locale}.
|
from a Norwegian-law specialist model. Write a concise overall assessment.
|
||||||
|
|
||||||
|
Output language: {$locale}. Every human-readable string ("overall_assessment",
|
||||||
|
"next_steps[]", "disclaimer") MUST be written in {$locale}. Keep Norwegian statute
|
||||||
|
references and case names in their original form.
|
||||||
|
|
||||||
ISSUES + ANSWERS:
|
ISSUES + ANSWERS:
|
||||||
{$issuesBlock}
|
{$issuesBlock}
|
||||||
|
|
||||||
Return JSON only:
|
Return JSON only:
|
||||||
{
|
{
|
||||||
"overall_assessment": "<3-5 sentences summarising the legal picture across all issues>",
|
"overall_assessment": "<3-5 sentences in {$locale} summarising the legal picture across all issues>",
|
||||||
"next_steps": ["<concrete action 1>", "<concrete action 2>", "<concrete action 3>"],
|
"next_steps": ["<concrete action 1 in {$locale}>", "<concrete action 2>", "<concrete action 3>"],
|
||||||
"disclaimer": "This is automated legal analysis, not legal advice. Verify with a qualified Norwegian lawyer before acting."
|
"disclaimer": "{$disclaimerText}"
|
||||||
}
|
}
|
||||||
PROMPT;
|
PROMPT;
|
||||||
|
|
||||||
@@ -250,12 +301,24 @@ PROMPT;
|
|||||||
$issues = $this->extractIssues($text, $language, $docType);
|
$issues = $this->extractIssues($text, $language, $docType);
|
||||||
|
|
||||||
if (empty($issues)) {
|
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 [
|
return [
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'issues' => [],
|
'issues' => [],
|
||||||
'overall_assessment' => 'No discrete legal issues identified in this document.',
|
'overall_assessment' => $emptyAssessment,
|
||||||
'next_steps' => [],
|
'next_steps' => [],
|
||||||
'disclaimer' => 'Automated analysis — not legal advice.',
|
'disclaimer' => $emptyDisclaimer,
|
||||||
'model' => self::LEGAL_MODEL,
|
'model' => self::LEGAL_MODEL,
|
||||||
'latency_ms' => (int)round(microtime(true) * 1000) - $startMs,
|
'latency_ms' => (int)round(microtime(true) * 1000) - $startMs,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -395,6 +395,46 @@ function dbnToolsTranslations(): array
|
|||||||
'js_field_tags' => 'Tags (comma-separated)',
|
'js_field_tags' => 'Tags (comma-separated)',
|
||||||
'js_field_lang' => 'Language',
|
'js_field_lang' => 'Language',
|
||||||
'js_field_author' => 'Author',
|
'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' => [
|
'no' => [
|
||||||
'meta_title' => 'Do Better Norge - juridiske AI-verktøy',
|
'meta_title' => 'Do Better Norge - juridiske AI-verktøy',
|
||||||
@@ -722,6 +762,46 @@ function dbnToolsTranslations(): array
|
|||||||
'js_field_tags' => 'Tagger (komma-separert)',
|
'js_field_tags' => 'Tagger (komma-separert)',
|
||||||
'js_field_lang' => 'Språk',
|
'js_field_lang' => 'Språk',
|
||||||
'js_field_author' => 'Forfatter',
|
'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' => [
|
'uk' => [
|
||||||
'meta_title' => 'Do Better Norge - юридичні AI інструменти',
|
'meta_title' => 'Do Better Norge - юридичні AI інструменти',
|
||||||
@@ -1049,6 +1129,46 @@ function dbnToolsTranslations(): array
|
|||||||
'js_field_tags' => 'Теги (через кому)',
|
'js_field_tags' => 'Теги (через кому)',
|
||||||
'js_field_lang' => 'Мова',
|
'js_field_lang' => 'Мова',
|
||||||
'js_field_author' => 'Автор',
|
'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' => [
|
'pl' => [
|
||||||
'meta_title' => 'Do Better Norge - prawne narzędzia AI',
|
'meta_title' => 'Do Better Norge - prawne narzędzia AI',
|
||||||
@@ -1376,6 +1496,46 @@ function dbnToolsTranslations(): array
|
|||||||
'js_field_tags' => 'Tagi (oddzielone przecinkami)',
|
'js_field_tags' => 'Tagi (oddzielone przecinkami)',
|
||||||
'js_field_lang' => 'Język',
|
'js_field_lang' => 'Język',
|
||||||
'js_field_author' => 'Autor',
|
'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',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,37 @@
|
|||||||
</main><!-- /appShell -->
|
</main><!-- /appShell -->
|
||||||
<?php require_once __DIR__ . '/footer.php'; ?>
|
<?php require_once __DIR__ . '/footer.php'; ?>
|
||||||
<link rel="stylesheet" href="/assets/css/doc-picker.css">
|
<link rel="stylesheet" href="/assets/css/doc-picker.css">
|
||||||
|
<?php
|
||||||
|
// Expose current UI language + add-on i18n map for tools.js (legal-analysis add-on
|
||||||
|
// can fire from any tool page, so it needs strings without depending on per-tool maps).
|
||||||
|
$_footerLang = $uiLang ?? dbnToolsCurrentLanguage();
|
||||||
|
$_footerAddonI18n = [
|
||||||
|
'addonButton' => 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),
|
||||||
|
];
|
||||||
|
?>
|
||||||
|
<script>
|
||||||
|
window.DBN_CURRENT_LANG = <?= json_encode($_footerLang) ?>;
|
||||||
|
window.DBN_ADDON_I18N = <?= json_encode($_footerAddonI18n, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>;
|
||||||
|
</script>
|
||||||
<script src="assets/js/tools.js" defer></script>
|
<script src="assets/js/tools.js" defer></script>
|
||||||
<?php if (!empty($extraScripts) && is_array($extraScripts)): foreach ($extraScripts as $extraScript): ?>
|
<?php if (!empty($extraScripts) && is_array($extraScripts)): foreach ($extraScripts as $extraScript): ?>
|
||||||
<script src="<?= htmlspecialchars((string)$extraScript) ?>" defer></script>
|
<script src="<?= htmlspecialchars((string)$extraScript) ?>" defer></script>
|
||||||
|
|||||||
+53
-16
@@ -6,28 +6,58 @@ $toolKind = 'Deep Legal Q&A';
|
|||||||
$toolBadge = 'Two-pass';
|
$toolBadge = 'Two-pass';
|
||||||
$extraScripts = ['assets/js/legal-analysis.js'];
|
$extraScripts = ['assets/js/legal-analysis.js'];
|
||||||
require_once __DIR__ . '/includes/layout.php';
|
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'),
|
||||||
|
];
|
||||||
?>
|
?>
|
||||||
<form id="laForm" class="tool-form" novalidate>
|
<form id="laForm" class="tool-form" novalidate>
|
||||||
|
|
||||||
<div class="lang-switcher" id="laLangSwitcher" role="group" aria-label="UI language">
|
<div class="lang-switcher" id="laLangSwitcher" role="group" aria-label="UI language">
|
||||||
<button type="button" class="lang-btn la-lang-btn is-active" data-lang="no">🇳🇴 NO</button>
|
<button type="button" class="lang-btn la-lang-btn <?= $laLang === 'en' ? 'is-active' : '' ?>" data-lang="en">🇬🇧 EN</button>
|
||||||
<button type="button" class="lang-btn la-lang-btn" data-lang="en">🇬🇧 EN</button>
|
<button type="button" class="lang-btn la-lang-btn <?= $laLang === 'no' ? 'is-active' : '' ?>" data-lang="no">🇳🇴 NO</button>
|
||||||
|
<button type="button" class="lang-btn la-lang-btn <?= $laLang === 'uk' ? 'is-active' : '' ?>" data-lang="uk">🇺🇦 UK</button>
|
||||||
|
<button type="button" class="lang-btn la-lang-btn <?= $laLang === 'pl' ? 'is-active' : '' ?>" data-lang="pl">🇵🇱 PL</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-row" id="laDocTypeControl">
|
<div class="control-row" id="laDocTypeControl">
|
||||||
<span class="control-label">Document type</span>
|
<span class="control-label"><?= htmlspecialchars($laT('la_doc_type_label')) ?></span>
|
||||||
<label><input type="radio" name="laDocType" value="auto" checked> Auto-detect</label>
|
<label><input type="radio" name="laDocType" value="auto" checked> <?= htmlspecialchars($laT('la_doc_type_auto')) ?></label>
|
||||||
<label><input type="radio" name="laDocType" value="barnevernet"> Barnevernet</label>
|
<label><input type="radio" name="laDocType" value="barnevernet"> Barnevernet</label>
|
||||||
<label><input type="radio" name="laDocType" value="adopsjon"> Adopsjon</label>
|
<label><input type="radio" name="laDocType" value="adopsjon"> Adopsjon</label>
|
||||||
<label><input type="radio" name="laDocType" value="emergency"> Akutt-plassering</label>
|
<label><input type="radio" name="laDocType" value="emergency"> Akutt-plassering</label>
|
||||||
<label><input type="radio" name="laDocType" value="samvær"> Samvær</label>
|
<label><input type="radio" name="laDocType" value="samvær"> Samvær</label>
|
||||||
<label><input type="radio" name="laDocType" value="fylkesnemnd"> Fylkesnemnd</label>
|
<label><input type="radio" name="laDocType" value="fylkesnemnd"> Fylkesnemnd</label>
|
||||||
<label><input type="radio" name="laDocType" value="other"> Annet</label>
|
<label><input type="radio" name="laDocType" value="other"> <?= htmlspecialchars($laT('la_doc_type_other')) ?></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="upload-hint">
|
<p class="upload-hint"><?= htmlspecialchars($laT('la_engine_hint')) ?></p>
|
||||||
Engine: <strong>dbn-legal-agent-v3</strong> (Norwegian legal fine-tune on GPU). Expect ~30-60 seconds per issue, up to 5 issues per run.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div id="docPickerSection" class="doc-picker-section">
|
<div id="docPickerSection" class="doc-picker-section">
|
||||||
<button type="button" id="docPickerBtn" class="doc-picker-btn" aria-haspopup="dialog">
|
<button type="button" id="docPickerBtn" class="doc-picker-btn" aria-haspopup="dialog">
|
||||||
@@ -51,27 +81,34 @@ require_once __DIR__ . '/includes/layout.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="input-label" for="laInput">Pasted text <small class="control-hint">(optional if file or doc selected)</small></label>
|
<label class="input-label" for="laInput"><?= htmlspecialchars($laT('la_input_label')) ?> <small class="control-hint"><?= htmlspecialchars($laT('la_input_hint')) ?></small></label>
|
||||||
<textarea id="laInput" name="text" rows="8" placeholder="Paste a case note, court decision, vedtak, brev, or any legal document text. You can also upload a file or select from My Docs above — at least one source is required."></textarea>
|
<textarea id="laInput" name="text" rows="8" placeholder="<?= htmlspecialchars($laT('la_input_placeholder')) ?>"></textarea>
|
||||||
|
|
||||||
<div class="form-footer">
|
<div class="form-footer">
|
||||||
<p id="laStatus" class="form-status" role="status" aria-live="polite"></p>
|
<p id="laStatus" class="form-status" role="status" aria-live="polite"></p>
|
||||||
<button id="laRunButton" type="submit">Run legal analysis</button>
|
<button id="laRunButton" type="submit"><?= htmlspecialchars($laT('la_run_button')) ?></button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<section id="laResults" class="results" aria-live="polite">
|
<section id="laResults" class="results" aria-live="polite">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h3>Ready</h3>
|
<h3><?= htmlspecialchars($laT('la_ready_title')) ?></h3>
|
||||||
<p>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.</p>
|
<p><?= htmlspecialchars($laT('la_ready_intro')) ?></p>
|
||||||
<p class="upload-hint">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.</p>
|
<p class="upload-hint"><?= htmlspecialchars($laT('la_ready_pipeline')) ?></p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.DBN_LA_I18N = <?= json_encode($laI18n, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>;
|
||||||
|
window.DBN_LA_LANG = <?= json_encode($laLang) ?>;
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- Hidden stubs so tools.js element refs don't crash on this page -->
|
<!-- Hidden stubs so tools.js element refs don't crash on this page -->
|
||||||
<div class="is-hidden" id="languageControl" aria-hidden="true">
|
<div class="is-hidden" id="languageControl" aria-hidden="true">
|
||||||
<input type="radio" name="language" value="en">
|
<input type="radio" name="language" value="en" <?= $laLang === 'en' ? 'checked' : '' ?>>
|
||||||
<input type="radio" name="language" value="no" checked>
|
<input type="radio" name="language" value="no" <?= $laLang === 'no' ? 'checked' : '' ?>>
|
||||||
|
<input type="radio" name="language" value="uk" <?= $laLang === 'uk' ? 'checked' : '' ?>>
|
||||||
|
<input type="radio" name="language" value="pl" <?= $laLang === 'pl' ? 'checked' : '' ?>>
|
||||||
</div>
|
</div>
|
||||||
<div class="is-hidden" id="redactionControl" aria-hidden="true"></div>
|
<div class="is-hidden" id="redactionControl" aria-hidden="true"></div>
|
||||||
<div class="is-hidden" id="audioZone" aria-hidden="true">
|
<div class="is-hidden" id="audioZone" aria-hidden="true">
|
||||||
|
|||||||
Reference in New Issue
Block a user