Jaspers Hub

jaspers / screener-mcp

pluginOfficialReviewed★ 0↓ 33

Jaspers Screener: Screens US SEC registrants by fundamentals, holders and insiders, then reads filings against written criteria with cited quotes.

5.9 KB
1import type { ReactElement } from 'react'
2import { cellText, fieldLabel, readingAt, 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 /** The rows are of the list before the one a call is out for. */
18 stale: boolean
19 /** What an empty table says: that nothing matches, or that a verdict filter hides what does. */
20 empty: string
21 /** A qualitative run's criteria and what it found, by CIK. */
22 run: { criteria: Criterion[]; results: Map<number, CompanyResult> } | null
23 /** The ticker whose evidence is open. */
24 open: string
25 onOpen: (ticker: string) => void
26}
27
28export function Table({ rows, columns, fields, sort, onSort, loading, stale, empty, run, open, onOpen }: Props): ReactElement {
29 return (
30 <div className={stale ? 'sc-table-wrap sc-stale' : 'sc-table-wrap'}>
31 <table className="sc-table">
32 <thead>
33 <tr>
34 <th className="sc-company">Company</th>
35 {run && <th className="sc-verdict-head">Verdict</th>}
36 {run?.criteria.map((criterion) => (
37 <th key={criterion.id} className="sc-verdict-head" title={criterion.question}>
38 {criterion.id}
39 </th>
40 ))}
41 {columns.map((column) => {
42 const field = fields.get(column)
43 return (
44 <th key={column} className="sc-num" title={field?.description}>
45 {/* The server refuses a sort on a list, so its header is not a button. */}
46 {field && !field.sortable ? (
47 <span className="sc-sort">{fieldLabel(column)}</span>
48 ) : (
49 <button type="button" className="sc-sort" onClick={() => onSort(column)}>
50 {fieldLabel(column)}
51 {sort.field === column ? (sort.dir === 'desc' ? ' ↓' : ' ↑') : ''}
52 </button>
53 )}
54 </th>
55 )
56 })}
57 </tr>
58 </thead>
59 <tbody>
60 {rows.map((row, index) => {
61 const ticker = typeof row.ticker === 'string' ? row.ticker : null
62 const result = run?.results.get(Number(row.cik))
63 return (
64 <tr key={String(row.cik ?? ticker ?? index)} className={ticker !== null && ticker === open ? 'sc-row-open' : undefined}>
65 <td className="sc-company">
66 {result && ticker ? (
67 <button type="button" className="sc-ticker sc-ticker-open" title="Show the evidence" onClick={() => onOpen(ticker)}>
68 {ticker}
69 </button>
70 ) : (
71 <span className="sc-ticker">{ticker ?? '—'}</span>
72 )}
73 <span className="sc-name" title={typeof row.sic_description === 'string' ? row.sic_description : undefined}>
74 {String(row.name ?? '')}
75 </span>
76 </td>
77 {run && <Verdict result={result} />}
78 {run?.criteria.map((criterion) => <CriterionCell key={criterion.id} result={result} id={criterion.id} />)}
79 {columns.map((column) => {
80 const sign = signOf(column, row[column])
81 return (
82 <td key={column} className={sign ? `sc-num sc-${sign}` : 'sc-num'}>
83 {cellText(row[column], fields.get(column))}
84 </td>
85 )
86 })}
87 </tr>
88 )
89 })}
90 </tbody>
91 </table>
92 {rows.length === 0 && !loading && <p className="sc-empty">{empty}</p>}
93 </div>
94 )
95}
96
97/** A company's verdict over all its criteria, or where it is in the run. */
98function Verdict({ result }: { result: CompanyResult | undefined }): ReactElement {
99 if (!result) return <td className="sc-verdict sc-status">—</td>
100 if (result.status === 'error') {
101 return (
102 <td className="sc-verdict sc-error" title={result.error ?? undefined}>
103 error
104 </td>
105 )
106 }
107 if (result.status !== 'done') return <td className="sc-verdict sc-status">{result.status === 'running' ? <Reading step={result.step} /> : 'queued'}</td>
108 const verdict = result.verdict ?? 'unverified'
109 return <td className={`sc-verdict sc-v-${verdict}`}>{verdict}</td>
110}
111
112/** A company being read: the steps behind it and the one it is in, where the server says which. */
113function Reading({ step }: { step: string | null }): ReactElement {
114 const reading = readingAt(step)
115 if (!reading) return <>reading…</>
116 return (
117 <span className="sc-rail" title={`Step ${reading.at + 1} of ${reading.of} of reading a company`}>
118 {Array.from({ length: reading.of }, (_, at) => (
119 <i key={at} className={at < reading.at ? 'sc-rail-done' : at === reading.at ? 'sc-rail-now' : undefined} />
120 ))}
121 {reading.word}
122 </span>
123 )
124}
125
126/** One criterion's answer; a verification that did not hold is marked, with what was wrong as its tooltip. */
127function CriterionCell({ result, id }: { result: CompanyResult | undefined; id: string }): ReactElement {
128 const answer = result?.criteria.find((criterion) => criterion.id === id)
129 if (!answer) return <td className="sc-verdict sc-status">—</td>
130 const mark = verificationMark(answer.verification)
131 return (
132 <td className={`sc-verdict sc-v-${answer.verdict}`} title={mark ?? answer.rationale}>
133 {answer.verdict}
134 {mark && <span className="sc-mark"> !</span>}
135 </td>
136 )
137}