Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions web/packages/studio/src/components/Breadcrumbs/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { Breadcrumbs } from '@studio/components/Breadcrumbs';
import { BreadcrumbsProvider } from '@studio/providers/breadcrumbs/BreadcrumbsProvider';
import {
useBreadcrumbs,
type BreadcrumbsItemProps,
} from '@studio/providers/breadcrumbs/useBreadcrumbs';
import { render, screen } from '@studio/tests/util/render';
import type { FC } from 'react';
import { MemoryRouter } from 'react-router';

const SetCrumbs: FC<{ items: BreadcrumbsItemProps[] }> = ({ items }) => {
useBreadcrumbs({ items });
return null;
};

const renderCrumbs = (items: BreadcrumbsItemProps[]) =>
render(
<MemoryRouter initialEntries={['/workspaces/default/agents/optimizations/sweep-3']}>
<BreadcrumbsProvider>
<SetCrumbs items={items} />
<Breadcrumbs />
</BreadcrumbsProvider>
</MemoryRouter>
);

describe('Breadcrumbs', () => {
it('strips the query string by default, so an "up" link does not leak detail state', () => {
renderCrumbs([{ slotLabel: 'Agents', href: '/workspaces/default/agents?tab=optimizations' }]);

expect(screen.getByRole('link', { name: 'Agents' })).toHaveAttribute(
'href',
'/workspaces/default/agents'
);
});

it('keeps the query string when the item opts in with preserveQuery', () => {
renderCrumbs([
{
slotLabel: 'Optimizations',
href: '/workspaces/default/agents/email-analyzer?tab=optimizations',
preserveQuery: true,
},
]);

// Without this the parent route falls back to its default tab — the bug this flag exists for.
expect(screen.getByRole('link', { name: 'Optimizations' })).toHaveAttribute(
'href',
'/workspaces/default/agents/email-analyzer?tab=optimizations'
);
});
});
5 changes: 3 additions & 2 deletions web/packages/studio/src/components/Breadcrumbs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { FC, useMemo } from 'react';
import { Link, useParams } from 'react-router';

// Breadcrumb links navigate "up" the hierarchy, so query/hash from the current detail context is irrelevant at the parent level and only leaks state.
// Items that need theirs — a parent whose tab comes from `?tab=` — opt out with `preserveQuery`.
const pathnameOnly = (href: string) => href.split(/[?#]/)[0];

export const Breadcrumbs: FC = () => {
Expand All @@ -20,8 +21,8 @@ export const Breadcrumbs: FC = () => {
allItems.push(WORKSPACE_BREADCRUMB_ITEM);
}
return allItems.concat(
breadcrumbs.map(({ href = '#', slotLabel }) => ({
children: <Link to={pathnameOnly(href)}>{slotLabel}</Link>,
breadcrumbs.map(({ href = '#', slotLabel, preserveQuery }) => ({
children: <Link to={preserveQuery ? href : pathnameOnly(href)}>{slotLabel}</Link>,
}))
);
}, [breadcrumbs, workspace]);
Expand Down
2 changes: 2 additions & 0 deletions web/packages/studio/src/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const ROUTE_PARAMS = {
agentName: 'agentName',
agentDeploymentName: 'agentDeploymentName',
agentEvalJobName: 'agentEvalJobName',
optimizeJobName: 'optimizeJobName',
jobName: 'jobName',
/** Benchmark entity name segment under evaluation/benchmarks/:name */
benchmarkName: 'benchmarkName',
Expand Down Expand Up @@ -145,6 +146,7 @@ export const ROUTES = {
agentDeploymentDetail: `/workspaces/:${P.workspace}/agent-deployments/:${P.agentDeploymentName}`,
/** Detail view for a single agent-evaluation job. */
agentEvaluationDetail: `/workspaces/:${P.workspace}/agents/evaluations/:${P.agentEvalJobName}`,
agentOptimizationDetail: `/workspaces/:${P.workspace}/agents/optimizations/:${P.optimizeJobName}`,
modelCompare: `/workspaces/:${P.workspace}/playground`,
agentMonitor: `/workspaces/:${P.workspace}/agents/monitor`,
/** Plugin-owned page; the plugin's internal router owns sub-paths via a `/*` suffix. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const BreadcrumbsContext = createContext<BreadcrumbsContextValue | null>(
export type BreadcrumbsItemProps = {
href?: string;
slotLabel: ReactNode;
preserveQuery?: boolean;
};

export type BreadCrumbItemsProps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useAgentsListOptimizeJobs } from '@nemo/sdk/generated/agents/agents';
import type { OptimizeJob, OptimizeJobsListFilter } from '@nemo/sdk/generated/agents/schema';
import { Banner, Text } from '@nvidia/foundations-react-core';
import { useWorkspaceFromPath } from '@studio/hooks/useWorkspaceFromPath';
import { getWorkspaceJobDetailRoute } from '@studio/routes/utils';
import { getAgentOptimizationDetailRoute } from '@studio/routes/utils';
import { keepPreviousData } from '@tanstack/react-query';
import { type ComponentProps, type FC, useCallback } from 'react';
import { useNavigate } from 'react-router';
Expand Down Expand Up @@ -133,7 +133,7 @@ export const OptimizeJobsTable: FC<OptimizeJobsTableProps> = ({ agentName }) =>
dataViewState={dataViewState}
searchField="name"
makeColumns={makeColumns}
onRowClick={(row) => navigate(getWorkspaceJobDetailRoute(workspace, row.name))}
onRowClick={(row) => navigate(getAgentOptimizationDetailRoute(workspace, row.name))}
attributes={{
DataViewSearchBar: { placeholder: 'Search by name...' },
DataViewRoot: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,11 @@ export const AgentEvaluationDetailRoute: FC = () => {
setBreadcrumbs([
{ slotLabel: 'Agents', href: getAgentsListRoute(workspace) },
agentName
? { slotLabel: 'Evaluations', href: getAgentEvaluationsTabRoute(workspace, agentName) }
? {
slotLabel: 'Evaluations',
href: getAgentEvaluationsTabRoute(workspace, agentName),
preserveQuery: true,
}
: { slotLabel: 'Evaluations' },
{ slotLabel: jobName },
]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { StatTile } from '@nemo/common/src/components/StatTile';
import { formatDurationMs } from '@nemo/common/src/utils/date';
import { Grid } from '@nvidia/foundations-react-core';
import type { StudyResults } from '@studio/routes/agents/AgentOptimizationDetailRoute/studyResults';
import type { FC } from 'react';

const EM_DASH = '—';

/** Optuna's `TrialState.COMPLETE`, as written to the `state` column. */
const COMPLETE = 'COMPLETE';

const formatScore = (value: number | null): string =>
value === null ? EM_DASH : value.toLocaleString(undefined, { maximumFractionDigits: 4 });

export interface StudyStatTilesProps {
results: StudyResults;
}

export const StudyStatTiles: FC<StudyStatTilesProps> = ({ results }) => {
const { summary, trials, metricNames } = results;

const totalTrials = summary?.nTrials ?? trials.length;
const frontierCount = trials.filter((trial) => trial.paretoOptimal).length;

const primaryMetric = metricNames[0];
const bestValue = summary?.bestValues[0] ?? null;

const timedTrials = trials.filter(
(trial) => trial.state === COMPLETE && trial.durationSeconds !== null
);
const averageDurationMs = timedTrials.length
? (timedTrials.reduce((total, trial) => total + (trial.durationSeconds ?? 0), 0) /
timedTrials.length) *
1_000
: null;

return (
<Grid cols={{ base: 1, md: 2, xl: 4 }} gap="density-xl" className="shrink-0">
<StatTile
variant="metric"
label="Best score"
value={formatScore(bestValue)}
trailingLabel={primaryMetric}
/>
<StatTile
variant="metric"
label="Trials"
value={totalTrials ? String(totalTrials) : EM_DASH}
/>
<StatTile
variant="metric"
label="Trials on frontier"
value={trials.length ? String(frontierCount) : EM_DASH}
/>
<StatTile
variant="metric"
label="Avg trial duration"
value={averageDurationMs === null ? EM_DASH : formatDurationMs(averageDurationMs)}
/>
</Grid>
);
};
Loading
Loading