import { useEffect, useMemo, useRef, useState } from "react"; import { Buildings, MagnifyingGlass, FilePdf, Database, ArrowRight, DownloadSimple, WarningCircle, CheckCircle, } from "@phosphor-icons/react"; import { api, type Company, type RunSummary, ApiError } from "../lib/api"; import { ErrorNote, TableSkeleton, Empty, Spinner, Note } from "./common"; interface Hit { scrip_code: string; name: string; issuer_name: string; isin: string | null; industry: string; group: string; market_cap: string | null; is_sme: boolean; prepared: boolean; has_filing?: boolean | null; filing_year?: string | null; } interface IngestEvent { stage: string; detail: string; pct: number; at: number } export function Intake({ onStarted, embedded = false }: { onStarted: (run: RunSummary) => void; embedded?: boolean }) { const [q, setQ] = useState(""); const [scope, setScope] = useState<"sme" | "all">("sme"); const [hits, setHits] = useState(null); const [universe, setUniverse] = useState | null>(null); const [prepared, setPrepared] = useState>({}); const [error, setError] = useState(null); const [searchErr, setSearchErr] = useState(null); const [sel, setSel] = useState(null); const [starting, setStarting] = useState(false); // live ingestion const [job, setJob] = useState<{ id: string; status: string } | null>(null); const [events, setEvents] = useState([]); const [ingestErr, setIngestErr] = useState(null); const es = useRef(null); useEffect(() => { api.companies() .then((r) => setPrepared(Object.fromEntries(r.companies.map((c) => [c.scrip_code, c])))) .catch(() => { /* live search still works without the prepared index */ }); }, []); // Debounced live search against the real BSE master. useEffect(() => { const t = setTimeout(() => { setSearchErr(null); setHits(null); fetch(`/api/search?q=${encodeURIComponent(q)}&scope=${scope}&limit=40`) .then(async (r) => { if (!r.ok) throw new Error((await r.json()).detail ?? r.statusText); return r.json(); }) .then((d) => { setHits(d.results); setUniverse(d.universe); }) .catch((e) => setSearchErr(String(e.message ?? e))); }, q ? 260 : 0); return () => clearTimeout(t); }, [q, scope]); useEffect(() => () => es.current?.close(), []); const selPrepared = sel ? prepared[sel.scrip_code] : undefined; const isReady = Boolean(sel && (sel.prepared || job?.status === "done")); const ingest = () => { if (!sel) return; setIngestErr(null); setEvents([]); setJob(null); api.ingest(sel.scrip_code, sel.name) .then((j) => { setJob({ id: j.id, status: j.status }); es.current?.close(); const src = new EventSource(`/api/ingest/${j.id}/stream`); es.current = src; src.addEventListener("progress", (e) => setEvents((p) => [...p, JSON.parse((e as MessageEvent).data)])); src.addEventListener("done", (e) => { const d = JSON.parse((e as MessageEvent).data); setJob({ id: d.id, status: "done" }); setPrepared((p) => ({ ...p, [d.summary.scrip_code]: d.summary as Company })); setHits((h) => h?.map((x) => x.scrip_code === d.summary.scrip_code ? { ...x, prepared: true } : x) ?? null); src.close(); }); src.addEventListener("failed", (e) => { const d = JSON.parse((e as MessageEvent).data); setJob({ id: d.id, status: "failed" }); setIngestErr(d.error); src.close(); }); src.onerror = () => src.close(); }) .catch((e) => setIngestErr(e instanceof ApiError ? e.message : String(e))); }; const start = async () => { if (!sel) return; setStarting(true); setError(null); try { onStarted(await api.createRun(sel.scrip_code)); } catch (e) { setError(e instanceof ApiError ? e.message : String(e)); setStarting(false); } }; const pct = events.length ? events[events.length - 1].pct : 0; return ( <> {!embedded && (

Find an issuer

Search every company listed on BSE. Records already read are available at once; any other issuer is ingested live, by downloading its actual annual report and parsing it.

)} {error &&
}

BSE issuers

{universe && ( {scope === "sme" ? universe.sme : universe.total} searchable · {" "}{universe.prepared} already read )}
setQ(e.target.value)} autoFocus />
{searchErr &&
} {!hits && !searchErr && } {hits?.length === 0 && ( } title="No issuer matches that search"> Try a company name, a BSE scrip code, an ISIN, or switch to all of BSE. )} {hits?.map((c) => { const p = prepared[c.scrip_code]; return ( ); })}
); } function Row({ k, v }: { k: string; v: string }) { return (
{k} {v}
); } function Source({ icon, title, detail }: { icon: React.ReactNode; title: string; detail: string }) { return (
{icon}
{title}
{detail}
); }