Screener MCP: Screens US SEC registrants by fundamentals, holders and insider data, then checks a shortlist's filings against written criteria with cited quotes.
| 1 | import { useState, type ReactElement } from 'react' |
| 2 | import { 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 | |
| 8 | interface 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 | |
| 23 | export 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 | |
| 113 | interface StatusProps extends Pick<Props, 'run' | 'snapshot' | 'screen' | 'settled' | 'onRun' | 'onRetry' | 'onClear'> { |
| 114 | /** There is at least one criterion. */ |
| 115 | ready: boolean |
| 116 | } |
| 117 | |
| 118 | function 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 | } |