/** * Cohort explorer: pick a dimension tab, click a cohort value to drill — * the value becomes a filter breadcrumb (e.g. Pedestrian → Night → Occluded) * and the next level groups by the newly selected dimension. Every node shows * cube metrics via POST /api/evaluations/query. "Why?" runs the factor * decomposition for the current filter set. */ import { useMemo, useState } from 'react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Chip from '@mui/material/Chip'; import CircularProgress from '@mui/material/CircularProgress'; import Tab from '@mui/material/Tab'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; import TableCell from '@mui/material/TableCell'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; import Tabs from '@mui/material/Tabs'; import Typography from '@mui/material/Typography'; import { HelpCircle, ChevronRight } from 'lucide-react'; import type { DimName, WhyResponse } from '../../types/megaeval'; import { DIM_NAMES } from '../../types/megaeval'; import { fmtCompact, queryEvaluations, whyAnalysis } from '../../services/megaeval'; import { usePoll } from '../../services/labeleval'; import { ErrorNote, HBar, LoadingBox, SectionCard, fmtNum, fmtPct } from '../labeleval/shared'; import { HeadCell } from '../help/InfoTip'; import { PctBarCell, QueryBadge, rowNum, rowStr } from './shared'; interface Crumb { dim: DimName; value: string; } function crumbsToFilters(crumbs: Crumb[]): Partial> { const filters: Partial> = {}; for (const c of crumbs) { filters[c.dim] = [...(filters[c.dim] ?? []), c.value]; } return filters; } function WhyPanel({ why }: { why: WhyResponse }) { const maxCohortN = Math.max(1, ...why.top_cohorts.map((c) => c.n)); return ( {fmtCompact(why.failure_count)} failures decomposed · {why.method} {why.factors.map((f) => ( ))} {why.top_cohorts.length ? ( <> Top affected cohorts {why.top_cohorts.map((c) => ( ))} ) : null} ); } export default function CohortTab({ runId, refreshKey }: { runId: string; refreshKey: number }) { const [crumbs, setCrumbs] = useState([]); const [dim, setDim] = useState('class'); const [why, setWhy] = useState(null); const [whyBusy, setWhyBusy] = useState(false); const [whyError, setWhyError] = useState(null); const filters = useMemo(() => crumbsToFilters(crumbs), [crumbs]); const filtersKey = JSON.stringify(filters); const query = usePoll( () => queryEvaluations({ evaluation_id: runId, filters, group_by: [dim], metrics: ['n', 'recall', 'precision', 'mean_iou', 'anomaly_rate'], }), null, [runId, refreshKey, dim, filtersKey] ); const usedDims = new Set(crumbs.map((c) => c.dim)); const drillInto = (value: string) => { const next = [...crumbs, { dim, value }]; setCrumbs(next); setWhy(null); // Auto-advance the group-by dimension to the next unused one. const nextUsed = new Set(next.map((c) => c.dim)); const nextDim = DIM_NAMES.find((d) => !nextUsed.has(d)); if (nextDim) setDim(nextDim); }; const removeCrumb = (idx: number) => { setCrumbs((prev) => prev.filter((_, i) => i !== idx)); setWhy(null); }; const runWhy = async () => { setWhyBusy(true); setWhyError(null); try { setWhy(await whyAnalysis({ run_id: runId, filters, metric: 'recall' })); } catch (err) { setWhyError(err instanceof Error ? err.message : String(err)); } finally { setWhyBusy(false); } }; const rows = useMemo( () => query.data ? // usePoll keeps the previous response while a new query is in flight; // drop rows that don't carry the currently-selected dimension so stale // groupings never render (and never collide on duplicate row keys). query.data.rows .filter((r) => typeof r[dim] === 'string') .sort((a, b) => b.n - a.n) : [], [query.data, dim] ); return ( } sx={{ flex: '2 1 560px' }} > {/* Filter breadcrumb */} { setCrumbs([]); setWhy(null); }} sx={{ height: 22, bgcolor: crumbs.length ? '#232a31' : '#12303f', color: '#81d4fa', fontWeight: 700 }} /> {crumbs.map((c, i) => ( removeCrumb(i)} sx={{ height: 22, bgcolor: '#12303f', color: '#81d4fa', fontFamily: 'monospace', fontSize: 11 }} /> ))} {/* Dimension tabs */} setDim(v)} variant="scrollable" scrollButtons="auto" sx={{ minHeight: 32, mb: 1, '& .MuiTab-root': { minHeight: 32, py: 0.5, fontSize: 12 } }} > {DIM_NAMES.map((d) => ( ))} {query.error ? : null} {query.loading && !query.data ? : null} {query.data ? ( {dim.replace(/_/g, ' ')} {rows.map((row, i) => { const value = rowStr(row, dim); return ( drillInto(value)} sx={{ cursor: 'pointer' }} > {value} {fmtCompact(row.n)} {fmtNum(rowNum(row, 'mean_iou'), 3)} {fmtPct(rowNum(row, 'anomaly_rate'))} ); })}
) : null} Click a row to drill into that cohort; the next level groups by the selected dimension tab. {whyError ? ( ) : null} {why ? : null} ); }