08d1e3cee3
Removes user-facing engine/model/key/beam controls. The server now picks the best available engine automatically: 1. Microsoft Azure Speech — short clips (≤1MB, no diarization, audio/*) 2. Google Cloud Speech v2 — long audio, diarization, all languages 3. OpenAI Whisper GPU — local fallback Results display which provider was used (e.g. "Transcribed with Google Cloud Speech") via transcript-engine-badge and traceMeta. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
213 lines
9.0 KiB
React
213 lines
9.0 KiB
React
/* global React */
|
||
const { useState, useEffect, useRef, useMemo } = React;
|
||
|
||
/* ─── Stamp ─────────────────────────────────────────────────────── */
|
||
function Stamp({ children, ink }) {
|
||
return <span className={"stamp" + (ink ? " is-ink" : "")}>{children}</span>;
|
||
}
|
||
|
||
/* ─── Art-chip (instrument badge) ───────────────────────────────── */
|
||
function ArtChip({ code, tone = "red", title }) {
|
||
const cls = "art-chip" + (tone === "blue" ? " is-blue" : tone === "amber" ? " is-amber" : "");
|
||
return <span className={cls} title={title}>{code}</span>;
|
||
}
|
||
|
||
/* ─── Redact span ───────────────────────────────────────────────── */
|
||
function Redact({ children, len }) {
|
||
// render fixed-width blocks if `len` is passed; otherwise show children styled as bar
|
||
const text = len ? "█".repeat(len) : (children || "████");
|
||
return <span className="redact" title="Redacted under GDPR / Personopplysningsloven">{text}</span>;
|
||
}
|
||
|
||
/* ─── Cite chip (richer) ─────────────────────────────────────────── */
|
||
function CiteChip({ n, active, onClick }) {
|
||
return (
|
||
<span className="cite-chip" role="button" onClick={onClick}
|
||
style={active ? { background: "var(--dbn-coral)", color: "#fff" } : {}}>
|
||
{n}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
/* ─── Topbar with case-number ───────────────────────────────────── */
|
||
function Topbar({ caseNo, status }) {
|
||
return (
|
||
<header className="topbar">
|
||
<div>
|
||
<p className="eyebrow">Do Better Norge · tools.dobetternorge.no</p>
|
||
<h1>Legal Tools <span style={{color: "var(--dbn-coral)"}}>·</span> Case Workbench</h1>
|
||
<div className="case-no">
|
||
<span className="pulse"></span>
|
||
<span>{caseNo}</span>
|
||
<span style={{opacity: 0.5}}>·</span>
|
||
<span>session in memory · nothing stored</span>
|
||
</div>
|
||
</div>
|
||
<div className="topbar-actions" style={{gap: 10}}>
|
||
<Stamp>ECHR Art. 8 monitoring</Stamp>
|
||
<span className="status-pill" style={{background: "var(--dbn-coral-soft)", color: "var(--dbn-coral)"}}>{status}</span>
|
||
<button className="secondary-button" type="button">Sign out</button>
|
||
</div>
|
||
</header>
|
||
);
|
||
}
|
||
|
||
/* ─── Manifesto strip — replaces vanilla disclaimer with a headline + stats */
|
||
function Manifesto({ headline, stats, intensity }) {
|
||
return (
|
||
<section className="manifesto" role="banner" data-screen-label="00 Manifesto Strip">
|
||
<div className="manifesto-copy">
|
||
<p className="manifesto-eyebrow">{headline.eyebrow}</p>
|
||
<h2 className="manifesto-title">{headline.title}</h2>
|
||
<p className="manifesto-sub">{headline.sub}</p>
|
||
</div>
|
||
<div className="manifesto-stats" aria-label="Headline statistics">
|
||
<div className="manifesto-stat">
|
||
<div className="manifesto-stat-num">{stats.echrViolations}</div>
|
||
<div className="manifesto-stat-label">ECHR violations since 2015</div>
|
||
</div>
|
||
<div className="manifesto-stat">
|
||
<div className="manifesto-stat-num">{stats.echrLossRate}%</div>
|
||
<div className="manifesto-stat-label">cases lost 2017–22</div>
|
||
</div>
|
||
<div className="manifesto-stat">
|
||
<div className="manifesto-stat-num is-blue">{stats.tribunalDecisions.toLocaleString()}</div>
|
||
<div className="manifesto-stat-label">tribunal decisions analysed</div>
|
||
</div>
|
||
<div className="manifesto-stat">
|
||
<div className="manifesto-stat-num">{stats.pendingStrasbourg}+</div>
|
||
<div className="manifesto-stat-label">pending Strasbourg</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
/* ─── Disclaimer (kept, but reskinned amber) ────────────────────── */
|
||
function Disclaimer() {
|
||
return (
|
||
<div className="disclaimer" role="note">
|
||
<strong style={{color:"var(--dbn-amber-ink)"}}>Notice.</strong>
|
||
Legal information and preparation support, not final legal advice. Pasted text is processed in memory by default
|
||
under <span className="mono">ECHR Art. 8</span> and Norwegian Personopplysningsloven principles.
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ─── Tool rail (themed) ────────────────────────────────────────── */
|
||
const TOOLS = [
|
||
{ slug: "ask", label: "Ask", sub: "source-grounded", tag: "Art. 8" },
|
||
{ slug: "search", label: "Search", sub: "lovdata + echr", tag: "1.7K" },
|
||
{ slug: "advocate", label: "Advocate", sub: "take a side", tag: "★" },
|
||
{ slug: "redact", label: "Redact", sub: "in-memory only", tag: "GDPR" },
|
||
{ slug: "timeline", label: "Timeline", sub: "events extracted", tag: "12 min" },
|
||
{ slug: "cases", label: "Casebook", sub: "23 violations", tag: "23" },
|
||
{ slug: "voices", label: "Voices", sub: "verified accounts", tag: "4" },
|
||
];
|
||
|
||
function ToolRail({ active, onSelect }) {
|
||
return (
|
||
<nav className="tool-rail" aria-label="Tools">
|
||
{TOOLS.map(t => (
|
||
<button
|
||
key={t.slug}
|
||
className={"tool-tab" + (t.slug === active ? " is-active" : "")}
|
||
onClick={() => onSelect(t.slug)}
|
||
type="button"
|
||
aria-current={t.slug === active ? "page" : undefined}
|
||
>
|
||
<span>{t.label}</span>
|
||
<small>{t.sub}</small>
|
||
<span className="tag">{t.tag}</span>
|
||
</button>
|
||
))}
|
||
</nav>
|
||
);
|
||
}
|
||
|
||
/* ─── Reasoning panel (Evidence Trail) ──────────────────────────── */
|
||
const TRACE_WAITING = [
|
||
{ label: "Waiting for query", detail: "Open a tool on the left. The evidence trail is built as the answer is generated.", status: "waiting" },
|
||
];
|
||
|
||
function TraceList({ steps }) {
|
||
return (
|
||
<ol className="trace-list">
|
||
{steps.map((s, i) => (
|
||
<li key={i}>
|
||
<span className={"trace-status " + (s.status || "")}></span>
|
||
<div>
|
||
<strong>{s.label}</strong>
|
||
<p>{s.detail}</p>
|
||
</div>
|
||
</li>
|
||
))}
|
||
</ol>
|
||
);
|
||
}
|
||
|
||
function ReasoningPanel({ steps, caseNo, instrument }) {
|
||
return (
|
||
<aside className="reasoning-panel" aria-labelledby="reasoningTitle">
|
||
<div className="reasoning-head">
|
||
<div>
|
||
<p className="file-label">File · Evidence trail</p>
|
||
<h2 id="reasoningTitle" style={{fontSize: "1.2rem", margin: 0}}>Reasoning</h2>
|
||
</div>
|
||
<Stamp ink>In memory</Stamp>
|
||
</div>
|
||
<TraceList steps={steps && steps.length ? steps : TRACE_WAITING} />
|
||
<div className="case-meta" aria-label="Session metadata">
|
||
<span>Case file</span><span>{caseNo}</span>
|
||
<span>Primary instrument</span><span>{instrument}</span>
|
||
<span>Reviewer</span><span>azure · mini · pass</span>
|
||
<span>Retention</span><span>in-memory · 0 b stored</span>
|
||
</div>
|
||
</aside>
|
||
);
|
||
}
|
||
|
||
/* ─── Footer strip ──────────────────────────────────────────────── */
|
||
function FootStrip() {
|
||
return (
|
||
<div className="foot-strip" role="contentinfo">
|
||
<span>Do Better Norge · independent advocacy · founded by affected parents</span>
|
||
<span>Powered by CaveauAI · Blue Note Logic · in-memory RAG · no telemetry</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/* ─── Run helper (timed promise sequence for fake tool runs) ────── */
|
||
function useRunSequence() {
|
||
const timers = useRef([]);
|
||
useEffect(() => () => timers.current.forEach(clearTimeout), []);
|
||
function clear() { timers.current.forEach(clearTimeout); timers.current = []; }
|
||
function run(sequence, onStep, onDone) {
|
||
clear();
|
||
let cum = 0;
|
||
sequence.forEach((entry, idx) => {
|
||
cum += entry.delay;
|
||
const t = setTimeout(() => {
|
||
onStep(entry.step, idx, idx === sequence.length - 1);
|
||
if (idx === sequence.length - 1) onDone && onDone();
|
||
}, cum);
|
||
timers.current.push(t);
|
||
});
|
||
}
|
||
return { run, clear };
|
||
}
|
||
|
||
/* ─── Headline labeller / instrument helper ─────────────────────── */
|
||
function lookupInstrument(code) {
|
||
const m = (window.DBN_DATA.instruments || []).find(i => i.code === code);
|
||
return m ? m.full : code;
|
||
}
|
||
|
||
/* ─── Expose ────────────────────────────────────────────────────── */
|
||
Object.assign(window, {
|
||
Stamp, ArtChip, Redact, CiteChip,
|
||
Topbar, Manifesto, Disclaimer, ToolRail, TOOLS,
|
||
ReasoningPanel, TraceList, TRACE_WAITING,
|
||
FootStrip, useRunSequence, lookupInstrument,
|
||
});
|