FMP: News & Technicals: Connects Financial Modeling Prep's MCP server to show company news and technical indicators, with Form 13F as a source only.
| 1 | import type { ReactElement } from 'react' |
| 2 | import { useBridge, useData, usePanelState, usePublish, type PanelRef } from '@jaspers-ai/sdk' |
| 3 | import { newsOutput, readNewsState, readSymbol, type Row } from './fmp' |
| 4 | import { Loading, NoSymbol, Section, SymbolField, rowsOf, useInitialState } from './shared' |
| 5 | import './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 | |
| 10 | export 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 | |
| 16 | function 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 | |
| 34 | function 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 | |
| 50 | function 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 | } |