2013648ee0
All tool results can now be saved to My Case manually. Users click 'Save result', type a description, and confirm. This replaces the previous silent auto-save on barnevernet/timeline/etc., giving users control over what stays and what it's called (supports multiple runs of the same tool with different titles). - CaseResults: extend ELIGIBLE_TOOLS to include summarize, ask, redact, transcribe; add toolLabel/toolIcon entries; support explicit title via meta['title'] in save() - api/case/save-result.php: new client-initiated save endpoint; accepts tool + title + input_payload + output_payload + meta - Remove CaseResults::save() auto-save from barnevernet, deep-research, discrepancy, korrespond, timeline API endpoints - tools.js: add showSaveResultButton() (exposed as window.dbnShowSaveResultButton); wire for ask, redact, timeline, transcribe (both file-upload and stored-audio paths) - barnevernet.js: wire save button after final result render - summarize.js: wire save button after renderFinal(); passes sumResults container so widget appears in the correct #sumResults div - case-result.php: rich tool-specific rendering for summarize, ask, redact, transcribe, timeline; update re-run link map to include all new tools - tools.css: styles for .save-result-widget and its states (idle, prompt, done, error) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
369 lines
15 KiB
JavaScript
369 lines
15 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);
|
|
}
|
|
});
|
|
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();
|
|
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 engine = (document.querySelector('input[name="sumEngine"]:checked') || {}).value || 'azure_mini';
|
|
var slices = activeSlices();
|
|
|
|
var payload = {
|
|
text: combined,
|
|
language: _currentLang,
|
|
engine: engine,
|
|
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
|
|
);
|
|
}
|
|
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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
}());
|