Skip to content

Commit 42b4009

Browse files
authored
feat(studio): surface active eval jobs as a dedicated table (#1397)
Signed-off-by: Nathan Walston <nwalston@nvidia.com>
1 parent 965a736 commit 42b4009

3 files changed

Lines changed: 59 additions & 15 deletions

File tree

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { SegmentedControl, Stack } from '@nvidia/foundations-react-core';
4+
import { PlatformJobTerminalStatuses } from '@nemo/common/src/constants/query';
5+
import { SegmentedControl, Stack, Text } from '@nvidia/foundations-react-core';
56
import type { EvalJobRow } from '@studio/api/evaluation/utils';
67
import { EvaluationsTable } from '@studio/routes/agents/AgentDetailRoute/evaluations/EvaluationsTable';
78
import { ExperimentsTable } from '@studio/routes/agents/AgentDetailRoute/evaluations/ExperimentsTable';
@@ -12,10 +13,8 @@ import { type FC, useMemo, useState } from 'react';
1213

1314
const VIEW_EVALUATIONS = 'evaluations';
1415
const VIEW_EXPERIMENTS = 'experiments';
15-
const VIEW_JOBS = 'jobs';
1616

1717
const VIEW_ITEMS = [
18-
{ value: VIEW_JOBS, children: 'Active Jobs' },
1918
{ value: VIEW_EVALUATIONS, children: 'Completed Evaluations' },
2019
{ value: VIEW_EXPERIMENTS, children: 'Experiments' },
2120
];
@@ -26,15 +25,27 @@ interface EvaluationsTabProps {
2625
jobs: EvalJobRow[];
2726
}
2827

29-
/** Three readings of the same work: published evaluations flat, rolled up by experiment, or the
30-
* jobs that produced them. Jobs are separate because they answer a different question — what is
31-
* running right now — which Intake cannot answer until a run publishes. */
28+
/** Two readings of the same published work — flat evaluations or rolled up by experiment — with
29+
* the jobs still running pinned above them. Active jobs answer a different question ("what is
30+
* running right now") that Intake cannot answer until a run publishes, so they get their own
31+
* always-visible section instead of a segmented-control tab. */
3232
export const EvaluationsTab: FC<EvaluationsTabProps> = ({ workspace, evals, jobs }) => {
33-
const [view, setView] = useState<string>(VIEW_JOBS);
33+
const [view, setView] = useState<string>(VIEW_EVALUATIONS);
3434
const experiments = useMemo(() => groupByExperiment(evals), [evals]);
35+
const activeJobs = useMemo(
36+
() =>
37+
jobs.filter((job) => !PlatformJobTerminalStatuses.some((status) => status === job.status)),
38+
[jobs]
39+
);
3540

3641
return (
3742
<Stack gap="density-lg" className="w-full">
43+
{activeJobs.length > 0 && (
44+
<Stack gap="density-sm">
45+
<Text kind="title/sm">Active jobs</Text>
46+
<JobsTable workspace={workspace} jobs={activeJobs} evaluations={evals} />
47+
</Stack>
48+
)}
3849
<SegmentedControl
3950
className="w-fit"
4051
aria-label="Evaluation view"
@@ -45,8 +56,9 @@ export const EvaluationsTab: FC<EvaluationsTabProps> = ({ workspace, evals, jobs
4556
{view === VIEW_EXPERIMENTS && (
4657
<ExperimentsTable workspace={workspace} experiments={experiments} />
4758
)}
48-
{view === VIEW_JOBS && <JobsTable workspace={workspace} jobs={jobs} evaluations={evals} />}
49-
{view === VIEW_EVALUATIONS && <EvaluationsTable workspace={workspace} evaluations={evals} />}
59+
{view === VIEW_EVALUATIONS && (
60+
<EvaluationsTable workspace={workspace} evaluations={evals} jobs={jobs} />
61+
)}
5062
</Stack>
5163
);
5264
};

web/packages/studio/src/routes/agents/AgentDetailRoute/evaluations/EvaluationsTable.tsx

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState';
1010
import { useStudioDataViewState } from '@nemo/common/src/hooks/useStudioDataViewState';
1111
import { deleteEvaluation, getListEvaluationsQueryKey } from '@nemo/sdk/generated/platform/api';
1212
import { Button, Flex, Text } from '@nvidia/foundations-react-core';
13+
import { type EvalJobRow, evalJobDetailRoute } from '@studio/api/evaluation/utils';
1314
import { BulkDeleteModal } from '@studio/components/BulkDeleteModal';
1415
import {
1516
evaluatorScores,
@@ -20,20 +21,30 @@ import type { AgentEvaluationRow } from '@studio/routes/agents/AgentDetailRoute/
2021
import { getEvaluationDetailRoute } from '@studio/routes/utils';
2122
import { useQueryClient } from '@tanstack/react-query';
2223
import { FlaskConical, Trash } from 'lucide-react';
23-
import { type ComponentProps, type FC, useCallback, useState } from 'react';
24+
import { type ComponentProps, type FC, useCallback, useMemo, useState } from 'react';
2425
import { useNavigate } from 'react-router';
2526

2627
interface EvaluationsTableProps {
2728
workspace: string;
2829
evaluations: AgentEvaluationRow[];
30+
/** All evaluator jobs for the agent (any status), used to link a published evaluation back to
31+
* the job that produced it. Absent until the reverse join finds a match. */
32+
jobs: EvalJobRow[];
2933
}
3034

3135
/** Every published evaluation for the agent, ungrouped. */
32-
export const EvaluationsTable: FC<EvaluationsTableProps> = ({ workspace, evaluations }) => {
36+
export const EvaluationsTable: FC<EvaluationsTableProps> = ({ workspace, evaluations, jobs }) => {
3337
const navigate = useNavigate();
3438
const queryClient = useQueryClient();
3539
const dataViewState = useStudioDataViewState();
3640
const [deleteRows, setDeleteRows] = useState<AgentEvaluationRow[]>([]);
41+
// Reverse of JobsTable's job -> evaluation link: a completed job carries the evaluation it
42+
// published to, so index by that name to recover the job from an evaluation row.
43+
const jobByEvaluation = useMemo(() => {
44+
const map: Record<string, EvalJobRow> = {};
45+
for (const job of jobs) if (job.evaluationName) map[job.evaluationName] = job;
46+
return map;
47+
}, [jobs]);
3748

3849
const handleDelete = useCallback(
3950
async (rows: AgentEvaluationRow[]) => {
@@ -55,7 +66,7 @@ export const EvaluationsTable: FC<EvaluationsTableProps> = ({ workspace, evaluat
5566

5667
const makeColumns: ComponentProps<typeof StudioDataView<AgentEvaluationRow>>['makeColumns'] =
5768
useCallback(
58-
({ accessor }, { rowSelectionColumn }) => [
69+
({ accessor }, { rowSelectionColumn, rowActionsColumn }) => [
5970
rowSelectionColumn({ size: ROW_SELECTION_COLUMN_SIZE }),
6071
accessor('name', {
6172
header: 'Evaluation',
@@ -109,8 +120,21 @@ export const EvaluationsTable: FC<EvaluationsTableProps> = ({ workspace, evaluat
109120
cell: ({ row }) =>
110121
row.original.created_at ? <RelativeTime datetime={row.original.created_at} /> : '—',
111122
}),
123+
rowActionsColumn({
124+
rowActions: (row) => {
125+
const job = jobByEvaluation[row.name];
126+
return job
127+
? [
128+
{
129+
children: 'View job',
130+
onSelect: () => navigate(evalJobDetailRoute(workspace, job)),
131+
},
132+
]
133+
: false;
134+
},
135+
}),
112136
],
113-
[]
137+
[jobByEvaluation, navigate, workspace]
114138
);
115139

116140
return (

web/packages/studio/src/routes/agents/AgentDetailRoute/evaluations/JobsTable.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export const JobsTable: FC<JobsTableProps> = ({ workspace, jobs, evaluations })
7979
);
8080

8181
const makeColumns: ComponentProps<typeof StudioDataView<EvalJobRow>>['makeColumns'] = useCallback(
82-
({ accessor }) => [
82+
({ accessor }, { rowActionsColumn }) => [
8383
accessor('name', {
8484
header: 'Job',
8585
cell: ({ row }) => <Text title={row.original.name}>{row.original.name}</Text>,
@@ -115,8 +115,16 @@ export const JobsTable: FC<JobsTableProps> = ({ workspace, jobs, evaluations })
115115
<DurationCell row={row.original} durationMs={getValue<number | undefined>()} />
116116
),
117117
}),
118+
rowActionsColumn({
119+
rowActions: (row) => [
120+
{
121+
children: 'View job',
122+
onSelect: () => navigate(evalJobDetailRoute(workspace, row)),
123+
},
124+
],
125+
}),
118126
],
119-
[durationMsFor]
127+
[durationMsFor, navigate, workspace]
120128
);
121129

122130
return (

0 commit comments

Comments
 (0)