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:
+8
-1
@@ -12,6 +12,8 @@ require_once __DIR__ . '/includes/layout.php';
|
||||
<div class="lang-switcher" id="advLangSwitcher" role="group" aria-label="UI language">
|
||||
<button type="button" class="lang-btn is-active" data-lang="en">🇬🇧 EN</button>
|
||||
<button type="button" class="lang-btn" data-lang="no">🇳🇴 NO</button>
|
||||
<button type="button" class="lang-btn" data-lang="uk">🇺🇦 UK</button>
|
||||
<button type="button" class="lang-btn" data-lang="pl">🇵🇱 PL</button>
|
||||
</div>
|
||||
|
||||
<!-- Role selector — the defining field for advocate mode -->
|
||||
@@ -198,7 +200,12 @@ require_once __DIR__ . '/includes/layout.php';
|
||||
</div>
|
||||
|
||||
<!-- Hidden stubs so tools.js element refs don't crash on this page -->
|
||||
<div class="is-hidden" id="languageControl" aria-hidden="true"><input type="radio" name="language" value="en" checked></div>
|
||||
<div class="is-hidden" id="languageControl" aria-hidden="true">
|
||||
<input type="radio" name="language" value="en" checked>
|
||||
<input type="radio" name="language" value="no">
|
||||
<input type="radio" name="language" value="uk">
|
||||
<input type="radio" name="language" value="pl">
|
||||
</div>
|
||||
<div class="is-hidden" id="redactionControl" aria-hidden="true"></div>
|
||||
<div class="is-hidden" id="audioZone" aria-hidden="true">
|
||||
<input type="file" id="audioInput" style="display:none">
|
||||
|
||||
@@ -14,6 +14,461 @@
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
/* Do Better Norge v1 workbench */
|
||||
:root {
|
||||
--dbn-paper: #f6f2ea;
|
||||
--dbn-ink: #16130f;
|
||||
--dbn-blue: #00205b;
|
||||
--dbn-red: #ba0c2f;
|
||||
--dbn-teal: #0f766e;
|
||||
--dbn-line: rgba(22, 19, 15, 0.16);
|
||||
}
|
||||
|
||||
body {
|
||||
background:
|
||||
linear-gradient(90deg, rgba(0, 32, 91, 0.055) 1px, transparent 1px),
|
||||
linear-gradient(180deg, rgba(186, 12, 47, 0.04), transparent 38rem),
|
||||
var(--dbn-paper);
|
||||
background-size: 44px 44px, auto, auto;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
padding: 18px clamp(14px, 2vw, 28px) 34px;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
align-items: flex-start;
|
||||
border-bottom: 2px solid rgba(22, 19, 15, 0.14);
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.topbar h1 {
|
||||
color: var(--dbn-ink);
|
||||
font-size: clamp(1.8rem, 2.8vw, 3.2rem);
|
||||
line-height: 0.95;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.title-mark {
|
||||
color: var(--dbn-red);
|
||||
}
|
||||
|
||||
.case-no {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 9px;
|
||||
color: #5d574e;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.case-sep {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.pulse {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--dbn-teal);
|
||||
box-shadow: 0 0 0 5px rgba(15, 118, 110, 0.12);
|
||||
}
|
||||
|
||||
.shell-lang-switcher {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--dbn-line);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.64);
|
||||
}
|
||||
|
||||
.shell-lang-switcher a {
|
||||
display: inline-flex;
|
||||
min-width: 36px;
|
||||
min-height: 30px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
color: var(--dbn-blue);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 900;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.shell-lang-switcher a.is-active,
|
||||
.shell-lang-switcher a:hover {
|
||||
background: var(--dbn-blue);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.manifesto {
|
||||
max-width: 1500px;
|
||||
margin: 0 auto 12px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.15fr) minmax(360px, 0.85fr);
|
||||
gap: 14px;
|
||||
border: 1px solid rgba(22, 19, 15, 0.18);
|
||||
border-radius: 8px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(186, 12, 47, 0.14), transparent 44%),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.72), rgba(255, 255, 255, 0.42));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.manifesto-copy {
|
||||
padding: clamp(20px, 3vw, 36px);
|
||||
}
|
||||
|
||||
.manifesto-eyebrow {
|
||||
margin: 0 0 8px;
|
||||
color: var(--dbn-red);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.manifesto-title {
|
||||
max-width: 780px;
|
||||
margin: 0;
|
||||
color: var(--dbn-ink);
|
||||
font-size: clamp(2rem, 5vw, 5.4rem);
|
||||
line-height: 0.92;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.manifesto-sub {
|
||||
max-width: 760px;
|
||||
margin: 14px 0 0;
|
||||
color: #3f3932;
|
||||
font-size: clamp(1rem, 1.3vw, 1.18rem);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.manifesto-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
border-left: 1px solid rgba(22, 19, 15, 0.16);
|
||||
}
|
||||
|
||||
.manifesto-stat {
|
||||
min-height: 120px;
|
||||
padding: 20px;
|
||||
border: 0 solid rgba(22, 19, 15, 0.13);
|
||||
border-bottom-width: 1px;
|
||||
border-right-width: 1px;
|
||||
background: rgba(255, 255, 255, 0.38);
|
||||
}
|
||||
|
||||
.manifesto-stat strong {
|
||||
display: block;
|
||||
color: var(--dbn-blue);
|
||||
font-size: clamp(1.8rem, 3vw, 3.2rem);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.manifesto-stat span {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #4b453d;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.workspace {
|
||||
grid-template-columns: 204px minmax(0, 1fr) 370px;
|
||||
}
|
||||
|
||||
.tool-rail,
|
||||
.tool-panel,
|
||||
.reasoning-panel,
|
||||
.gate-panel,
|
||||
.cap-card,
|
||||
.dashboard-tool-card {
|
||||
border-color: rgba(22, 19, 15, 0.15);
|
||||
box-shadow: 0 18px 45px rgba(22, 19, 15, 0.08);
|
||||
}
|
||||
|
||||
.tool-tab {
|
||||
position: relative;
|
||||
min-height: 74px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tool-tab em {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
bottom: 8px;
|
||||
color: rgba(0, 32, 91, 0.22);
|
||||
font-size: 1.25rem;
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.tool-tab.is-active {
|
||||
background: #fdf8ef;
|
||||
border-color: rgba(186, 12, 47, 0.28);
|
||||
color: var(--dbn-ink);
|
||||
}
|
||||
|
||||
.tool-tab.is-active::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
width: 4px;
|
||||
background: var(--dbn-red);
|
||||
}
|
||||
|
||||
.dashboard-shell .disclaimer {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.tool-dashboard-grid {
|
||||
max-width: 1500px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.dashboard-tool-card {
|
||||
position: relative;
|
||||
min-height: 250px;
|
||||
padding: 22px;
|
||||
border: 1px solid var(--dbn-line);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
color: var(--dbn-ink);
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dashboard-tool-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(186, 12, 47, 0.36);
|
||||
}
|
||||
|
||||
.dashboard-tool-card__icon {
|
||||
color: rgba(0, 32, 91, 0.14);
|
||||
font-size: 4.8rem;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dashboard-tool-card__badge {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 18px;
|
||||
color: var(--dbn-red);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dashboard-tool-card h2 {
|
||||
margin: 12px 0 8px;
|
||||
font-size: 1.45rem;
|
||||
}
|
||||
|
||||
.dashboard-tool-card p {
|
||||
color: #514b43;
|
||||
line-height: 1.48;
|
||||
}
|
||||
|
||||
.dashboard-tool-card strong {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
color: var(--dbn-blue);
|
||||
}
|
||||
|
||||
.dbn-public-tools {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.showcase-header {
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border-bottom: 1px solid var(--dbn-line);
|
||||
}
|
||||
|
||||
.showcase-header-inner {
|
||||
align-items: flex-start;
|
||||
padding-top: 22px;
|
||||
padding-bottom: 22px;
|
||||
}
|
||||
|
||||
.showcase-header-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.showcase-title {
|
||||
max-width: 980px;
|
||||
color: var(--dbn-ink);
|
||||
font-size: clamp(2.4rem, 6vw, 6.4rem);
|
||||
line-height: 0.9;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.showcase-tagline {
|
||||
max-width: 780px;
|
||||
color: #403a33;
|
||||
font-size: clamp(1.05rem, 1.6vw, 1.32rem);
|
||||
}
|
||||
|
||||
.landing-hero {
|
||||
max-width: 1500px;
|
||||
margin: 18px auto;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 420px);
|
||||
gap: 14px;
|
||||
padding: clamp(22px, 4vw, 54px);
|
||||
border: 1px solid var(--dbn-line);
|
||||
border-radius: 8px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(0, 32, 91, 0.12), transparent 34%),
|
||||
linear-gradient(315deg, rgba(186, 12, 47, 0.16), transparent 40%),
|
||||
#fffaf2;
|
||||
}
|
||||
|
||||
.landing-hero h2 {
|
||||
max-width: 840px;
|
||||
margin: 0;
|
||||
font-size: clamp(2rem, 5vw, 5.3rem);
|
||||
line-height: 0.92;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.landing-hero p {
|
||||
max-width: 740px;
|
||||
color: #403a33;
|
||||
font-size: 1.08rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.landing-hero__stats {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.landing-hero__stats div {
|
||||
padding: 18px;
|
||||
border: 1px solid rgba(0, 32, 91, 0.16);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.landing-hero__stats strong {
|
||||
display: block;
|
||||
color: var(--dbn-blue);
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.landing-hero__stats span {
|
||||
color: #514b43;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.cap-grid--six {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.evidence-inner {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.evidence-inner article {
|
||||
padding: 20px;
|
||||
border-left: 4px solid rgba(186, 12, 47, 0.45);
|
||||
background: rgba(255, 255, 255, 0.58);
|
||||
}
|
||||
|
||||
.google-access-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 48px;
|
||||
margin: 18px 0;
|
||||
border-radius: 8px;
|
||||
background: var(--dbn-blue);
|
||||
color: #fff;
|
||||
font-weight: 900;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.fallback-login {
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--dbn-line);
|
||||
}
|
||||
|
||||
.fallback-login summary {
|
||||
cursor: pointer;
|
||||
color: #655d53;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.workspace {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.tool-rail {
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tool-tab {
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.manifesto,
|
||||
.landing-hero {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.manifesto-stats {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.tool-dashboard-grid,
|
||||
.cap-grid--six,
|
||||
.evidence-inner {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.topbar,
|
||||
.showcase-header-inner {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.topbar-actions,
|
||||
.showcase-header-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.manifesto-stats,
|
||||
.tool-dashboard-grid,
|
||||
.cap-grid--six,
|
||||
.evidence-inner {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-tool-card {
|
||||
min-height: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -3712,3 +4167,46 @@ a.dr-source-title-link:hover {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
/* v1 final cascade overrides */
|
||||
body {
|
||||
background:
|
||||
linear-gradient(90deg, rgba(0, 32, 91, 0.055) 1px, transparent 1px),
|
||||
linear-gradient(180deg, rgba(186, 12, 47, 0.04), transparent 38rem),
|
||||
var(--dbn-paper);
|
||||
background-size: 44px 44px, auto, auto;
|
||||
color: var(--dbn-ink);
|
||||
}
|
||||
|
||||
.app-shell { padding: 18px clamp(14px, 2vw, 28px) 34px; }
|
||||
.topbar { align-items: flex-start; border-bottom: 2px solid rgba(22, 19, 15, 0.14); padding-bottom: 16px; }
|
||||
.topbar h1 { color: var(--dbn-ink); font-size: clamp(1.8rem, 2.8vw, 3.2rem); line-height: .95; letter-spacing: 0; }
|
||||
.workspace { grid-template-columns: 204px minmax(0, 1fr) 370px; }
|
||||
.tool-rail, .tool-panel, .reasoning-panel, .gate-panel, .cap-card, .dashboard-tool-card {
|
||||
border-color: rgba(22, 19, 15, 0.15);
|
||||
box-shadow: 0 18px 45px rgba(22, 19, 15, 0.08);
|
||||
}
|
||||
.tool-tab { position: relative; min-height: 74px; overflow: hidden; }
|
||||
.tool-tab em { position: absolute; right: 10px; bottom: 8px; color: rgba(0, 32, 91, .22); font-size: 1.25rem; font-style: normal; font-weight: 900; }
|
||||
.tool-tab.is-active { background: #fdf8ef; border-color: rgba(186, 12, 47, .28); color: var(--dbn-ink); }
|
||||
.tool-tab.is-active::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 4px; background: var(--dbn-red); }
|
||||
.showcase-title { max-width: 980px; color: var(--dbn-ink); font-size: clamp(2.4rem, 6vw, 6.4rem); line-height: .9; letter-spacing: 0; }
|
||||
.showcase-tagline { max-width: 780px; color: #403a33; font-size: clamp(1.05rem, 1.6vw, 1.32rem); }
|
||||
.cap-grid--six { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.evidence-inner { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.google-access-button { display: flex; align-items: center; justify-content: center; min-height: 48px; margin: 18px 0; border-radius: 8px; background: var(--dbn-blue); color: #fff; font-weight: 900; text-decoration: none; }
|
||||
.fallback-login { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--dbn-line); }
|
||||
.fallback-login summary { cursor: pointer; color: #655d53; font-weight: 800; }
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.workspace { grid-template-columns: 1fr; }
|
||||
.tool-rail { flex-direction: row; overflow-x: auto; }
|
||||
.tool-tab { min-width: 170px; }
|
||||
.tool-dashboard-grid, .cap-grid--six, .evidence-inner { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.topbar, .showcase-header-inner { flex-direction: column; }
|
||||
.topbar-actions, .showcase-header-actions { width: 100%; justify-content: flex-start; }
|
||||
.manifesto-stats, .tool-dashboard-grid, .cap-grid--six, .evidence-inner { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
'use strict';
|
||||
|
||||
const els = {};
|
||||
let lang = 'en';
|
||||
let lang = window.DBN_TOOLS_LANG || localStorage.getItem('dbn-ui-lang') || 'en';
|
||||
let uploadFiles = [];
|
||||
let lastResult = null;
|
||||
let branchContext = null;
|
||||
@@ -121,10 +121,12 @@
|
||||
|
||||
function bindLang() {
|
||||
els.langButtons.forEach((b) => {
|
||||
b.classList.toggle('is-active', b.dataset.lang === lang);
|
||||
b.addEventListener('click', () => {
|
||||
els.langButtons.forEach((x) => x.classList.remove('is-active'));
|
||||
b.classList.add('is-active');
|
||||
lang = b.dataset.lang || 'en';
|
||||
localStorage.setItem('dbn-ui-lang', lang);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
'use strict';
|
||||
|
||||
const els = {};
|
||||
let lang = 'en';
|
||||
let lang = window.DBN_TOOLS_LANG || localStorage.getItem('dbn-ui-lang') || 'en';
|
||||
let uploadFiles = [];
|
||||
let lastResult = null;
|
||||
let branchContext = null;
|
||||
@@ -148,10 +148,12 @@
|
||||
|
||||
function bindLang() {
|
||||
els.langButtons.forEach((b) => {
|
||||
b.classList.toggle('is-active', b.dataset.lang === lang);
|
||||
b.addEventListener('click', () => {
|
||||
els.langButtons.forEach((x) => x.classList.remove('is-active'));
|
||||
b.classList.add('is-active');
|
||||
lang = b.dataset.lang || 'en';
|
||||
localStorage.setItem('dbn-ui-lang', lang);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
'use strict';
|
||||
|
||||
const els = {};
|
||||
let lang = 'en';
|
||||
let lang = window.DBN_TOOLS_LANG || localStorage.getItem('dbn-ui-lang') || 'en';
|
||||
let uploadFiles = [];
|
||||
let lastResult = null;
|
||||
let branchContext = null;
|
||||
@@ -102,10 +102,12 @@
|
||||
|
||||
function bindLang() {
|
||||
els.langButtons.forEach((b) => {
|
||||
b.classList.toggle('is-active', b.dataset.lang === lang);
|
||||
b.addEventListener('click', () => {
|
||||
els.langButtons.forEach((x) => x.classList.remove('is-active'));
|
||||
b.classList.add('is-active');
|
||||
lang = b.dataset.lang || 'en';
|
||||
localStorage.setItem('dbn-ui-lang', lang);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+36
-9
@@ -391,7 +391,14 @@ const VOCAB_PRESETS = {
|
||||
custom: '',
|
||||
};
|
||||
|
||||
let uiLang = localStorage.getItem('dbn-ui-lang') || 'en';
|
||||
let uiLang = window.DBN_TOOLS_LANG || localStorage.getItem('dbn-ui-lang') || 'en';
|
||||
|
||||
function syncOutputLanguage(lang) {
|
||||
const normalized = ['en', 'no', 'uk', 'pl'].includes(lang) ? lang : 'en';
|
||||
document.querySelectorAll('input[name="language"]').forEach((input) => {
|
||||
if (input.value === normalized) input.checked = true;
|
||||
});
|
||||
}
|
||||
|
||||
const TRANSCRIBE_I18N = {
|
||||
en: {
|
||||
@@ -670,6 +677,7 @@ function currentUiT(key, ...args) {
|
||||
function applyTranscribeI18n(lang) {
|
||||
uiLang = lang;
|
||||
localStorage.setItem('dbn-ui-lang', lang);
|
||||
syncOutputLanguage(lang);
|
||||
document.querySelectorAll('[data-i18n]').forEach((el) => {
|
||||
const text = currentUiT(el.dataset.i18n);
|
||||
if (text != null) el.textContent = text;
|
||||
@@ -695,6 +703,7 @@ function currentRedactT(key) {
|
||||
function applyRedactI18n(lang) {
|
||||
uiLang = lang;
|
||||
localStorage.setItem('dbn-ui-lang', lang);
|
||||
syncOutputLanguage(lang);
|
||||
document.querySelectorAll('[data-i18n]').forEach((el) => {
|
||||
const text = currentRedactT(el.dataset.i18n);
|
||||
if (text != null) el.textContent = text;
|
||||
@@ -750,6 +759,7 @@ function currentTimelineT(key) {
|
||||
function applyTimelineI18n(lang) {
|
||||
uiLang = lang;
|
||||
localStorage.setItem('dbn-ui-lang', lang);
|
||||
syncOutputLanguage(lang);
|
||||
document.querySelectorAll('[data-i18n]').forEach((el) => {
|
||||
const text = currentTimelineT(el.dataset.i18n);
|
||||
if (text != null) el.textContent = text;
|
||||
@@ -952,7 +962,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
els.form?.addEventListener('submit', runTool);
|
||||
els.passcodeForm?.addEventListener('submit', submitPasscode);
|
||||
els.healthButton.addEventListener('click', checkHealth);
|
||||
els.healthButton?.addEventListener('click', checkHealth);
|
||||
setupUpload();
|
||||
setupAliases();
|
||||
setupAudio();
|
||||
@@ -968,7 +978,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (document.getElementById('uiLangSwitcher')) {
|
||||
applyTranscribeI18n(uiLang);
|
||||
}
|
||||
els.results.addEventListener('click', (e) => {
|
||||
els.results?.addEventListener('click', (e) => {
|
||||
if (e.target.closest('#exportCsvBtn')) exportTimelineCSV(lastTimelineEvents);
|
||||
if (e.target.closest('#dlTxt')) downloadTranscriptTxt();
|
||||
if (e.target.closest('#dlSrt')) downloadTranscriptSrt();
|
||||
@@ -978,7 +988,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (e.target.closest('#rdlDocx')) downloadRedactedDocx();
|
||||
});
|
||||
const activeTool = document.body.dataset.activeTool || state.activeTool;
|
||||
setTool(activeTool);
|
||||
if (els.form && tools[activeTool]) {
|
||||
setTool(activeTool);
|
||||
}
|
||||
|
||||
if (state.authenticated) {
|
||||
checkHealth();
|
||||
@@ -990,18 +1002,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
function setTool(toolName) {
|
||||
state.activeTool = toolName;
|
||||
const tool = tools[toolName];
|
||||
if (!tool || !els.toolKind || !els.input) return;
|
||||
const serverRenderedShell = els.tabs.some((tab) => tab.tagName === 'A');
|
||||
els.tabs.forEach((button) => {
|
||||
const active = button.dataset.tool === toolName;
|
||||
button.classList.toggle('is-active', active);
|
||||
button.setAttribute('aria-pressed', String(active));
|
||||
});
|
||||
|
||||
els.toolKind.textContent = tool.kind;
|
||||
els.toolTitle.textContent = tool.title;
|
||||
els.toolBadge.textContent = tool.badge;
|
||||
els.inputLabel.textContent = tool.label;
|
||||
if (!serverRenderedShell) {
|
||||
els.toolKind.textContent = tool.kind;
|
||||
els.toolTitle.textContent = tool.title;
|
||||
els.toolBadge.textContent = tool.badge;
|
||||
els.inputLabel.textContent = tool.label;
|
||||
}
|
||||
els.input.value = '';
|
||||
els.input.placeholder = tool.placeholder;
|
||||
if (!serverRenderedShell) {
|
||||
els.input.placeholder = tool.placeholder;
|
||||
}
|
||||
els.languageControl.classList.toggle('is-hidden', !tool.usesLanguage);
|
||||
els.redactionControl.classList.toggle('is-hidden', toolName !== 'redact');
|
||||
els.uploadZone.classList.toggle('is-hidden', toolName !== 'redact' && toolName !== 'timeline');
|
||||
@@ -1031,6 +1049,12 @@ async function submitPasscode(event) {
|
||||
throw new Error(data.error?.message || 'Credentials were not accepted.');
|
||||
}
|
||||
state.authenticated = true;
|
||||
if (!els.app) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const dest = params.get('return') || '/';
|
||||
window.location.href = dest.startsWith('/') && !dest.startsWith('//') ? dest : '/';
|
||||
return;
|
||||
}
|
||||
els.gate.classList.add('is-hidden');
|
||||
els.app.classList.remove('is-hidden');
|
||||
els.loginPassword.value = '';
|
||||
@@ -1116,6 +1140,7 @@ function resetUpload() {
|
||||
}
|
||||
|
||||
function setupUpload() {
|
||||
if (!els.uploadZone || !els.uploadInput) return;
|
||||
els.uploadZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
els.uploadZone.classList.add('is-drag-over');
|
||||
@@ -1217,6 +1242,7 @@ async function handleFiles(fileList) {
|
||||
}
|
||||
|
||||
function setupAliases() {
|
||||
if (!els.addAliasRow || !els.aliasRows) return;
|
||||
els.addAliasRow.addEventListener('click', () => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'alias-row';
|
||||
@@ -1287,6 +1313,7 @@ async function postJson(url, payload) {
|
||||
|
||||
function setBusy(isBusy) {
|
||||
const button = document.querySelector('#runButton');
|
||||
if (!button) return;
|
||||
button.disabled = isBusy;
|
||||
if (state.activeTool === 'transcribe') {
|
||||
button.textContent = isBusy ? currentUiT('running') : currentUiT('run');
|
||||
|
||||
+8
-1
@@ -12,6 +12,8 @@ require_once __DIR__ . '/includes/layout.php';
|
||||
<div class="lang-switcher" id="bvjLangSwitcher" role="group" aria-label="UI language">
|
||||
<button type="button" class="lang-btn is-active" data-lang="en">🇬🇧 EN</button>
|
||||
<button type="button" class="lang-btn" data-lang="no">🇳🇴 NO</button>
|
||||
<button type="button" class="lang-btn" data-lang="uk">🇺🇦 UK</button>
|
||||
<button type="button" class="lang-btn" data-lang="pl">🇵🇱 PL</button>
|
||||
</div>
|
||||
|
||||
<!-- Role selector -->
|
||||
@@ -199,7 +201,12 @@ require_once __DIR__ . '/includes/layout.php';
|
||||
</div>
|
||||
|
||||
<!-- Hidden stubs so tools.js element refs don't crash on this page -->
|
||||
<div class="is-hidden" id="languageControl" aria-hidden="true"><input type="radio" name="language" value="en" checked></div>
|
||||
<div class="is-hidden" id="languageControl" aria-hidden="true">
|
||||
<input type="radio" name="language" value="en" checked>
|
||||
<input type="radio" name="language" value="no">
|
||||
<input type="radio" name="language" value="uk">
|
||||
<input type="radio" name="language" value="pl">
|
||||
</div>
|
||||
<div class="is-hidden" id="redactionControl" aria-hidden="true"></div>
|
||||
<div class="is-hidden" id="audioZone" aria-hidden="true">
|
||||
<input type="file" id="audioInput" style="display:none">
|
||||
|
||||
@@ -74,6 +74,8 @@ require_once __DIR__ . '/includes/layout.php';
|
||||
<div class="lang-pills" role="group" aria-label="Language">
|
||||
<button class="mode-pill is-active" data-lang="en" type="button">EN</button>
|
||||
<button class="mode-pill" data-lang="no" type="button">NO</button>
|
||||
<button class="mode-pill" data-lang="uk" type="button">UK</button>
|
||||
<button class="mode-pill" data-lang="pl" type="button">PL</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+8
-1
@@ -12,6 +12,8 @@ require_once __DIR__ . '/includes/layout.php';
|
||||
<div class="lang-switcher" id="drLangSwitcher" role="group" aria-label="UI language">
|
||||
<button type="button" class="lang-btn is-active" data-lang="en">🇬🇧 EN</button>
|
||||
<button type="button" class="lang-btn" data-lang="no">🇳🇴 NO</button>
|
||||
<button type="button" class="lang-btn" data-lang="uk">🇺🇦 UK</button>
|
||||
<button type="button" class="lang-btn" data-lang="pl">🇵🇱 PL</button>
|
||||
</div>
|
||||
|
||||
<div class="control-row" id="drEngineControl">
|
||||
@@ -177,7 +179,12 @@ require_once __DIR__ . '/includes/layout.php';
|
||||
</div>
|
||||
|
||||
<!-- Hidden stubs so tools.js element refs don't crash on this page -->
|
||||
<div class="is-hidden" id="languageControl" aria-hidden="true"><input type="radio" name="language" value="en" checked></div>
|
||||
<div class="is-hidden" id="languageControl" aria-hidden="true">
|
||||
<input type="radio" name="language" value="en" checked>
|
||||
<input type="radio" name="language" value="no">
|
||||
<input type="radio" name="language" value="uk">
|
||||
<input type="radio" name="language" value="pl">
|
||||
</div>
|
||||
<div class="is-hidden" id="redactionControl" aria-hidden="true"></div>
|
||||
<div class="is-hidden" id="audioZone" aria-hidden="true">
|
||||
<input type="file" id="audioInput" style="display:none">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
+6
-1
@@ -86,7 +86,12 @@ require_once __DIR__ . '/includes/layout.php';
|
||||
</section>
|
||||
|
||||
<!-- Hidden stubs so tools.js element refs don't crash on this page -->
|
||||
<div class="is-hidden" id="languageControl" aria-hidden="true"><input type="radio" name="language" value="en" checked></div>
|
||||
<div class="is-hidden" id="languageControl" aria-hidden="true">
|
||||
<input type="radio" name="language" value="en" checked>
|
||||
<input type="radio" name="language" value="no">
|
||||
<input type="radio" name="language" value="uk">
|
||||
<input type="radio" name="language" value="pl">
|
||||
</div>
|
||||
<div class="is-hidden" id="redactionControl" aria-hidden="true"></div>
|
||||
<div class="is-hidden" id="audioZone" aria-hidden="true">
|
||||
<input type="file" id="audioInput" style="display:none">
|
||||
|
||||
+6
-1
@@ -112,7 +112,12 @@ $azureConfigured = !empty(dbnToolsEnv('DBN_AZURE_SPEECH_KEY'));
|
||||
</details>
|
||||
|
||||
<!-- Hidden stubs so tools.js refs don't crash on this page -->
|
||||
<div class="is-hidden" id="languageControl" aria-hidden="true"><input type="radio" name="language" value="en" checked></div>
|
||||
<div class="is-hidden" id="languageControl" aria-hidden="true">
|
||||
<input type="radio" name="language" value="en" checked>
|
||||
<input type="radio" name="language" value="no">
|
||||
<input type="radio" name="language" value="uk">
|
||||
<input type="radio" name="language" value="pl">
|
||||
</div>
|
||||
<div class="is-hidden" id="redactionControl" aria-hidden="true"></div>
|
||||
<div class="is-hidden" id="uploadZone" aria-hidden="true">
|
||||
<input type="file" id="uploadInput" style="display:none">
|
||||
|
||||
Reference in New Issue
Block a user