import { useCallback, useRef, useState, type ReactElement } from 'react' import { RefreshButton } from './buttons' import { Meter } from './Meter' import { readsText, type CompanyStatus } from './sec' // One company the store holds, as a chip with its ticker, a line along its foot while it is read or // embedded, and a mark when something failed. Hovered or focused, it opens a card: its name, how far // its reading and embedding are, in words and as a bar, what failed, and its refresh. Companies are // everyone's, so there is no stop and no delete. The card is placed against the window, so the // scrolling list does not clip it. One card is open at a time: hovering another chip swaps it at once. /** How long the card waits after the pointer leaves, so it can cross the gap from the chip to the card. */ const CLOSE_MS = 150 const CARD_WIDTH = 300 /** Which company's card is open, for the whole list: showing one closes any other at once. */ export function useCompanyCard(): { open: string | null show: (ticker: string) => void hide: (ticker: string) => void } { const [open, setOpen] = useState(null) const timer = useRef | null>(null) const show = useCallback((ticker: string) => { if (timer.current) clearTimeout(timer.current) setOpen(ticker) }, []) const hide = useCallback((ticker: string) => { if (timer.current) clearTimeout(timer.current) timer.current = setTimeout(() => setOpen((now) => (now === ticker ? null : now)), CLOSE_MS) }, []) return { open, show, hide } } export function CompanyChip({ c, run, open, onShow, onHide, }: { c: CompanyStatus run: (source: string, args: unknown) => Promise open: boolean onShow: () => void onHide: () => void }): ReactElement { const chip = useRef(null) const [place, setPlace] = useState<{ left: number; width: number; top?: number; bottom?: number } | null>(null) const show = (): void => { onShow() const r = chip.current?.getBoundingClientRect() if (!r) return const width = Math.min(CARD_WIDTH, window.innerWidth - 16) const left = Math.max(8, Math.min(r.left, window.innerWidth - width - 8)) // Low in the window, the card opens upward. setPlace( r.bottom > window.innerHeight * 0.6 ? { left, width, bottom: window.innerHeight - r.top + 4 } : { left, width, top: r.bottom + 4 }, ) } const reading = c.progress?.running ? c.progress : null const share = reading ? reading.done / Math.max(reading.total, 1) : c.unembedded > 0 ? (c.chunks - c.unembedded) / Math.max(c.chunks, 1) : null const trouble = Boolean(c.progress?.failed || c.progress?.error) return ( {open && place && (
)}
) } export function CompanyDetails({ c, run, }: { c: CompanyStatus run: (source: string, args: unknown) => Promise }): ReactElement { return ( <> {c.ticker} {c.name} {!c.progress?.running && ( void run('refresh', { ticker: c.ticker }).catch(() => {})} /> )} {line(c)} {c.progress?.running ? ( ) : c.unembedded > 0 ? ( ) : null} {c.progress?.failed ? ( {c.progress.failed} failed: refresh retries them ) : null} {c.progress?.error && {c.progress.error}} ) } /** How far a company is: reading its filings, then embedding them, then done; after what it reads. */ function line(c: CompanyStatus): string { const reads = readsText(c.reads) const years = reads ? `${reads} · ` : '' const p = c.progress if (p?.running) return `${years}${p.total ? `reading ${p.done.toLocaleString()} of ${p.total.toLocaleString()} filings…` : 'listing filings…'}` const filings = `${years}${c.filings.toLocaleString()} filings` return c.unembedded > 0 ? `${filings} · embedding, ${c.unembedded.toLocaleString()} chunks to go` : filings }