Bot-Derpy's picture
PrimaryDraft: SEBI TechSprint prototype (27 deterministic checks, 40 real BSE SME issuers)
baf8a7b verified
Raw History Blame Contribute Delete
9.76 kB
import { useEffect, useRef, useState } from "react";
import { Stack, ArrowRight, X } from "@phosphor-icons/react";
import { api, factValue, type Fact, type RunSummary } from "../lib/api";
import { Empty, Note, ProvenanceList, Spinner, StatusChip } from "./common";
const FILTERS = [
{ k: "", label: "all" },
{ k: "verified", label: "verified" },
{ k: "derived", label: "derived" },
{ k: "unverified", label: "unverified" },
{ k: "conflicted", label: "conflicted" },
{ k: "attested", label: "attested" },
{ k: "resolved", label: "resolved" },
] as const;
export function Ledger({ run, onNext }: { run: RunSummary; onNext: () => void }) {
const [facts, setFacts] = useState<Fact[]>([]);
const [total, setTotal] = useState(run.fact_count);
const [streaming, setStreaming] = useState(true);
const [filter, setFilter] = useState<string>("");
const [open, setOpen] = useState<Fact | null>(null);
const seen = useRef(new Set<string>());
useEffect(() => {
// Stream the ledger so the reviewer watches it being assembled from the filing.
const es = new EventSource(`/api/runs/${run.id}/ledger/stream?delay_ms=14`);
es.addEventListener("start", (e) => setTotal(JSON.parse((e as MessageEvent).data).total));
es.addEventListener("fact", (e) => {
const { fact } = JSON.parse((e as MessageEvent).data);
if (seen.current.has(fact.id)) return;
seen.current.add(fact.id);
setFacts((prev) => [...prev, fact]);
});
es.addEventListener("done", () => { setStreaming(false); es.close(); });
es.onerror = () => { setStreaming(false); es.close(); };
return () => es.close();
}, [run.id]);
const shown = filter ? facts.filter((f) => f.status === filter) : facts;
const counts = facts.reduce<Record<string, number>>((a, f) => {
a[f.status] = (a[f.status] ?? 0) + 1;
return a;
}, {});
return (
<>
<div className="page-head">
<h1 className="page">Disclosure ledger</h1>
<p className="lede">
Each row is one atomic disclosure value read from the issuer's own filing, bound to the
page it came from. Status is earned, never assumed: a figure is verified only when two
independent sections agree or an accounting identity supports it. Everything else
abstains and says what to confirm.
</p>
</div>
<div className="stats" style={{ marginBottom: 14 }}>
<div className="stat">
<div className="stat-v">{facts.length}</div>
<div className="stat-l">facts extracted{streaming ? " so far" : ""}</div>
</div>
<div className="stat">
<div className="stat-v ok">{counts.verified ?? 0}</div>
<div className="stat-l">verified by corroboration or identity</div>
</div>
<div className="stat">
<div className="stat-v">{counts.derived ?? 0}</div>
<div className="stat-l">derived, with lineage recorded</div>
</div>
<div className="stat">
<div className="stat-v warn">{counts.unverified ?? 0}</div>
<div className="stat-l">abstained, awaiting confirmation</div>
</div>
<div className="stat">
<div className="stat-v stop">{counts.conflicted ?? 0}</div>
<div className="stat-l">sources disagree</div>
</div>
</div>
{streaming && (
<div className="bar" style={{ marginBottom: 14 }}>
<i style={{ width: `${total ? (facts.length / total) * 100 : 0}%` }} />
</div>
)}
<section className="panel">
<div className="panel-h">
<Stack size={16} />
<h2>Facts</h2>
<span className="count">{shown.length} shown</span>
<div className="filters" style={{ marginLeft: "auto" }}>
{FILTERS.map((f) => (
<button
key={f.k}
className="filter"
aria-pressed={filter === f.k}
onClick={() => setFilter(f.k)}
>
{f.label}
{f.k && counts[f.k] ? ` ${counts[f.k]}` : ""}
</button>
))}
</div>
</div>
<div style={{ maxHeight: "58vh", overflowY: "auto" }}>
{shown.length === 0 && !streaming ? (
<Empty title={filter ? `No ${filter} facts` : "The ledger is empty"}>
{filter
? "No figure in this filing earned that status."
: "No disclosure value could be read from the source records."}
</Empty>
) : (
<table className="tbl">
<thead>
<tr>
<th style={{ width: "34%" }}>Disclosure</th>
<th className="r" style={{ width: "16%" }}>Value</th>
<th style={{ width: "12%" }}>Status</th>
<th>Source</th>
</tr>
</thead>
<tbody>
{shown.map((f, i) => (
<tr
key={f.id}
className={i >= shown.length - 3 && streaming ? "arrived" : ""}
onClick={() => setOpen(f)}
style={{ cursor: "pointer" }}
>
<td>
<div className="f-label">{f.label}</div>
<div className="f-id">{f.id}</div>
{f.note && <div className="f-note">{f.note}</div>}
</td>
<td className="r num" style={{ fontWeight: 500 }}>{factValue(f)}</td>
<td><StatusChip status={f.status} /></td>
<td>
<div className="f-src">{f.provenance[0]?.locator ?? "n/a"}</div>
{f.provenance.length > 1 && (
<div className="tag tag-ok" style={{ marginTop: 3, display: "inline-block" }}>
{f.provenance.length} sources agree
</div>
)}
{f.identities.length > 0 && (
<div className="f-id" style={{ marginTop: 3 }}>{f.identities[0]}</div>
)}
</td>
</tr>
))}
</tbody>
</table>
)}
</div>
</section>
<div style={{ display: "flex", gap: 10, marginTop: 16, alignItems: "center" }}>
<button className="btn btn-primary" onClick={onNext} disabled={streaming}>
{streaming ? <><Spinner /> Reading filing</> : <>Draft the offer document <ArrowRight size={15} /></>}
</button>
{!streaming && (counts.unverified ?? 0) > 0 && (
<span style={{ fontSize: 12.5, color: "var(--text-2)" }}>
{counts.unverified} figures abstained. The engine will list each one for attestation.
</span>
)}
</div>
{open && <FactDrawer fact={open} onClose={() => setOpen(null)} />}
</>
);
}
function FactDrawer({ fact, onClose }: { fact: Fact; onClose: () => void }) {
useEffect(() => {
const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose();
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [onClose]);
return (
<div
style={{
position: "fixed", inset: 0, background: "rgba(11,27,43,0.4)",
zIndex: 50, display: "grid", placeItems: "center", padding: 20,
}}
onClick={onClose}
>
<div
className="panel"
style={{ maxWidth: 620, width: "100%", maxHeight: "82vh", overflowY: "auto" }}
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-label={fact.label}
>
<div className="panel-h">
<h2>{fact.label}</h2>
<button className="btn btn-ghost btn-sm" style={{ marginLeft: "auto" }} onClick={onClose}>
<X size={13} /> Close
</button>
</div>
<div className="panel-b" style={{ display: "grid", gap: 14 }}>
<div style={{ display: "flex", gap: 12, alignItems: "baseline" }}>
<span className="num" style={{ fontSize: 22, fontWeight: 600 }}>{factValue(fact)}</span>
<StatusChip status={fact.status} />
<span className="f-id">{fact.id}</span>
</div>
{fact.note && <Note tone="warn">{fact.note}</Note>}
{fact.identities.length > 0 && (
<div>
<div className="f-src" style={{ marginBottom: 5 }}>Supported by</div>
{fact.identities.map((i) => (
<div key={i} className="mono" style={{ fontSize: 11.5, color: "var(--ok)" }}>{i}</div>
))}
</div>
)}
{fact.derived_from.length > 0 && (
<div>
<div className="f-src" style={{ marginBottom: 5 }}>Derived from</div>
{fact.derived_from.map((d) => (
<div key={d} className="f-id">{d}</div>
))}
</div>
)}
<div>
<div className="f-src" style={{ marginBottom: 6 }}>
Provenance ({fact.provenance.length})
</div>
<ProvenanceList provenance={fact.provenance} />
</div>
{fact.section_refs.length > 0 && (
<div>
<div className="f-src" style={{ marginBottom: 5 }}>Used in sections</div>
<div style={{ display: "flex", gap: 5, flexWrap: "wrap" }}>
{fact.section_refs.map((s) => (
<span key={s} className="tag">{s.replace(/_/g, " ")}</span>
))}
</div>
</div>
)}
</div>
</div>
</div>
);
}