Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export function StatusResult({
kind="secondary"
size="small"
>
Clear filters
Clear Filters
</Button>
) : undefined;
return renderEmptyState ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,18 @@ describe('EntityEmptyState', () => {
expect(screen.getByText(descriptor.subheading)).toBeInTheDocument();
});

it('toggles between the NeMo CLI command and the agent prompt', async () => {
it('toggles between the agent prompt and the NeMo CLI command', async () => {
const user = userEvent.setup();
wrap(<Guardrails onCreate={vi.fn()} />);

const help = screen.getByTestId('entity-empty-state-help');
// CLI is the default selection.
expect(help).toHaveTextContent(descriptor.cliCommand as string);
expect(help).not.toHaveTextContent(descriptor.skillPrompt as string);

await user.click(screen.getByRole('radio', { name: 'Ask an agent' }));
// Ask an agent is the default selection.
expect(help).toHaveTextContent(descriptor.skillPrompt as string);
expect(help).not.toHaveTextContent(descriptor.cliCommand as string);

await user.click(screen.getByRole('radio', { name: 'CLI' }));
expect(help).toHaveTextContent(descriptor.cliCommand as string);
expect(help).not.toHaveTextContent(descriptor.skillPrompt as string);
});

it('invokes onCreate from the primary CTA', async () => {
Expand Down Expand Up @@ -79,7 +79,7 @@ describe('EntityEmptyState', () => {
screen.queryByRole('button', { name: descriptor.createAction?.label })
).not.toBeInTheDocument();

await userEvent.click(screen.getByRole('button', { name: 'Clear filters' }));
await userEvent.click(screen.getByRole('button', { name: 'Clear Filters' }));
expect(onClearFilters).toHaveBeenCalledTimes(1);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const EntityEmptyState: FC<EntityEmptyStateProps> = ({
slotFooter={
onClearFilters ? (
<Button kind="tertiary" onClick={onClearFilters}>
Clear filters
Clear Filters
</Button>
) : null
}
Expand Down Expand Up @@ -135,7 +135,7 @@ const SelfServiceHelp: FC<{ cliCommand?: string; skillPrompt?: string }> = ({
skillPrompt,
}) => {
const toast = useToast();
const [kind, setKind] = useState<HelpKind>(cliCommand ? 'cli' : 'agent');
const [kind, setKind] = useState<HelpKind>(skillPrompt ? 'agent' : 'cli');
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const items: { value: HelpKind; children: React.ReactNode }[] = [];
if (skillPrompt)
Expand Down
247 changes: 244 additions & 3 deletions web/packages/common/src/components/EntityEmptyState/registry.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { ShieldCheck, type LucideIcon } from 'lucide-react';
import {
Anchor,
BrainCircuit,
ChartNetwork,
Database,
FlaskConical,
FolderOpen,
HatGlasses,
Lightbulb,
ListChecks,
LockKeyhole,
Radar,
Rocket,
ShieldCheck,
UsersRound,
VenetianMask,
type LucideIcon,
} from 'lucide-react';

/**
* A create call-to-action for a first-use empty state.
Expand Down Expand Up @@ -37,7 +54,34 @@ export interface EmptyStateDescriptor {
}

/** Keys of entities that have a standardized empty state. */
export type EntityKey = 'guardrails';
export type EntityKey =
| 'guardrails'
| 'guardrailChecks'
| 'filesets'
| 'filesetFiles'
| 'customModels'
| 'baseModels'
| 'deployments'
| 'inferenceProviders'
| 'virtualModels'
| 'secrets'
| 'members'
| 'jobs'
| 'anonymizerJobs'
| 'dataDesignerJobs'
| 'safeSynthesizerJobs'
| 'agentEvaluations'
| 'evaluationResults'
| 'evaluationSessions'
| 'experiments'
| 'evalComparison'
| 'optimizerInsights'
| 'insightExperiments'
| 'insightTraces'
| 'telemetryTraces'
| 'telemetrySpans'
| 'agentMonitorRuns'
| 'agents';

/**
* Canonical empty-state registry. Grows one entry at a time as entities migrate
Expand All @@ -50,8 +94,205 @@ export const ENTITY_EMPTY_STATES: Record<EntityKey, EmptyStateDescriptor> = {
subheading:
'Guardrail configs add content-safety, jailbreak, and PII rails to the models in this workspace.',
// Create is a modal owned by the route, so the callsite supplies `onCreate`.
createAction: { label: 'Create guardrail config' },
createAction: { label: 'Create Guardrail Config' },
cliCommand: 'nemo guardrail configs create <config-name>',
skillPrompt: 'Help me create my first guardrail config with the nemo-guardrails skill',
},
guardrailChecks: {
icon: ListChecks,
heading: 'No tests yet',
subheading: 'Add a test case on the Tests tab, then run it to see its result here.',
cliCommand:
'nemo guardrail check --config <config-name> --messages \'[{"role":"user","content":"<message>"}]\'',
skillPrompt: 'Help me verify my guardrail config with the nemo-guardrails skill',
},
filesets: {
icon: Database,
heading: 'No filesets yet',
subheading:
'Filesets group the files your agents and jobs read from — training data, models, or other artifacts.',
createAction: { label: 'Create Fileset' },
cliCommand: 'nemo files filesets create <fileset-name> --workspace <workspace>',
skillPrompt: 'Help me create my first fileset with the nemo-files skill',
},
filesetFiles: {
icon: FolderOpen,
heading: 'No files yet',
subheading: 'Upload files to this fileset to make them available to agents and jobs.',
createAction: { label: 'Upload Files' },
cliCommand: 'nemo files upload <local-path> --fileset <fileset-name> --workspace <workspace>',
skillPrompt: 'Help me upload files to a fileset with the nemo-files skill',
},
customModels: {
icon: BrainCircuit,
heading: 'No custom models yet',
subheading: 'Customize a model with fine-tuning or prompt tuning to meet your specific needs.',
createAction: { label: 'Customize Model' },
cliCommand: 'nemo customization automodel submit <job-spec>.json --workspace <workspace>',
skillPrompt: 'Help me create my first custom model with the nemo-customizer skill',
},
baseModels: {
icon: BrainCircuit,
heading: 'No base models available',
subheading:
'Registered inference providers in this workspace automatically surface their base models here.',
},
deployments: {
icon: Rocket,
heading: 'No deployments yet',
subheading: 'Deploy a custom model to serve it for inference.',
createAction: { label: 'Create Deployment' },
cliCommand: 'nemo inference deployments create <deployment-name> --input-file <config>.json',
skillPrompt: 'Help me deploy my first model with the nemo-build-agent skill',
},
inferenceProviders: {
icon: Radar,
heading: 'No inference providers yet',
subheading:
'Register an inference provider to make its models available for chat and evaluation.',
createAction: { label: 'Add Inference Provider' },
cliCommand:
'nemo inference providers create <provider-name> --workspace <workspace> --host-url "<host-url>" --api-key-secret-name "<secret-name>"',
skillPrompt: 'Help me create my first inference provider with the nemo-inference skill',
},
virtualModels: {
icon: Radar,
heading: 'No virtual models yet',
subheading:
'Virtual models route inference traffic across one or more providers, with optional switchyard and guardrail middleware.',
createAction: { label: 'Create Virtual Model' },
cliCommand:
'nemo inference virtual-models create <model-name> --workspace <workspace> --models \'[{"model":"<workspace>/<provider-model>","backend_format":"OPENAI_CHAT"}]\'',
skillPrompt: 'Help me create my first virtual model with the nemo-inference skill',
},
secrets: {
icon: LockKeyhole,
heading: 'No secrets yet',
subheading:
'Store API keys and credentials as secrets so providers and jobs can reference them securely.',
createAction: { label: 'Create Secret' },
cliCommand:
'nemo secrets create <secret-name> --value "<secret-value>" --workspace <workspace>',
skillPrompt: 'Help me create my first secret with the nemo-secrets skill',
},
members: {
icon: UsersRound,
heading: 'No members yet',
subheading:
'Add a member to grant Viewer, Editor, or Admin access beyond the implicit workspace owners.',
createAction: { label: 'Add Member' },
cliCommand:
'nemo workspaces members create --workspace <workspace-id> --principal <email> --roles <RoleName>',
},
jobs: {
icon: FlaskConical,
heading: 'No jobs yet',
subheading:
'Jobs from customization, evaluation, anonymization, and data generation appear here once submitted.',
},
anonymizerJobs: {
icon: VenetianMask,
heading: 'No anonymizer jobs yet',
subheading:
'Detect and protect PII in your datasets through context-aware replacement and rewriting.',
createAction: { label: 'Anonymize Data' },
cliCommand: 'nemo anonymizer run submit --spec-file <run-spec>.yaml --workspace <workspace>',
skillPrompt: 'Help me create my first anonymizer job with the nemo-anonymizer skill',
},
dataDesignerJobs: {
icon: Lightbulb,
heading: 'No Data Designer jobs yet',
subheading: 'Create and manage Data Designer jobs to generate or transform synthetic datasets.',
createAction: { label: 'New Job' },
cliCommand: 'nemo data-designer create run <config-path>.yaml --num-records <n>',
skillPrompt:
'Help me create my first synthetic dataset with the nemo-data-designer-plugin skill',
},
safeSynthesizerJobs: {
icon: ShieldCheck,
heading: 'No Safe Synthesizer jobs yet',
subheading: 'Generate a private version of a sensitive tabular dataset.',
createAction: { label: 'Synthesize Data' },
cliCommand:
'nemo safe-synthesizer run-local --workspace <workspace> --spec-file <job-spec>.json --data-source <input.csv> --output-dir <output-dir>',
skillPrompt:
'Help me create my first Safe Synthesizer run with the nemo-safe-synthesizer skill',
},
agentEvaluations: {
icon: FlaskConical,
heading: 'No evaluation jobs yet',
subheading:
'Apply a model_optimization suggestion or submit an evaluate-agent job to see results here.',
cliCommand:
'nemo evaluator agent-evaluate submit --spec-file <spec>.json --workspace <workspace>',
skillPrompt:
'Help me create my first agent evaluation with the nemo-nemo-evaluator-plugin skill',
},
evaluationResults: {
icon: FlaskConical,
heading: 'No evaluations yet',
subheading: 'Submit an evaluation job to score a model or agent against a benchmark.',
createAction: { label: 'Create Evaluation' },
cliCommand: 'nemo evaluator evaluate submit --spec-file <spec>.json --workspace <workspace>',
skillPrompt: 'Help me create my first evaluation with the nemo-nemo-evaluator-plugin skill',
},
evaluationSessions: {
icon: FlaskConical,
heading: 'No test cases',
subheading: 'Run an experiment to see test case results here.',
},
experiments: {
icon: FlaskConical,
heading: 'No experiments yet',
subheading: 'Log an experiment to compare evaluation runs across models and configurations.',
createAction: { label: 'Create Experiment' },
cliCommand: 'nemo experiments create <experiment-name> --input-file <config>.json',
skillPrompt: 'Help me log my first experiment with the nemo-experiments-upload skill',
},
evalComparison: {
icon: ChartNetwork,
heading: 'No evaluations selected',
subheading: 'Select evaluations to compare their results side by side.',
},
optimizerInsights: {
icon: Lightbulb,
heading: 'No insights yet',
subheading: 'Run an optimizer analysis on an agent to surface insights here.',
cliCommand: 'nemo agents optimize-skills run --spec-file <spec>.yml',
skillPrompt: 'Help me run my first optimizer analysis with the nemo-skills-optimization skill',
},
insightExperiments: {
icon: FlaskConical,
heading: 'No experiments yet',
subheading: 'This insight has no linked experiments yet.',
createAction: { label: 'Run Experiment' },
},
insightTraces: {
icon: Anchor,
heading: 'No traces yet',
subheading: 'This insight has no linked traces yet.',
},
telemetryTraces: {
icon: Anchor,
heading: 'No traces yet',
subheading: 'Trace summaries will appear here after spans are ingested.',
},
telemetrySpans: {
icon: Anchor,
heading: 'No spans yet',
subheading: 'Spans will appear here once your agent starts sending telemetry.',
},
agentMonitorRuns: {
icon: HatGlasses,
heading: 'No runs yet',
subheading:
'Agent invocations populate this list once telemetry reaches the nemo-agent-telemetry fileset.',
},
agents: {
icon: HatGlasses,
heading: 'No agents yet',
subheading: 'Build and deploy an agent to see it listed here.',
cliCommand: 'nemo agents create --name <agent-name> --agent-config <agent.yaml>',
skillPrompt: 'Help me create my first agent with the nemo-build-agent skill',
},
};
14 changes: 13 additions & 1 deletion web/packages/common/src/components/ErrorPanel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,19 @@ const getErrorCode = (error: unknown): string | undefined => {
* ```
*/
export const ErrorPanel: FC<ErrorPanelProps> = ({ title, errorMessage, attributes }) => {
const error = useRouteError();
// `useRouteError` (and the data-router hooks it calls internally) throws when there is no
// ancestor `RouterProvider`/data router — which is expected when `ErrorPanel` is rendered
// inline (e.g. a DataView's `renderErrorState`) rather than as a route `errorElement`. Degrade
// to `undefined` in that case instead of crashing the tree.
let error: unknown;
try {
// Called unconditionally every render; the try/catch only guards against a missing
// data-router ancestor, not conditional invocation.
// eslint-disable-next-line react-hooks/rules-of-hooks
error = useRouteError();
} catch {
error = undefined;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
aahunt-nv marked this conversation as resolved.
Outdated
const errorCode = getErrorCode(error);
const errorMessageInternal =
errorMessage ?? getRouterErrorMessage(error) ?? DEFAULT_ERROR_MESSAGE;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// SPDX-License-Identifier: Apache-2.0

import * as DataView from '@nemo/common/src/components/DataView/internal';
import { EntityEmptyState } from '@nemo/common/src/components/EntityEmptyState';
import { KVPair } from '@nemo/common/src/components/KVPair';
import { TableEmptyState } from '@nemo/common/src/components/TableEmptyState';
import { formatFileSize } from '@nemo/common/src/components/UploadModal/utils';
import { useToast } from '@nemo/common/src/providers/toast/useToast';
import { useFilesRetrieveFileset as useGetDataset } from '@nemo/sdk/generated/platform/api';
Expand Down Expand Up @@ -133,11 +133,7 @@ export const CustomizationFilesetDetailsPanel = ({ filesetUri }: Props) => {
<DataView.TableContent
className="studio-data-view-table"
renderEmptyState={() => (
<TableEmptyState
className="py-4"
header="No Dataset Details Found"
emptyMessage="No dataset details available."
/>
<EntityEmptyState entity="filesetFiles" variant="first-use" />
)}
/>
<DataView.Pagination />
Expand Down
Loading
Loading