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.8 KB
1import type { ReactElement } from 'react'
2import { useBridge, useData, usePanelState, usePublish, type PanelRef } from '@jaspers-ai/sdk'
3import { newsOutput, readNewsState, readSymbol, type Row } from './fmp'
4import { Loading, NoSymbol, Section, SymbolField, rowsOf, useInitialState } from './shared'
5import './styles.css'
6
7// Stock news for one company: FMP's search-stock-news, newest first. A title opens its article in
8// the system browser through the bridge, since the frame can open nothing itself.
9
10export function NewsView({ panel }: { panel: PanelRef }): ReactElement {
11 const initial = useInitialState(panel)
12 if (initial === undefined) return <Loading />
13 return <News panel={panel} initial={initial} />
14}
15
16function News({ panel, initial }: { panel: PanelRef; initial: unknown }): ReactElement {
17 const first = readNewsState(initial)
18 const [symbol, setSymbol] = usePanelState<string>(panel, 'symbol', first.symbol)
19 const [limit] = usePanelState<number>(panel, 'limit', first.limit)
20 const state = readNewsState({ symbol, limit })
21 return (
22 <div className="fm-root">
23 <div className="fm-bar">
24 <SymbolField symbol={state.symbol} onSymbol={setSymbol} />
25 <span className="fm-muted">News</span>
26 </div>
27 <div className="fm-body">
28 {state.symbol ? <Body panel={panel} symbol={state.symbol} limit={state.limit} /> : <NoSymbol panel={panel} output={newsOutput('', [])} />}
29 </div>
30 </div>
31 )
32}
33
34function Body({ panel, symbol, limit }: { panel: PanelRef; symbol: string; limit: number }): ReactElement {
35 const bridge = useBridge()
36 const news = useData('fmp/news', { endpoint: 'search-stock-news', symbols: [symbol], limit })
37 const rows = rowsOf(news)
38 usePublish(panel, newsOutput(symbol, news.data))
39 return (
40 <Section title={`${symbol} news`} run={news} empty={rows.length === 0}>
41 <ul className="fm-list">
42 {rows.map((row, index) => (
43 <Item key={index} row={row} onOpen={(url) => void bridge.openLink(url)} />
44 ))}
45 </ul>
46 </Section>
47 )
48}
49
50function Item({ row, onOpen }: { row: Row; onOpen: (url: string) => void }): ReactElement {
51 const url = typeof row['url'] === 'string' ? row['url'] : ''
52 const title = String(row['title'] ?? '')
53 return (
54 <li className="fm-item">
55 <div className="fm-item-head">
56 <span className="fm-muted">{String(row['publishedDate'] ?? '').slice(0, 16)}</span>
57 <span className="fm-muted">{String(row['site'] ?? row['publisher'] ?? '')}</span>
58 </div>
59 {url.startsWith('https://') ? (
60 <button type="button" className="fm-link" onClick={() => onOpen(url)}>
61 {title}
62 </button>
63 ) : (
64 <span className="fm-title">{title}</span>
65 )}
66 {typeof row['text'] === 'string' && row['text'] && <p className="fm-snippet">{row['text']}</p>}
67 </li>
68 )
69}