11/**
2- * Targets table showing targets grouped across all runs.
2+ * Targets tab with drill-down from target -> experiment-grouped runs.
33 *
4- * Displays target name, number of runs, experiments, pass rate, and
5- * eval counts (passed/total). Links are not needed since targets are
6- * informational groupings .
4+ * The summary table opens a target detail view. That detail view groups runs
5+ * by experiment and reuses the existing run-detail routes for the final click,
6+ * so category breakdowns and individual test cases stay consistent everywhere .
77 */
88
9- import { useTargets } from '~/lib/api' ;
10- import type { TargetSummary } from '~/lib/types' ;
9+ import { useQuery } from '@tanstack/react-query' ;
10+ import { useEffect , useMemo , useState } from 'react' ;
11+
12+ import {
13+ benchmarkRunListOptions ,
14+ benchmarkTargetsOptions ,
15+ runListOptions ,
16+ targetsOptions ,
17+ } from '~/lib/api' ;
18+ import type { RunMeta , TargetsResponse } from '~/lib/types' ;
1119
1220import { PassRatePill } from './PassRatePill' ;
21+ import { RunList } from './RunList' ;
22+
23+ interface TargetsTabProps {
24+ benchmarkId ?: string ;
25+ }
26+
27+ interface ExperimentRunGroup {
28+ name : string ;
29+ runs : RunMeta [ ] ;
30+ latestTimestamp : string | null ;
31+ evalCount : number ;
32+ passedCount : number ;
33+ passRate : number ;
34+ }
35+
36+ export function TargetsTab ( { benchmarkId } : TargetsTabProps = { } ) {
37+ const [ selectedTargetName , setSelectedTargetName ] = useState < string | null > ( null ) ;
38+ const targetsQuery = useQuery (
39+ benchmarkId ? benchmarkTargetsOptions ( benchmarkId ) : targetsOptions ,
40+ ) ;
41+ const runsQuery = useQuery ( benchmarkId ? benchmarkRunListOptions ( benchmarkId ) : runListOptions ) ;
42+ const targets = ( targetsQuery . data as TargetsResponse | undefined ) ?. targets ?? [ ] ;
43+ const runs = runsQuery . data ?. runs ?? [ ] ;
44+ const error = targetsQuery . error ?? runsQuery . error ;
45+ const isLoading = targetsQuery . isLoading || runsQuery . isLoading ;
46+
47+ const selectedTarget = useMemo (
48+ ( ) => targets . find ( ( target ) => target . name === selectedTargetName ) ?? null ,
49+ [ selectedTargetName , targets ] ,
50+ ) ;
51+
52+ useEffect ( ( ) => {
53+ if ( selectedTargetName && ! targets . some ( ( target ) => target . name === selectedTargetName ) ) {
54+ setSelectedTargetName ( null ) ;
55+ }
56+ } , [ selectedTargetName , targets ] ) ;
57+
58+ const experimentGroups = useMemo ( ( ) => {
59+ if ( ! selectedTarget ) return [ ] ;
60+
61+ const groups = new Map < string , RunMeta [ ] > ( ) ;
62+ for ( const run of runs ) {
63+ const targetName = run . target ?? 'default' ;
64+ if ( targetName !== selectedTarget . name ) continue ;
1365
14- export function TargetsTab ( ) {
15- const { data, isLoading } = useTargets ( ) ;
66+ const experimentName = run . experiment ?? 'default' ;
67+ const existing = groups . get ( experimentName ) ?? [ ] ;
68+ existing . push ( run ) ;
69+ groups . set ( experimentName , existing ) ;
70+ }
71+
72+ return [ ...groups . entries ( ) ]
73+ . map ( ( [ name , experimentRuns ] ) => buildExperimentGroup ( name , experimentRuns ) )
74+ . sort ( ( a , b ) => {
75+ if ( a . latestTimestamp && b . latestTimestamp && a . latestTimestamp !== b . latestTimestamp ) {
76+ return b . latestTimestamp . localeCompare ( a . latestTimestamp ) ;
77+ }
78+ if ( a . latestTimestamp ) return - 1 ;
79+ if ( b . latestTimestamp ) return 1 ;
80+ return a . name . localeCompare ( b . name ) ;
81+ } ) ;
82+ } , [ runs , selectedTarget ] ) ;
1683
1784 if ( isLoading ) {
1885 return < LoadingSkeleton /> ;
1986 }
2087
21- const targets = data ?. targets ?? [ ] ;
88+ if ( error ) {
89+ return (
90+ < div className = "rounded-lg border border-red-900/50 bg-red-950/20 p-6 text-red-400" >
91+ Failed to load targets: { error . message }
92+ </ div >
93+ ) ;
94+ }
2295
2396 if ( targets . length === 0 ) {
2497 return (
@@ -31,60 +104,191 @@ export function TargetsTab() {
31104 ) ;
32105 }
33106
34- return (
35- < div className = "overflow-hidden rounded-lg border border-gray-800" >
36- < table className = "w-full text-left text-sm" >
37- < thead className = "border-b border-gray-800 bg-gray-900/50" >
38- < tr >
39- < th className = "px-4 py-3 font-medium text-gray-400" > Target</ th >
40- < th className = "px-4 py-3 text-right font-medium text-gray-400" > Runs</ th >
41- < th className = "px-4 py-3 text-right font-medium text-gray-400" > Experiments</ th >
42- < th className = "px-4 py-3 font-medium text-gray-400" > Pass Rate</ th >
43- < th className = "px-4 py-3 text-right font-medium text-gray-400" > Evals</ th >
44- </ tr >
45- </ thead >
46- < tbody className = "divide-y divide-gray-800/50" >
47- { targets . map ( ( target : TargetSummary ) => (
48- < tr key = { target . name } className = "transition-colors hover:bg-gray-900/30" >
49- < td className = "px-4 py-3 font-medium text-gray-200" > { target . name } </ td >
50- < td className = "px-4 py-3 text-right tabular-nums text-gray-400" >
51- { target . run_count }
52- </ td >
53- < td className = "px-4 py-3 text-right tabular-nums text-gray-400" >
54- { target . experiment_count }
55- </ td >
56- < td className = "px-4 py-3" >
57- < PassRatePill rate = { target . pass_rate } />
58- </ td >
59- < td className = "px-4 py-3 text-right tabular-nums text-gray-400" >
60- < span className = "text-emerald-400" > { target . passed_count } </ span >
61- < span className = "text-gray-600" > /</ span >
62- < span > { target . eval_count } </ span >
63- </ td >
107+ if ( ! selectedTarget ) {
108+ return (
109+ < div className = "overflow-hidden rounded-lg border border-gray-800" >
110+ < table className = "w-full text-left text-sm" >
111+ < thead className = "border-b border-gray-800 bg-gray-900/50" >
112+ < tr >
113+ < th className = "px-4 py-3 font-medium text-gray-400" > Target</ th >
114+ < th className = "px-4 py-3 text-right font-medium text-gray-400" > Runs</ th >
115+ < th className = "px-4 py-3 text-right font-medium text-gray-400" > Experiments</ th >
116+ < th className = "px-4 py-3 font-medium text-gray-400" > Pass Rate</ th >
117+ < th className = "px-4 py-3 text-right font-medium text-gray-400" > Evals</ th >
64118 </ tr >
119+ </ thead >
120+ < tbody className = "divide-y divide-gray-800/50" >
121+ { targets . map ( ( target ) => (
122+ < tr key = { target . name } className = "transition-colors hover:bg-gray-900/30" >
123+ < td className = "px-4 py-3" >
124+ < button
125+ type = "button"
126+ onClick = { ( ) => setSelectedTargetName ( target . name ) }
127+ className = "font-medium text-cyan-400 hover:text-cyan-300 hover:underline"
128+ >
129+ { target . name }
130+ </ button >
131+ </ td >
132+ < td className = "px-4 py-3 text-right tabular-nums text-gray-400" >
133+ { target . run_count }
134+ </ td >
135+ < td className = "px-4 py-3 text-right tabular-nums text-gray-400" >
136+ { target . experiment_count }
137+ </ td >
138+ < td className = "px-4 py-3" >
139+ < PassRatePill rate = { target . pass_rate } />
140+ </ td >
141+ < td className = "px-4 py-3 text-right tabular-nums text-gray-400" >
142+ < span className = "text-emerald-400" > { target . passed_count } </ span >
143+ < span className = "text-gray-600" > / </ span >
144+ < span > { target . eval_count } </ span >
145+ </ td >
146+ </ tr >
147+ ) ) }
148+ </ tbody >
149+ </ table >
150+ </ div >
151+ ) ;
152+ }
153+
154+ return (
155+ < div className = "space-y-6" >
156+ < div className = "space-y-3" >
157+ < button
158+ type = "button"
159+ onClick = { ( ) => setSelectedTargetName ( null ) }
160+ className = "rounded-md px-3 py-1.5 text-sm text-gray-400 transition-colors hover:text-gray-200"
161+ >
162+ ← Back to targets
163+ </ button >
164+ < div className = "rounded-lg border border-gray-800 bg-gray-900/50 p-4" >
165+ < div className = "flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between" >
166+ < div >
167+ < h2 className = "text-xl font-semibold text-white" > { selectedTarget . name } </ h2 >
168+ < p className = "mt-1 text-sm text-gray-400" >
169+ { selectedTarget . run_count } run{ selectedTarget . run_count === 1 ? '' : 's' } ·{ ' ' }
170+ { selectedTarget . experiment_count } experiment
171+ { selectedTarget . experiment_count === 1 ? '' : 's' } ·{ ' ' }
172+ < span className = "text-emerald-400" > { selectedTarget . passed_count } </ span >
173+ < span className = "text-gray-600" > / </ span >
174+ { selectedTarget . eval_count } evals passed
175+ </ p >
176+ </ div >
177+ < div className = "w-full max-w-52" >
178+ < PassRatePill rate = { selectedTarget . pass_rate } />
179+ </ div >
180+ </ div >
181+ </ div >
182+ </ div >
183+
184+ { experimentGroups . length === 0 ? (
185+ < div className = "rounded-lg border border-gray-800 bg-gray-900 p-8 text-center" >
186+ < p className = "text-lg text-gray-400" > No runs found for this target</ p >
187+ < p className = "mt-2 text-sm text-gray-500" >
188+ This target summary exists, but there are no matching runs to group by experiment.
189+ </ p >
190+ </ div >
191+ ) : (
192+ < div className = "space-y-6" >
193+ { experimentGroups . map ( ( group ) => (
194+ < section key = { group . name } className = "space-y-3" >
195+ < div className = "flex flex-col gap-3 rounded-lg border border-gray-800 bg-gray-900/40 p-4 sm:flex-row sm:items-center sm:justify-between" >
196+ < div >
197+ < h3 className = "text-lg font-medium text-gray-200" >
198+ { formatExperimentName ( group . name ) }
199+ </ h3 >
200+ < p className = "mt-1 text-sm text-gray-400" >
201+ { group . runs . length } run{ group . runs . length === 1 ? '' : 's' } ·{ ' ' }
202+ < span className = "text-emerald-400" > { group . passedCount } </ span >
203+ < span className = "text-gray-600" > / </ span >
204+ { group . evalCount } evals passed
205+ { group . latestTimestamp && (
206+ < span className = "ml-2 text-gray-500" >
207+ · Last run { formatTimestamp ( group . latestTimestamp ) }
208+ </ span >
209+ ) }
210+ </ p >
211+ </ div >
212+ < div className = "w-full max-w-52" >
213+ < PassRatePill rate = { group . passRate } />
214+ </ div >
215+ </ div >
216+ < RunList runs = { group . runs } benchmarkId = { benchmarkId } />
217+ </ section >
65218 ) ) }
66- </ tbody >
67- </ table >
219+ </ div >
220+ ) }
68221 </ div >
69222 ) ;
70223}
71224
225+ function buildExperimentGroup ( name : string , runs : RunMeta [ ] ) : ExperimentRunGroup {
226+ let evalCount = 0 ;
227+ let passedCount = 0 ;
228+ let latestTimestamp : string | null = null ;
229+
230+ for ( const run of runs ) {
231+ evalCount += run . test_count ;
232+ passedCount += Math . round ( run . pass_rate * run . test_count ) ;
233+ if ( run . timestamp && ( ! latestTimestamp || run . timestamp > latestTimestamp ) ) {
234+ latestTimestamp = run . timestamp ;
235+ }
236+ }
237+
238+ return {
239+ name,
240+ runs,
241+ latestTimestamp,
242+ evalCount,
243+ passedCount,
244+ passRate : evalCount > 0 ? passedCount / evalCount : 0 ,
245+ } ;
246+ }
247+
248+ function formatExperimentName ( name : string ) : string {
249+ return name === 'default' ? 'Default experiment' : name ;
250+ }
251+
252+ function formatTimestamp ( ts : string ) : string {
253+ const date = new Date ( ts ) ;
254+ if ( Number . isNaN ( date . getTime ( ) ) ) return ts ;
255+
256+ const diffMs = Date . now ( ) - date . getTime ( ) ;
257+ const diffMin = Math . floor ( diffMs / 60_000 ) ;
258+ const diffHour = Math . floor ( diffMs / 3_600_000 ) ;
259+
260+ if ( diffMin < 1 ) return 'just now' ;
261+ if ( diffMin < 60 ) return `${ diffMin } min ago` ;
262+ if ( diffHour < 24 ) return `${ diffHour } hour${ diffHour === 1 ? '' : 's' } ago` ;
263+ return date . toLocaleDateString ( ) ;
264+ }
265+
72266function LoadingSkeleton ( ) {
73267 return (
74- < div className = "overflow-hidden rounded-lg border border-gray-800" >
75- < div className = "animate-pulse" >
76- < div className = "border-b border-gray-800 bg-gray-900/50 px-4 py-3" >
77- < div className = "h-4 w-48 rounded bg-gray-800" />
78- </ div >
79- { [ 'sk-1' , 'sk-2' , 'sk-3' , 'sk-4' , 'sk-5' ] . map ( ( id ) => (
80- < div key = { id } className = "flex gap-4 border-b border-gray-800/50 px-4 py-3" >
81- < div className = "h-4 w-32 rounded bg-gray-800" />
82- < div className = "h-4 w-12 rounded bg-gray-800" />
83- < div className = "h-4 w-12 rounded bg-gray-800" />
268+ < div className = "space-y-4" >
269+ < div className = "rounded-lg border border-gray-800 bg-gray-900/50 p-4" >
270+ < div className = "h-6 w-40 animate-pulse rounded bg-gray-800" />
271+ < div className = "mt-3 h-4 w-72 animate-pulse rounded bg-gray-800" />
272+ </ div >
273+ < div className = "overflow-hidden rounded-lg border border-gray-800" >
274+ < div className = "animate-pulse" >
275+ < div className = "border-b border-gray-800 bg-gray-900/50 px-4 py-3" >
84276 < div className = "h-4 w-48 rounded bg-gray-800" />
85- < div className = "h-4 w-20 rounded bg-gray-800" />
86277 </ div >
87- ) ) }
278+ { [ 'sk-1' , 'sk-2' , 'sk-3' , 'sk-4' , 'sk-5' ] . map ( ( id ) => (
279+ < div key = { id } className = "flex gap-4 border-b border-gray-800/50 px-4 py-3" >
280+ < div className = "h-4 w-32 rounded bg-gray-800" />
281+ < div className = "h-4 w-12 rounded bg-gray-800" />
282+ < div className = "h-4 w-12 rounded bg-gray-800" />
283+ < div className = "h-4 w-48 rounded bg-gray-800" />
284+ < div className = "h-4 w-20 rounded bg-gray-800" />
285+ </ div >
286+ ) ) }
287+ </ div >
288+ </ div >
289+ < div className = "rounded-lg border border-gray-800 bg-gray-900/40 p-4" >
290+ < div className = "h-5 w-48 animate-pulse rounded bg-gray-800" />
291+ < div className = "mt-3 h-4 w-56 animate-pulse rounded bg-gray-800" />
88292 </ div >
89293 </ div >
90294 ) ;
0 commit comments