Files
dobetternorge-tools/assets/js/summarize.js
T
daveadmin a8b1bb87a6 feat(tools): converge two-tier Quick/Pro selector onto .no fork
Port the dobetterlegal-tools two-tier quality stack to dobetternorge.no:
QUALITY_TIERS registry + resolveTier (ToolModels), dbnToolsResolveToolRun
(bootstrap), tier read+charge in the 6 analytical endpoints, Quick/Pro
UI + payload.tier on the 6 tool pages/JS, and the bounded
corpusContextForSummarize RAG fix (per-passage trim + total budget +
reranker_enabled). Back-compat: requests without `tier` keep legacy
engine behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-15 12:23:46 +02:00

388 lines
16 KiB
JavaScript

/**
* summarize.js — Custom handler for the Summarize Document tool.
*
* Handles file upload (via api/extract.php), corpus slice toggles,
* engine selection, JSON submission, and NDJSON streaming response.
*/
(function () {
'use strict';
// ── Element refs ──────────────────────────────────────────────────────────
var form = document.getElementById('sumForm');
var runBtn = document.getElementById('sumRunButton');
var statusEl = document.getElementById('sumStatus');
var resultsEl = document.getElementById('sumResults');
var traceList = document.getElementById('traceList');
var textarea = document.getElementById('sumInput');
var uploadZone = document.getElementById('sumUploadZone');
var uploadInput = document.getElementById('sumUploadInput');
var uploadPrompt = document.getElementById('sumUploadPrompt');
var uploadFileInfo = document.getElementById('sumUploadFileInfo');
var uploadFileList = document.getElementById('sumUploadFileList');
var uploadClear = document.getElementById('sumUploadClear');
// ── State ─────────────────────────────────────────────────────────────────
var _extractedFiles = []; // [{ name, text, chars }]
var _currentLang = 'en';
var _lastPayload = null;
// ── Lang switcher ─────────────────────────────────────────────────────────
document.querySelectorAll('.sum-lang-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
document.querySelectorAll('.sum-lang-btn').forEach(function (b) {
b.classList.toggle('is-active', b === btn);
});
_currentLang = btn.dataset.lang || 'en';
});
});
// ── Corpus slice toggles ──────────────────────────────────────────────────
document.querySelectorAll('.sum-slice').forEach(function (btn) {
btn.addEventListener('click', function () {
var on = btn.classList.toggle('is-on');
btn.setAttribute('aria-pressed', String(on));
var badge = btn.querySelector('.dr-slice__badge');
if (badge) badge.textContent = on ? 'on' : 'off';
});
});
function activeSlices() {
var out = [];
document.querySelectorAll('.sum-slice.is-on').forEach(function (btn) {
if (btn.dataset.slice) out.push(btn.dataset.slice);
});
return out;
}
// ── File upload ───────────────────────────────────────────────────────────
if (uploadZone) {
uploadZone.addEventListener('dragover', function (e) {
e.preventDefault();
uploadZone.classList.add('is-drag-over');
});
uploadZone.addEventListener('dragleave', function (e) {
if (!uploadZone.contains(e.relatedTarget)) {
uploadZone.classList.remove('is-drag-over');
}
});
uploadZone.addEventListener('drop', function (e) {
e.preventDefault();
uploadZone.classList.remove('is-drag-over');
if (e.dataTransfer && e.dataTransfer.files.length) {
handleFiles(e.dataTransfer.files);
}
});
// Stop label-for and the input itself from bubbling into the zone click
// handler — otherwise the picker opens twice (native + programmatic).
var browseLabel = uploadZone.querySelector('label[for="' + (uploadInput && uploadInput.id) + '"]');
if (browseLabel) {
browseLabel.addEventListener('click', function (e) { e.stopPropagation(); });
}
if (uploadInput) {
uploadInput.addEventListener('click', function (e) { e.stopPropagation(); });
}
uploadZone.addEventListener('click', function (e) {
if (e.target === uploadClear || (uploadClear && uploadClear.contains(e.target))) return;
if (e.target === uploadInput) return;
var lbl = e.target.closest && e.target.closest('label');
if (lbl && lbl.getAttribute('for') === uploadInput.id) return;
if (uploadInput) uploadInput.click();
});
}
if (uploadInput) {
uploadInput.addEventListener('change', function () {
if (uploadInput.files && uploadInput.files.length) {
handleFiles(uploadInput.files);
}
});
}
if (uploadClear) {
uploadClear.addEventListener('click', function (e) {
e.stopPropagation();
resetUpload();
});
}
function resetUpload() {
_extractedFiles = [];
if (uploadInput) uploadInput.value = '';
if (uploadPrompt) uploadPrompt.classList.remove('is-hidden');
if (uploadFileInfo) uploadFileInfo.classList.add('is-hidden');
if (uploadFileList) uploadFileList.innerHTML = '';
}
function handleFiles(fileList) {
var files = Array.from(fileList).slice(0, 5);
if (!files.length) return;
setStatus('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();
runSummarize();
});
}
async function runSummarize() {
// Build text: extracted files + textarea
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');
// Doc picker ids
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 tier = (document.querySelector('input[name="sumTier"]:checked') || {}).value || 'quick';
var slices = activeSlices();
var payload = {
text: combined,
language: _currentLang,
tier: tier,
depth: (document.querySelector('input[name="sumDepth"]:checked') || {}).value || 'standard',
slices: slices,
};
if (docIds.length) payload.doc_ids = docIds;
_lastPayload = payload;
setBusy(true);
setStatus('Running…');
showProgress([]);
try {
var resp = await fetch('api/summarize.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 = '';
var steps = [];
while (true) {
var _ref = await reader.read();
var done = _ref.done;
var value = _ref.value;
if (done) break;
buffer += decoder.decode(value, { stream: true });
var lines = buffer.split('\n');
buffer = lines.pop(); // keep incomplete line
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; }
if (data.event === 'progress') {
steps.push(data);
showProgress(steps);
setStatus(data.detail || '');
} else if (data.event === 'final') {
renderFinal(data);
if (typeof window.dbnShowSaveResultButton === 'function') {
window.dbnShowSaveResultButton(
'summarize',
_lastPayload || {},
data,
{ model: data.engine || null, latency_ms: data.latency_ms || 0 },
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;
}
} else if (data.event === 'error') {
showError(data.error || 'An error occurred.');
}
}
}
} catch (err) {
showError(err.message || 'Request failed.');
} finally {
setBusy(false);
setStatus('');
}
}
// ── Result rendering ──────────────────────────────────────────────────────
function showProgress(steps) {
if (!resultsEl) return;
var items = steps.map(function (s) {
return '<li><span class="trace-status running"></span>'
+ '<div><strong>' + esc(stepLabel(s.step)) + '</strong>'
+ '<p>' + esc(s.detail || '') + '</p></div></li>';
}).join('');
resultsEl.innerHTML = '<ol class="trace-list">' + items
+ '<li><span class="trace-status running"></span><div><strong>Working…</strong></div></li></ol>';
}
function stepLabel(step) {
var labels = {
text_ready: 'Document prepared',
corpus_search: 'Searching legal corpus',
corpus_done: 'Corpus search done',
generating: 'Generating summary',
};
return labels[step] || step;
}
function renderFinal(data) {
if (!resultsEl) return;
var sections = [];
if (data.what_we_found) {
sections.push(
'<section class="result-section">'
+ '<h3>Summary</h3>'
+ '<p>' + esc(data.what_we_found) + '</p>'
+ '</section>'
);
}
if (Array.isArray(data.key_facts) && data.key_facts.length) {
sections.push(detailBlock('Key Facts', data.key_facts));
}
if (Array.isArray(data.dates) && data.dates.length) {
sections.push(detailBlock('Dates', data.dates));
}
if (Array.isArray(data.parties) && data.parties.length) {
sections.push(detailBlock('Parties', data.parties));
}
if (Array.isArray(data.legal_references_detected) && data.legal_references_detected.length) {
sections.push(detailBlock('Legal References Detected', data.legal_references_detected));
}
if (Array.isArray(data.what_remains_uncertain) && data.what_remains_uncertain.length) {
sections.push(detailBlock('What Remains Uncertain', data.what_remains_uncertain));
}
if (data.next_practical_step) {
sections.push(
'<section class="result-section">'
+ '<h3>Next Practical Step</h3>'
+ '<p>' + esc(data.next_practical_step) + '</p>'
+ '</section>'
);
}
if (data.corpus_used) {
sections.push(
'<p class="upload-hint" style="margin-top:.5rem">'
+ 'Summary enriched with relevant passages from the Do Better Norge legal corpus.'
+ '</p>'
);
}
if (data.disclaimer) {
sections.push('<p class="disclaimer-note">' + esc(data.disclaimer) + '</p>');
}
resultsEl.innerHTML = sections.join('');
// Update reasoning panel trace
if (traceList && Array.isArray(data.trace) && data.trace.length) {
traceList.innerHTML = data.trace.map(function (item) {
return '<li>'
+ '<span class="trace-status ' + esc(item.status || 'complete') + '"></span>'
+ '<div><strong>' + esc(item.label || '') + '</strong>'
+ '<p>' + esc(item.detail || '') + '</p></div>'
+ '</li>';
}).join('');
}
}
function showError(msg) {
if (resultsEl) {
resultsEl.innerHTML = '<div class="empty-state"><h3>Error</h3><p>' + esc(msg) + '</p></div>';
}
setStatus('');
}
function detailBlock(title, items) {
return '<div class="detail-block"><h4>' + esc(title) + '</h4>'
+ '<ul>' + items.map(function (v) { return '<li>' + esc(String(v)) + '</li>'; }).join('') + '</ul>'
+ '</div>';
}
// ── Helpers ───────────────────────────────────────────────────────────────
function setBusy(on) {
if (runBtn) runBtn.disabled = on;
if (runBtn) runBtn.textContent = on ? 'Running…' : 'Summarize';
}
function setStatus(msg) {
if (statusEl) statusEl.textContent = msg;
}
function esc(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
}());