Add Legal Analysis tool — two-pass DBN-legal pipeline
Restores the dbn-legal-agent-v3 fine-tune on ocelot (was silently aliased to plain qwen2.5:14b in LiteLLM since the viper retirement) and ships a new tool that uses it via a two-pass flow: Pass 1 (Azure 4o-mini) → extract up to 5 distinct legal issues Pass 2 (ocelot v3 only) → answer each issue, ≤350 tokens, with corpus Pass 3 (Azure 4o-mini) → synthesise overall assessment + next steps The 12GB-VRAM constraint motivates the split: dbn-legal-agent-v3 stays hot in VRAM through the 5 sequential per-issue calls because issue extraction and synthesis run on Azure, not on ocelot. New surface: - includes/LegalAnalysisAgent.php - api/legal-analysis.php (NDJSON streaming endpoint) - legal-analysis.php (dedicated tool page) - assets/js/legal-analysis.js (streamed UI with per-issue cards) - Save-result + case-result.php rendering for legal-analysis output - Nav registration in all four UI languages Add-on integration: a "⚖️🇳🇴 Run deep legal analysis on this text" button now appears on Summarize, Ask, and Redact result pages and streams the same pipeline inline below the existing result. Existing tools relabelled: the misleading "🇳🇴 Norwegian specialist v3 ⭐" option on advocate/deep-research/discrepancy/barnevernet is now honestly "DBN Legal Agent" — now that the real fine-tune is actually deployed, the label finally matches reality. The advocate.php v2 option was removed since the v2 GGUF is retired. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
/**
|
||||
* legal-analysis.js — Custom handler for the Legal Analysis tool.
|
||||
*
|
||||
* Two-pass flow: extract distinct legal issues (Azure) → answer each issue
|
||||
* with dbn-legal-agent-v3 fine-tune on ocelot → synthesise overall assessment.
|
||||
* Streams NDJSON so the UI fills in as each issue completes.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ── Element refs ──────────────────────────────────────────────────────────
|
||||
var form = document.getElementById('laForm');
|
||||
var runBtn = document.getElementById('laRunButton');
|
||||
var statusEl = document.getElementById('laStatus');
|
||||
var resultsEl = document.getElementById('laResults');
|
||||
var textarea = document.getElementById('laInput');
|
||||
|
||||
var uploadZone = document.getElementById('laUploadZone');
|
||||
var uploadInput = document.getElementById('laUploadInput');
|
||||
var uploadPrompt = document.getElementById('laUploadPrompt');
|
||||
var uploadFileInfo = document.getElementById('laUploadFileInfo');
|
||||
var uploadFileList = document.getElementById('laUploadFileList');
|
||||
var uploadClear = document.getElementById('laUploadClear');
|
||||
|
||||
// ── State ─────────────────────────────────────────────────────────────────
|
||||
var _extractedFiles = [];
|
||||
var _currentLang = 'no';
|
||||
var _lastPayload = null;
|
||||
var _issueCards = {}; // id -> DOM element
|
||||
|
||||
// ── Lang switcher ─────────────────────────────────────────────────────────
|
||||
document.querySelectorAll('.la-lang-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
document.querySelectorAll('.la-lang-btn').forEach(function (b) {
|
||||
b.classList.toggle('is-active', b === btn);
|
||||
});
|
||||
_currentLang = btn.dataset.lang || 'no';
|
||||
});
|
||||
});
|
||||
|
||||
// ── File upload (same pattern as summarize) ───────────────────────────────
|
||||
if (uploadZone) {
|
||||
uploadZone.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
uploadZone.classList.add('is-drag-over');
|
||||
});
|
||||
uploadZone.addEventListener('dragleave', function (e) {
|
||||
if (!uploadZone.contains(e.relatedTarget)) {
|
||||
uploadZone.classList.remove('is-drag-over');
|
||||
}
|
||||
});
|
||||
uploadZone.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
uploadZone.classList.remove('is-drag-over');
|
||||
if (e.dataTransfer && e.dataTransfer.files.length) {
|
||||
handleFiles(e.dataTransfer.files);
|
||||
}
|
||||
});
|
||||
uploadZone.addEventListener('click', function (e) {
|
||||
if (e.target === uploadClear || (uploadClear && uploadClear.contains(e.target))) return;
|
||||
if (e.target.tagName === 'LABEL') return;
|
||||
if (uploadInput) uploadInput.click();
|
||||
});
|
||||
}
|
||||
if (uploadInput) {
|
||||
uploadInput.addEventListener('change', function () {
|
||||
if (uploadInput.files && uploadInput.files.length) handleFiles(uploadInput.files);
|
||||
});
|
||||
}
|
||||
if (uploadClear) {
|
||||
uploadClear.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
resetUpload();
|
||||
});
|
||||
}
|
||||
|
||||
function resetUpload() {
|
||||
_extractedFiles = [];
|
||||
if (uploadInput) uploadInput.value = '';
|
||||
if (uploadPrompt) uploadPrompt.classList.remove('is-hidden');
|
||||
if (uploadFileInfo) uploadFileInfo.classList.add('is-hidden');
|
||||
if (uploadFileList) uploadFileList.innerHTML = '';
|
||||
}
|
||||
|
||||
function handleFiles(fileList) {
|
||||
var files = Array.from(fileList).slice(0, 5);
|
||||
if (!files.length) return;
|
||||
|
||||
setStatus('Extracting text from ' + files.length + ' file(s)…');
|
||||
setBusy(true);
|
||||
|
||||
var promises = files.map(function (file) {
|
||||
var fd = new FormData();
|
||||
fd.append('file', file);
|
||||
return fetch('api/extract.php', { method: 'POST', credentials: 'same-origin', body: fd })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (!data.ok) throw new Error(data.error || 'Extraction failed for ' + file.name);
|
||||
return { name: file.name, text: data.text || '', chars: data.chars || 0 };
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(promises)
|
||||
.then(function (results) {
|
||||
_extractedFiles = results;
|
||||
renderFileList(results);
|
||||
setStatus('');
|
||||
setBusy(false);
|
||||
})
|
||||
.catch(function (err) {
|
||||
setStatus('Error: ' + err.message);
|
||||
setBusy(false);
|
||||
});
|
||||
}
|
||||
|
||||
function renderFileList(files) {
|
||||
if (!uploadFileList) return;
|
||||
uploadFileList.innerHTML = files.map(function (f) {
|
||||
return '<li><span class="upload-filename">' + esc(f.name) + '</span>'
|
||||
+ ' <span class="upload-chars">' + f.chars.toLocaleString() + ' chars</span></li>';
|
||||
}).join('');
|
||||
if (uploadPrompt) uploadPrompt.classList.add('is-hidden');
|
||||
if (uploadFileInfo) uploadFileInfo.classList.remove('is-hidden');
|
||||
}
|
||||
|
||||
// ── Form submission ───────────────────────────────────────────────────────
|
||||
if (form) {
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
runAnalysis();
|
||||
});
|
||||
}
|
||||
|
||||
async function runAnalysis() {
|
||||
var pastedText = textarea ? textarea.value.trim() : '';
|
||||
var fileText = _extractedFiles.map(function (f) { return f.text; }).join('\n\n---\n\n');
|
||||
var combined = [fileText, pastedText].filter(Boolean).join('\n\n---\n\n');
|
||||
|
||||
var docIdsEl = document.getElementById('docPickerIds');
|
||||
var rawDocIds = docIdsEl ? docIdsEl.value.trim() : '';
|
||||
var docIds = rawDocIds ? rawDocIds.split(',').map(Number).filter(Boolean) : [];
|
||||
|
||||
if (!combined && !docIds.length) {
|
||||
setStatus('Paste text, upload a file, or select a document before running.');
|
||||
return;
|
||||
}
|
||||
|
||||
var docType = (document.querySelector('input[name="laDocType"]:checked') || {}).value || 'auto';
|
||||
|
||||
var payload = {
|
||||
text: combined,
|
||||
language: _currentLang,
|
||||
doc_type: docType,
|
||||
};
|
||||
if (docIds.length) payload.doc_ids = docIds;
|
||||
_lastPayload = payload;
|
||||
_issueCards = {};
|
||||
|
||||
setBusy(true);
|
||||
setStatus('Running…');
|
||||
renderInitial();
|
||||
|
||||
try {
|
||||
var resp = await fetch('api/legal-analysis.php', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!resp.ok || !resp.body) {
|
||||
throw new Error('Server returned ' + resp.status);
|
||||
}
|
||||
|
||||
var reader = resp.body.getReader();
|
||||
var decoder = new TextDecoder();
|
||||
var buffer = '';
|
||||
|
||||
while (true) {
|
||||
var _ref = await reader.read();
|
||||
if (_ref.done) break;
|
||||
buffer += decoder.decode(_ref.value, { stream: true });
|
||||
var lines = buffer.split('\n');
|
||||
buffer = lines.pop();
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
var data;
|
||||
try { data = JSON.parse(line); } catch (_) { continue; }
|
||||
handleEvent(data);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
showError(err.message || 'Request failed.');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
setStatus('');
|
||||
}
|
||||
}
|
||||
|
||||
function handleEvent(data) {
|
||||
if (data.event === 'progress') {
|
||||
setStatus(data.detail || '');
|
||||
if (data.step === 'issue_searching_corpus' || data.step === 'issue_answering') {
|
||||
markIssueRunning(data.issue_id, data.step);
|
||||
}
|
||||
} else if (data.event === 'issues_extracted') {
|
||||
renderIssueList(data.issues || []);
|
||||
} else if (data.event === 'issue_answered') {
|
||||
fillIssueCard(data.issue);
|
||||
} else if (data.event === 'final') {
|
||||
renderFinal(data.result || {});
|
||||
if (typeof window.dbnShowSaveResultButton === 'function') {
|
||||
window.dbnShowSaveResultButton(
|
||||
'legal-analysis',
|
||||
_lastPayload || {},
|
||||
data.result || {},
|
||||
{
|
||||
model: (data.result && data.result.model) || 'dbn-legal-agent-v3',
|
||||
latency_ms: (data.result && data.result.latency_ms) || 0,
|
||||
},
|
||||
resultsEl
|
||||
);
|
||||
}
|
||||
} else if (data.event === 'error') {
|
||||
showError(data.message || data.error || 'An error occurred.');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Result rendering ──────────────────────────────────────────────────────
|
||||
function renderInitial() {
|
||||
if (!resultsEl) return;
|
||||
resultsEl.innerHTML =
|
||||
'<div class="la-pipeline">'
|
||||
+ '<div class="la-step running"><strong>Pass 1</strong> — Extracting legal issues from your document…</div>'
|
||||
+ '</div>'
|
||||
+ '<ol id="laIssueList" class="la-issues"></ol>';
|
||||
}
|
||||
|
||||
function renderIssueList(issues) {
|
||||
var list = document.getElementById('laIssueList');
|
||||
if (!list) return;
|
||||
list.innerHTML = '';
|
||||
_issueCards = {};
|
||||
issues.forEach(function (issue) {
|
||||
var li = document.createElement('li');
|
||||
li.className = 'la-issue pending';
|
||||
li.dataset.issueId = String(issue.id);
|
||||
li.innerHTML =
|
||||
'<div class="la-issue__head">'
|
||||
+ '<span class="la-issue__num">#' + issue.id + '</span>'
|
||||
+ '<span class="la-severity la-severity-' + esc(issue.severity_hint || 'medium') + '">'
|
||||
+ esc((issue.severity_hint || 'medium').toUpperCase())
|
||||
+ '</span>'
|
||||
+ '</div>'
|
||||
+ '<h4 class="la-issue__q">' + esc(issue.question) + '</h4>'
|
||||
+ (issue.brief_context ? '<p class="la-issue__ctx"><em>' + esc(issue.brief_context) + '</em></p>' : '')
|
||||
+ '<div class="la-issue__status">Waiting…</div>';
|
||||
list.appendChild(li);
|
||||
_issueCards[issue.id] = li;
|
||||
});
|
||||
// Mark Pass 1 complete
|
||||
var pipeline = resultsEl.querySelector('.la-pipeline');
|
||||
if (pipeline) {
|
||||
pipeline.innerHTML = '<div class="la-step done"><strong>Pass 1</strong> — Found ' + issues.length + ' legal issue(s)</div>'
|
||||
+ '<div class="la-step running"><strong>Pass 2</strong> — Asking dbn-legal-agent-v3 about each issue…</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function markIssueRunning(issueId, step) {
|
||||
var card = _issueCards[issueId];
|
||||
if (!card) return;
|
||||
var status = card.querySelector('.la-issue__status');
|
||||
if (!status) return;
|
||||
if (step === 'issue_searching_corpus') {
|
||||
status.textContent = 'Searching legal corpus…';
|
||||
card.classList.add('running');
|
||||
} else if (step === 'issue_answering') {
|
||||
status.textContent = 'Asking dbn-legal-agent-v3…';
|
||||
}
|
||||
}
|
||||
|
||||
function fillIssueCard(issue) {
|
||||
if (!issue || !issue.id) return;
|
||||
var card = _issueCards[issue.id];
|
||||
if (!card) return;
|
||||
card.classList.remove('pending', 'running');
|
||||
card.classList.add('answered');
|
||||
|
||||
// Refresh severity from real answer
|
||||
var sevEl = card.querySelector('.la-severity');
|
||||
if (sevEl) {
|
||||
sevEl.className = 'la-severity la-severity-' + esc(issue.severity || 'medium');
|
||||
sevEl.textContent = esc((issue.severity || 'medium').toUpperCase());
|
||||
}
|
||||
|
||||
var statusBlock = card.querySelector('.la-issue__status');
|
||||
if (statusBlock) statusBlock.remove();
|
||||
|
||||
var answerHtml = '<div class="la-issue__answer"><h5>Svar</h5><p>'
|
||||
+ esc(issue.answer || '').replace(/\n/g, '<br>')
|
||||
+ '</p></div>';
|
||||
|
||||
var basisHtml = '';
|
||||
if (issue.legal_basis) {
|
||||
basisHtml = '<div class="la-issue__basis"><strong>Lovgrunnlag:</strong> '
|
||||
+ esc(issue.legal_basis) + '</div>';
|
||||
}
|
||||
var checkHtml = '';
|
||||
if (issue.what_to_check) {
|
||||
checkHtml = '<p class="la-issue__check"><em>' + esc(issue.what_to_check) + '</em></p>';
|
||||
}
|
||||
card.insertAdjacentHTML('beforeend', answerHtml + basisHtml + checkHtml);
|
||||
}
|
||||
|
||||
function renderFinal(result) {
|
||||
// Add Pass 3 synthesis at the top
|
||||
var topHtml = '';
|
||||
if (result.overall_assessment) {
|
||||
topHtml = '<section class="la-synthesis result-section">'
|
||||
+ '<h3>Overall assessment</h3>'
|
||||
+ '<p>' + esc(result.overall_assessment) + '</p>';
|
||||
if (Array.isArray(result.next_steps) && result.next_steps.length) {
|
||||
topHtml += '<h4>Next steps</h4><ul>'
|
||||
+ result.next_steps.map(function (s) { return '<li>' + esc(s) + '</li>'; }).join('')
|
||||
+ '</ul>';
|
||||
}
|
||||
if (result.disclaimer) {
|
||||
topHtml += '<p class="disclaimer-note">' + esc(result.disclaimer) + '</p>';
|
||||
}
|
||||
topHtml += '</section>';
|
||||
}
|
||||
|
||||
// Mark Pass 2/3 complete in the pipeline strip
|
||||
var pipeline = resultsEl.querySelector('.la-pipeline');
|
||||
if (pipeline) {
|
||||
pipeline.innerHTML =
|
||||
'<div class="la-step done"><strong>Pass 1</strong> — Issues extracted</div>'
|
||||
+ '<div class="la-step done"><strong>Pass 2</strong> — Specialist answered ' + (result.issues || []).length + ' issue(s)</div>'
|
||||
+ '<div class="la-step done"><strong>Pass 3</strong> — Synthesis complete</div>';
|
||||
}
|
||||
|
||||
// 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 =
|
||||
'<li class="la-issue answered"><p><em>No discrete legal issues were identified.</em></p></li>';
|
||||
}
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
if (resultsEl) {
|
||||
resultsEl.innerHTML = '<div class="empty-state"><h3>Error</h3><p>' + esc(msg) + '</p></div>';
|
||||
}
|
||||
setStatus('');
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
function setBusy(on) {
|
||||
if (runBtn) runBtn.disabled = on;
|
||||
if (runBtn) runBtn.textContent = on ? 'Running…' : 'Run legal analysis';
|
||||
}
|
||||
function setStatus(msg) {
|
||||
if (statusEl) statusEl.textContent = msg;
|
||||
}
|
||||
function esc(s) {
|
||||
return String(s == null ? '' : s)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
}());
|
||||
@@ -229,6 +229,13 @@
|
||||
resultsEl
|
||||
);
|
||||
}
|
||||
// Offer deep legal analysis on the summarised text
|
||||
if (typeof window.dbnInjectLegalAnalysisButton === 'function') {
|
||||
var sourceText = combined || (_lastPayload && _lastPayload.text) || '';
|
||||
if (sourceText && sourceText.length >= 80) {
|
||||
window.dbnInjectLegalAnalysisButton(sourceText, _currentLang, 'summarize', resultsEl);
|
||||
}
|
||||
}
|
||||
if (data.balance != null) {
|
||||
var credEl = document.getElementById('creditsRemaining');
|
||||
if (credEl) credEl.textContent = data.balance;
|
||||
|
||||
@@ -1132,6 +1132,16 @@ async function runTool(event) {
|
||||
latency_ms: data.latency_ms || 0,
|
||||
});
|
||||
}
|
||||
// Offer "Run deep legal analysis" on ask/redact results
|
||||
if (['ask', 'redact'].includes(state.activeTool)) {
|
||||
const askText = state.activeTool === 'ask'
|
||||
? ((lastToolPayload && lastToolPayload.question) || (data.answer || data.what_we_found || ''))
|
||||
: (lastToolPayload && lastToolPayload.text) || (data.redacted_text || '');
|
||||
const resEl = els.results || document.getElementById('results');
|
||||
if (askText && resEl) {
|
||||
dbnInjectLegalAnalysisButton(askText, (lastToolPayload && lastToolPayload.language) || 'no', state.activeTool, resEl);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
els.status.textContent = error.message;
|
||||
renderTrace([
|
||||
@@ -2552,6 +2562,184 @@ function showSaveResultButton(tool, inputPayload, outputPayload, meta, container
|
||||
|
||||
window.dbnShowSaveResultButton = showSaveResultButton;
|
||||
|
||||
// ── Legal Analysis add-on (Run deep legal analysis on any text/result) ──────
|
||||
// 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 dbnRunLegalAnalysisAddon(text, lang, sourceTool, containerEl) {
|
||||
if (!containerEl) containerEl = document.getElementById('results');
|
||||
if (!containerEl || !text || text.length < 80) return;
|
||||
|
||||
// Remove any prior legal-analysis section
|
||||
const prior = containerEl.parentNode.querySelector('.la-addon-section');
|
||||
if (prior) prior.remove();
|
||||
|
||||
const section = document.createElement('section');
|
||||
section.className = 'la-addon-section result-section';
|
||||
section.style.marginTop = '1.2rem';
|
||||
section.style.padding = '1rem 1.2rem';
|
||||
section.style.border = '1px dashed var(--dbn-teal, #0f766e)';
|
||||
section.style.borderRadius = '8px';
|
||||
section.style.background = '#f0fdfa';
|
||||
section.innerHTML =
|
||||
'<h3 style="margin-top:0;color:#0e7490;">⚖️🇳🇴 Deep Legal Analysis</h3>'
|
||||
+ '<div class="la-pipeline" style="margin-bottom:1rem;">'
|
||||
+ '<div class="la-step running"><strong>Pass 1</strong> — Extracting legal issues…</div>'
|
||||
+ '</div>'
|
||||
+ '<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);
|
||||
|
||||
const issueListEl = section.querySelector('#laAddonIssues');
|
||||
const pipelineEl = section.querySelector('.la-pipeline');
|
||||
const issueCards = {};
|
||||
|
||||
const payload = {
|
||||
text: text,
|
||||
language: lang || 'no',
|
||||
doc_type: 'auto',
|
||||
source_tool: sourceTool || 'addon',
|
||||
};
|
||||
|
||||
fetch('api/legal-analysis.php', {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}).then(async function (resp) {
|
||||
if (!resp.ok || !resp.body) throw new Error('Server returned ' + resp.status);
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
while (true) {
|
||||
const _r = await reader.read();
|
||||
if (_r.done) break;
|
||||
buffer += decoder.decode(_r.value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop();
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
let data;
|
||||
try { data = JSON.parse(trimmed); } catch (_) { continue; }
|
||||
laAddonEvent(data, issueListEl, pipelineEl, issueCards);
|
||||
}
|
||||
}
|
||||
}).catch(function (err) {
|
||||
section.innerHTML += '<p style="color:#b91c1c;margin-top:0.5rem;">Error: ' + escapeHtml(err.message || String(err)) + '</p>';
|
||||
});
|
||||
}
|
||||
|
||||
function laAddonEvent(data, listEl, pipelineEl, cards) {
|
||||
if (data.event === 'issues_extracted') {
|
||||
listEl.innerHTML = '';
|
||||
(data.issues || []).forEach(function (issue) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'la-issue pending';
|
||||
li.dataset.issueId = String(issue.id);
|
||||
const sev = issue.severity_hint || 'medium';
|
||||
li.innerHTML =
|
||||
'<div class="la-issue__head"><span class="la-issue__num">#' + issue.id + '</span>'
|
||||
+ '<span class="la-severity la-severity-' + escapeHtml(sev) + '">' + escapeHtml(sev.toUpperCase()) + '</span></div>'
|
||||
+ '<h4 class="la-issue__q">' + escapeHtml(issue.question || '') + '</h4>'
|
||||
+ (issue.brief_context ? '<p class="la-issue__ctx"><em>' + escapeHtml(issue.brief_context) + '</em></p>' : '')
|
||||
+ '<div class="la-issue__status">Waiting…</div>';
|
||||
listEl.appendChild(li);
|
||||
cards[issue.id] = li;
|
||||
});
|
||||
pipelineEl.innerHTML =
|
||||
'<div class="la-step done"><strong>Pass 1</strong> — Found ' + (data.issues || []).length + ' issue(s)</div>'
|
||||
+ '<div class="la-step running"><strong>Pass 2</strong> — Asking dbn-legal-agent-v3…</div>';
|
||||
} 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…';
|
||||
}
|
||||
card.classList.add('running');
|
||||
}
|
||||
} else if (data.event === 'issue_answered' && data.issue) {
|
||||
const iss = data.issue;
|
||||
const card = cards[iss.id];
|
||||
if (!card) return;
|
||||
card.classList.remove('pending', 'running');
|
||||
card.classList.add('answered');
|
||||
const sev = iss.severity || 'medium';
|
||||
const sevEl = card.querySelector('.la-severity');
|
||||
if (sevEl) {
|
||||
sevEl.className = 'la-severity la-severity-' + escapeHtml(sev);
|
||||
sevEl.textContent = escapeHtml(sev.toUpperCase());
|
||||
}
|
||||
const statusEl = card.querySelector('.la-issue__status');
|
||||
if (statusEl) statusEl.remove();
|
||||
let html = '<div class="la-issue__answer"><h5>Svar</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.what_to_check) html += '<p class="la-issue__check"><em>' + escapeHtml(iss.what_to_check) + '</em></p>';
|
||||
card.insertAdjacentHTML('beforeend', html);
|
||||
} else if (data.event === 'final' && data.result) {
|
||||
const res = data.result;
|
||||
pipelineEl.innerHTML =
|
||||
'<div class="la-step done"><strong>Pass 1</strong> — Issues extracted</div>'
|
||||
+ '<div class="la-step done"><strong>Pass 2</strong> — Specialist answered ' + (res.issues || []).length + '</div>'
|
||||
+ '<div class="la-step done"><strong>Pass 3</strong> — Synthesis complete</div>';
|
||||
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;">'
|
||||
+ '<h3 style="margin-top:0;color:#0e7490;">Overall assessment</h3>'
|
||||
+ '<p>' + escapeHtml(res.overall_assessment) + '</p>';
|
||||
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>'
|
||||
+ res.next_steps.map(function (s) { return '<li>' + escapeHtml(s) + '</li>'; }).join('')
|
||||
+ '</ul>';
|
||||
}
|
||||
if (res.disclaimer) {
|
||||
syn += '<p style="font-size:0.85rem;color:#64748b;margin-top:0.5rem;"><em>' + escapeHtml(res.disclaimer) + '</em></p>';
|
||||
}
|
||||
syn += '</section>';
|
||||
pipelineEl.insertAdjacentHTML('afterend', syn);
|
||||
}
|
||||
// Save button (legal-analysis result is itself a saveable run)
|
||||
if (typeof window.dbnShowSaveResultButton === 'function') {
|
||||
const containerSection = pipelineEl.parentNode;
|
||||
window.dbnShowSaveResultButton(
|
||||
'legal-analysis',
|
||||
{ text: '', language: 'no', doc_type: 'auto', source_tool: 'addon' }, // input was the prior tool's text
|
||||
res,
|
||||
{ model: res.model || 'dbn-legal-agent-v3', latency_ms: res.latency_ms || 0 },
|
||||
containerSection
|
||||
);
|
||||
}
|
||||
} else if (data.event === 'error') {
|
||||
pipelineEl.innerHTML += '<div style="color:#b91c1c;margin-top:0.4rem;">Error: ' + escapeHtml(data.message || data.error || 'unknown') + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
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.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;';
|
||||
btn.addEventListener('mouseenter', function () { btn.style.background = '#0d5e57'; });
|
||||
btn.addEventListener('mouseleave', function () { btn.style.background = '#0f766e'; });
|
||||
btn.addEventListener('click', function () {
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Running deep legal analysis…';
|
||||
btn.style.background = '#94a3b8';
|
||||
dbnRunLegalAnalysisAddon(text, lang, sourceTool, containerEl);
|
||||
});
|
||||
containerEl.appendChild(btn);
|
||||
}
|
||||
|
||||
window.dbnRunLegalAnalysisAddon = dbnRunLegalAnalysisAddon;
|
||||
window.dbnInjectLegalAnalysisButton = dbnInjectLegalAnalysisButton;
|
||||
|
||||
let _freeTierBalance = (typeof window.DBN_FREE_TIER_BALANCE === 'number') ? window.DBN_FREE_TIER_BALANCE : -1;
|
||||
|
||||
function dbnUpdateCredits(balance) {
|
||||
|
||||
Reference in New Issue
Block a user