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>
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
/* 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,
|
||||
});
|
||||
Reference in New Issue
Block a user