Bot-Derpy's picture
PrimaryDraft: SEBI TechSprint prototype (27 deterministic checks, 40 real BSE SME issuers)
baf8a7b verified
Raw History Blame Contribute Delete
4.71 kB
import { useEffect, useState } from "react";
import {
Buildings, Stack, FileText, ShieldCheck, SealCheck, Check,
} from "@phosphor-icons/react";
import { api, type RunSummary } from "./lib/api";
import { Intake } from "./components/Intake";
import { CinIntake } from "./components/CinIntake";
import { Ledger } from "./components/Ledger";
import { Draft } from "./components/Draft";
import { Board } from "./components/Board";
import { Handoff } from "./components/Handoff";
const STEPS = [
{ id: "intake", label: "Issuer", icon: Buildings },
{ id: "ledger", label: "Disclosure ledger", icon: Stack },
{ id: "draft", label: "Draft", icon: FileText },
{ id: "board", label: "Consistency check", icon: ShieldCheck },
{ id: "handoff", label: "Handoff", icon: SealCheck },
] as const;
type StepId = (typeof STEPS)[number]["id"];
function IntakeTabs({ onStarted }: { onStarted: (r: RunSummary) => void }) {
const [mode, setMode] = useState<"listed" | "cin">("listed");
return (
<>
<div className="page-head">
<h1 className="page">Select an issuer</h1>
<p className="lede">
A listed company can be found by search and read from its own filing. A company that
has not listed yet has no exchange history at all, so it is identified by its
Corporate Identity Number and its records are the ones it supplies.
</p>
<div className="filters" style={{ marginTop: 12 }}>
<button className="filter" aria-pressed={mode === "listed"}
onClick={() => setMode("listed")}>listed on BSE</button>
<button className="filter" aria-pressed={mode === "cin"}
onClick={() => setMode("cin")}>pre-IPO, by CIN</button>
</div>
</div>
{mode === "listed" ? <Intake onStarted={onStarted} embedded />
: <CinIntake onStarted={onStarted} />}
</>
);
}
export function App() {
const [step, setStep] = useState<StepId>("intake");
const [run, setRun] = useState<RunSummary | null>(null);
const [health, setHealth] = useState<any>(null);
useEffect(() => { api.health().then(setHealth).catch(() => setHealth(null)); }, []);
const reached = (id: StepId) => {
if (!run) return id === "intake";
return STEPS.findIndex((s) => s.id === id) <= STEPS.findIndex((s) => s.id === step);
};
return (
<div className="app">
<header className="topbar">
<span className="brand">Primary<b>Draft</b></span>
<span className="brand-sub">the IPO offer document that checks itself</span>
<div className="topbar-right">
{health && (
<>
<span className="badge badge-accent">{health.rule_count} deterministic checks</span>
<span className="badge badge-quiet">{health.companies} real BSE SME issuers</span>
<span className="badge badge-quiet">
narrative AI {health.llm?.enabled ? "on" : "off"}
</span>
</>
)}
</div>
</header>
<nav className="steps" aria-label="Workflow">
{STEPS.map((s, i) => {
const Icon = s.icon;
const isCurrent = s.id === step;
const done = run !== null && STEPS.findIndex((x) => x.id === step) > i;
return (
<button
key={s.id}
className={`step${done ? " step-done" : ""}`}
aria-current={isCurrent ? "step" : undefined}
disabled={!reached(s.id)}
onClick={() => reached(s.id) && setStep(s.id)}
>
<span className="step-i">{done ? <Check size={15} weight="bold" /> : <Icon size={15} />}</span>
{s.label}
</button>
);
})}
{run && (
<span style={{
marginLeft: "auto", alignSelf: "center", paddingRight: 8,
fontSize: 12, color: "var(--text-2)",
}}>
{run.company_name} <span className="mono" style={{ fontSize: 11 }}>{run.cin}</span>
</span>
)}
</nav>
<main className="main">
{step === "intake" && (
<IntakeTabs onStarted={(r) => { setRun(r); setStep("ledger"); }} />
)}
{step === "ledger" && run && (
<Ledger run={run} onNext={() => setStep("draft")} />
)}
{step === "draft" && run && (
<Draft run={run} onRunUpdate={setRun} onNext={() => setStep("board")} />
)}
{step === "board" && run && (
<Board run={run} onRunUpdate={setRun} onNext={() => setStep("handoff")} />
)}
{step === "handoff" && run && <Handoff runId={run.id} />}
</main>
</div>
);
}