Jaspers Hub

jaspers / screener-mcp

pluginOfficialReviewed★ 0↓ 33

Screener MCP: Screens US SEC registrants by fundamentals, holders and insider data, then checks a shortlist's filings against written criteria with cited quotes.

5.2 KB
1import type { ReactElement } from 'react'
2import { cellText, fieldLabel, signOf, verificationMark, type CompanyResult, type Criterion, type Field, type Sort } from './screener'
3
4// The table: the company, a run's verdicts when there is one, then the fields filtered, sorted and
5// asked for. A header sorts, and sorting is the server's job — the sort goes into the panel's
6// state, the state into the source's arguments — so a click here is a change of state like any the
7// orchestrator makes.
8
9interface Props {
10 rows: Record<string, unknown>[]
11 columns: string[]
12 fields: Map<string, Field>
13 sort: Sort
14 onSort: (field: string) => void
15 /** Nothing is drawn as empty while the first run is still out. */
16 loading: boolean
17 /** What an empty table says: that nothing matches, or that a verdict filter hides what does. */
18 empty: string
19 /** A qualitative run's criteria and what it found, by CIK. */
20 run: { criteria: Criterion[]; results: Map<number, CompanyResult> } | null
21 /** The ticker whose evidence is open. */
22 open: string
23 onOpen: (ticker: string) => void
24}
25
26export function Table({ rows, columns, fields, sort, onSort, loading, empty, run, open, onOpen }: Props): ReactElement {
27 return (
28 <div className="sc-table-wrap">
29 <table className="sc-table">
30 <thead>
31 <tr>
32 <th className="sc-company">Company</th>
33 {run && <th className="sc-verdict-head">Verdict</th>}
34 {run?.criteria.map((criterion) => (
35 <th key={criterion.id} className="sc-verdict-head" title={criterion.question}>
36 {criterion.id}
37 </th>
38 ))}
39 {columns.map((column) => {
40 const field = fields.get(column)
41 return (
42 <th key={column} className="sc-num" title={field?.description}>
43 {/* The server refuses a sort on a list, so its header is not a button. */}
44 {field && !field.sortable ? (
45 <span className="sc-sort">{fieldLabel(column)}</span>
46 ) : (
47 <button type="button" className="sc-sort" onClick={() => onSort(column)}>
48 {fieldLabel(column)}
49 {sort.field === column ? (sort.dir === 'desc' ? ' ↓' : ' ↑') : ''}
50 </button>
51 )}
52 </th>
53 )
54 })}
55 </tr>
56 </thead>
57 <tbody>
58 {rows.map((row, index) => {
59 const ticker = typeof row.ticker === 'string' ? row.ticker : null
60 const result = run?.results.get(Number(row.cik))
61 return (
62 <tr key={String(row.cik ?? ticker ?? index)} className={ticker !== null && ticker === open ? 'sc-row-open' : undefined}>
63 <td className="sc-company">
64 {result && ticker ? (
65 <button type="button" className="sc-ticker sc-ticker-open" title="Show the evidence" onClick={() => onOpen(ticker)}>
66 {ticker}
67 </button>
68 ) : (
69 <span className="sc-ticker">{ticker ?? '—'}</span>
70 )}
71 <span className="sc-name" title={typeof row.sic_description === 'string' ? row.sic_description : undefined}>
72 {String(row.name ?? '')}
73 </span>
74 </td>
75 {run && <Verdict result={result} />}
76 {run?.criteria.map((criterion) => <CriterionCell key={criterion.id} result={result} id={criterion.id} />)}
77 {columns.map((column) => {
78 const sign = signOf(column, row[column])
79 return (
80 <td key={column} className={sign ? `sc-num sc-${sign}` : 'sc-num'}>
81 {cellText(row[column], fields.get(column))}
82 </td>
83 )
84 })}
85 </tr>
86 )
87 })}
88 </tbody>
89 </table>
90 {rows.length === 0 && !loading && <p className="sc-empty">{empty}</p>}
91 </div>
92 )
93}
94
95/** A company's verdict over all its criteria, or where it is in the run. */
96function Verdict({ result }: { result: CompanyResult | undefined }): ReactElement {
97 if (!result) return <td className="sc-verdict sc-status">—</td>
98 if (result.status === 'error') {
99 return (
100 <td className="sc-verdict sc-error" title={result.error ?? undefined}>
101 error
102 </td>
103 )
104 }
105 if (result.status !== 'done') return <td className="sc-verdict sc-status">{result.status === 'running' ? 'reading…' : 'queued'}</td>
106 const verdict = result.verdict ?? 'unverified'
107 return <td className={`sc-verdict sc-v-${verdict}`}>{verdict}</td>
108}
109
110/** One criterion's answer; a verification that did not hold is marked, with what was wrong as its tooltip. */
111function CriterionCell({ result, id }: { result: CompanyResult | undefined; id: string }): ReactElement {
112 const answer = result?.criteria.find((criterion) => criterion.id === id)
113 if (!answer) return <td className="sc-verdict sc-status">—</td>
114 const mark = verificationMark(answer.verification)
115 return (
116 <td className={`sc-verdict sc-v-${answer.verdict}`} title={mark ?? answer.rationale}>
117 {answer.verdict}
118 {mark && <span className="sc-mark"> !</span>}
119 </td>
120 )
121}