diff --git a/web/packages/studio/src/components/CreateFilesetStart/DescribeWithAiPanel.tsx b/web/packages/studio/src/components/CreateFilesetStart/DescribeWithAiPanel.tsx index 01d928e53a..cb314110e8 100644 --- a/web/packages/studio/src/components/CreateFilesetStart/DescribeWithAiPanel.tsx +++ b/web/packages/studio/src/components/CreateFilesetStart/DescribeWithAiPanel.tsx @@ -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.'; @@ -36,6 +38,8 @@ export const DescribeWithAiPanel: FC = ({ workspace, o }); const modelValue: ModelSelection | null = modelField.value ? { model: modelField.value } : null; + const showSuggestions = form.watch('prompt').trim().length === 0 && !isBusy; + return (
@@ -66,13 +70,24 @@ export const DescribeWithAiPanel: FC = ({ workspace, o + form.setValue('prompt', prompt, { shouldValidate: true, shouldDirty: true }) + } + /> + ) : undefined + } /> diff --git a/web/packages/studio/src/components/CreateFilesetStart/constants.ts b/web/packages/studio/src/components/CreateFilesetStart/constants.ts index 25d3e8c026..65fdacda47 100644 --- a/web/packages/studio/src/components/CreateFilesetStart/constants.ts +++ b/web/packages/studio/src/components/CreateFilesetStart/constants.ts @@ -3,6 +3,7 @@ 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. */ @@ -10,6 +11,28 @@ 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', diff --git a/web/packages/studio/src/components/ModelChat/index.tsx b/web/packages/studio/src/components/ModelChat/index.tsx index 2b37ebba3f..35dc9cb802 100644 --- a/web/packages/studio/src/components/ModelChat/index.tsx +++ b/web/packages/studio/src/components/ModelChat/index.tsx @@ -169,6 +169,7 @@ export const ModelChat: FC = ({ : undefined } diff --git a/web/packages/studio/src/components/PromptSuggestionTags/index.tsx b/web/packages/studio/src/components/PromptSuggestionTags/index.tsx new file mode 100644 index 0000000000..207392abb8 --- /dev/null +++ b/web/packages/studio/src/components/PromptSuggestionTags/index.tsx @@ -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 = ({ + suggestions, + onSelect, + disabled, + className, +}) => ( + + {suggestions.map((suggestion) => ( + onSelect(suggestion.prompt)} + > + {suggestion.label} + + ))} + +); diff --git a/web/packages/studio/src/components/PromptSuggestionTags/types.ts b/web/packages/studio/src/components/PromptSuggestionTags/types.ts new file mode 100644 index 0000000000..595ccf280f --- /dev/null +++ b/web/packages/studio/src/components/PromptSuggestionTags/types.ts @@ -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; +} + +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; +} diff --git a/web/packages/studio/src/components/chat/CompareComposer.tsx b/web/packages/studio/src/components/chat/CompareComposer.tsx index a762c317df..c5af24eee1 100644 --- a/web/packages/studio/src/components/chat/CompareComposer.tsx +++ b/web/packages/studio/src/components/chat/CompareComposer.tsx @@ -105,6 +105,7 @@ export const CompareComposer: FC = ({ setDraft(text)} + disabled={readyPanelCount === 0} slotEnd={slotSeedEnd} /> )} diff --git a/web/packages/studio/src/components/chat/SeedQuestions.tsx b/web/packages/studio/src/components/chat/SeedQuestions.tsx index 39c0237d58..c2c577704a 100644 --- a/web/packages/studio/src/components/chat/SeedQuestions.tsx +++ b/web/packages/studio/src/components/chat/SeedQuestions.tsx @@ -2,11 +2,15 @@ // 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. */ @@ -14,32 +18,31 @@ interface SeedQuestionsProps { } /** - * 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 = ({ questions = DEFAULT_SEED_QUESTIONS, onSelect, + disabled, slotStart, slotEnd, }) => { + const suggestions = useMemo( + () => questions.map((question) => ({ label: question, prompt: question })), + [questions] + ); + return (
{slotStart &&
{slotStart}
} -
- {questions.map((q) => ( - - ))} -
+ {slotEnd &&
{slotEnd}
}
);