Download app/frontend/src/components/Draft.tsx from ml-intern-explorers/primarydraft: direct link, hf CLI and curl.
- Browser
- Download file 8.74 kB
-
https://huggingface.co/spaces/ml-intern-explorers/primarydraft/resolve/main/app/frontend/src/components/Draft.tsx
- Command line
-
hf download hf://spaces/ml-intern-explorers/primarydraft/app/frontend/src/components/Draft.tsx
-
curl -L -o Draft.tsx https://huggingface.co/spaces/ml-intern-explorers/primarydraft/resolve/main/app/frontend/src/components/Draft.tsx
8.74 kB
| import { useEffect, useMemo, useRef, useState } from "react"; | |
| import { FileText, ArrowRight, Sliders } from "@phosphor-icons/react"; | |
| import { | |
| api, inr, ApiError, | |
| type DraftSection, type IssueTerms, type RunSummary, | |
| } from "../lib/api"; | |
| import { CitationPopover, Empty, ErrorNote, Note, Spinner, TableSkeleton } from "./common"; | |
| export function Draft({ | |
| run, onNext, onRunUpdate, | |
| }: { run: RunSummary; onNext: () => void; onRunUpdate: (r: RunSummary) => void }) { | |
| const [sections, setSections] = useState<DraftSection[] | null>(null); | |
| const [issue, setIssue] = useState<IssueTerms | null>(null); | |
| const [active, setActive] = useState<string>(""); | |
| const [error, setError] = useState<string | null>(null); | |
| const [busy, setBusy] = useState(false); | |
| const docRef = useRef<HTMLDivElement>(null); | |
| const load = async () => { | |
| setError(null); | |
| setSections(null); | |
| try { | |
| const [d, t] = await Promise.all([api.draft(run.id), api.getIssue(run.id)]); | |
| setSections(d.sections); | |
| setIssue(t); | |
| setActive(d.sections[0]?.id ?? ""); | |
| onRunUpdate(d); | |
| } catch (e) { | |
| setError(e instanceof ApiError ? e.message : String(e)); | |
| } | |
| }; | |
| useEffect(() => { load(); }, [run.id]); | |
| const current = useMemo( | |
| () => sections?.find((s) => s.id === active) ?? sections?.[0], | |
| [sections, active], | |
| ); | |
| const saveIssue = async (next: IssueTerms) => { | |
| setBusy(true); | |
| try { | |
| const saved = await api.putIssue(run.id, next); | |
| setIssue(saved); | |
| const d = await api.draft(run.id); | |
| setSections(d.sections); | |
| onRunUpdate(d); | |
| } catch (e) { | |
| setError(e instanceof ApiError ? e.message : String(e)); | |
| } finally { | |
| setBusy(false); | |
| } | |
| }; | |
| return ( | |
| <> | |
| <div className="page-head"> | |
| <h1 className="page">Offer document draft</h1> | |
| <p className="lede"> | |
| Drafted from the ledger alone. Templates carry tokens, never figures, so a number that | |
| is not bound to a sourced fact cannot reach the page. Hover any figure to see the filing | |
| and page it came from. Figures in grey dashes are your proposed issue terms, not facts | |
| about the issuer. | |
| </p> | |
| </div> | |
| {error && <div style={{ marginBottom: 14 }}><ErrorNote message={error} onRetry={load} /></div>} | |
| <div className="grid cols-sidebar"> | |
| <div className="doc-wrap"> | |
| <nav className="panel toc"> | |
| <div className="panel-h"><h2>Sections</h2></div> | |
| <div style={{ padding: "6px 0" }}> | |
| {!sections && <div style={{ padding: 12 }}><TableSkeleton rows={6} /></div>} | |
| {sections?.map((s) => ( | |
| <button | |
| key={s.id} | |
| aria-current={s.id === active} | |
| onClick={() => { setActive(s.id); docRef.current?.scrollTo({ top: 0 }); }} | |
| > | |
| <span className="toc-n">{s.number}</span> | |
| {s.title} | |
| </button> | |
| ))} | |
| </div> | |
| </nav> | |
| <div> | |
| {!sections ? ( | |
| <div className="panel"><div className="panel-b"><TableSkeleton rows={12} /></div></div> | |
| ) : !current ? ( | |
| <div className="panel"> | |
| <Empty title="No draft yet">The draft has not been generated.</Empty> | |
| </div> | |
| ) : ( | |
| <div className="doc" ref={docRef}> | |
| <h3>{current.number}. {current.title}</h3> | |
| <div className="clause">{current.icdr_clause}</div> | |
| <div dangerouslySetInnerHTML={{ __html: current.html }} /> | |
| <div style={{ | |
| marginTop: 22, paddingTop: 14, borderTop: "1px solid var(--line)", | |
| fontSize: 11.5, color: "var(--text-3)", fontFamily: "var(--mono)", | |
| }}> | |
| {current.word_count} words · {current.fact_ids.length} sourced figures · | |
| narrative from {current.narrative_source} | |
| </div> | |
| </div> | |
| )} | |
| <CitationPopover container={docRef} /> | |
| </div> | |
| </div> | |
| <aside style={{ display: "grid", gap: 14 }}> | |
| <section className="panel"> | |
| <div className="panel-h"> | |
| <Sliders size={16} /> | |
| <h2>Proposed issue terms</h2> | |
| </div> | |
| <div className="panel-b"> | |
| {!issue ? <TableSkeleton rows={5} /> : ( | |
| <IssueForm issue={issue} busy={busy} onSave={saveIssue} /> | |
| )} | |
| </div> | |
| </section> | |
| <Note tone="quiet"> | |
| These terms are your input as merchant banker. They are recorded separately from the | |
| disclosure ledger and never treated as facts about the issuer. The engine checks them | |
| against the issuer's real financials and against the SME limits. | |
| </Note> | |
| <button className="btn btn-primary" onClick={onNext} disabled={!sections} | |
| style={{ justifyContent: "center", padding: "11px 18px" }}> | |
| Run the consistency engine <ArrowRight size={15} /> | |
| </button> | |
| </aside> | |
| </div> | |
| </> | |
| ); | |
| } | |
| function IssueForm({ | |
| issue, busy, onSave, | |
| }: { issue: IssueTerms; busy: boolean; onSave: (t: IssueTerms) => void }) { | |
| const [d, setD] = useState(issue); | |
| useEffect(() => setD(issue), [issue]); | |
| const num = (k: keyof IssueTerms) => ({ | |
| value: String(d[k] ?? 0), | |
| onChange: (e: React.ChangeEvent<HTMLInputElement>) => | |
| setD({ ...d, [k]: Number(e.target.value) || 0 }), | |
| }); | |
| const gross = d.fresh_issue_shares * d.price_per_share; | |
| const net = gross - d.issue_expenses; | |
| const objTotal = d.objects.reduce((a, o) => a + o.amount, 0); | |
| const gap = objTotal - net; | |
| return ( | |
| <> | |
| <div className="field"> | |
| <label htmlFor="px">Issue price per share</label> | |
| <input id="px" className="ctl" type="number" {...num("price_per_share")} /> | |
| </div> | |
| <div className="field"> | |
| <label htmlFor="fresh">Fresh issue shares</label> | |
| <input id="fresh" className="ctl" type="number" {...num("fresh_issue_shares")} /> | |
| <div className="help">Gross proceeds {inr(gross)}</div> | |
| </div> | |
| <div className="field"> | |
| <label htmlFor="ofs">Offer for sale shares</label> | |
| <input id="ofs" className="ctl" type="number" {...num("ofs_shares")} /> | |
| <div className="help"> | |
| {inr(d.ofs_shares * d.price_per_share)}, capped at 20% of the issue for an SME | |
| </div> | |
| </div> | |
| <div className="field"> | |
| <label htmlFor="exp">Estimated issue expenses</label> | |
| <input id="exp" className="ctl" type="number" {...num("issue_expenses")} /> | |
| <div className="help">Net proceeds {inr(net)}</div> | |
| </div> | |
| <div style={{ borderTop: "1px solid var(--line)", paddingTop: 12, marginTop: 4 }}> | |
| <div style={{ fontSize: 12, fontWeight: 500, color: "var(--text-2)", marginBottom: 8 }}> | |
| Objects of the issue | |
| </div> | |
| {d.objects.map((o, i) => ( | |
| <div className="obj-row" key={o.id}> | |
| <span className="lbl">{o.label}</span> | |
| <input | |
| className="ctl" | |
| type="number" | |
| aria-label={o.label} | |
| value={String(o.amount)} | |
| onChange={(e) => { | |
| const objects = [...d.objects]; | |
| objects[i] = { ...o, amount: Number(e.target.value) || 0 }; | |
| setD({ ...d, objects }); | |
| }} | |
| /> | |
| </div> | |
| ))} | |
| <div style={{ | |
| display: "flex", justifyContent: "space-between", fontSize: 12, | |
| marginTop: 8, paddingTop: 8, borderTop: "1px solid var(--line-soft)", | |
| }}> | |
| <span style={{ color: "var(--text-2)" }}>Objects total</span> | |
| <span className="num" style={{ fontWeight: 600 }}>{inr(objTotal)}</span> | |
| </div> | |
| <div style={{ display: "flex", justifyContent: "space-between", fontSize: 12, marginTop: 4 }}> | |
| <span style={{ color: "var(--text-2)" }}>Net proceeds available</span> | |
| <span className="num" style={{ fontWeight: 600 }}>{inr(net)}</span> | |
| </div> | |
| {Math.abs(gap) > 1 && ( | |
| <div className="delta" style={{ marginTop: 8 }}> | |
| {gap > 0 ? "over by " : "short by "}{inr(Math.abs(gap))} | |
| </div> | |
| )} | |
| </div> | |
| <button | |
| className="btn btn-ghost btn-sm" | |
| style={{ width: "100%", justifyContent: "center", marginTop: 10 }} | |
| onClick={() => onSave(d)} | |
| disabled={busy} | |
| > | |
| {busy ? <><Spinner /> Redrafting</> : "Apply terms and redraft"} | |
| </button> | |
| </> | |
| ); | |
| } | |