Jaspers Hub

jaspers / screener-mcp

pluginOfficialReviewed★ 0↓ 33

Screener MCP: Screens US SEC filers by fundamentals, holders and insider data, then checks criteria in words against filings via the Jaspers screener server.

5.9 KB
1import { useState, type ReactElement } from 'react'
2import { MAX_CRITERIA, progressText, slug, VERDICTS, type Criterion, type Run, type RunSnapshot, type Screen, type Verdict } from './screener'
3
4// The qualitative stage's bar: the criteria in words, the button that has every company on the list
5// read against them, and where that run is. A run costs model calls for every company, so it starts
6// on the button (or on the orchestrator's word, which is the same write), never on a criterion alone.
7
8interface Props {
9 criteria: Criterion[]
10 onCriteria: (next: Criterion[]) => void
11 run: Run | null
12 snapshot: RunSnapshot | null
13 screen: Screen | null
14 /** The server's session holds the list on screen and no screen call is out, so a run would freeze the right list. */
15 settled: boolean
16 verdicts: Verdict[]
17 onVerdicts: (next: Verdict[]) => void
18 onRun: () => void
19 onRetry: () => void
20 onClear: () => void
21}
22
23export function QualitativeBar(props: Props): ReactElement {
24 const { criteria, onCriteria, run, snapshot, screen, settled, verdicts, onVerdicts, onRun, onRetry, onClear } = props
25 const [question, setQuestion] = useState('')
26 const [problem, setProblem] = useState('')
27 const busy = run !== null && run.status !== 'done' && run.status !== 'error'
28
29 function add(): void {
30 const typed = question.trim()
31 // The server's own bounds on a criterion, said before it has to refuse.
32 if (typed.length < 5 || typed.length > 500) {
33 setProblem('A criterion is a question of 5 to 500 characters.')
34 return
35 }
36 onCriteria([...criteria, { id: slug(typed, criteria.map((c) => c.id)), question: typed }])
37 setQuestion('')
38 setProblem('')
39 }
40
41 return (
42 <div className="sc-bar">
43 <span className="sc-status">Criteria</span>
44 {criteria.map((criterion) => (
45 <span key={criterion.id} className="sc-chip sc-q" title={`${criterion.id}: ${criterion.question}`}>
46 <span className="sc-q-text">{criterion.question}</span>
47 <button
48 type="button"
49 className="sc-chip-x"
50 aria-label={`Remove ${criterion.question}`}
51 disabled={busy}
52 onClick={() => onCriteria(criteria.filter((c) => c.id !== criterion.id))}
53 >
54 ×
55 </button>
56 </span>
57 ))}
58 {!busy && criteria.length < MAX_CRITERIA && (
59 <>
60 <input
61 className="sc-input sc-q-input"
62 aria-label="A criterion, as a yes or no question"
63 placeholder="A yes/no question the filings can answer, e.g. Does the company depend on a sole-source supplier?"
64 value={question}
65 onChange={(event) => setQuestion(event.target.value)}
66 onKeyDown={(event) => {
67 if (event.key === 'Enter') add()
68 }}
69 />
70 <button type="button" className="sc-btn" disabled={!question.trim()} onClick={add}>
71 Add
72 </button>
73 </>
74 )}
75 {problem && <span className="sc-error">{problem}</span>}
76
77 <span className="sc-run">
78 <RunStatus
79 run={run}
80 snapshot={snapshot}
81 screen={screen}
82 settled={settled}
83 ready={criteria.length > 0}
84 onRun={onRun}
85 onRetry={onRetry}
86 onClear={onClear}
87 />
88 </span>
89
90 {snapshot && (
91 <span className="sc-verdicts">
92 {VERDICTS.map((verdict) => {
93 const on = verdicts.includes(verdict)
94 return (
95 <button
96 key={verdict}
97 type="button"
98 className={on ? `sc-btn sc-toggle sc-on sc-v-${verdict}` : `sc-btn sc-toggle sc-v-${verdict}`}
99 aria-pressed={on}
100 title={on ? `Showing ${verdict}; click to stop filtering on it` : `Show only ${verdict}`}
101 onClick={() => onVerdicts(on ? verdicts.filter((v) => v !== verdict) : [...verdicts, verdict])}
102 >
103 {verdict} {snapshot.progress[verdict]}
104 </button>
105 )
106 })}
107 </span>
108 )}
109 </div>
110 )
111}
112
113interface StatusProps extends Pick<Props, 'run' | 'snapshot' | 'screen' | 'settled' | 'onRun' | 'onRetry' | 'onClear'> {
114 /** There is at least one criterion. */
115 ready: boolean
116}
117
118function RunStatus({ run, snapshot, screen, settled, ready, onRun, onRetry, onClear }: StatusProps): ReactElement | null {
119 if (run === null) {
120 if (!screen) return null
121 if (!screen.ready) {
122 const over = screen.count - screen.max
123 return (
124 <span className="sc-status">
125 {screen.count === 0 ? 'Nothing to read' : `${over.toLocaleString('en-US')} over the ${screen.max} the qualitative stage takes`}
126 </span>
127 )
128 }
129 return (
130 <button
131 type="button"
132 className="sc-btn sc-primary"
133 disabled={!ready || !settled}
134 onClick={onRun}
135 title="Every company on the list is read against every criterion. It costs model calls for each."
136 >
137 Run on {screen.count} {screen.count === 1 ? 'company' : 'companies'}
138 </button>
139 )
140 }
141 if (run.status === 'error') {
142 return (
143 <>
144 <span className="sc-error">{run.error ?? 'The run failed.'}</span>
145 <button type="button" className="sc-btn" onClick={onRetry}>
146 {run.id ? 'Read again' : 'Try again'}
147 </button>
148 <button type="button" className="sc-btn" onClick={onClear}>
149 Clear
150 </button>
151 </>
152 )
153 }
154 return (
155 <>
156 <span className="sc-status">{snapshot ? progressText(snapshot.progress, snapshot.status) : run.id ? 'Reading the run…' : ready ? 'Starting…' : 'A run was asked for. Add a criterion and it starts.'}</span>
157 {/* There is no stopping a run: clearing takes it off this panel, and the server goes on reading. */}
158 {run.id && (
159 <button type="button" className="sc-btn" onClick={onClear} title="Takes the run off this panel. The server keeps it, and finishes it if it is still going.">
160 Clear
161 </button>
162 )}
163 </>
164 )
165}