import { useEffect, useState, type ReactElement, type ReactNode } from 'react' import { useBridge, useData, usePublish, type PanelRef } from '@jaspers-ai/sdk' import { display as d } from './format' import { cleanError, position, tone, type Run } from './views' // What the five views share: the panel's state as one value, writes to it, the bar and its symbol // field, a section that shows its run's state before its content, figures with their sign, a range // bar, and the minute timer the quote views ask again on. /** The panel's whole state, a round trip after the frame mounts; undefined until then. */ export function usePanelStateValue(panel: PanelRef): unknown { return useData(`workspaces/${panel.workspaceId}/panels/${panel.id}/state`) } /** Writes keys of the panel's state in order; main checks each write against the view's schema. */ export function useStateWriter(panel: PanelRef): (changes: [string, unknown][]) => void { const bridge = useBridge() return (changes) => { void (async () => { for (const [key, value] of changes) await bridge.setState(panel, [key], value) })() } } /** Runs `refetch` every `ms` while the view is mounted. */ export function useEvery(refetch: () => void, ms: number): void { useEffect(() => { const timer = setInterval(refetch, ms) return () => clearInterval(timer) }, [refetch, ms]) } export function Loading(): ReactElement { return (
Loading…
) } export function Bar({ children }: { children: ReactNode }): ReactElement { return
{children}
} /** * The symbol field. Enter hands the typed symbol over; a symbol the orchestrator sets shows here. * Enter is read off the key: the frame is sandboxed without allow-forms, so a form never submits. */ export function SymbolField({ symbol, onSymbol, placeholder = 'Symbol', clear = false, }: { symbol: string onSymbol: (symbol: string) => void placeholder?: string /** Empty the field after Enter, for a field that adds rather than sets. */ clear?: boolean }): ReactElement { const [draft, setDraft] = useState(symbol) useEffect(() => setDraft(symbol), [symbol]) return ( setDraft(event.target.value)} onKeyDown={(event) => { if (event.key !== 'Enter' || event.nativeEvent.isComposing) return event.preventDefault() const next = draft.trim().toUpperCase() if (next) onSymbol(next) if (clear) setDraft('') }} /> ) } /** A view with nothing to show yet says so, and publishes that. */ export function Empty({ panel, output, children }: { panel: PanelRef; output: Record; children: ReactNode }): ReactElement { usePublish(panel, output) return

{children}

} /** A titled block: its run's error (Yahoo's own words) or loading line first, then its content. */ export function Section({ title, run, aside, empty, children, }: { title: string run?: Pick aside?: ReactNode empty?: boolean children?: ReactNode }): ReactElement { return (

{title} {aside && {aside}} {run?.loading && loading…}

{run?.error ?

{cleanError(run.error)}

: empty && !run?.loading ?

Nothing.

: children}
) } /** A signed figure, green up and red down. */ export function Signed({ value, percent = false, hint }: { value: number | null | undefined; percent?: boolean; hint?: number | null }): ReactElement { const shown = percent ? d.signedPercent(value) : d.signed(value, typeof hint === 'number' ? hint : 2) return {shown} } /** A track from low to high with a mark where the value is, and the ends written under it. */ export function RangeBar({ label, low, high, value, mark, hint, }: { label: string low: number | null high: number | null value: number | null /** A second, hollow mark, like the mean target. */ mark?: { value: number | null; title: string } hint?: number | null }): ReactElement | null { const at = position(low, high, value) if (at === null) return null const second = mark ? position(low, high, mark.value) : null return (
{label}
{second !== null && }
{d.price(low, hint)} {d.price(high, hint)}
) } /** Market state in words. */ export function marketState(state: string | null): string { switch (state) { case 'PRE': return 'Pre-market' case 'REGULAR': return 'Market open' case 'POST': return 'After hours' case null: return '' default: return 'Market closed' } }