Files
dobetternorge-tools/Do Better Tools v1/shell.jsx
T
daveadmin 08d1e3cee3 feat: auto-select STT engine (Azure → Google Cloud → Whisper) and show provider in results
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>
2026-05-16 13:22:24 +02:00

213 lines
9.0 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 201722</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>&nbsp;
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,
});