5589e891f4
The "browse" label's native for=input trigger AND the upload-zone click handler both called uploadInput.click(), so the picker opened twice when the user clicked the browse text. Stop propagation on the label and the input itself, plus tighten the zone handler to recognise any label-for descendant. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
387 lines
16 KiB
JavaScript
387 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 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
|
|
);
|
|
}
|
|
// 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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
}());
|