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 + + '
    1. 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 = '

    Error

    ' + esc(msg) + '

    '; + } + setStatus(''); + } + + function detailBlock(title, items) { + return '

    ' + esc(title) + '

    ' + + '' + + '
    '; + } + + // ── 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 @@ - +
    + +
    + + + + +
    + +
    + Engine + + + +
    +

    Azure engines use your BNL Azure credits. GPU runs the local LiteLLM proxy on the GPU server.

    + +
    + Legal corpus enrichment (optional) +

    When one or more slices are enabled, the tool searches the Do Better Norge legal corpus for relevant passages and prepends them to the prompt. All slices are off by default — enable only what applies to your document.

    +
    + + + + + + + + +
    +
    + +
    + +
    + +
    + +
    + +
    + +

    Drop up to 5 files here, or

    +

    PDF, DOCX, TXT — text extracted in memory, never stored

    +
    + +
    + + + + + +
    + +
    +
    +

    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.

    +
    +
    + + + + + + + + + + +