Jaspers Hub

jaspers / fmp

pluginOfficialReviewed★ 0↓ 4

FMP: News & Technicals: Connects Financial Modeling Prep's MCP server to show company news and technical indicators, with Form 13F as a source only.

2.9 KB
1import { useEffect, useState, type ReactElement, type ReactNode } from 'react'
2import { useData, usePublish, type PanelRef } from '@jaspers-ai/sdk'
3
4// What the two views share: the bar with the symbol field, a section that shows its run's state
5// before its rows, and the gate that waits for the panel's state to arrive.
6
7/** The panel's state, a round trip after the frame mounts; nothing to draw until it is here. */
8export function useInitialState(panel: PanelRef): unknown {
9 return useData(`workspaces/${panel.workspaceId}/panels/${panel.id}/state`)
10}
11
12export function Loading(): ReactElement {
13 return (
14 <div className="fm-root">
15 <div className="fm-bar">
16 <span className="fm-muted">Loading…</span>
17 </div>
18 </div>
19 )
20}
21
22/**
23 * The symbol field: typing and Enter write the panel's state, and a state the orchestrator sets shows
24 * here. Enter is read off the key, not a form: the view's frame is sandboxed without allow-forms, so a
25 * form never submits.
26 */
27export function SymbolField({ symbol, onSymbol }: { symbol: string; onSymbol: (symbol: string) => void }): ReactElement {
28 const [draft, setDraft] = useState(symbol)
29 useEffect(() => setDraft(symbol), [symbol])
30 return (
31 <input
32 className="fm-input fm-input-symbol"
33 name="symbol"
34 value={draft}
35 placeholder="Symbol"
36 spellCheck={false}
37 onChange={(event) => setDraft(event.target.value)}
38 onKeyDown={(event) => {
39 if (event.key !== 'Enter' || event.nativeEvent.isComposing) return
40 event.preventDefault()
41 onSymbol(draft.trim().toUpperCase())
42 }}
43 />
44 )
45}
46
47/** A view with no symbol yet says so, and publishes that it shows nothing. */
48export function NoSymbol({ panel, output }: { panel: PanelRef; output: Record<string, unknown> }): ReactElement {
49 usePublish(panel, output)
50 return <p className="fm-empty">Enter a symbol.</p>
51}
52
53interface RunState {
54 loading: boolean
55 error: string | null
56}
57
58/** A titled block: its run's error (FMP's own words, plan refusals included) or loading line first, then its rows. */
59export function Section({ title, run, empty, children }: { title: string; run: RunState; empty?: boolean; children?: ReactNode }): ReactElement {
60 return (
61 <section className="fm-section">
62 <h2 className="fm-h">
63 {title}
64 {run.loading && <span className="fm-muted fm-loading">loading…</span>}
65 </h2>
66 {run.error ? <p className="fm-error">{cleanError(run.error)}</p> : empty && !run.loading ? <p className="fm-muted">Nothing returned.</p> : children}
67 </section>
68 )
69}
70
71/** The message as the server or main wrote it, without the wrapping the IPC adds. */
72export function cleanError(message: string): string {
73 return message.replace(/^Error invoking remote method '[^']*': (?:Error: )?/, '')
74}
75
76/** The rows a run gave, or none while it has not. */
77export function rowsOf(run: { data: Record<string, unknown>[] | undefined }): Record<string, unknown>[] {
78 return run.data ?? []
79}