import { useData, usePanelState, usePublish, type PanelRef } from '@jaspers-ai/sdk' import { useEffect, useState } from 'react' import { fmt, int, sign } from './format' import './styles.css' interface Snap { symbol: string last: number | null bid: number | null ask: number | null change: number | null change_pct: number | null day_volume: number | null [field: string]: unknown } interface State { symbols?: string[] feed?: string } const DEFAULT = ['AAPL', 'MSFT', 'NVDA'] export function Watchlist({ panel }: { panel: PanelRef }) { const state = useData(`workspaces/${panel.workspaceId}/panels/${panel.id}/state`) as State | undefined if (state === undefined) return

Loading…

return } function List({ panel, feed }: { panel: PanelRef; feed: string }) { const [symbols, setSymbols] = usePanelState(panel, 'symbols', DEFAULT) as [string[], (v: string[]) => void] const [draft, setDraft] = useState('') const list = symbols ?? [] const add = () => { const wanted = draft .split(/[\s,]+/) .map((s) => s.trim().toUpperCase()) .filter((s) => s && !list.includes(s)) if (wanted.length) setSymbols([...list, ...wanted].slice(0, 100)) setDraft('') } const remove = (symbol: string) => setSymbols(list.filter((s) => s !== symbol)) return (

Alpaca watchlist

{feed} setDraft(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') add() }} />
{list.length === 0 ? : }
) } function Empty({ panel }: { panel: PanelRef }) { usePublish(panel, { symbols: [], count: 0, rows: [] }) return

No symbols. Add one above.

} function Rows(props: { panel: PanelRef; symbols: string[]; feed: string; onRemove: (s: string) => void }) { const { panel, symbols, feed, onRemove } = props const { data, error, refetch } = useData('alpaca/stock-snapshots', { symbols, feed }) // Refresh every 15 seconds while the view is open; each refresh is one quick read. useEffect(() => { const timer = setInterval(() => refetch?.(), 15_000) return () => clearInterval(timer) }, [refetch]) const rows = (data ?? []) as Snap[] usePublish(panel, { symbols: rows.map((r) => r.symbol), count: rows.length, rows: rows.slice(0, 50).map((r) => ({ symbol: r.symbol, last: r.last, change_pct: r.change_pct })), }) if (error) return

{error}

if (!data) return

Loading…

const bySymbol = new Map(rows.map((r) => [r.symbol, r])) return ( {symbols.map((symbol) => { const r = bySymbol.get(symbol) return ( ) })}
Symbol Last Bid Ask Change Change % Volume
{symbol} {fmt(r?.last)} {fmt(r?.bid)} {fmt(r?.ask)} {fmt(r?.change)} {r ? `${fmt(r.change_pct)}%` : '–'} {int(r?.day_volume)}
) }