Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { Meta, StoryObj } from '@storybook/react';
import { AddModelPalette } from '@studio/components/AddModelPalette';
import type { BuilderModel } from '@studio/routes/DataDesignerJobBuildRoute/models';

const meta = {
component: AddModelPalette,
title: 'Components/AddModelPalette',
parameters: {
layout: 'fullscreen',
},
args: {
modelGroups: [],
onAddModel: () => {},
onSelectModel: () => {},
},
decorators: [
(Story) => (
<div className="h-dvh w-[264px] border-r border-base bg-surface-base p-4">
<Story />
</div>
),
],
} satisfies Meta<typeof AddModelPalette>;

export default meta;
type Story = StoryObj<typeof meta>;

const models: BuilderModel[] = [
{
id: 'model-0',
alias: 'default',
model: 'openai/gpt-4o-mini',
provider: 'openai',
inferenceParams: { temperature: 0.7 },
},
{
id: 'model-1',
alias: 'judge',
model: 'meta/llama-3.1-70b-instruct',
provider: 'nvidia',
inferenceParams: { temperature: 0, max_tokens: 1024 },
},
];

/** A few configured models, the second selected for editing. */
export const WithModels: Story = {
args: { models, selectedId: 'model-1' },
};

export const Empty: Story = {
args: { models: [], selectedId: null },
};
81 changes: 81 additions & 0 deletions web/packages/studio/src/components/AddModelPalette/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { ModelWorkspaceGroup } from '@nemo/common/src/api/models/useModels';
import { ModelSelectV2 } from '@nemo/common/src/components/ModelSelectV2/ModelSelectV2';
import type { ModelSelection } from '@nemo/common/src/components/ModelSelectV2/types';
import { Stack, Text } from '@nvidia/foundations-react-core';
import { CardIconBadge, SelectableCard } from '@studio/components/common/SelectableCard';
import {
type BuilderModel,
providerForModel,
} from '@studio/routes/DataDesignerJobBuildRoute/models';
import { Cpu } from 'lucide-react';
import type { FC } from 'react';

export interface AddModelPaletteProps {
models: BuilderModel[];
selectedId?: string | null;
modelGroups: ModelWorkspaceGroup[];
isLoadingModels?: boolean;
onAddModel: (selection: ModelSelection, provider: string) => void;
onSelectModel: (id: string) => void;
className?: string;
}
export const AddModelPalette: FC<AddModelPaletteProps> = ({
models,
selectedId,
modelGroups,
isLoadingModels,
onAddModel,
onSelectModel,
className,
}) => (
<Stack gap="density-lg" className={`flex h-full min-h-0 flex-col ${className ?? ''}`}>
<Stack gap="density-xxs" className="shrink-0">
<Text kind="body/bold/md">Models</Text>
<Text kind="body/regular/xs" className="text-secondary">
Referenced by LLM columns via their model alias
</Text>
</Stack>

<div className="shrink-0">
<ModelSelectV2
value={null}
onValueChange={(selection) =>
onAddModel(selection, providerForModel(modelGroups, selection.model))
}
groups={modelGroups}
loading={isLoadingModels}
placeholder="Add a model"
fullWidth
dropdownSide="bottom"
aria-label="Add a model"
/>
</div>

<Stack gap="1.5" className="min-h-0 flex-1 overflow-y-auto">
{models.length === 0 ? (
<Text kind="body/regular/sm" className="text-secondary">
No models yet. Add one to reference it from an LLM column.
</Text>
) : (
models.map((model) => (
<SelectableCard
key={model.id}
title={model.alias || 'Untitled model'}
subtitle={model.model || 'No model set'}
selected={model.id === selectedId}
onActivate={() => onSelectModel(model.id)}
className="w-full"
leading={
<CardIconBadge>
<Cpu size={15} className="text-accent-teal" aria-hidden />
</CardIconBadge>
}
/>
))
)}
</Stack>
</Stack>
);
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const ColumnConfigPanel: FC<ColumnConfigPanelProps> = ({
}) => {
const { option, name, values } = column;
const { icon: Icon, label, description, color } = option;
const fields = getColumnFields(option.columnType);
const fields = getColumnFields(option);
const nameError = validateColumnName(name, takenNames);

const setValue = (key: string, value: string) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import { SamplerType } from '@nemo/sdk/generated/data-designer/schema';
import type { FilesetTemplate } from '@studio/components/CreateFilesetStart/types';
import { DEFAULT_BUILD_MODEL_NAME } from '@studio/constants/constants';
import { GraduationCap } from 'lucide-react';

/**
Expand All @@ -23,6 +24,10 @@ export const FILESET_TEMPLATES: FilesetTemplate[] = [
columnType: 'sampler',
samplerType: SamplerType.category,
name: 'domain',
values: {
values:
'science, technology, history, arts, business, health, education, sports, travel, cooking',
},
},
{
columnType: 'llm-text',
Expand All @@ -43,6 +48,7 @@ export const FILESET_TEMPLATES: FilesetTemplate[] = [
},
},
],
models: [{ alias: 'default', model: DEFAULT_BUILD_MODEL_NAME }],
},
];

Expand Down
19 changes: 19 additions & 0 deletions web/packages/studio/src/components/CreateFilesetStart/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { InferenceParams } from '@nemo/sdk/generated/platform/schema';
import type { BadgeProps } from '@nvidia/foundations-react-core';
import type { AddColumnSelection } from '@studio/components/AddColumnPalette/types';
import type { LucideIcon } from 'lucide-react';
Expand Down Expand Up @@ -34,6 +35,22 @@ export interface TemplateColumnSpec extends AddColumnSelection {
values?: Record<string, string>;
}

/** Picking one preloads the build canvas with its columns and any models they reference. */

export interface TemplateModelSpec {
/** Alias the template's columns reference via `model_alias`. */
alias: string;
/** Preferred model URN (e.g. `nvidia/llama-3.3-nemotron-super-49b-v1.5`); optional. */
model?: string;
/** Optional inference parameter defaults. */
inferenceParams?: Partial<InferenceParams>;
}

/**
* A ready-made recipe shown as a card in the secondary area when the "Start from a
* template" option is selected. Picking one preloads the build canvas with its columns
* and any models they reference.
*/
export interface FilesetTemplate {
/** Stable id passed to {@link CreateFilesetStartProps.onContinue} when chosen. */
id: string;
Expand All @@ -42,6 +59,8 @@ export interface FilesetTemplate {
icon: LucideIcon;
tag: StartOptionTag;
columns: TemplateColumnSpec[];
/** Models preloaded into the job config, referenced by the columns' `model_alias`. */
models?: TemplateModelSpec[];
}

export interface TemplateCardProps {
Expand Down
123 changes: 123 additions & 0 deletions web/packages/studio/src/components/ModelConfigPanel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { ModelWorkspaceGroup } from '@nemo/common/src/api/models/useModels';
import { ModelSelectV2 } from '@nemo/common/src/components/ModelSelectV2/ModelSelectV2';
import type { ModelSelection } from '@nemo/common/src/components/ModelSelectV2/types';
import type { InferenceParams } from '@nemo/sdk/generated/platform/schema';
import { Button, Flex, FormField, Stack, Text, TextInput } from '@nvidia/foundations-react-core';
import { CardIconBadge } from '@studio/components/common/SelectableCard';
import {
type BuilderModel,
type BuilderModelPatch,
providerForModel,
validateModelAlias,
} from '@studio/routes/DataDesignerJobBuildRoute/models';
import { Cpu, Trash2, X } from 'lucide-react';
import type { FC } from 'react';

export interface ModelConfigPanelProps {
model: BuilderModel;
takenAliases: Set<string>;
modelGroups: ModelWorkspaceGroup[];
isLoadingModels?: boolean;
onChange: (patch: BuilderModelPatch) => void;
onRemove: () => void;
onClose: () => void;
}

/** Right-hand config panel for a model — sibling of ColumnConfigPanel, same inline layout. */
export const ModelConfigPanel: FC<ModelConfigPanelProps> = ({
model,
takenAliases,
modelGroups,
isLoadingModels,
onChange,
onRemove,
onClose,
}) => {
const aliasError = validateModelAlias(model.alias, takenAliases);
const modelValue: ModelSelection | null = model.model ? { model: model.model } : null;

const handleModelChange = (selection: ModelSelection) =>
onChange({ model: selection.model, provider: providerForModel(modelGroups, selection.model) });
const handleParamsChange = (params: Partial<InferenceParams>) =>
onChange({ inferenceParams: params });

return (
<aside
aria-label={`Configure ${model.alias || 'model'}`}
className="flex h-full w-full flex-col bg-surface-base"
>
<Flex
align="start"
justify="between"
gap="density-md"
className="shrink-0 border-b border-base p-density-lg"
>
<Flex align="center" gap="density-sm" className="min-w-0">
<CardIconBadge>
<Cpu size={16} className="text-accent-teal" aria-hidden />
</CardIconBadge>
<Stack gap="density-xxs" className="min-w-0">
<Text kind="body/bold/md" className="truncate">
Model
</Text>
<Text kind="body/regular/xs" className="text-secondary truncate">
Referenced by LLM columns via its alias
</Text>
</Stack>
</Flex>
<Button
kind="tertiary"
color="neutral"
size="small"
aria-label="Close model config"
onClick={onClose}
>
<X size={16} aria-hidden />
</Button>
</Flex>

<Stack gap="density-lg" padding="density-lg" className="min-h-0 flex-1 overflow-y-auto">
<FormField
slotLabel="Alias"
required
slotInfo="LLM columns reference this model via their model alias."
status={model.alias && aliasError ? 'error' : undefined}
slotError={model.alias ? (aliasError ?? undefined) : undefined}
>
<TextInput
value={model.alias}
onValueChange={(value) => onChange({ alias: value })}
placeholder="e.g. default"
attributes={{ Input: { 'aria-label': 'Model alias' } }}
/>
</FormField>

<FormField slotLabel="Model" required slotInfo="Model and inference parameters.">
<ModelSelectV2
value={modelValue}
onValueChange={handleModelChange}
groups={modelGroups}
loading={isLoadingModels}
placeholder="Select a model"
showParams
fullWidth
dropdownSide="bottom"
inferenceParams={model.inferenceParams}
onInferenceParamsChange={handleParamsChange}
aria-label="Model selector"
/>
</FormField>
</Stack>

<Flex align="center" justify="start" className="shrink-0 border-t border-base p-density-lg">
<Button kind="tertiary" color="danger" size="small" onClick={onRemove}>
<Trash2 size={16} aria-hidden />
Remove model
</Button>
</Flex>
</aside>
);
};
Loading