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