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
Expand Up @@ -7,15 +7,17 @@ import { LoadingButton } from '@nemo/common/src/components/LoadingButton';
import { ModelSelectV2 } from '@nemo/common/src/components/ModelSelectV2/ModelSelectV2';
import type { ModelSelection } from '@nemo/common/src/components/ModelSelectV2/types';
import { Flex, FormField, Stack } from '@nvidia/foundations-react-core';
import { PROMPT_SUGGESTIONS } from '@studio/components/CreateFilesetStart/constants';
import { GeneratedConfigResult } from '@studio/components/CreateFilesetStart/GeneratedConfigResult';
import type { DescribeWithAiPanelProps } from '@studio/components/CreateFilesetStart/types';
import { useDescribeWithAi } from '@studio/components/CreateFilesetStart/useDescribeWithAi';
import { PromptSuggestionTags } from '@studio/components/PromptSuggestionTags';
import { providerForSelection } from '@studio/routes/DataDesignerJobBuildRoute/models';
import type { FC } from 'react';
import { useController } from 'react-hook-form';

const PROMPT_PLACEHOLDER =
'100 customer support emails, each labelled as phishing or legitimate, with a short reason for the label and the sender domain. Sampled across categories (billing, returns, tech support) with subcategories per category (billing: overcharge, failed payment; returns: damaged item, wrong size)';
'Describe the rows you want: how many, what each column holds, and how the data should vary. Or start from an example below.';

const MODEL_HELP = 'Needs tool-calling support. This model will be used in LLM columns.';

Expand All @@ -36,6 +38,8 @@ export const DescribeWithAiPanel: FC<DescribeWithAiPanelProps> = ({ workspace, o
});
const modelValue: ModelSelection | null = modelField.value ? { model: modelField.value } : null;

const showSuggestions = form.watch('prompt').trim().length === 0 && !isBusy;

return (
<form onSubmit={generate} noValidate>
<Flex gap="density-xl" className="w-full flex-wrap items-stretch">
Expand Down Expand Up @@ -66,13 +70,24 @@ export const DescribeWithAiPanel: FC<DescribeWithAiPanelProps> = ({ workspace, o
</FormField>

<ControlledTextArea
useControllerProps={{ control: form.control, name: 'prompt' }}
label="What do you want to generate?"
required
formFieldProps={{ required: true }}
rows={8}
className="w-full resize-y"
className="w-full"
placeholder={PROMPT_PLACEHOLDER}
disabled={isBusy}
useControllerProps={{ name: 'prompt', control: form.control }}
layout="vertical"
slotEnd={
showSuggestions ? (
<PromptSuggestionTags
suggestions={PROMPT_SUGGESTIONS}
onSelect={(prompt) =>
form.setValue('prompt', prompt, { shouldValidate: true, shouldDirty: true })
}
/>
) : undefined
}
/>

<Flex justify="start">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,36 @@

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

/** "N recipe(s)" badge label, kept in sync with the number of authored templates. */
const RECIPE_COUNT_LABEL = `${FILESET_TEMPLATES.length} ${
FILESET_TEMPLATES.length === 1 ? 'recipe' : 'recipes'
}`;

/**
* Example prompts offered as pills inside an empty prompt field. Each is a complete,
* generation-ready description — the pill label is only the shorthand for it.
*/
export const PROMPT_SUGGESTIONS: PromptSuggestion[] = [
{
label: 'Phishing email triage',
prompt:
'200 customer support emails for training a phishing triage agent, each labelled as phishing or legitimate, with a short reason for the label and the sender domain. Sampled across categories (billing, returns, tech support) with subcategories per category (billing: overcharge, failed payment; returns: damaged item, wrong size)',
},
{
label: 'Support ticket routing',
prompt:
'100 inbound customer support tickets for training a triage agent. Each row has the raw ticket text as the customer wrote it, the queue it should route to (billing, shipping, technical, account cancellation), an urgency level (P1 to P4) and a one-line summary. Include ambiguous tickets that plausibly span two queues, and a few where the customer threatens to churn',
},
{
label: 'Refund policy Q&A',
prompt:
'50 evaluation examples for a customer-facing refund policy assistant. Each row has a passage from a returns and refunds policy, a question a real customer would ask, the answer grounded in that passage, and whether the policy actually covers the situation. Include questions the policy does not answer, marked as out of scope',
},
];

export const START_OPTIONS: StartOption[] = [
{
id: 'ai',
Expand Down
1 change: 1 addition & 0 deletions web/packages/studio/src/components/ModelChat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export const ModelChat: FC<ModelChatProps> = ({
<SeedQuestions
questions={showChatSeeds ? seedQuestions : []}
onSelect={seedComposer}
disabled={resolvedDisabled}
slotStart={
metricsInComposer && latestMetrics ? <StatsBadge metrics={latestMetrics} /> : undefined
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { Flex, Tag } from '@nvidia/foundations-react-core';
import type { PromptSuggestionTagsProps } from '@studio/components/PromptSuggestionTags/types';
import classNames from 'classnames';
import type { FC } from 'react';

/**
* Row of outline Tags, each writing a ready-made prompt into a nearby field.
* Shared by the chat composer's seed questions and the "Describe with AI" prompt field so
* the two read as one control family; the label is a shorthand, the prompt is what lands
* in the field, and the two are the same string when there is nothing to shorten.
*/
export const PromptSuggestionTags: FC<PromptSuggestionTagsProps> = ({
suggestions,
onSelect,
disabled,
className,
}) => (
<Flex className={classNames('w-full min-w-0 gap-2', className)} wrap="wrap" justify="start">
{suggestions.map((suggestion) => (
<Tag
key={suggestion.label}
color="gray"
kind="outline"
disabled={disabled}
onClick={() => onSelect(suggestion.prompt)}
>
{suggestion.label}
</Tag>
))}
</Flex>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/** A one-click example prompt offered next to a prompt field. */
export interface PromptSuggestion {
/** Short tag label — a few words, not the prompt itself. */
label: string;
/** Full prompt written into the field when the tag is clicked. */
prompt: string;
}
Comment thread
steramae-nvidia marked this conversation as resolved.

export interface PromptSuggestionTagsProps {
suggestions: PromptSuggestion[];
onSelect: (prompt: string) => void;
disabled?: boolean;
/** Merged into the row's classes, for callers that own the row's flex behaviour. */
className?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export const CompareComposer: FC<CompareComposerProps> = ({
<SeedQuestions
questions={showSeeds ? seedQuestions : []}
onSelect={(text) => setDraft(text)}
disabled={readyPanelCount === 0}
slotEnd={slotSeedEnd}
/>
)}
Expand Down
37 changes: 20 additions & 17 deletions web/packages/studio/src/components/chat/SeedQuestions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,47 @@
// SPDX-License-Identifier: Apache-2.0

import { DEFAULT_SEED_QUESTIONS } from '@studio/components/chat/defaultSeedQuestions';
import { type FC, type ReactNode } from 'react';
import { PromptSuggestionTags } from '@studio/components/PromptSuggestionTags';
import type { PromptSuggestion } from '@studio/components/PromptSuggestionTags/types';
import { type FC, type ReactNode, useMemo } from 'react';

interface SeedQuestionsProps {
questions?: string[];
onSelect: (prompt: string) => void;
/** Mirrors the composer's disabled state, so a seed can't target a dead composer. */
disabled?: boolean;
/** Rendered bottom-aligned at the leading end of the row (e.g. metrics). */
slotStart?: ReactNode;
/** Rendered bottom-aligned at the trailing end of the row. */
slotEnd?: ReactNode;
}

/**
* Row of bordered chip buttons that float just above the composer. Each
* question is its own distinct, clickable affordance — same border + radius
* as the composer card so they read as a related control family, but
* detached so they feel like floating action chips, not inline text.
* Seed questions for the chat composer: a row of {@link PromptSuggestionTags} flanked by
* optional slots for metrics and composer actions. A seed question is its own label, so it
* maps to a suggestion whose label and prompt are the same string.
*/
export const SeedQuestions: FC<SeedQuestionsProps> = ({
questions = DEFAULT_SEED_QUESTIONS,
onSelect,
disabled,
slotStart,
slotEnd,
}) => {
const suggestions = useMemo<PromptSuggestion[]>(
() => questions.map((question) => ({ label: question, prompt: question })),
[questions]
);

return (
<div className="flex items-start gap-2">
{slotStart && <div className="shrink-0 self-end">{slotStart}</div>}
<div className="flex min-w-0 flex-1 flex-wrap items-start gap-2">
{questions.map((q) => (
<button
key={q}
type="button"
onClick={() => onSelect(q)}
className="cursor-pointer rounded-full border border-base bg-surface-base px-3 py-1.5 text-xs text-fg-base transition-colors hover:border-emphasis hover:bg-surface-sunken"
>
{q}
</button>
))}
</div>
<PromptSuggestionTags
suggestions={suggestions}
onSelect={onSelect}
disabled={disabled}
className="flex-1"
/>
{slotEnd && <div className="shrink-0 self-end">{slotEnd}</div>}
</div>
);
Expand Down
Loading