Clarifying QuestionsNew
A short card of questions before an ambiguous task, each pre-filled with the assistant's suggested answer, that collapses into a summary once started.
Make a deck summarising our Q3 results
"use client";
import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import { Check, Pencil } from "lucide-react";
import { cn } from "@/lib/utils";
export interface ClarifyingQuestion {
id: string;
label: string;
options: string[];
/** The assistant's best guess, pre-selected and marked "Suggested". */
suggested?: string;
/** Adds a free-text "Other" option. */
allowOther?: boolean;
}
export type ClarifyingAnswers = Record<string, string>;
export interface ClarifyingQuestionsProps {
intro?: string;
questions: ClarifyingQuestion[];
/** Called with every answer — the suggested ones included — when the person starts. */
onSubmit: (answers: ClarifyingAnswers, edited: string[]) => void;
submitLabel?: string;
className?: string;
}
const OTHER = "__other__";
export function ClarifyingQuestions({
intro = "A few quick questions so I get this right. I've filled in my best guesses — change anything that's off.",
questions,
onSubmit,
submitLabel = "Start",
className,
}: ClarifyingQuestionsProps) {
const initial = React.useMemo(
() => Object.fromEntries(questions.map((q) => [q.id, q.suggested ?? ""])) as ClarifyingAnswers,
[questions]
);
const [answers, setAnswers] = React.useState<ClarifyingAnswers>(initial);
const [other, setOther] = React.useState<ClarifyingAnswers>({});
const [submitted, setSubmitted] = React.useState(false);
const id = React.useId();
const resolved = (q: ClarifyingQuestion) => (answers[q.id] === OTHER ? other[q.id]?.trim() ?? "" : answers[q.id]);
const missing = questions.filter((q) => !resolved(q));
const edited = questions.filter((q) => resolved(q) !== (q.suggested ?? "")).map((q) => q.id);
function submit() {
if (missing.length) return;
setSubmitted(true);
onSubmit(Object.fromEntries(questions.map((q) => [q.id, resolved(q)])), edited);
}
return (
<div className={cn("w-full rounded-2xl border bg-card", className)}>
<AnimatePresence mode="wait" initial={false}>
{submitted ? (
<motion.div
key="summary"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="overflow-hidden"
>
<div role="status" className="flex items-start gap-3 px-4 py-3">
<span className="mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full bg-foreground text-background">
<Check className="size-3" aria-hidden />
</span>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium">Got it — starting with these details</p>
<ul className="mt-1.5 flex flex-wrap gap-1.5">
{questions.map((q) => (
<li key={q.id} className="rounded-full border bg-muted/50 px-2 py-0.5 text-xs">
<span className="text-muted-foreground">{q.label}: </span>
{resolved(q)}
</li>
))}
</ul>
</div>
<button
type="button"
onClick={() => setSubmitted(false)}
aria-label="Change answers"
title="Change answers"
className="flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
>
<Pencil className="size-3.5" aria-hidden />
</button>
</div>
</motion.div>
) : (
<motion.form
key="form"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
onSubmit={(e) => {
e.preventDefault();
submit();
}}
className="flex flex-col gap-4 p-4"
>
<p className="text-sm text-pretty">{intro}</p>
{questions.map((q, qi) => (
<fieldset key={q.id} className="flex flex-col gap-2">
<legend className="mb-2 text-xs font-medium text-muted-foreground">
{qi + 1}. {q.label}
</legend>
<div className="flex flex-wrap gap-1.5">
{[...q.options, ...(q.allowOther ? [OTHER] : [])].map((option) => {
const checked = answers[q.id] === option;
return (
<label key={option} className="relative">
<input
type="radio"
name={`${id}-${q.id}`}
value={option}
checked={checked}
onChange={() => setAnswers((a) => ({ ...a, [q.id]: option }))}
className="peer sr-only"
/>
<span
className={cn(
"inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-3 py-1 text-xs transition-colors peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-ring",
checked
? "border-foreground bg-foreground text-background"
: "bg-background text-foreground hover:bg-accent"
)}
>
{option === OTHER ? "Other…" : option}
{option === q.suggested && (
<span className={cn("text-[10px]", checked ? "text-background/70" : "text-muted-foreground")}>
Suggested
</span>
)}
</span>
</label>
);
})}
</div>
<AnimatePresence initial={false}>
{answers[q.id] === OTHER && (
<motion.input
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
autoFocus
value={other[q.id] ?? ""}
onChange={(e) => setOther((o) => ({ ...o, [q.id]: e.target.value }))}
aria-label={`${q.label}, other`}
placeholder="Type your answer"
className="w-full rounded-lg border bg-background px-3 py-1.5 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
/>
)}
</AnimatePresence>
</fieldset>
))}
<div className="flex flex-wrap items-center justify-between gap-3 border-t pt-3">
<span aria-live="polite" className="text-xs text-muted-foreground">
{missing.length
? `${missing.length} still to answer`
: edited.length
? `${edited.length} changed from my suggestions`
: "Using my suggestions"}
</span>
<button
type="submit"
disabled={missing.length > 0}
className="rounded-xl bg-foreground px-4 py-1.5 text-sm font-medium text-background transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40"
>
{submitLabel}
</button>
</div>
</motion.form>
)}
</AnimatePresence>
</div>
);
}
A UX spec for this pattern — written for agents implementing or reusing it, not the code.
# Clarifying Questions
## Summary
Before starting on an ambiguous request, the assistant replies with a short card of two or three questions, all visible at once, each pre-filled with its best guess and marked "Suggested". The person corrects only what's wrong and presses Start. The card then collapses into a summary of the details the assistant will use.
## When to use
- Requests where a wrong assumption is expensive: a deck, a report, a research run, a long generation.
- Requests that are clear in intent but missing two or three details that change the result (audience, length, tone, scope).
- When the assistant can make sensible guesses — pre-filling them turns a quiz into a quick check.
## When not to use
- When the request is already specific, or a wrong guess is cheap to fix afterwards. Just answer, and let Refinement Controls handle tweaks.
- Mid-task decisions that block an agent — use Human in the Loop Question, which pauses the run and walks through one choice at a time.
- More than three or four questions. Ask for the essentials, or break the work into a planning step (Plan Review).
- Confirming a specific risky action — use Tool Approval.
## Anatomy
- Intro line: why the assistant is asking, and that it has filled in guesses.
- Question groups: a numbered label and a row of option chips; the suggested option is pre-selected and tagged "Suggested".
- Optional "Other…" chip that reveals a text field.
- Footer: a live status ("Using my suggestions", "1 changed from my suggestions", "1 still to answer") and the Start button.
- Summary: after Start, a compact row of the chosen details as chips, with an edit button to reopen the questions.
## Behavior
- All questions show at once, so people can scan and change only what matters; there is no step-by-step flow.
- Questions with a suggestion are answered from the start, so Start works immediately. Questions without one block Start until answered.
- Choosing "Other…" reveals and focuses a text field; an empty one counts as unanswered.
- Start submits every answer and reports which ones the person changed, so the assistant can weight them.
- The card collapses to the summary and the assistant begins. The edit button reopens the questions with the previous answers.
## Content guidelines
- Ask only what changes the output. Each question should be answerable with a tap.
- Keep options to two to four short, mutually exclusive labels; put the most likely one first or mark it as suggested.
- Phrase the intro as help, not a gate: "A few quick questions so I get this right."
- The summary repeats the details in the user's words, so mistakes are easy to spot.
## Accessibility
- Each question is a `<fieldset>` with a `<legend>`; options are native radio inputs styled as chips, so arrow keys move between them and screen readers announce the group.
- Chips show a visible focus ring; the selected state is shown by fill and announced by the radio.
- The footer status is `aria-live="polite"` and the summary is `role="status"`.
- The edit button has an accessible name ("Change answers").
## Related patterns
- **Human in the Loop Question** — Use it instead when an agent is mid-run and must pause for one decision at a time.
- **Prompt Enhancer** — Use it instead when filling in the details silently is acceptable and the person can undo.
- **Plan Review** — Use it as well for longer tasks: clarify the details first, then confirm the plan.
- **Follow-Up List** — Use it instead to suggest next questions after an answer, not to gather input before one.
Do’s and don’ts
Do use it for
- Requests where a wrong assumption is expensive: a deck, a report, a research run, a long generation.
- Requests that are clear in intent but missing two or three details that change the result (audience, length, tone, scope).
- When the assistant can make sensible guesses — pre-filling them turns a quiz into a quick check.
Don’t use it for
- When the request is already specific, or a wrong guess is cheap to fix afterwards. Just answer, and let Refinement Controls handle tweaks.
- Mid-task decisions that block an agent — use Human in the Loop Question, which pauses the run and walks through one choice at a time.
- More than three or four questions. Ask for the essentials, or break the work into a planning step (Plan Review).
- Confirming a specific risky action — use Tool Approval.
Related patterns
Human in the Loop Question
Use it instead when an agent is mid-run and must pause for one decision at a time.
Prompt Enhancer
Use it instead when filling in the details silently is acceptable and the person can undo.
Plan Review
Use it as well for longer tasks: clarify the details first, then confirm the plan.
Follow-Up List
Use it instead to suggest next questions after an answer, not to gather input before one.