Skip to content

Commit a5beb23

Browse files
sdspiegclaude
andcommitted
Statement Browser: line/threat intensity filters + sort by every parameter
Two new RRLS dropdowns — Line Intensity and Threat Intensity — with ordinal sort order (Very Low → Very High) baked into a FilterDef.ordinal field. Sort dropdown now dynamically includes every taxonomy filter (Theme, Audience, Nature of Threat, Escalation, Line Type, Threat Type, Line/Threat Intensity, Specificity, Immediacy for RRLS; Statement Type, Threat Type, Capability, Tone, Consequences, Conditionality, Specificity for NTS) on top of the built-in Date / Confidence / Speaker / Target sorts. Intensity sorts respect the ordinal scale; other taxonomy sorts are alphabetical. Confidence is already the filter-bar slider; the dashboard schema has no separate 'overall_intensity' field, so there's nothing additional to wire. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 72c0606 commit a5beb23

1 file changed

Lines changed: 48 additions & 9 deletions

File tree

src/components/Statements.tsx

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,23 @@ type Mode = 'rrls' | 'nts';
1111
interface FilterDef {
1212
key: string;
1313
label: string;
14+
// Optional ordinal sort order. If absent, values sort alphabetically.
15+
ordinal?: string[];
1416
}
1517

18+
// Ordinal ranking for intensity-style fields. Values outside this list fall to
19+
// the bottom when sorting ascending.
20+
const INTENSITY_ORDER = ['Very Low', 'Low', 'Medium', 'High', 'Very High'];
21+
1622
const RRLS_FILTERS: FilterDef[] = [
1723
{ key: 'theme', label: 'Theme' },
1824
{ key: 'audience', label: 'Audience' },
1925
{ key: 'nature_of_threat', label: 'Nature of Threat' },
2026
{ key: 'level_of_escalation', label: 'Escalation' },
2127
{ key: 'line_type', label: 'Line Type' },
2228
{ key: 'threat_type', label: 'Threat Type' },
29+
{ key: 'line_intensity', label: 'Line Intensity', ordinal: INTENSITY_ORDER },
30+
{ key: 'threat_intensity', label: 'Threat Intensity', ordinal: INTENSITY_ORDER },
2331
{ key: 'specificity', label: 'Specificity' },
2432
{ key: 'immediacy', label: 'Immediacy' },
2533
];
@@ -45,7 +53,13 @@ function highlightText(text: string, query: string): React.ReactNode {
4553
);
4654
}
4755

48-
type SortKey = 'date-desc' | 'date-asc' | 'conf-desc' | 'conf-asc' | 'speaker' | 'target';
56+
// Hard-coded sort keys (date, confidence, speaker, target). Taxonomy dims
57+
// use the `dim:<key>` pattern so every FilterDef contributes a sort option.
58+
type SortKey =
59+
| 'date-desc' | 'date-asc'
60+
| 'conf-desc' | 'conf-asc'
61+
| 'speaker' | 'target'
62+
| `dim:${string}`;
4963

5064
export default function Statements() {
5165
const [mode, setMode] = useState<Mode>('rrls');
@@ -159,14 +173,33 @@ export default function Statements() {
159173
return sa < sb ? -1 : sa > sb ? 1 : 0;
160174
};
161175
void at;
162-
switch (sortBy) {
163-
// byDate(dir): dir=1 means ascending (older→newer); dir=-1 means descending (newer→older)
164-
case 'date-desc': sorted.sort(byDate(-1)); break; // newest first
165-
case 'date-asc': sorted.sort(byDate(1)); break; // oldest first
166-
case 'conf-desc': sorted.sort(byConf(-1)); break; // high → low
167-
case 'conf-asc': sorted.sort(byConf(1)); break; // low → high
168-
case 'speaker': sorted.sort(byStr('speaker')); break;
169-
case 'target': sorted.sort(byStr('target')); break;
176+
if (sortBy.startsWith('dim:')) {
177+
const dimKey = sortBy.slice(4);
178+
const def = filterDefs.find(f => f.key === dimKey);
179+
sorted.sort((a, b) => {
180+
const va = String((a as unknown as Record<string, unknown>)[dimKey] ?? '');
181+
const vb = String((b as unknown as Record<string, unknown>)[dimKey] ?? '');
182+
if (def?.ordinal) {
183+
// Ordinal ranking — values not in the list sink to the bottom.
184+
let ia = def.ordinal.indexOf(va); if (ia === -1) ia = Infinity;
185+
let ib = def.ordinal.indexOf(vb); if (ib === -1) ib = Infinity;
186+
return ia - ib;
187+
}
188+
if (!va && !vb) return 0;
189+
if (!va) return 1;
190+
if (!vb) return -1;
191+
return va < vb ? -1 : va > vb ? 1 : 0;
192+
});
193+
} else {
194+
switch (sortBy) {
195+
// byDate(dir): dir=1 ascending (older→newer); dir=-1 descending (newer→older)
196+
case 'date-desc': sorted.sort(byDate(-1)); break; // newest first
197+
case 'date-asc': sorted.sort(byDate(1)); break; // oldest first
198+
case 'conf-desc': sorted.sort(byConf(-1)); break; // high → low
199+
case 'conf-asc': sorted.sort(byConf(1)); break; // low → high
200+
case 'speaker': sorted.sort(byStr('speaker')); break;
201+
case 'target': sorted.sort(byStr('target')); break;
202+
}
170203
}
171204
return sorted;
172205
}, [data, sourceFilter, speakerFilter, targetFilter, search, dimFilters, minConfidence, sortBy]);
@@ -249,6 +282,12 @@ export default function Statements() {
249282
<option value="conf-asc">Confidence (low → high)</option>
250283
<option value="speaker">Speaker (A → Z)</option>
251284
<option value="target">Target (A → Z)</option>
285+
<option disabled>──────────</option>
286+
{filterDefs.map(f => (
287+
<option key={f.key} value={`dim:${f.key}`}>
288+
{f.label} ({f.ordinal ? 'low → high' : 'A → Z'})
289+
</option>
290+
))}
252291
</select>
253292
</div>
254293

0 commit comments

Comments
 (0)