feat: Legal Tools v1 — multilingual landing, dashboard, SSO bridge

- Public landing page at / for unauthenticated users (EN/NO/UK/PL)
- Authenticated / shows Case Workbench dashboard with manifesto strip,
  stats, and launched-tool grid (Transcribe, Timeline, BVJ, Advocate,
  Deep Research, Corpus)
- Added includes/i18n.php with full 4-language translation layer
- Extended layout.php to Case Workbench shell with tool rail, lang switcher
- AI output language normalization extended to en/no/uk/pl in PHP agents
- SSO token validation in bootstrap.php / index.php (dobetternorge.no bridge)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 22:53:27 +02:00
parent ba6c197f1b
commit a3d46f9756
19 changed files with 1149 additions and 238 deletions
+12 -9
View File
@@ -62,7 +62,7 @@ final class DbnBvjAnalyzerAgent
): array {
$engine = in_array($engine, ['azure_mini', 'azure_full', 'gpu'], true)
? $engine : 'azure_mini';
$language = in_array($language, ['en', 'no'], true) ? $language : 'en';
$language = dbnToolsNormalizeUiLanguage($language);
$controls = $this->normalizeControls($controls);
if (empty($uploadedFiles)) {
@@ -440,7 +440,7 @@ final class DbnBvjAnalyzerAgent
private function classifyDocument(string $docText, string $language): array
{
$locale = $language === 'no' ? 'Norwegian' : 'English';
$locale = dbnToolsLanguageName($language);
$excerpt = mb_substr($docText, 0, 6000, 'UTF-8');
$prompt = <<<PROMPT
@@ -492,7 +492,7 @@ PROMPT;
private function extractParties(string $docText, string $language): array
{
$locale = $language === 'no' ? 'Norwegian' : 'English';
$locale = dbnToolsLanguageName($language);
$excerpt = mb_substr($docText, 0, 12000, 'UTF-8');
$prompt = <<<PROMPT
@@ -540,7 +540,7 @@ PROMPT;
private function extractTimeline(string $docText, string $language): array
{
$locale = $language === 'no' ? 'Norwegian' : 'English';
$locale = dbnToolsLanguageName($language);
$excerpt = mb_substr($docText, 0, 12000, 'UTF-8');
$prompt = <<<PROMPT
@@ -600,7 +600,7 @@ PROMPT;
int $count,
string $language
): array {
$locale = $language === 'no' ? 'Norwegian' : 'English';
$locale = dbnToolsLanguageName($language);
$docType = $docMeta['doc_type'] ?? 'BVJ document';
$roleStr = $advocateRole !== '' ? $advocateRole : 'the affected party';
@@ -698,7 +698,7 @@ PROMPT;
string $additionalNotes,
?callable $emit = null
): array {
$locale = $language === 'no' ? 'Norwegian' : 'English';
$locale = dbnToolsLanguageName($language);
$roleStr = $advocateRole !== '' ? $advocateRole : 'the affected party';
$docType = $docMeta['doc_type'] ?? 'BVJ Document';
$docDate = $docMeta['doc_date'] ?? 'unknown date';
@@ -708,9 +708,12 @@ PROMPT;
$sourceCount = count($numberedSources);
if (empty($numberedSources)) {
$emptyBrief = $language === 'no'
? 'Ingen kildetreff ble funnet i korpuset for de valgte skivene og spørsmålene.'
: 'No corpus sources were retrieved for the selected slices and sub-questions.';
$emptyBrief = match (dbnToolsNormalizeUiLanguage($language)) {
'no' => 'Ingen kildetreff ble funnet i korpuset for de valgte skivene og spørsmålene.',
'uk' => 'Для вибраних розділів і підпитань не знайдено джерел у корпусі.',
'pl' => 'Nie znaleziono źródeł w korpusie dla wybranych sekcji i pytań pomocniczych.',
default => 'No corpus sources were retrieved for the selected slices and sub-questions.',
};
return [
'json' => [
'advocacy_brief' => $emptyBrief,
+10 -7
View File
@@ -38,7 +38,7 @@ final class DbnDeepResearchAgent
$seedQuery = trim($seedQuery);
$pastedText = trim($pastedText);
$engine = in_array($engine, ['azure_mini', 'azure_full', 'gpu'], true) ? $engine : 'azure_mini';
$language = in_array($language, ['en', 'no'], true) ? $language : 'en';
$language = dbnToolsNormalizeUiLanguage($language);
$controls = $this->normalizeControls($controls);
@@ -444,7 +444,7 @@ final class DbnDeepResearchAgent
private function interpretSeed(string $seedDescription, string $language, string $advocateRole = '', ?array $priorContext = null, string $branchNotes = ''): array
{
$locale = $language === 'no' ? 'Norwegian' : 'English';
$locale = dbnToolsLanguageName($language);
$rolePrefix = $advocateRole !== ''
? "You are preparing a case-research brief for: {$advocateRole}. Frame your interpretation to identify the strongest legal angles for this party.\n\n"
: '';
@@ -511,7 +511,7 @@ PROMPT;
private function expandQueries(string $seedDescription, string $brief, int $targetCount, string $language, string $advocateRole = ''): array
{
$locale = $language === 'no' ? 'Norwegian' : 'English';
$locale = dbnToolsLanguageName($language);
if ($advocateRole !== '') {
$prompt = <<<PROMPT
@@ -942,14 +942,17 @@ PROMPT;
?array $priorContext = null,
string $branchNotes = ''
): array {
$locale = $language === 'no' ? 'Norwegian' : 'English';
$locale = dbnToolsLanguageName($language);
if (empty($numberedSources)) {
return [
'json' => [
'brief_markdown' => $language === 'no'
? 'Jeg fant ikke tilstrekkelig kildestøtte i korpuset til å gi et grunnlagsbasert svar.'
: 'I did not find enough source support in the corpus to give a grounded answer.',
'brief_markdown' => match (dbnToolsNormalizeUiLanguage($language)) {
'no' => 'Jeg fant ikke tilstrekkelig kildestøtte i korpuset til å gi et grunnlagsbasert svar.',
'uk' => 'Я не знайшов достатньої підтримки джерел у корпусі, щоб дати обґрунтовану відповідь.',
'pl' => 'Nie znalazłem wystarczającego wsparcia źródłowego w korpusie, aby udzielić ugruntowanej odpowiedzi.',
default => 'I did not find enough source support in the corpus to give a grounded answer.',
},
'what_we_found' => 'No retrieved sources passed the similarity threshold.',
'what_remains_uncertain' => ['No corpus evidence retrieved for the given query and slice selection.'],
'next_practical_step' => 'Try widening slice selection or rephrasing with more specific statutory or party terms.',
+10 -7
View File
@@ -139,9 +139,12 @@ final class DbnLegalToolsService
return [
'tool' => 'ask',
'language' => $language,
'answer' => $language === 'no'
? 'Jeg fant ikke nok kildestøtte i familie-rettskorpuset til å svare sikkert.'
: 'I did not find enough source support in the family-law corpus to answer safely.',
'answer' => match (dbnToolsNormalizeUiLanguage($language)) {
'no' => 'Jeg fant ikke nok kildestøtte i familierettskorpuset til å svare sikkert.',
'uk' => 'Я не знайшов достатньої підтримки в корпусі сімейного права, щоб відповісти безпечно.',
'pl' => 'Nie znalazłem wystarczającego wsparcia źródłowego w korpusie prawa rodzinnego, aby odpowiedzieć bezpiecznie.',
default => 'I did not find enough source support in the family-law corpus to answer safely.',
},
'what_we_found' => $search['what_we_found'],
'evidence_trail' => [],
'what_remains_uncertain' => $search['what_remains_uncertain'],
@@ -160,7 +163,7 @@ final class DbnLegalToolsService
$this->azure->requireChat();
$context = $this->buildEvidenceContext($hits);
$locale = $language === 'no' ? 'Norwegian' : 'English';
$locale = dbnToolsLanguageName($language);
$prompt = <<<PROMPT
Question:
{$question}
@@ -229,7 +232,7 @@ PROMPT;
$text = $this->requirePasteText($text);
$this->azure->requireChat();
$locale = $language === 'no' ? 'Norwegian' : 'English';
$locale = dbnToolsLanguageName($language);
$prompt = <<<PROMPT
Summarize this pasted case-preparation text in {$locale}. Do not invent missing facts.
@@ -296,7 +299,7 @@ PROMPT;
$this->azure->requireChat();
}
$locale = $language === 'no' ? 'Norwegian' : 'English';
$locale = dbnToolsLanguageName($language);
$focusInstruction = match ($focus) {
'deadlines' => "\nFocus specifically on: legal deadlines, filing dates, response windows, appeal periods, and statutory time limits. Deprioritise narrative events with no legal deadline significance.",
@@ -599,7 +602,7 @@ PROMPT;
private function legalJsonSystemPrompt(string $language): string
{
$locale = $language === 'no' ? 'Norwegian' : 'English';
$locale = dbnToolsLanguageName($language);
return <<<PROMPT
You are Do Better Norge Legal Tools in a source-grounded legal preparation workflow.
Use the DBN legal guardrails:
+9 -6
View File
@@ -107,6 +107,7 @@ function dbnToolsStartSession(): void
}
dbnToolsStartSession();
require_once __DIR__ . '/i18n.php';
function dbnToolsIsAuthenticated(): bool
{
@@ -224,8 +225,7 @@ function dbnToolsJsonInput(int $maxBytes = 50000): array
function dbnToolsNormalizeLanguage(mixed $value): string
{
$language = strtolower(trim((string)$value));
return in_array($language, ['no', 'en'], true) ? $language : 'en';
return dbnToolsNormalizeUiLanguage($value);
}
function dbnToolsNormalizeRegion(mixed $value): string
@@ -472,10 +472,13 @@ function dbnToolsHasActiveSubscription(int $clientId, int $packageId, ?PDO $db =
function dbnToolsDisclaimer(string $language): string
{
if ($language === 'no') {
return 'Juridisk informasjon og forberedelsesstøtte, ikke endelig juridisk rådgivning.';
}
return 'Legal information and preparation support, not final legal advice.';
$language = dbnToolsNormalizeUiLanguage($language);
return match ($language) {
'no' => 'Juridisk informasjon og forberedelsesstøtte, ikke endelig juridisk rådgivning.',
'uk' => 'Юридична інформація та підтримка підготовки, не остаточна юридична порада.',
'pl' => 'Informacje prawne i wsparcie przygotowania, nie ostateczna porada prawna.',
default => 'Legal information and preparation support, not final legal advice.',
};
}
function dbnToolsExcerpt(string $text, int $limit = 520): string
+314
View File
@@ -0,0 +1,314 @@
<?php
declare(strict_types=1);
function dbnToolsSupportedLanguages(): array
{
return ['en', 'no', 'uk', 'pl'];
}
function dbnToolsNormalizeUiLanguage(mixed $language): string
{
$language = strtolower(trim((string)$language));
if ($language === 'nb') {
return 'no';
}
return in_array($language, dbnToolsSupportedLanguages(), true) ? $language : 'en';
}
function dbnToolsCurrentLanguage(): string
{
if (isset($_GET['lang'])) {
$lang = dbnToolsNormalizeUiLanguage($_GET['lang']);
$_SESSION['dbn_tools_lang'] = $lang;
if (!headers_sent()) {
setcookie('dbn_tools_lang', $lang, [
'expires' => time() + 60 * 60 * 24 * 180,
'path' => '/',
'secure' => dbnToolsIsHttps(),
'httponly' => false,
'samesite' => 'Lax',
]);
}
return $lang;
}
if (!empty($_SESSION['dbn_tools_lang'])) {
return dbnToolsNormalizeUiLanguage($_SESSION['dbn_tools_lang']);
}
if (!empty($_COOKIE['dbn_tools_lang'])) {
$lang = dbnToolsNormalizeUiLanguage($_COOKIE['dbn_tools_lang']);
$_SESSION['dbn_tools_lang'] = $lang;
return $lang;
}
return 'en';
}
function dbnToolsLanguageName(string $language): string
{
return match (dbnToolsNormalizeUiLanguage($language)) {
'no' => 'Norwegian',
'uk' => 'Ukrainian',
'pl' => 'Polish',
default => 'English',
};
}
function dbnToolsLanguageLabel(string $language): string
{
return match (dbnToolsNormalizeUiLanguage($language)) {
'no' => 'NO',
'uk' => 'UK',
'pl' => 'PL',
default => 'EN',
};
}
function dbnToolsTranslations(): array
{
return [
'en' => [
'meta_title' => 'Do Better Norge - AI Legal Tools',
'brand_line' => 'Do Better Norge - tools.dobetternorge.no',
'suite_title' => 'Legal Tools',
'workspace_title' => 'Case Workbench',
'session_active' => 'Session active',
'health' => 'Health',
'sign_out' => 'Sign out',
'retention' => 'Session in memory - nothing stored by default',
'disclaimer' => 'Legal information and preparation support, not final legal advice. Pasted text and uploads are processed in memory by default.',
'manifesto_eyebrow' => 'Family rights - Norway - since 2019',
'manifesto_title' => 'They took her child in twelve minutes.',
'manifesto_sub' => 'Open a tool. Build a chronology, research the law, protect privacy, and prepare your next step with cited support.',
'stat_echr' => 'ECHR violations since 2015',
'stat_loss' => 'ECHR cases lost 2017-22',
'stat_tribunal' => 'tribunal decisions analysed',
'stat_pending' => 'pending Strasbourg cases',
'reasoning_eyebrow' => 'File - Evidence trail',
'reasoning_title' => 'Reasoning',
'waiting_title' => 'Waiting',
'waiting_text' => 'Run a tool to see interpretation, retrieval, confidence, uncertainty, and next step.',
'dashboard_eyebrow' => 'Approved tools suite',
'dashboard_title' => 'Choose a legal AI tool',
'dashboard_sub' => 'Built for families, advocates, and supporters preparing Norwegian family-rights and child-welfare cases.',
'open_tool' => 'Open tool',
'landing_kicker' => 'AI legal preparation for family-rights cases in Norway',
'landing_title' => 'Legal tools for families who need the record to make sense.',
'landing_sub' => 'Transcribe meetings, build timelines, analyze Barnevernet documents, research ECHR and Norwegian sources, and prepare cited advocacy briefs.',
'primary_access' => 'Continue with Do Better Norge / Google',
'secondary_access' => 'Sign in with Caveau account',
'member_note' => 'Use your Do Better Norge account. Google login is handled on the main site, then you return here securely.',
'email' => 'Email',
'password' => 'Password',
'sign_in' => 'Sign in',
'register' => 'Register free at dobetternorge.no',
'cause_title' => 'Evidence over outrage.',
'cause_text' => 'Every tool is designed around the same principle as the movement: document the facts, cite the law, and make the next practical step visible.',
'privacy_title' => 'Private by design',
'privacy_text' => 'Uploads are processed in memory by default. The app records only operational metadata such as tool name, latency, language, and anonymous session id.',
'source_title' => 'Sources stay visible',
'source_text' => 'Research tools keep citations, sections, source excerpts, and uncertainty notes next to the answer.',
'tools_title' => 'Launched tools',
],
'no' => [
'meta_title' => 'Do Better Norge - juridiske AI-verktøy',
'brand_line' => 'Do Better Norge - tools.dobetternorge.no',
'suite_title' => 'Juridiske verktøy',
'workspace_title' => 'Saksarbeidsbenk',
'session_active' => 'Økt aktiv',
'health' => 'Helse',
'sign_out' => 'Logg ut',
'retention' => 'Økt i minnet - ingenting lagres som standard',
'disclaimer' => 'Juridisk informasjon og forberedelsesstøtte, ikke endelig juridisk rådgivning. Tekst og opplastinger behandles som standard i minnet.',
'manifesto_eyebrow' => 'Familierettigheter - Norge - siden 2019',
'manifesto_title' => 'De tok barnet hennes på tolv minutter.',
'manifesto_sub' => 'Åpne et verktøy. Bygg kronologi, undersøk loven, beskytt personvern og forbered neste steg med kilder.',
'stat_echr' => 'EMD-brudd siden 2015',
'stat_loss' => 'EMD-saker tapt 2017-22',
'stat_tribunal' => 'nemndsvedtak analysert',
'stat_pending' => 'saker venter i Strasbourg',
'reasoning_eyebrow' => 'Fil - evidensspor',
'reasoning_title' => 'Resonnement',
'waiting_title' => 'Venter',
'waiting_text' => 'Kjør et verktøy for å se tolkning, kilder, tillit, usikkerhet og neste steg.',
'dashboard_eyebrow' => 'Godkjent verktøypakke',
'dashboard_title' => 'Velg et juridisk AI-verktøy',
'dashboard_sub' => 'Laget for familier, støttespillere og advokater som forbereder norske familie- og barnevernssaker.',
'open_tool' => 'Åpne verktøy',
'landing_kicker' => 'Juridisk AI-forberedelse for familierettssaker i Norge',
'landing_title' => 'Juridiske verktøy for familier som trenger orden i saksbildet.',
'landing_sub' => 'Transkriber møter, bygg tidslinjer, analyser barnevernsdokumenter, undersøk EMD og norske kilder, og forbered kildebelagte prosesskriv.',
'primary_access' => 'Fortsett med Do Better Norge / Google',
'secondary_access' => 'Logg inn med Caveau-konto',
'member_note' => 'Bruk Do Better Norge-kontoen din. Google-pålogging skjer på hovedsiden, så kommer du trygt tilbake hit.',
'email' => 'E-post',
'password' => 'Passord',
'sign_in' => 'Logg inn',
'register' => 'Registrer deg gratis på dobetternorge.no',
'cause_title' => 'Bevis fremfor raseri.',
'cause_text' => 'Hvert verktøy følger samme prinsipp som bevegelsen: dokumenter fakta, vis lovgrunnlaget og gjør neste praktiske steg tydelig.',
'privacy_title' => 'Personvern først',
'privacy_text' => 'Opplastinger behandles som standard i minnet. Appen lagrer bare operasjonelle metadata som verktøy, tidsbruk, språk og anonym økt-id.',
'source_title' => 'Kildene er synlige',
'source_text' => 'Forskningsverktøyene holder sitater, paragrafer, kildeutdrag og usikkerhet ved siden av svaret.',
'tools_title' => 'Lanserte verktøy',
],
'uk' => [
'meta_title' => 'Do Better Norge - юридичні AI інструменти',
'brand_line' => 'Do Better Norge - tools.dobetternorge.no',
'suite_title' => 'Юридичні інструменти',
'workspace_title' => 'Робочий простір справи',
'session_active' => 'Сесія активна',
'health' => 'Стан',
'sign_out' => 'Вийти',
'retention' => 'Сесія в памʼяті - за замовчуванням нічого не зберігається',
'disclaimer' => 'Юридична інформація та підтримка підготовки, не остаточна юридична порада. Текст і файли за замовчуванням обробляються в памʼяті.',
'manifesto_eyebrow' => 'Права сімʼї - Норвегія - з 2019',
'manifesto_title' => 'Її дитину забрали за дванадцять хвилин.',
'manifesto_sub' => 'Відкрийте інструмент. Побудуйте хронологію, дослідіть право, захистіть приватність і підготуйте наступний крок з джерелами.',
'stat_echr' => 'порушень ЄСПЛ з 2015',
'stat_loss' => 'справ ЄСПЛ програно 2017-22',
'stat_tribunal' => 'рішень трибуналів проаналізовано',
'stat_pending' => 'справ очікують у Страсбурзі',
'reasoning_eyebrow' => 'Файл - слід доказів',
'reasoning_title' => 'Обґрунтування',
'waiting_title' => 'Очікування',
'waiting_text' => 'Запустіть інструмент, щоб побачити тлумачення, джерела, впевненість, невизначеність і наступний крок.',
'dashboard_eyebrow' => 'Схвалений набір інструментів',
'dashboard_title' => 'Оберіть юридичний AI інструмент',
'dashboard_sub' => 'Для сімей, представників і союзників, які готують справи про сімейні права та захист дітей у Норвегії.',
'open_tool' => 'Відкрити інструмент',
'landing_kicker' => 'AI підготовка для справ про сімейні права в Норвегії',
'landing_title' => 'Юридичні інструменти для сімей, яким потрібно впорядкувати матеріали справи.',
'landing_sub' => 'Транскрибуйте зустрічі, будуйте хронології, аналізуйте документи Barnevernet, досліджуйте ЄСПЛ і норвезькі джерела та готуйте аргументи з цитатами.',
'primary_access' => 'Продовжити через Do Better Norge / Google',
'secondary_access' => 'Увійти з обліковим записом Caveau',
'member_note' => 'Використайте свій обліковий запис Do Better Norge. Google-вхід відбувається на основному сайті, після чого ви безпечно повертаєтесь сюди.',
'email' => 'Email',
'password' => 'Пароль',
'sign_in' => 'Увійти',
'register' => 'Зареєструватися безкоштовно на dobetternorge.no',
'cause_title' => 'Докази важливіші за обурення.',
'cause_text' => 'Кожен інструмент побудований на тому самому принципі: задокументувати факти, процитувати право і зробити наступний практичний крок видимим.',
'privacy_title' => 'Приватність за задумом',
'privacy_text' => 'Файли за замовчуванням обробляються в памʼяті. Зберігаються лише технічні метадані: інструмент, затримка, мова та анонімний id сесії.',
'source_title' => 'Джерела залишаються видимими',
'source_text' => 'Дослідницькі інструменти показують цитати, розділи, уривки джерел і примітки про невизначеність поруч із відповіддю.',
'tools_title' => 'Запущені інструменти',
],
'pl' => [
'meta_title' => 'Do Better Norge - prawne narzędzia AI',
'brand_line' => 'Do Better Norge - tools.dobetternorge.no',
'suite_title' => 'Narzędzia prawne',
'workspace_title' => 'Panel pracy nad sprawą',
'session_active' => 'Sesja aktywna',
'health' => 'Stan',
'sign_out' => 'Wyloguj',
'retention' => 'Sesja w pamięci - domyślnie nic nie jest zapisywane',
'disclaimer' => 'Informacje prawne i wsparcie przygotowania, nie ostateczna porada prawna. Tekst i pliki są domyślnie przetwarzane w pamięci.',
'manifesto_eyebrow' => 'Prawa rodzinne - Norwegia - od 2019',
'manifesto_title' => 'Zabrali jej dziecko w dwanaście minut.',
'manifesto_sub' => 'Otwórz narzędzie. Zbuduj chronologię, zbadaj prawo, chroń prywatność i przygotuj kolejny krok z cytowanymi źródłami.',
'stat_echr' => 'naruszeń ETPC od 2015',
'stat_loss' => 'spraw ETPC przegranych 2017-22',
'stat_tribunal' => 'decyzji trybunałów przeanalizowano',
'stat_pending' => 'spraw oczekuje w Strasburgu',
'reasoning_eyebrow' => 'Plik - ślad dowodów',
'reasoning_title' => 'Uzasadnienie',
'waiting_title' => 'Oczekiwanie',
'waiting_text' => 'Uruchom narzędzie, aby zobaczyć interpretację, źródła, pewność, niepewność i następny krok.',
'dashboard_eyebrow' => 'Zatwierdzony pakiet narzędzi',
'dashboard_title' => 'Wybierz prawne narzędzie AI',
'dashboard_sub' => 'Dla rodzin, rzeczników i sojuszników przygotowujących norweskie sprawy rodzinne i dotyczące ochrony dzieci.',
'open_tool' => 'Otwórz narzędzie',
'landing_kicker' => 'Prawne przygotowanie AI dla spraw rodzinnych w Norwegii',
'landing_title' => 'Narzędzia prawne dla rodzin, które muszą uporządkować akta sprawy.',
'landing_sub' => 'Transkrybuj spotkania, buduj osie czasu, analizuj dokumenty Barnevernet, badaj ETPC i norweskie źródła oraz przygotowuj argumenty z cytatami.',
'primary_access' => 'Kontynuuj przez Do Better Norge / Google',
'secondary_access' => 'Zaloguj przez konto Caveau',
'member_note' => 'Użyj konta Do Better Norge. Logowanie Google odbywa się na głównej stronie, a potem bezpiecznie wracasz tutaj.',
'email' => 'Email',
'password' => 'Hasło',
'sign_in' => 'Zaloguj',
'register' => 'Zarejestruj się bezpłatnie na dobetternorge.no',
'cause_title' => 'Dowody ponad oburzenie.',
'cause_text' => 'Każde narzędzie opiera się na tej samej zasadzie: udokumentować fakty, przytoczyć prawo i pokazać następny praktyczny krok.',
'privacy_title' => 'Prywatność w projekcie',
'privacy_text' => 'Pliki są domyślnie przetwarzane w pamięci. Aplikacja zapisuje tylko metadane operacyjne, takie jak narzędzie, czas, język i anonimowy identyfikator sesji.',
'source_title' => 'Źródła pozostają widoczne',
'source_text' => 'Narzędzia badawcze pokazują cytaty, sekcje, fragmenty źródeł i notatki o niepewności obok odpowiedzi.',
'tools_title' => 'Uruchomione narzędzia',
],
];
}
function dbnToolsT(string $key, ?string $language = null): string
{
$language = dbnToolsNormalizeUiLanguage($language ?? dbnToolsCurrentLanguage());
$all = dbnToolsTranslations();
return (string)($all[$language][$key] ?? $all['en'][$key] ?? $key);
}
function dbnToolsLaunchedTools(?string $language = null): array
{
$language = dbnToolsNormalizeUiLanguage($language ?? dbnToolsCurrentLanguage());
$copy = [
'en' => [
'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'],
'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'],
'deep-research' => ['Deep Research', 'Agent + RAG', 'Expand a question into research angles, search legal slices, and synthesize a cited brief.', 'Family-legal'],
'corpus' => ['Corpus', 'Legal knowledge base', 'Inspect indexed sources, corpus health, legal categories, and retrieval behavior.', '~220 K passages'],
],
'no' => [
'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'],
'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'],
'deep-research' => ['Dyp research', 'Agent + RAG', 'Utvid et spørsmål til forskningsvinkler, søk juridiske kilder og lag et kildebelagt notat.', 'Familierett'],
'corpus' => ['Korpus', 'Juridisk kunnskapsbase', 'Se indekserte kilder, korpushelse, juridiske kategorier og søkeoppsett.', '~220 K utdrag'],
],
'uk' => [
'transcribe' => ['Транскрипція', 'Аудіо та зустрічі', 'Перетворюйте аудіо або відео на текст із розділенням мовців і юридичною лексикою.', 'Whisper / GPU'],
'timeline' => ['Хронологія', 'Події та строки', 'Витягуйте дати, слухання, етапи Barnevernet і юридичні строки з нотаток або файлів.', 'Обробити і забути'],
'barnevernet' => ['BVJ аналізатор', 'Документи Barnevernet', 'Аналізуйте документи захисту дітей з вашої позиції, з процесуальними ризиками та джерелами.', 'Документ + RAG'],
'advocate' => ['Адвокат', 'Позиційний бриф', 'Оберіть, кого представляєте, і створіть бриф із джерелами на підтримку цієї позиції.', 'ЄСПЛ + Lovdata'],
'deep-research' => ['Глибоке дослідження', 'Agent + RAG', 'Розгортає питання в дослідницькі напрями, шукає юридичні джерела та створює бриф.', 'Сімейне право'],
'corpus' => ['Корпус', 'Юридична база знань', 'Переглядайте індексовані джерела, стан корпусу, категорії та поведінку пошуку.', '~220 тис. уривків'],
],
'pl' => [
'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'],
'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'],
'deep-research' => ['Głębokie badanie', 'Agent + RAG', 'Rozwija pytanie w kierunki badawcze, przeszukuje źródła prawne i tworzy brief z cytatami.', 'Prawo rodzinne'],
'corpus' => ['Korpus', 'Prawna baza wiedzy', 'Sprawdzaj indeksowane źródła, stan korpusu, kategorie prawne i działanie wyszukiwania.', '~220 tys. fragmentów'],
],
];
$selected = $copy[$language] ?? $copy['en'];
$order = ['transcribe', 'timeline', 'barnevernet', 'advocate', 'deep-research', 'corpus'];
$icons = [
'transcribe' => 'TR',
'timeline' => 'TL',
'barnevernet' => 'BVJ',
'advocate' => 'ADV',
'deep-research' => 'DR',
'corpus' => 'KB',
];
$out = [];
foreach ($order as $slug) {
[$label, $sub, $description, $badge] = $selected[$slug];
$out[$slug] = [
'label' => $label,
'sub' => $sub,
'description' => $description,
'badge' => $badge,
'url' => $slug . '.php',
'icon' => $icons[$slug],
];
}
return $out;
}
+49 -28
View File
@@ -8,55 +8,76 @@ if (!dbnToolsIsAuthenticated()) {
exit;
}
$navItems = [
'ask' => ['Ask', 'Source-grounded'],
'search' => ['Search', 'Legal sources'],
'deep-research' => ['Deep research', 'Agent + RAG'],
'advocate' => ['Advocate', 'Take a side'],
'barnevernet' => ['BVJ Analyzer', 'Document'],
'summarize' => ['Summarize', 'Pasted text'],
'timeline' => ['Timeline', 'Events'],
'redact' => ['Redact', 'Privacy'],
'transcribe' => ['Transcribe', 'Audio'],
'corpus' => ['Corpus', 'Data & stack'],
];
$toolName = $toolName ?? 'ask';
$toolTitle = $toolTitle ?? 'Legal Tools';
$toolKind = $toolKind ?? '';
$toolBadge = $toolBadge ?? '';
$uiLang = dbnToolsCurrentLanguage();
$navItems = dbnToolsLaunchedTools($uiLang);
$toolName = $toolName ?? 'transcribe';
$toolMeta = $navItems[$toolName] ?? null;
$toolTitle = $toolMeta['label'] ?? ($toolTitle ?? dbnToolsT('suite_title', $uiLang));
$toolKind = $toolMeta['sub'] ?? ($toolKind ?? '');
$toolBadge = $toolMeta['badge'] ?? ($toolBadge ?? '');
$langPath = strtok((string)($_SERVER['REQUEST_URI'] ?? '/'), '?') ?: '/';
?>
<!doctype html>
<html lang="en">
<html lang="<?= htmlspecialchars($uiLang) ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars($toolTitle) ?> Do Better Norge</title>
<title><?= htmlspecialchars($toolTitle) ?> - Do Better Norge</title>
<link rel="stylesheet" href="assets/css/tools.css">
</head>
<body data-authenticated="true" data-active-tool="<?= htmlspecialchars($toolName) ?>">
<script>window.DBN_TOOLS_AUTHENTICATED = true;</script>
<script>
window.DBN_TOOLS_AUTHENTICATED = true;
window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
</script>
<main id="appShell" class="app-shell">
<header class="topbar">
<div>
<p class="eyebrow">Do Better Norge</p>
<h1>Legal Tools</h1>
<p class="eyebrow"><?= htmlspecialchars(dbnToolsT('brand_line', $uiLang)) ?></p>
<h1><?= htmlspecialchars(dbnToolsT('suite_title', $uiLang)) ?> <span class="title-mark">.</span> <?= htmlspecialchars(dbnToolsT('workspace_title', $uiLang)) ?></h1>
<div class="case-no">
<span class="pulse"></span>
<span>family-legal</span>
<span class="case-sep">.</span>
<span><?= htmlspecialchars(dbnToolsT('retention', $uiLang)) ?></span>
</div>
</div>
<div class="topbar-actions">
<span id="healthPill" class="status-pill">Session active</span>
<button id="healthButton" class="secondary-button" type="button">Health</button>
<nav class="shell-lang-switcher" aria-label="Language">
<?php foreach (dbnToolsSupportedLanguages() as $langCode): ?>
<a href="<?= htmlspecialchars($langPath . '?lang=' . $langCode) ?>" class="<?= $langCode === $uiLang ? 'is-active' : '' ?>"><?= htmlspecialchars(dbnToolsLanguageLabel($langCode)) ?></a>
<?php endforeach; ?>
</nav>
<span id="healthPill" class="status-pill"><?= htmlspecialchars(dbnToolsT('session_active', $uiLang)) ?></span>
<button id="healthButton" class="secondary-button" type="button"><?= htmlspecialchars(dbnToolsT('health', $uiLang)) ?></button>
</div>
</header>
<section class="manifesto" role="banner">
<div class="manifesto-copy">
<p class="manifesto-eyebrow"><?= htmlspecialchars(dbnToolsT('manifesto_eyebrow', $uiLang)) ?></p>
<h2 class="manifesto-title"><?= htmlspecialchars(dbnToolsT('manifesto_title', $uiLang)) ?></h2>
<p class="manifesto-sub"><?= htmlspecialchars(dbnToolsT('manifesto_sub', $uiLang)) ?></p>
</div>
<div class="manifesto-stats" aria-label="Headline statistics">
<div class="manifesto-stat"><strong>23</strong><span><?= htmlspecialchars(dbnToolsT('stat_echr', $uiLang)) ?></span></div>
<div class="manifesto-stat"><strong>64%</strong><span><?= htmlspecialchars(dbnToolsT('stat_loss', $uiLang)) ?></span></div>
<div class="manifesto-stat"><strong>1,731</strong><span><?= htmlspecialchars(dbnToolsT('stat_tribunal', $uiLang)) ?></span></div>
<div class="manifesto-stat"><strong>20+</strong><span><?= htmlspecialchars(dbnToolsT('stat_pending', $uiLang)) ?></span></div>
</div>
</section>
<div class="disclaimer" role="note">
Legal information and preparation support, not final legal advice. Pasted text is processed in memory by default.
<?= htmlspecialchars(dbnToolsT('disclaimer', $uiLang)) ?>
</div>
<section class="workspace" aria-label="Legal tools workspace">
<nav class="tool-rail" aria-label="Tools">
<?php foreach ($navItems as $slug => [$label, $sub]): ?>
<a href="<?= $slug ?>.php" class="tool-tab<?= $slug === $toolName ? ' is-active' : '' ?>" data-tool="<?= $slug ?>"<?= $slug === $toolName ? ' aria-current="page"' : '' ?>>
<span><?= $label ?></span>
<small><?= $sub ?></small>
<?php foreach ($navItems as $slug => $item): ?>
<a href="<?= htmlspecialchars($item['url']) ?>" class="tool-tab<?= $slug === $toolName ? ' is-active' : '' ?>" data-tool="<?= htmlspecialchars($slug) ?>"<?= $slug === $toolName ? ' aria-current="page"' : '' ?>>
<span><?= htmlspecialchars($item['label']) ?></span>
<small><?= htmlspecialchars($item['sub']) ?></small>
<em><?= htmlspecialchars($item['icon']) ?></em>
</a>
<?php endforeach; ?>
</nav>
+4 -4
View File
@@ -5,15 +5,15 @@
<?= $reasoningPanelOverride ?>
<?php else: ?>
<div class="reasoning-head">
<p class="eyebrow">Evidence trail</p>
<h2 id="reasoningTitle">Reasoning</h2>
<p class="eyebrow"><?= htmlspecialchars(dbnToolsT('reasoning_eyebrow', $uiLang ?? dbnToolsCurrentLanguage())) ?></p>
<h2 id="reasoningTitle"><?= htmlspecialchars(dbnToolsT('reasoning_title', $uiLang ?? dbnToolsCurrentLanguage())) ?></h2>
</div>
<ol id="traceList" class="trace-list">
<li>
<span class="trace-status waiting"></span>
<div>
<strong>Waiting</strong>
<p>Run a tool to see interpretation, retrieval, confidence, uncertainty, and next step.</p>
<strong><?= htmlspecialchars(dbnToolsT('waiting_title', $uiLang ?? dbnToolsCurrentLanguage())) ?></strong>
<p><?= htmlspecialchars(dbnToolsT('waiting_text', $uiLang ?? dbnToolsCurrentLanguage())) ?></p>
</div>
</li>
</ol>