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,66 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { Badge, Flex, Text } from '@nvidia/foundations-react-core';
import type { StartOption } from '@studio/components/CreateFilesetStart/types';
import type { FC } from 'react';

export interface StartOptionCardProps {
option: StartOption;
/** Whether this tile reads as selected (draws the brand-green border). */
selected: boolean;
/** Fired on click / keyboard activation. Only invoked for enabled options. */
onSelect: () => void;
}

/**
* A single "How do you want to start?" tile: a leading icon badge above a title,
* description, and a metadata badge, in a bordered card rendered as a `<button>`.
*
* Disabled options are still shown so the full set of entry points is visible, but
* they are inert — no hover affordance, no selection, and `aria-disabled`.
*/
export const StartOptionCard: FC<StartOptionCardProps> = ({ option, selected, onSelect }) => {
const Icon = option.icon;
const interactive = option.enabled;

const stateClasses = !interactive
? 'cursor-not-allowed border-base opacity-50'
: selected
? 'cursor-pointer border-[#76b900]'
: 'cursor-pointer border-base hover:-translate-y-0.5 hover:border-[#76b900] hover:bg-surface-hover hover:shadow-md';

return (
<button
type="button"
onClick={interactive ? onSelect : undefined}
aria-pressed={interactive ? selected : undefined}
aria-disabled={!interactive}
className={`flex h-[240px] w-full flex-col items-start gap-3 rounded-md border bg-surface-raised p-5 text-left transition focus-visible:border-strong focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#76b900] ${stateClasses}`}
>
<Flex
align="center"
justify="center"
className="size-10 shrink-0 rounded-md bg-surface-sunken"
>
<Icon size={20} className="text-primary" aria-hidden />
</Flex>

<Text kind="body/bold/md" className="text-primary">
{option.title}
</Text>

<Text kind="body/regular/sm" className="text-secondary">
{option.description}
</Text>

<div className="flex-1" />

{option.tag ? (
<Badge color={option.tag.color} kind={option.tag.kind}>
{option.tag.label}
</Badge>
) : null}
</button>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { Divider, Flex, Stack, Text } from '@nvidia/foundations-react-core';
import type { StartOption } from '@studio/components/CreateFilesetStart/types';
import { Layers, Sparkles, Wand2 } from 'lucide-react';
import type { FC, ReactNode } from 'react';

interface DetailPoint {
icon: typeof Layers;
title: string;
description: string;
}

const SCRATCH_POINTS: DetailPoint[] = [
{
icon: Layers,
title: 'Add columns block by block',
description:
'Drop in samplers, LLM generations, transforms and validators in any order on an empty canvas.',
},
{
icon: Wand2,
title: 'Wire columns together',
description: 'Reference earlier columns in prompts and expressions to build up each record.',
},
{
icon: Sparkles,
title: 'Preview and run',
description: 'Generate a sample at any time, tweak, and run the full job when it looks right.',
},
];

/** Per-option content for the section that appears below the tiles once a tile is selected. */
const DETAIL_CONTENT: Partial<Record<StartOption['id'], ReactNode>> = {
scratch: (
<Flex gap="density-md" className="w-full flex-wrap">
{SCRATCH_POINTS.map(({ icon: Icon, title, description }) => (
<Stack
key={title}
gap="density-xs"
className="min-w-[260px] flex-1 rounded-md border border-base bg-surface-raised p-5"
>
<Flex
align="center"
justify="center"
className="size-8 shrink-0 rounded-md bg-surface-sunken"
>
<Icon size={16} className="text-primary" aria-hidden />
</Flex>
<Text kind="body/semibold/sm" className="text-primary">
{title}
</Text>
<Text kind="body/regular/sm" className="text-secondary">
{description}
</Text>
</Stack>
))}
</Flex>
),
};

export interface StartOptionDetailProps {
option: StartOption;
}

/**
* The lower half of the new-fileset view. Its content changes based on the selected
* tile. Today only "Build from scratch" is wired up; selecting it shows what the empty
* canvas offers.
*/
export const StartOptionDetail: FC<StartOptionDetailProps> = ({ option }) => {
const content = DETAIL_CONTENT[option.id];
if (!content) {
return null;
}

return (
<Stack gap="density-md" className="w-full">
<Divider />
<Text kind="label/bold/sm" className="text-secondary">
{option.title}
</Text>
{content}
</Stack>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { StartOption } from '@studio/components/CreateFilesetStart/types';
import { LayoutGrid, Plus, Sparkles } from 'lucide-react';

/**
* The "How do you want to start?" tiles, in display order. Only "Build from scratch"
* is enabled today; the others are placeholders for upcoming entry points.
*/
export const START_OPTIONS: StartOption[] = [
{
id: 'ai',
title: 'Describe with AI',
description:
'Tell us what you need in plain language. AI drafts the columns and prompts — then you refine everything visually.',
icon: Sparkles,
enabled: false,
},
{
id: 'template',
title: 'Start from a template',
description: 'Pick a ready-made recipe for SFT, classification, RAG eval, tool-use and more.',
icon: LayoutGrid,
tag: { label: '8 recipes', color: 'blue', kind: 'outline' },
enabled: false,
},
{
id: 'scratch',
title: 'Build from scratch',
description: 'Open an empty canvas and add columns block by block, your way.',
icon: Plus,
enabled: true,
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { CreateFilesetStart } from '@studio/components/CreateFilesetStart';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

describe('CreateFilesetStart', () => {
it('renders all four start options', () => {
render(<CreateFilesetStart onContinue={vi.fn()} />);

expect(screen.getByText('Describe with AI')).toBeInTheDocument();
expect(screen.getByText('Start from a template')).toBeInTheDocument();
expect(screen.getByText('Build from scratch')).toBeInTheDocument();
});

it('shows no Continue footer until a selectable option is chosen', () => {
render(<CreateFilesetStart onContinue={vi.fn()} />);

expect(screen.queryByRole('button', { name: /continue/i })).not.toBeInTheDocument();
});

it('does not select disabled options (they are no-ops)', async () => {
const user = userEvent.setup();
const onContinue = vi.fn();
render(<CreateFilesetStart onContinue={onContinue} />);

await user.click(screen.getByText('Describe with AI'));

expect(screen.queryByRole('button', { name: /continue/i })).not.toBeInTheDocument();
expect(onContinue).not.toHaveBeenCalled();
});

it('selecting Build from scratch reveals Continue and invokes onContinue with "scratch"', async () => {
const user = userEvent.setup();
const onContinue = vi.fn();
render(<CreateFilesetStart onContinue={onContinue} />);

await user.click(screen.getByText('Build from scratch'));

const continueButton = screen.getByRole('button', { name: /continue/i });
expect(continueButton).toBeInTheDocument();

await user.click(continueButton);
expect(onContinue).toHaveBeenCalledTimes(1);
expect(onContinue).toHaveBeenCalledWith('scratch');
});
});
79 changes: 79 additions & 0 deletions web/packages/studio/src/components/CreateFilesetStart/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import {
Block,
Button,
Flex,
Grid,
GridItem,
PageHeader,
Stack,
Text,
} from '@nvidia/foundations-react-core';
import { START_OPTIONS } from '@studio/components/CreateFilesetStart/constants';
import { StartOptionCard } from '@studio/components/CreateFilesetStart/StartOptionCard';
import { StartOptionDetail } from '@studio/components/CreateFilesetStart/StartOptionDetail';
import type { StartOptionId } from '@studio/components/CreateFilesetStart/types';
import { ArrowRight } from 'lucide-react';
import { useState, type FC } from 'react';

export interface CreateFilesetStartProps {
/** Fired when the user confirms a selected start option via the Continue footer. */
onContinue: (optionId: StartOptionId) => void;
}

/**
* The Data Designer "Create a fileset" landing view: a row of start-option tiles whose
* lower half changes with the selection, plus a bottom-anchored footer with a Continue
* action that appears once a (selectable) tile is chosen.
*/
export const CreateFilesetStart: FC<CreateFilesetStartProps> = ({ onContinue }) => {
const [selectedId, setSelectedId] = useState<StartOptionId | null>(null);
const selectedOption = START_OPTIONS.find((option) => option.id === selectedId) ?? null;

return (
<Stack className="h-full">
<Block className="flex-1 overflow-auto">
<Stack gap="density-2xl" padding="density-2xl">
<PageHeader
slotHeading="Create a fileset"
slotDescription="Generate synthetic data visually — no JSON to write. Start from a template, clone a fileset you already built, or describe what you need and let AI lay out the columns."
/>

<Stack gap="density-md">
<Text kind="label/bold/sm" className="text-secondary">
How do you want to start?
</Text>
<Grid cols={START_OPTIONS.length} gap="density-md">
{START_OPTIONS.map((option) => (
<GridItem key={option.id}>
<StartOptionCard
option={option}
selected={selectedId === option.id}
onSelect={() => setSelectedId(option.id)}
/>
</GridItem>
))}
</Grid>
</Stack>

{selectedOption ? <StartOptionDetail option={selectedOption} /> : null}
</Stack>
</Block>

{selectedOption ? (
<Flex
align="center"
justify="end"
className="shrink-0 gap-4 border-t border-base bg-surface-base px-10 py-4"
>
<Button color="brand" kind="primary" onClick={() => onContinue(selectedOption.id)}>
Continue
<ArrowRight size={16} aria-hidden />
</Button>
</Flex>
) : null}
</Stack>
);
};
32 changes: 32 additions & 0 deletions web/packages/studio/src/components/CreateFilesetStart/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { BadgeProps } from '@nvidia/foundations-react-core';
import type { LucideIcon } from 'lucide-react';

/** The four ways to start a Data Designer fileset shown as tiles on the new-fileset view. */
export type StartOptionId = 'ai' | 'template' | 'clone' | 'scratch';

export interface StartOptionTag {
label: string;
color: NonNullable<BadgeProps['color']>;
kind: NonNullable<BadgeProps['kind']>;
}

export interface StartOption {
id: StartOptionId;
/** Tile title. */
title: string;
/** One-line tile description. */
description: string;
/** Leading Lucide icon. */
icon: LucideIcon;
/** Small badge rendered at the bottom of the tile. */
tag?: StartOptionTag;
/**
* Whether this option is wired up. Disabled options still render (so the full set
* of future entry points is visible) but are no-ops — they cannot be selected and
* never reveal a detail panel or the Continue footer.
*/
enabled: boolean;
}
3 changes: 3 additions & 0 deletions web/packages/studio/src/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ export const ROUTES = {
dataDesignerJobList: `/workspaces/:${P.workspace}/data-designer`,
dataDesignerJobDetails: `/workspaces/:${P.workspace}/data-designer/:${P.dataDesignerJobName}`,
dataDesignerJobNew: `/workspaces/:${P.workspace}/data-designer/new`,
dataDesignerJobBuild: `/workspaces/:${P.workspace}/data-designer/new/build`,
/** Legacy job-creation form, not linked from any UI — reachable only by typing the URL. */
dataDesignerJobNewLegacy: `/workspaces/:${P.workspace}/data-designer/new/legacy`,
secrets: `/workspaces/:${P.workspace}/secrets`,
guardrails: `/workspaces/:${P.workspace}/guardrails`,
guardrailDetail: `/workspaces/:${P.workspace}/guardrails/:${P.guardrailConfigName}`,
Expand Down
Loading