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:
@@ -3,6 +3,24 @@ declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
|
||||
function dbnToolsSafeReturn(mixed $value, string $default = '/'): string
|
||||
{
|
||||
$return = trim((string)$value);
|
||||
if ($return === '') {
|
||||
return $default;
|
||||
}
|
||||
if (!str_starts_with($return, '/') || str_starts_with($return, '//')) {
|
||||
return $default;
|
||||
}
|
||||
if (preg_match('/[\r\n]/', $return)) {
|
||||
return $default;
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
$uiLang = dbnToolsCurrentLanguage();
|
||||
$returnPath = dbnToolsSafeReturn($_GET['return'] ?? '/', '/');
|
||||
|
||||
// Handle SSO token from dobetternorge.no
|
||||
if (isset($_GET['sso']) && !dbnToolsIsAuthenticated()) {
|
||||
$ssoSecret = (string) dbnToolsEnv('DBN_SSO_SECRET', '');
|
||||
@@ -16,201 +34,183 @@ if (isset($_GET['sso']) && !dbnToolsIsAuthenticated()) {
|
||||
$_SESSION['dbn_tools_user_id'] = (int)$tokenData['uid'];
|
||||
$_SESSION['dbn_tools_user_email'] = (string)$tokenData['email'];
|
||||
$_SESSION['dbn_tools_user_role'] = 'sso';
|
||||
header('Location: ask.php');
|
||||
header('Location: ' . $returnPath);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
// Invalid/expired token — redirect back to main site to re-login
|
||||
header('Location: https://dobetternorge.no/account.php?error=' . urlencode('Session expired. Please log in and try again.'));
|
||||
exit;
|
||||
}
|
||||
|
||||
if (dbnToolsIsAuthenticated()) {
|
||||
$return = $_GET['return'] ?? '';
|
||||
$dest = ($return && str_starts_with($return, '/') && !str_contains($return, '//'))
|
||||
? $return : 'ask.php';
|
||||
header('Location: ' . $dest);
|
||||
exit;
|
||||
}
|
||||
$tools = dbnToolsLaunchedTools($uiLang);
|
||||
$langPath = '/';
|
||||
$toolsLogin = 'https://dobetternorge.no/tools-login.php?return=' . urlencode($returnPath);
|
||||
?>
|
||||
<!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>Do Better Norge — AI Legal Research</title>
|
||||
<meta name="description" content="AI-powered family law research for Norway. Source-cited answers from curated legal corpora, with a post-generation reviewer pass. Powered by CaveauAI.">
|
||||
<title><?= htmlspecialchars(dbnToolsT('meta_title', $uiLang)) ?></title>
|
||||
<meta name="description" content="AI legal preparation tools for Do Better Norge: transcribe, timeline, Barnevernet document analysis, advocate briefs, deep research, and corpus search.">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="https://tools.dobetternorge.no/">
|
||||
<meta property="og:title" content="Do Better Norge — AI Legal Research">
|
||||
<meta property="og:description" content="Source-cited answers from curated Norwegian law corpora. Every claim checked against real sources before it reaches you.">
|
||||
<meta property="og:title" content="<?= htmlspecialchars(dbnToolsT('meta_title', $uiLang)) ?>">
|
||||
<meta property="og:description" content="<?= htmlspecialchars(dbnToolsT('landing_sub', $uiLang)) ?>">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://tools.dobetternorge.no/">
|
||||
<meta name="theme-color" content="#f7f8fb">
|
||||
<meta name="theme-color" content="#f6f2ea">
|
||||
<link rel="stylesheet" href="assets/css/tools.css">
|
||||
</head>
|
||||
<body data-authenticated="false">
|
||||
<body data-authenticated="<?= dbnToolsIsAuthenticated() ? 'true' : 'false' ?>">
|
||||
<script>
|
||||
window.DBN_TOOLS_AUTHENTICATED = <?= dbnToolsIsAuthenticated() ? 'true' : 'false' ?>;
|
||||
window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
|
||||
</script>
|
||||
|
||||
<div id="publicLanding" class="showcase-page">
|
||||
|
||||
<header class="showcase-header">
|
||||
<div class="showcase-header-inner">
|
||||
<div class="showcase-brand">
|
||||
<p class="eyebrow">Do Better Norge</p>
|
||||
<h1 class="showcase-title">AI Legal Research</h1>
|
||||
<p class="showcase-tagline">Source-cited answers from curated Norwegian law corpora</p>
|
||||
<a href="https://caveauai.bluenotelogic.com/" class="powered-badge" rel="noopener" target="_blank">Powered by CaveauAI</a>
|
||||
</div>
|
||||
<a href="#access" class="cta-button">Access Legal Tools →</a>
|
||||
<?php if (dbnToolsIsAuthenticated()): ?>
|
||||
<main id="appShell" class="app-shell dashboard-shell">
|
||||
<header class="topbar">
|
||||
<div>
|
||||
<p class="eyebrow"><?= htmlspecialchars(dbnToolsT('brand_line', $uiLang)) ?></p>
|
||||
<h1><?= htmlspecialchars(dbnToolsT('dashboard_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>
|
||||
</header>
|
||||
</div>
|
||||
<div class="topbar-actions">
|
||||
<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="hiw-section">
|
||||
<div class="section-inner">
|
||||
<h2 class="section-heading">How it works</h2>
|
||||
<p class="section-sub">From question to reviewed, cited answer in three steps</p>
|
||||
<div class="hiw-steps">
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">01</div>
|
||||
<h3>Curated legal corpus</h3>
|
||||
<p>Norwegian family law, ECHR rulings, and Lovdata sources are indexed, chunked, and embedded by CaveauAI’s ingestion pipeline.</p>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">02</div>
|
||||
<h3>Hybrid retrieval</h3>
|
||||
<p>Your question triggers vector similarity search and keyword retrieval — sources are scored, re-ranked, and presented as a labelled evidence trail.</p>
|
||||
</div>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-num">03</div>
|
||||
<h3>Reviewed, cited answer</h3>
|
||||
<p>A post-generation reviewer checks every claim against retrieved sources before the answer reaches you. Citations are attached to real corpus documents.</p>
|
||||
</div>
|
||||
</div>
|
||||
<section class="manifesto dashboard-manifesto">
|
||||
<div class="manifesto-copy">
|
||||
<p class="manifesto-eyebrow"><?= htmlspecialchars(dbnToolsT('dashboard_eyebrow', $uiLang)) ?></p>
|
||||
<h2 class="manifesto-title"><?= htmlspecialchars(dbnToolsT('manifesto_title', $uiLang)) ?></h2>
|
||||
<p class="manifesto-sub"><?= htmlspecialchars(dbnToolsT('dashboard_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"><?= htmlspecialchars(dbnToolsT('disclaimer', $uiLang)) ?></div>
|
||||
|
||||
<section class="tool-dashboard-grid" aria-label="Available tools">
|
||||
<?php foreach ($tools as $slug => $item): ?>
|
||||
<a class="dashboard-tool-card" href="<?= htmlspecialchars($item['url']) ?>">
|
||||
<span class="dashboard-tool-card__icon"><?= htmlspecialchars($item['icon']) ?></span>
|
||||
<span class="dashboard-tool-card__badge"><?= htmlspecialchars($item['badge']) ?></span>
|
||||
<h2><?= htmlspecialchars($item['label']) ?></h2>
|
||||
<p><?= htmlspecialchars($item['description']) ?></p>
|
||||
<strong><?= htmlspecialchars(dbnToolsT('open_tool', $uiLang)) ?></strong>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</section>
|
||||
</main>
|
||||
<?php else: ?>
|
||||
<main id="publicLanding" class="showcase-page dbn-public-tools">
|
||||
<header class="showcase-header">
|
||||
<div class="showcase-header-inner">
|
||||
<div class="showcase-brand">
|
||||
<p class="eyebrow"><?= htmlspecialchars(dbnToolsT('landing_kicker', $uiLang)) ?></p>
|
||||
<h1 class="showcase-title"><?= htmlspecialchars(dbnToolsT('landing_title', $uiLang)) ?></h1>
|
||||
<p class="showcase-tagline"><?= htmlspecialchars(dbnToolsT('landing_sub', $uiLang)) ?></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="cap-section">
|
||||
<div class="section-inner">
|
||||
<h2 class="section-heading">Eight tools, one suite</h2>
|
||||
<div class="cap-grid">
|
||||
<div class="cap-card">
|
||||
<span class="cap-label">Ask</span>
|
||||
<h3>Ask</h3>
|
||||
<p>Source-grounded legal questions with citations and explicit uncertainty notes.</p>
|
||||
</div>
|
||||
<div class="cap-card">
|
||||
<span class="cap-label">Search</span>
|
||||
<h3>Search</h3>
|
||||
<p>Retrieve up to seven relevant legal sources with titles, sections, and excerpts.</p>
|
||||
</div>
|
||||
<div class="cap-card">
|
||||
<span class="cap-label">Deep research</span>
|
||||
<h3>Deep research</h3>
|
||||
<p>Upload a case file or paste a question. An agent expands it into 3–5 angles, runs hybrid rank/rerank RAG across the corpus + your upload, and returns a cited brief.</p>
|
||||
</div>
|
||||
<div class="cap-card cap-card--featured">
|
||||
<span class="cap-label">Advocate</span>
|
||||
<h3>Case Advocate</h3>
|
||||
<p>Pick who you represent. The agent takes your side — generating adversarial sub-questions, identifying the opposing party’s weaknesses, and producing a partisan brief grounded in Lovdata statutes and ECHR judgments.</p>
|
||||
</div>
|
||||
<div class="cap-card">
|
||||
<span class="cap-label">Summarize</span>
|
||||
<h3>Summarize</h3>
|
||||
<p>Extract facts, dates, parties, and legal references from pasted text.</p>
|
||||
</div>
|
||||
<div class="cap-card">
|
||||
<span class="cap-label">Timeline</span>
|
||||
<h3>Timeline</h3>
|
||||
<p>Build a chronological event sequence from case notes or documents.</p>
|
||||
</div>
|
||||
<div class="cap-card">
|
||||
<span class="cap-label">Redact</span>
|
||||
<h3>Redact</h3>
|
||||
<p>Remove sensitive personal data with configurable Nordic / ECHR / Global profiles.</p>
|
||||
</div>
|
||||
<div class="cap-card">
|
||||
<span class="cap-label">Transcribe</span>
|
||||
<h3>Transcribe</h3>
|
||||
<p>Convert audio recordings to text with optional speaker separation and Norwegian role labelling.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="showcase-header-actions">
|
||||
<nav class="shell-lang-switcher" aria-label="Language">
|
||||
<?php foreach (dbnToolsSupportedLanguages() as $langCode): ?>
|
||||
<a href="<?= htmlspecialchars($langPath . '?lang=' . $langCode . '&return=' . urlencode($returnPath)) ?>" class="<?= $langCode === $uiLang ? 'is-active' : '' ?>"><?= htmlspecialchars(dbnToolsLanguageLabel($langCode)) ?></a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
<a href="#access" class="cta-button"><?= htmlspecialchars(dbnToolsT('primary_access', $uiLang)) ?></a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="evidence-section">
|
||||
<div class="section-inner evidence-inner">
|
||||
<div class="evidence-copy">
|
||||
<h2>Every answer shows its work</h2>
|
||||
<p>Alongside each answer, the evidence trail shows which sources were retrieved, how they scored, which claims they support, and what remains uncertain. The legal reviewer pass checks all claims against the corpus before the answer is returned.</p>
|
||||
<ul class="evidence-list">
|
||||
<li>Source document, section, and authority type</li>
|
||||
<li>Similarity score per retrieved chunk</li>
|
||||
<li>Reviewer decision: approved / revised / insufficient support</li>
|
||||
<li>Explicit uncertainty statement when the corpus can’t support a claim</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="evidence-mock" aria-hidden="true">
|
||||
<p class="mock-label">Evidence Trail</p>
|
||||
<div class="mock-step mock-done">
|
||||
<span class="mock-dot"></span>
|
||||
<div><strong>Query interpreted</strong><p>Barneloven §42 custody arrangements</p></div>
|
||||
</div>
|
||||
<div class="mock-step mock-done">
|
||||
<span class="mock-dot"></span>
|
||||
<div><strong>3 sources retrieved</strong><p>Barneloven, ECHR Johansen v. Norway, Ot.prp. nr. 56</p></div>
|
||||
</div>
|
||||
<div class="mock-step mock-done">
|
||||
<span class="mock-dot"></span>
|
||||
<div><strong>Reviewer: approved</strong><p>All claims supported by corpus sources</p></div>
|
||||
</div>
|
||||
<div class="mock-step">
|
||||
<span class="mock-dot mock-dot--amber"></span>
|
||||
<div><strong>Uncertainty noted</strong><p>Recent amendments after 2024 may not be indexed</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<section class="landing-hero">
|
||||
<div class="landing-hero__copy">
|
||||
<p class="manifesto-eyebrow"><?= htmlspecialchars(dbnToolsT('manifesto_eyebrow', $uiLang)) ?></p>
|
||||
<h2><?= htmlspecialchars(dbnToolsT('manifesto_title', $uiLang)) ?></h2>
|
||||
<p><?= htmlspecialchars(dbnToolsT('manifesto_sub', $uiLang)) ?></p>
|
||||
</div>
|
||||
<div class="landing-hero__stats">
|
||||
<div><strong>23</strong><span><?= htmlspecialchars(dbnToolsT('stat_echr', $uiLang)) ?></span></div>
|
||||
<div><strong>1,731</strong><span><?= htmlspecialchars(dbnToolsT('stat_tribunal', $uiLang)) ?></span></div>
|
||||
<div><strong>20+</strong><span><?= htmlspecialchars(dbnToolsT('stat_pending', $uiLang)) ?></span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="cap-section">
|
||||
<div class="section-inner">
|
||||
<p class="eyebrow"><?= htmlspecialchars(dbnToolsT('tools_title', $uiLang)) ?></p>
|
||||
<div class="cap-grid cap-grid--six">
|
||||
<?php foreach ($tools as $item): ?>
|
||||
<article class="cap-card">
|
||||
<span class="cap-label"><?= htmlspecialchars($item['badge']) ?></span>
|
||||
<h3><?= htmlspecialchars($item['label']) ?></h3>
|
||||
<p><?= htmlspecialchars($item['description']) ?></p>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="access" class="access-section" aria-labelledby="accessTitle">
|
||||
<div class="gate-panel">
|
||||
<p class="eyebrow">Do Better Norge</p>
|
||||
<h2 id="accessTitle">Access Legal Tools</h2>
|
||||
<p class="gate-copy">Legal information and preparation support, not final legal advice.</p>
|
||||
<section class="evidence-section">
|
||||
<div class="section-inner evidence-inner">
|
||||
<article>
|
||||
<h2><?= htmlspecialchars(dbnToolsT('cause_title', $uiLang)) ?></h2>
|
||||
<p><?= htmlspecialchars(dbnToolsT('cause_text', $uiLang)) ?></p>
|
||||
</article>
|
||||
<article>
|
||||
<h2><?= htmlspecialchars(dbnToolsT('privacy_title', $uiLang)) ?></h2>
|
||||
<p><?= htmlspecialchars(dbnToolsT('privacy_text', $uiLang)) ?></p>
|
||||
</article>
|
||||
<article>
|
||||
<h2><?= htmlspecialchars(dbnToolsT('source_title', $uiLang)) ?></h2>
|
||||
<p><?= htmlspecialchars(dbnToolsT('source_text', $uiLang)) ?></p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div style="margin-bottom:20px;padding:14px 18px;background:rgba(0,32,91,.06);border-radius:10px;border:1px solid rgba(0,32,91,.12);font-size:14px;color:#333;text-align:center;">
|
||||
<strong>Do Better Norge member?</strong>
|
||||
<a href="https://dobetternorge.no/account.php" style="color:#00205B;font-weight:600;margin-left:6px;">Log in at dobetternorge.no →</a><br>
|
||||
<span style="color:#888;font-size:13px;">Then open Tools from your account dashboard</span>
|
||||
</div>
|
||||
|
||||
<div style="text-align:center;margin:16px 0 12px;font-size:13px;color:#aaa;letter-spacing:.05em;">OR SIGN IN WITH CAVEAU ACCOUNT</div>
|
||||
<section id="access" class="access-section" aria-labelledby="accessTitle">
|
||||
<div class="gate-panel">
|
||||
<p class="eyebrow">Do Better Norge</p>
|
||||
<h2 id="accessTitle"><?= htmlspecialchars(dbnToolsT('primary_access', $uiLang)) ?></h2>
|
||||
<p class="gate-copy"><?= htmlspecialchars(dbnToolsT('member_note', $uiLang)) ?></p>
|
||||
<a class="google-access-button" href="<?= htmlspecialchars($toolsLogin) ?>"><?= htmlspecialchars(dbnToolsT('primary_access', $uiLang)) ?></a>
|
||||
<p class="gate-copy"><a href="https://dobetternorge.no/register.php"><?= htmlspecialchars(dbnToolsT('register', $uiLang)) ?></a></p>
|
||||
|
||||
<details class="fallback-login">
|
||||
<summary><?= htmlspecialchars(dbnToolsT('secondary_access', $uiLang)) ?></summary>
|
||||
<form id="passcodeForm" class="passcode-form">
|
||||
<label for="loginEmail">Email</label>
|
||||
<label for="loginEmail"><?= htmlspecialchars(dbnToolsT('email', $uiLang)) ?></label>
|
||||
<input id="loginEmail" name="email" type="email" autocomplete="username email" required>
|
||||
<label for="loginPassword">Password</label>
|
||||
<label for="loginPassword"><?= htmlspecialchars(dbnToolsT('password', $uiLang)) ?></label>
|
||||
<div class="passcode-row">
|
||||
<input id="loginPassword" name="password" type="password" autocomplete="current-password" required>
|
||||
<button type="submit">Sign in</button>
|
||||
<button type="submit"><?= htmlspecialchars(dbnToolsT('sign_in', $uiLang)) ?></button>
|
||||
</div>
|
||||
<p id="gateStatus" class="form-status" role="status" aria-live="polite"></p>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<?php endif; ?>
|
||||
|
||||
<p style="text-align:center;margin-top:16px;font-size:13px;color:#888;">
|
||||
No account yet?
|
||||
<a href="https://dobetternorge.no/register.php" style="color:#00205B;font-weight:600;">Register free at dobetternorge.no</a>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="showcase-footer">
|
||||
<p>Do Better Norge · Built with <a href="https://caveauai.bluenotelogic.com/" rel="noopener" target="_blank">CaveauAI</a> by Blue Note Logic</p>
|
||||
<p class="footer-disclaimer">Legal information and preparation support, not final legal advice. Not a substitute for professional legal counsel.</p>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
<script>window.DBN_TOOLS_AUTHENTICATED = false;</script>
|
||||
<script src="assets/js/tools.js" defer></script>
|
||||
<script src="assets/js/tools.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user