diff --git a/api/summarize.php b/api/summarize.php
index 621c48f..98f868a 100644
--- a/api/summarize.php
+++ b/api/summarize.php
@@ -2,13 +2,81 @@
declare(strict_types=1);
require_once __DIR__ . '/../includes/LegalTools.php';
+require_once __DIR__ . '/../includes/ToolModels.php';
dbnToolsRequireMethod('POST');
dbnToolsRequireAuth();
-$input = dbnToolsJsonInput(70000);
-$language = dbnToolsNormalizeLanguage($input['language'] ?? 'en');
+$ftUid = dbnToolsFreeTierCheck('summarize');
-dbnToolsWithTelemetry('summarize', $language, function () use ($input, $language): array {
- $text = dbnToolsString($input, 'text', 32000);
- return (new DbnLegalToolsService())->summarize($text, $language);
-});
+$input = dbnToolsJsonInput(400000);
+$language = dbnToolsNormalizeLanguage($input['language'] ?? 'en');
+$engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? 'azure_mini'));
+$slices = is_array($input['slices'] ?? null) ? array_values(array_filter($input['slices'])) : [];
+
+// Streaming headers — flush each NDJSON line as it's written
+header('Content-Type: application/x-ndjson; charset=utf-8');
+header('X-Accel-Buffering: no');
+header('Cache-Control: no-cache, no-store');
+while (ob_get_level()) {
+ ob_end_clean();
+}
+
+$emit = static function (string $event, array $payload): void {
+ echo json_encode(['event' => $event] + $payload, JSON_UNESCAPED_UNICODE) . "\n";
+ flush();
+};
+
+try {
+ $text = dbnToolsInjectDocContent($input, dbnToolsString($input, 'text', 128000));
+
+ if (mb_strlen(trim($text), 'UTF-8') < 50) {
+ $emit('error', [
+ 'error' => 'Paste text, upload a file, or select a document before running.',
+ 'code' => 'empty_text',
+ ]);
+ exit;
+ }
+
+ $emit('progress', [
+ 'step' => 'text_ready',
+ 'detail' => mb_strlen($text, 'UTF-8') . ' chars ready.',
+ ]);
+
+ $corpusContext = '';
+ if (!empty($slices)) {
+ $sliceCount = count($slices);
+ $emit('progress', [
+ 'step' => 'corpus_search',
+ 'detail' => 'Searching legal corpus (' . $sliceCount . ' slice' . ($sliceCount === 1 ? '' : 's') . ')…',
+ ]);
+ $svc = new DbnLegalToolsService();
+ $query = mb_substr(trim($text), 0, 600, 'UTF-8');
+ $corpusContext = $svc->corpusContextForSummarize($query, 8);
+ $emit('progress', [
+ 'step' => 'corpus_done',
+ 'detail' => $corpusContext !== ''
+ ? 'Legal context retrieved.'
+ : 'No matching passages found; summarising without corpus.',
+ ]);
+ }
+
+ $emit('progress', [
+ 'step' => 'generating',
+ 'detail' => 'Generating summary…',
+ ]);
+
+ $result = (new DbnLegalToolsService())->summarizeWithContext($text, $language, $engine, $corpusContext);
+
+ if ($ftUid > 0) {
+ $balance = dbnToolsFreeTierDeduct($ftUid, 'summarize');
+ $result['balance'] = $balance;
+ }
+
+ $emit('final', $result);
+
+} catch (DbnToolsHttpException $e) {
+ $emit('error', ['error' => $e->getMessage(), 'code' => (string)$e->getCode()]);
+} catch (Throwable $e) {
+ error_log('summarize error: ' . $e->getMessage());
+ $emit('error', ['error' => 'An unexpected error occurred. Please try again.', 'code' => 'server_error']);
+}
diff --git a/assets/js/summarize.js b/assets/js/summarize.js
new file mode 100644
index 0000000..f5e71ad
--- /dev/null
+++ b/assets/js/summarize.js
@@ -0,0 +1,357 @@
+/**
+ * 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';
+
+ // ── 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 '
' + esc(f.name) + ''
+ + ' ' + f.chars.toLocaleString() + ' chars';
+ }).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;
+
+ 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 (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 ''
+ + '' + esc(stepLabel(s.step)) + ''
+ + '
' + esc(s.detail || '') + '
';
+ }).join('');
+ resultsEl.innerHTML = '' + items
+ + 'Working…
';
+ }
+
+ 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(
+ ''
+ + 'Summary
'
+ + '' + esc(data.what_we_found) + '
'
+ + ''
+ );
+ }
+
+ 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(
+ ''
+ + 'Next Practical Step
'
+ + '' + esc(data.next_practical_step) + '
'
+ + ''
+ );
+ }
+
+ if (data.corpus_used) {
+ sections.push(
+ ''
+ + 'Summary enriched with relevant passages from the Do Better Norge legal corpus.'
+ + '
'
+ );
+ }
+
+ if (data.disclaimer) {
+ sections.push('' + esc(data.disclaimer) + '
');
+ }
+
+ 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 ''
+ + ''
+ + '' + esc(item.label || '') + ''
+ + '
' + esc(item.detail || '') + '
'
+ + '';
+ }).join('');
+ }
+ }
+
+ function showError(msg) {
+ if (resultsEl) {
+ resultsEl.innerHTML = '';
+ }
+ setStatus('');
+ }
+
+ function detailBlock(title, items) {
+ return '' + esc(title) + '
'
+ + '
' + items.map(function (v) { return '- ' + esc(String(v)) + '
'; }).join('') + '
'
+ + '
';
+ }
+
+ // ── 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, ''');
+ }
+
+}());
diff --git a/includes/LegalTools.php b/includes/LegalTools.php
index c11273d..6023245 100644
--- a/includes/LegalTools.php
+++ b/includes/LegalTools.php
@@ -1211,6 +1211,153 @@ PROMPT;
return dbnToolsCallGpuLlm($messages, $options);
}
+ // ── Summarize: corpus context + engine-aware summary ─────────────────────
+
+ /**
+ * Search the shared legal corpus and return top-N passages as a formatted
+ * context string. Returns '' on failure so the caller can degrade gracefully.
+ */
+ public function corpusContextForSummarize(string $query, int $limit = 8): string
+ {
+ try {
+ $client = dbnToolsRequireClient();
+ $package = $this->requireFamilyPackage((int)$client['id']);
+ dbnToolsBootCaveau();
+ $gatewayUrl = 'http://10.0.1.10:4000';
+ try {
+ $config = getConfig();
+ $u = trim((string)($config['ai_gateway']['url'] ?? ''));
+ if ($u !== '') $gatewayUrl = $u;
+ } catch (Throwable) {}
+ $rag = new ClientRagPipeline((int)$client['id'], $gatewayUrl, 20);
+ $chunks = $rag->searchAll($query, $limit, null, [
+ 'search_private' => true,
+ 'search_shared' => true,
+ 'package_ids' => [(int)$package['id']],
+ 'chunk_limit' => $limit,
+ 'search_method' => 'keyword',
+ 'min_private' => 0,
+ 'include_beta_website' => true,
+ ]);
+ $parts = [];
+ foreach ($chunks as $c) {
+ $title = (string)($c['title'] ?? ($c['source'] ?? 'Legal source'));
+ $content = (string)($c['content'] ?? ($c['text'] ?? ''));
+ if ($content !== '') {
+ $parts[] = "=== {$title} ===\n{$content}";
+ }
+ }
+ return implode("\n\n", $parts);
+ } catch (Throwable $e) {
+ error_log('summarize corpus search failed: ' . $e->getMessage());
+ return '';
+ }
+ }
+
+ /**
+ * Engine-aware structured summarization, optionally enriched with corpus context.
+ */
+ public function summarizeWithContext(
+ string $text,
+ string $language = 'en',
+ string $engine = 'azure_mini',
+ string $corpusContext = ''
+ ): array {
+ $text = $this->requirePasteText($text);
+ $engine = in_array($engine, ['azure_mini', 'azure_full', 'gpu'], true) ? $engine : 'azure_mini';
+
+ $locale = dbnToolsLanguageName($language);
+
+ $enriched = $text;
+ $corpusUsed = $corpusContext !== '';
+ if ($corpusUsed) {
+ $enriched = "[Relevant legal context from Do Better Norge corpus]\n"
+ . $corpusContext
+ . "\n\n---\n\nDocument to summarise:\n"
+ . $text;
+ }
+
+ $prompt = <<legalJsonSystemPrompt($language);
+ $messages = [
+ ['role' => 'system', 'content' => $system],
+ ['role' => 'user', 'content' => $prompt],
+ ];
+ $maxTok = ($engine === 'azure_full') ? 8000 : 4000;
+ $chatOpts = ['json' => true, 'temperature' => 0.1, 'max_tokens' => $maxTok, 'timeout' => 120];
+
+ $deployLabel = $this->azure->chatDeployment();
+ try {
+ if ($engine === 'gpu') {
+ $response = $this->callGpuLlm($messages, $chatOpts);
+ $deployLabel = 'GPU (local)';
+ } elseif ($engine === 'azure_full') {
+ $response = $this->azure->withDeployment('gpt-4o')->chat($messages, $chatOpts);
+ $deployLabel = 'gpt-4o';
+ } else {
+ $response = $this->azure->withDeployment('gpt-4o-mini')->chat($messages, $chatOpts);
+ $deployLabel = 'gpt-4o-mini';
+ }
+ } catch (Throwable $e) {
+ dbnToolsAbort('LLM request failed: ' . $e->getMessage(), 502, 'llm_error');
+ }
+
+ $raw = (string)($response['choices'][0]['message']['content'] ?? '');
+ $json = $this->azure->decodeJsonObject($raw);
+ if (!$json) {
+ dbnToolsAbort('LLM returned unparseable JSON.', 502, 'llm_parse_error');
+ }
+
+ $corpusNote = $corpusUsed
+ ? 'Summary enriched with ' . count(array_filter(explode('=== ', $corpusContext))) . ' passage(s) from the Do Better Norge legal corpus.'
+ : 'No corpus search performed; summarised from document text only.';
+
+ $trace = [
+ $this->trace('Document preparation', 'Text validated and prepared for summarisation.', 'complete'),
+ $this->trace('Corpus enrichment', $corpusNote, $corpusUsed ? 'complete' : 'complete'),
+ $this->trace('Summary generation', 'Structured summary generated via ' . $deployLabel . '.', 'complete'),
+ $this->trace('Uncertainty', $this->uncertaintySummary($json['what_remains_uncertain'] ?? []), 'complete'),
+ $this->trace('Next practical step', (string)($json['next_practical_step'] ?? 'Review the summary against the original document.'), 'complete'),
+ ];
+
+ return [
+ 'tool' => 'summarize',
+ 'language' => $language,
+ 'what_we_found' => (string)($json['what_we_found'] ?? ''),
+ 'key_facts' => $json['key_facts'] ?? [],
+ 'dates' => $json['dates'] ?? [],
+ 'parties' => $json['parties'] ?? [],
+ 'legal_references_detected' => $json['legal_references_detected'] ?? [],
+ 'what_remains_uncertain' => $json['what_remains_uncertain'] ?? [],
+ 'next_practical_step' => (string)($json['next_practical_step'] ?? ''),
+ 'corpus_used' => $corpusUsed,
+ 'trace' => $trace,
+ 'trace_metadata' => [
+ 'chunk_count' => 1,
+ 'source_count' => 1,
+ 'deployment' => $deployLabel,
+ ],
+ 'disclaimer' => dbnToolsDisclaimer($language),
+ ];
+ }
+
private function applyGenericTags(string $text): string
{
// Collapse contextual role tags (e.g. [FATHER], [JUDGE: Andersen], [CHILD_1]) → [PERSON]
diff --git a/includes/i18n.php b/includes/i18n.php
index e5430ea..d051d0e 100644
--- a/includes/i18n.php
+++ b/includes/i18n.php
@@ -1510,6 +1510,7 @@ function dbnToolsLaunchedTools(?string $language = null): array
'transcribe' => ['Transcribe', 'Audio and meetings', 'Turn audio or video into text with speaker separation and legal vocabulary support.', 'Whisper / GPU'],
'timeline' => ['Timeline', 'Events and deadlines', 'Extract dates, hearings, Barnevernet milestones, and legal deadlines from notes or files.', 'Process-and-forget'],
'redact' => ['Redact', 'Privacy protection', 'Remove names, ID numbers, phone numbers, and addresses before sharing documents.', 'Deterministic first'],
+ 'summarize' => ['Summarize', 'Document summary', 'Extract key facts, dates, parties, and legal references from any document — with optional legal corpus enrichment.', 'Process-and-forget'],
'korrespond' => ['Korrespond', 'Draft & reply to authorities', 'Draft replies or new correspondence to NAV, Barnevernet, schools, Bufdir and other Norwegian authorities — Norwegian + your language, side-by-side, citations verified against the legal corpus.', 'Hard-RAG · Norsk + EN/PL/UK'],
'barnevernet' => ['BVJ Analyzer', 'Barnevernet documents', 'Analyze child-welfare documents from your perspective with procedural red flags and citations.', 'Document + RAG'],
'advocate' => ['Advocate', 'Partisan brief', 'Choose who you represent and generate a source-grounded brief for that position.', 'ECHR + Lovdata'],
@@ -1522,6 +1523,7 @@ function dbnToolsLaunchedTools(?string $language = null): array
'transcribe' => ['Transkriber', 'Lyd og møter', 'Gjør lyd eller video om til tekst med talerinndeling og juridisk ordforråd.', 'Whisper / GPU'],
'timeline' => ['Tidslinje', 'Hendelser og frister', 'Hent ut datoer, møter, barnevernsmilepæler og juridiske frister fra notater eller filer.', 'Behandles og glemmes'],
'redact' => ['Sladder', 'Personvern', 'Fjern navn, ID-numre, telefonnumre og adresser før du deler dokumenter.', 'Deterministisk først'],
+ 'summarize' => ['Sammendrag', 'Dokumentsammendrag', 'Hent ut nøkkelfakta, datoer, parter og juridiske referanser fra et dokument — med valgfri korpusberikelse.', 'Behandles og glemmes'],
'korrespond' => ['Korrespond', 'Brev og svar til myndighetene', 'Skriv utkast til svar eller nytt brev til NAV, barnevernet, skolen, Bufdir og andre norske myndigheter — bokmål + ditt språk side om side, med verifiserte lovhenvisninger.', 'Hard-RAG · Norsk + EN/PL/UK'],
'barnevernet' => ['BVJ-analyse', 'Barnevernsdokumenter', 'Analyser barnevernsdokumenter fra ditt perspektiv med prosessuelle røde flagg og kilder.', 'Dokument + RAG'],
'advocate' => ['Advokatmodus', 'Partsinnlegg', 'Velg hvem du representerer og lag et kildebelagt innlegg for den posisjonen.', 'EMD + Lovdata'],
@@ -1534,6 +1536,7 @@ function dbnToolsLaunchedTools(?string $language = null): array
'transcribe' => ['Транскрипція', 'Аудіо та зустрічі', 'Перетворюйте аудіо або відео на текст із розділенням мовців і юридичною лексикою.', 'Whisper / GPU'],
'timeline' => ['Хронологія', 'Події та строки', 'Витягуйте дати, слухання, етапи Barnevernet і юридичні строки з нотаток або файлів.', 'Обробити і забути'],
'redact' => ['Редагування', 'Захист приватності', 'Видаляйте імена, ідентифікаційні номери, телефони та адреси перед поширенням документів.', 'Детермінований метод'],
+ 'summarize' => ['Резюме', 'Резюме документа', 'Витягуйте ключові факти, дати, сторони та юридичні посилання — з можливістю збагачення корпусом.', 'Обробити і забути'],
'korrespond' => ['Korrespond', 'Листи і відповіді органам влади', 'Створюйте чернетки відповідей або нових листів до NAV, Barnevernet, школи, Bufdir та інших норвезьких органів — норвезькою + вашою мовою поряд, із перевіреними посиланнями на закон.', 'Hard-RAG · Norsk + EN/PL/UK'],
'barnevernet' => ['BVJ аналізатор', 'Документи Barnevernet', 'Аналізуйте документи захисту дітей з вашої позиції, з процесуальними ризиками та джерелами.', 'Документ + RAG'],
'advocate' => ['Адвокат', 'Позиційний бриф', 'Оберіть, кого представляєте, і створіть бриф із джерелами на підтримку цієї позиції.', 'ЄСПЛ + Lovdata'],
@@ -1546,6 +1549,7 @@ function dbnToolsLaunchedTools(?string $language = null): array
'transcribe' => ['Transkrypcja', 'Audio i spotkania', 'Zamień audio lub wideo na tekst z rozdzieleniem mówców i słownictwem prawnym.', 'Whisper / GPU'],
'timeline' => ['Oś czasu', 'Wydarzenia i terminy', 'Wyodrębniaj daty, rozprawy, etapy Barnevernet i terminy prawne z notatek lub plików.', 'Przetwórz i zapomnij'],
'redact' => ['Redakcja', 'Ochrona prywatności', 'Usuń imiona, numery identyfikacyjne, telefony i adresy przed udostępnieniem dokumentów.', 'Metoda deterministyczna'],
+ 'summarize' => ['Streszczenie', 'Streszczenie dokumentu', 'Wyodrębniaj kluczowe fakty, daty, strony i odniesienia prawne — z opcjonalnym wzbogaceniem korpusem.', 'Przetwórz i zapomnij'],
'korrespond' => ['Korrespond', 'Pisma i odpowiedzi do urzędów', 'Twórz projekty odpowiedzi lub nowych pism do NAV, Barnevernet, szkoły, Bufdir i innych norweskich organów — norweski + Twój język obok siebie, ze zweryfikowanymi odniesieniami do ustaw.', 'Hard-RAG · Norsk + EN/PL/UK'],
'barnevernet' => ['Analizator BVJ', 'Dokumenty Barnevernet', 'Analizuj dokumenty opieki nad dziećmi z Twojej perspektywy, z ryzykami proceduralnymi i źródłami.', 'Dokument + RAG'],
'advocate' => ['Adwokat', 'Stronniczy brief', 'Wybierz, kogo reprezentujesz, i wygeneruj brief oparty na źródłach dla tej pozycji.', 'ETPC + Lovdata'],
@@ -1557,11 +1561,12 @@ function dbnToolsLaunchedTools(?string $language = null): array
];
$selected = $copy[$language] ?? $copy['en'];
- $order = ['transcribe', 'timeline', 'redact', 'korrespond', 'barnevernet', 'advocate', 'deep-research', 'discrepancy', 'corpus', 'citations'];
+ $order = ['transcribe', 'timeline', 'redact', 'summarize', 'korrespond', 'barnevernet', 'advocate', 'deep-research', 'discrepancy', 'corpus', 'citations'];
$icons = [
'transcribe' => 'TR',
'timeline' => 'TL',
'redact' => 'RX',
+ 'summarize' => 'SZ',
'korrespond' => 'KOR',
'barnevernet' => 'BVJ',
'advocate' => 'ADV',
diff --git a/summarize.php b/summarize.php
index d1b8fc7..d53ace7 100644
--- a/summarize.php
+++ b/summarize.php
@@ -1,10 +1,163 @@
-
+
+
+
+
+
Ready
+
Upload a file, select from My Docs, or paste text — then click Summarize. Enable corpus slices to enrich the summary with relevant legal passages.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+