Skip to content

feat: add clarifying questions to ai chat#3825

Open
aaroncaballero-f wants to merge 1 commit intomainfrom
feat/ai-chat-clarifying-questions
Open

feat: add clarifying questions to ai chat#3825
aaroncaballero-f wants to merge 1 commit intomainfrom
feat/ai-chat-clarifying-questions

Conversation

@aaroncaballero-f
Copy link
Copy Markdown
Contributor

@aaroncaballero-f aaroncaballero-f commented Apr 1, 2026

Description

Description

Add inline clarifying questions to the F0AiChat component. When the AI backend needs structured user input before proceeding, it renders a multi-step question panel directly inside the chat textarea area. Supports radio (single-select) and checkbox (multi-select) modes, with an optional free-text custom answer.

Screenshots (if applicable)

clarifyingQuestions.mp4
image image

Implementation details

New files

  • types.tsClarifyingQuestion interface with selection state, custom answer state, step navigation, and callbacks
  • useClarifyingQuestionAction.tsx — CopilotKit action registration + headless ClarifyingQuestionController managing multi-step state, per-step persistence of selections/custom text, and structured message submission on completion
  • ClarifyingQuestionPanel.tsx — UI panel with radio/checkbox option rendering, custom answer row (idle + editing states), step navigation header, loading skeleton, and confirm button
  • ClarifyingQuestion.stories.tsx — 5 stories (Checkbox, Radio, Multi-Step, Custom Answer Radio, Custom Answer Checkbox) with a shared useClarifyingQuestionStory hook

Modified files

  • ChatTextarea.tsx — Integrates the clarifying question panel via AnimatePresence, disables textarea input while clarifying is active

Key design decisions

  • Radio mode: custom answer is mutually exclusive with predefined options
  • Checkbox mode: custom answer coexists with selections, toggled independently via checkbox
  • Text preservation: custom text is always preserved; isCustomAnswerActive boolean controls inclusion in submission
  • All animations respect useReducedMotion, all focusables use focusRing()

This is an improve of what Daviz proposed on his PR: #3611

@aaroncaballero-f aaroncaballero-f self-assigned this Apr 1, 2026
Copilot AI review requested due to automatic review settings April 1, 2026 14:20
@aaroncaballero-f aaroncaballero-f requested a review from a team as a code owner April 1, 2026 14:20
@github-actions github-actions bot added feat react Changes affect packages/react labels Apr 1, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

✅ No New Circular Dependencies

No new circular dependencies detected. Current count: 0

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

📦 Alpha Package Version Published

Use pnpm i github:factorialco/f0#npm/alpha-pr-3825 to install the package

Use pnpm i github:factorialco/f0#196328bd8229a1038e723710164024b81224e18e to install this specific commit

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

🔍 Visual review for your branch is published 🔍

Here are the links to:

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

Coverage Report for packages/react

Status Category Percentage Covered / Total
🔵 Lines 45.07% 10941 / 24272
🔵 Statements 44.36% 11278 / 25421
🔵 Functions 37.16% 2468 / 6640
🔵 Branches 36.74% 7090 / 19295
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/react/src/lib/providers/i18n/i18n-provider-defaults.ts 100% 100% 100% 100%
packages/react/src/sds/ai/F0AiChat/internal-types.ts 25% 100% 25% 33.33% 207-216
packages/react/src/sds/ai/F0AiChat/actions/index.ts 100% 100% 100% 100%
packages/react/src/sds/ai/F0AiChat/actions/registry.ts 100% 100% 100% 100%
packages/react/src/sds/ai/F0AiChat/actions/core/clarifyingQuestion/types.ts 100% 100% 100% 100%
packages/react/src/sds/ai/F0AiChat/actions/core/clarifyingQuestion/useClarifyingQuestionAction.tsx 0.79% 0% 0% 0.8% 27-271, 280-362
packages/react/src/sds/ai/F0AiChat/components/input/ChatTextarea.tsx 1.56% 0% 0% 1.66% 45-401
packages/react/src/sds/ai/F0AiChat/components/input/ClarifyingQuestionPanel.tsx 0% 0% 0% 0% 23-350
packages/react/src/sds/ai/F0AiChat/providers/AiChatStateProvider.tsx 4.9% 0% 0% 5.05% 38-46, 70-316, 323-382
Generated in workflow #12458 for commit 4e90ae1 by the Vitest Coverage Report Action

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an inline “clarifying questions” flow to F0AiChat, allowing the AI backend to request structured, multi-step user input (radio/checkbox + optional custom answer) rendered inside the chat input area.

Changes:

  • Extended AiChatStateProvider / useAiChat() to store and expose an active clarifyingQuestion state.
  • Added a CopilotKit frontend action (AiWidgets.F0ClarifyingQuestion) that manages multi-step state and triggers the clarifying-question UI.
  • Updated ChatTextarea to swap the textarea UI for the new ClarifyingQuestionPanel while clarifying is active (with animations + reduced-motion support), plus added i18n strings.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/react/src/sds/ai/F0AiChat/providers/AiChatStateProvider.tsx Adds clarifying-question state to the provider context.
packages/react/src/sds/ai/F0AiChat/internal-types.ts Extends AiChatProviderReturnValue with clarifying-question fields.
packages/react/src/sds/ai/F0AiChat/components/input/ClarifyingQuestionPanel.tsx New UI panel for multi-step radio/checkbox + custom answer inside the input area.
packages/react/src/sds/ai/F0AiChat/components/input/ChatTextarea.tsx Integrates the panel via AnimatePresence and disables textarea submission while clarifying.
packages/react/src/sds/ai/F0AiChat/components/input/stories/ClarifyingQuestion/ClarifyingQuestion.stories.tsx Adds Storybook coverage for clarifying-question scenarios.
packages/react/src/sds/ai/F0AiChat/actions/registry.ts Registers the new clarifying-question Copilot action.
packages/react/src/sds/ai/F0AiChat/actions/index.ts Exposes useClarifyingQuestionAction from the actions barrel.
packages/react/src/sds/ai/F0AiChat/actions/core/clarifyingQuestion/useClarifyingQuestionAction.tsx Implements the Copilot action + controller that drives clarifying-question state and submission.
packages/react/src/sds/ai/F0AiChat/actions/core/clarifyingQuestion/types.ts Defines ClarifyingQuestion / option types used across controller and UI.
packages/react/src/lib/providers/i18n/i18n-provider-defaults.ts Adds default i18n strings for clarifying-question UI labels.

Comment on lines +120 to +137
// Save selections, custom text, and custom active state for current step
const updatedSelections = {
...allSelectionsRef.current,
[currentStep.question]: selected,
}
setAllSelections(updatedSelections)

const updatedCustomTexts = {
...allCustomTextsRef.current,
[currentStep.question]: currentCustomText,
}
setAllCustomTexts(updatedCustomTexts)

const updatedCustomActives = {
...allCustomActivesRef.current,
[currentStep.question]: currentCustomActive,
}
setAllCustomActives(updatedCustomActives)
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The per-step persistence maps (allSelections/allCustomTexts/allCustomActives) are keyed by currentStep.question. If two steps share the same question text (or the backend changes wording), state can collide across steps and produce incorrect restored selections. Prefer a stable key (e.g., step index, or an explicit stepId field from the backend) for storage and retrieval.

Copilot uses AI. Check for mistakes.
parts.push(`${step.question} → ${answer}`)
}
const message = parts.join("\n")
sendMessage(message)
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the final step, confirm() calls sendMessage(message) but never dismisses the clarifying UI (e.g. setClarifyingQuestion(null)) or resets local step state. If the CopilotKit action render stays mounted, the textarea can remain blocked on the panel even after submission. Consider clearing clarifyingQuestion (and optionally resetting step state) immediately after sending the message.

Suggested change
sendMessage(message)
sendMessage(message)
// Dismiss clarifying UI and reset local step state after final submission
setClarifyingQuestion(null)
setCurrentStepIndex(0)
setSelectedIds([])
setCustomAnswerTextState("")
setIsCustomAnswerActive(false)
setAllSelections({})
setAllCustomTexts({})
setAllCustomActives({})

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +125
const confirm = useCallback(() => {
const idx = currentStepIndexRef.current
const currentStep = stepsRef.current[idx]
const selected = selectedIdsRef.current
const currentCustomText = customAnswerTextRef.current
const currentCustomActive = isCustomAnswerActiveRef.current

// Save selections, custom text, and custom active state for current step
const updatedSelections = {
...allSelectionsRef.current,
[currentStep.question]: selected,
}
setAllSelections(updatedSelections)
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

confirm() assumes stepsRef.current[idx] is always defined and immediately dereferences currentStep.question. If the backend updates steps to a shorter array (or empty) while the component is mounted, this can throw before the reset effect runs. Add a defensive guard to early-return (and clear the panel) when currentStep is missing.

Copilot uses AI. Check for mistakes.
Comment on lines +142 to +168
// Build a structured user message so the AI knows each answer per question
const parts: string[] = []
for (const step of stepsRef.current) {
const stepSelected = updatedSelections[step.question] ?? []
const stepCustom = updatedCustomTexts[step.question] ?? ""
const stepCustomActive = updatedCustomActives[step.question] ?? false
const labels = step.options
.filter((o) => stepSelected.includes(o.id))
.map((o) => o.label)

// In radio mode, custom text is mutually exclusive with predefined options.
// Only include custom text when no predefined option is selected.
// In checkbox mode, include custom text only when explicitly activated.
const isRadio = (step.selectionMode ?? "checkbox") === "radio"
const includeCustom = isRadio
? stepSelected.length === 0 && stepCustom.trim().length > 0
: stepCustomActive && stepCustom.trim().length > 0

if (includeCustom) {
labels.push(`(custom) ${stepCustom.trim()}`)
}

const answer = labels.length > 0 ? labels.join(", ") : "(skipped)"
parts.push(`${step.question} → ${answer}`)
}
const message = parts.join("\n")
sendMessage(message)
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The submitted message is built as a user-visible English summary (uses "→", "(custom)", "(skipped)"). This will show up in the chat transcript via UserMessage and isn't localized, which conflicts with the repo’s i18n convention for user-facing strings (see packages/react/.skills/f0-code-review/SKILL.md i18n section). Consider sending a hidden machine-readable payload (e.g. in the existing <tool-context ...> prefix which the UI strips) and/or localizing any visible text.

Copilot uses AI. Check for mistakes.
const isFirstStep = currentStepIndex === 0
const isFinalStep = currentStepIndex === totalSteps - 1
const stepLabel = isMultiStep
? `${currentStepIndex + 1} of ${totalSteps}`
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stepLabel uses a hardcoded English string ("${currentStepIndex + 1} of ${totalSteps}") which is user-facing in the UI. Please move this to i18n (e.g. add a translation key with placeholders, or reuse an existing one like wizard.stepOf).

Suggested change
? `${currentStepIndex + 1} of ${totalSteps}`
? (translation.wizard?.stepOf
?.replace("{current}", String(currentStepIndex + 1))
?.replace("{total}", String(totalSteps)) ??
`${currentStepIndex + 1}/${totalSteps}`)

Copilot uses AI. Check for mistakes.
Comment on lines +226 to +281
{allowCustomAnswer && !isEditingCustom && (
<div
className={cn(
"flex items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm",
"transition-colors",
"text-f1-foreground-secondary hover:bg-f1-background-secondary"
)}
>
{hasCustomText ? (
selectionMode === "radio" ? (
<button
type="button"
onClick={handleActivateCustom}
className={cn(
"flex h-5 w-5 shrink-0 items-center justify-center rounded-full transition-colors",
focusRing("rounded-full"),
!hasSelection
? "bg-f1-background-selected-bold"
: "border-2 border-solid border-f1-border bg-f1-background"
)}
>
{!hasSelection && (
<span className="h-2 w-2 rounded-full bg-f1-background" />
)}
</button>
) : (
<F0Checkbox
checked={isCustomAnswerActive}
onCheckedChange={() => {
setCustomAnswerActive(!isCustomAnswerActive)
}}
/>
)
) : (
<span
className={cn(
"flex shrink-0 items-center justify-center",
selectionMode === "radio" ? "size-5" : "size-6"
)}
>
<Pencil className="size-3.5 text-f1-foreground-secondary" />
</span>
)}
<button
type="button"
onClick={handleActivateCustom}
className={cn(
"min-w-0 flex-1 text-left",
focusRing("rounded-sm"),
hasCustomText ? "text-f1-foreground" : ""
)}
>
{hasCustomText
? customAnswerText
: translation.ai.clarifyingQuestion.typeYourAnswer}
</button>
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In radio mode, the custom answer row isn’t represented as a radio option within the radiogroup (it’s a plain div with buttons/inputs). This means screen readers won’t announce it as a selectable radio item, and keyboard radio-group navigation semantics won’t include it. Consider giving the custom row role="radio" + aria-checked (and consistent focus/keyboard handling) or switching to an actual radio input so it participates in the same group as the predefined options.

Copilot uses AI. Check for mistakes.
Comment on lines +397 to +410
const meta = {
title: "AI/F0AiChat/Input/ChatTextarea/ClarifyingQuestion",
parameters: {
layout: "centered",
},
decorators: [
(Story) => (
<F0AiChatProvider>
<Story />
</F0AiChatProvider>
),
],
} satisfies Meta

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Story meta is missing the standard tags: ["autodocs"] used elsewhere in F0AiChat stories, and satisfies Meta is untyped. For consistency and better type safety, add tags and use satisfies Meta<typeof SomeComponent> (and optionally set component in meta, as done in ChatTextarea.stories.tsx).

Copilot uses AI. Check for mistakes.
@aaroncaballero-f aaroncaballero-f force-pushed the feat/ai-chat-clarifying-questions branch from 6cd0483 to 4e90ae1 Compare April 1, 2026 14:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat react Changes affect packages/react

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants