import { useEffect, useState, type ReactElement, type ReactNode } from 'react' import { useData, usePublish, type PanelRef } from '@jaspers-ai/sdk' // What the two views share: the bar with the symbol field, a section that shows its run's state // before its rows, and the gate that waits for the panel's state to arrive. /** The panel's state, a round trip after the frame mounts; nothing to draw until it is here. */ export function useInitialState(panel: PanelRef): unknown { return useData(`workspaces/${panel.workspaceId}/panels/${panel.id}/state`) } export function Loading(): ReactElement { return (
Loading…
) } /** * The symbol field: typing and Enter write the panel's state, and a state the orchestrator sets shows * here. Enter is read off the key, not a form: the view's frame is sandboxed without allow-forms, so a * form never submits. */ export function SymbolField({ symbol, onSymbol }: { symbol: string; onSymbol: (symbol: string) => void }): 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() onSymbol(draft.trim().toUpperCase()) }} /> ) } /** A view with no symbol yet says so, and publishes that it shows nothing. */ export function NoSymbol({ panel, output }: { panel: PanelRef; output: Record }): ReactElement { usePublish(panel, output) return

Enter a symbol.

} interface RunState { loading: boolean error: string | null } /** A titled block: its run's error (FMP's own words, plan refusals included) or loading line first, then its rows. */ export function Section({ title, run, empty, children }: { title: string; run: RunState; empty?: boolean; children?: ReactNode }): ReactElement { return (

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

{run.error ?

{cleanError(run.error)}

: empty && !run.loading ?

Nothing returned.

: children}
) } /** The message as the server or main wrote it, without the wrapping the IPC adds. */ export function cleanError(message: string): string { return message.replace(/^Error invoking remote method '[^']*': (?:Error: )?/, '') } /** The rows a run gave, or none while it has not. */ export function rowsOf(run: { data: Record[] | undefined }): Record[] { return run.data ?? [] }