Skip to content

Commit 73d6439

Browse files
committed
fix(choice): align styling with library theme and add resolved state recipe
- Use secondary variant for selected options instead of heavy primary fill - Add cursor-pointer and conditional multi-line overrides matching Suggestion pattern - Simplify inner content structure when no description is present - Handle output-available state in AI SDK docs example - Replace domain-specific example data with generic AI content - Add resolved state section to documentation
1 parent 8c27c70 commit 73d6439

4 files changed

Lines changed: 115 additions & 55 deletions

File tree

apps/docs/content/components/(chatbot)/choice.mdx

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ description: A composable component for AI-driven clarification prompts where us
44
path: elements/components/choice
55
---
66

7-
The `Choice` component is designed for AI workflows where the model asks users to resolve ambiguity with structured options (timeframe, severity, scope, category, and similar constraints).
7+
The `Choice` component lets users pick from a set of options when an AI model needs clarification before continuing.
88

99
<Preview path="choice" />
1010

@@ -22,7 +22,10 @@ Add the following component to your frontend:
2222
"use client";
2323

2424
import type { ToolUIPart, UIMessage } from "ai";
25-
import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from "ai";
25+
import {
26+
DefaultChatTransport,
27+
lastAssistantMessageIsCompleteWithToolCalls,
28+
} from "ai";
2629
import { useChat } from "@ai-sdk/react";
2730
import {
2831
Choice,
@@ -52,9 +55,8 @@ type AskUserPart = ToolUIPart<{
5255
};
5356
}>;
5457

55-
const isAskUserPart = (
56-
part: UIMessage["parts"][number]
57-
): part is AskUserPart => part.type === "tool-ask_user";
58+
const isAskUserPart = (part: UIMessage["parts"][number]): part is AskUserPart =>
59+
part.type === "tool-ask_user";
5860

5961
const Example = () => {
6062
const { messages, addToolOutput, status } = useChat({
@@ -73,6 +75,21 @@ const Example = () => {
7375
return null;
7476
}
7577

78+
// Show compact summary when choice has been submitted
79+
if (part.state === "output-available") {
80+
const output = part.output;
81+
const values = Array.isArray(output) ? output : [output];
82+
const labels = values.map(
83+
(v) =>
84+
part.input.options.find((o) => o.value === v)?.label ?? v
85+
);
86+
return (
87+
<ChoiceStatus key={part.toolCallId}>
88+
{labels.join(", ")}
89+
</ChoiceStatus>
90+
);
91+
}
92+
7693
const multiSelect = Boolean(part.input.multi_select);
7794
const allowSubmit =
7895
part.state === "input-available" || part.state === "output-error";
@@ -109,7 +126,8 @@ const Example = () => {
109126
) : null}
110127
{part.state === "output-error" ? (
111128
<ChoiceStatus status="error">
112-
{part.errorText ?? "Could not submit your selection. Try again."}
129+
{part.errorText ??
130+
"Could not submit your selection. Try again."}
113131
</ChoiceStatus>
114132
) : null}
115133
</Choice>
@@ -127,7 +145,13 @@ export default Example;
127145
Add the following route to your backend:
128146

129147
```ts title="app/api/chat/route.ts"
130-
import { convertToModelMessages, stepCountIs, streamText, tool, UIMessage } from "ai";
148+
import {
149+
convertToModelMessages,
150+
stepCountIs,
151+
streamText,
152+
tool,
153+
UIMessage,
154+
} from "ai";
131155
import { z } from "zod";
132156

133157
export async function POST(req: Request) {
@@ -169,7 +193,7 @@ export async function POST(req: Request) {
169193
- Optional auto-submit on single selection
170194
- Built-in confirm action for multi-select workflows
171195
- Keyboard and screen-reader friendly option semantics
172-
- Composable structure for custom layouts and states
196+
- Composable structure for custom layouts and resolved states
173197

174198
## Examples
175199

@@ -181,6 +205,10 @@ export async function POST(req: Request) {
181205

182206
<Preview path="choice-multiple" />
183207

208+
### Resolved State
209+
210+
After submission, replace the interactive options with a compact summary. Both examples above demonstrate this pattern — select an option to see the resolved view.
211+
184212
## Props
185213

186214
### `<Choice />`

packages/elements/src/choice.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ export const ChoiceOption = ({
210210
} = useChoiceContext();
211211

212212
const isSelected = selectedValues.includes(value);
213-
const resolvedVariant = variant ?? (isSelected ? "default" : "outline");
213+
const resolvedVariant = variant ?? (isSelected ? "secondary" : "outline");
214214

215215
const handleClick = useCallback(
216216
(event: MouseEvent<HTMLButtonElement>) => {
@@ -231,7 +231,9 @@ export const ChoiceOption = ({
231231
aria-describedby={description ? descriptionId : undefined}
232232
aria-pressed={isSelected}
233233
className={cn(
234-
"h-auto max-w-full justify-start rounded-full px-4 py-2 text-left whitespace-normal",
234+
"cursor-pointer rounded-full px-4",
235+
description &&
236+
"h-auto py-2 max-w-full justify-start text-left whitespace-normal",
235237
className
236238
)}
237239
disabled={disabled || rootDisabled}
@@ -241,17 +243,19 @@ export const ChoiceOption = ({
241243
{...props}
242244
onClick={handleClick}
243245
>
244-
<span className="flex flex-col items-start gap-1">
245-
<span>{children ?? value}</span>
246-
{description ? (
246+
{description ? (
247+
<span className="flex flex-col items-start gap-1">
248+
<span>{children ?? value}</span>
247249
<span
248250
className="text-muted-foreground text-xs leading-snug"
249251
id={descriptionId}
250252
>
251253
{description}
252254
</span>
253-
) : null}
254-
</span>
255+
</span>
256+
) : (
257+
(children ?? value)
258+
)}
255259
</Button>
256260
);
257261
};

packages/examples/src/choice-multiple.tsx

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ import {
1010
} from "@repo/elements/choice";
1111
import { useCallback, useState } from "react";
1212

13+
const labels: Record<string, string> = {
14+
"key-points": "Key points",
15+
"action-items": "Action items",
16+
timeline: "Timeline",
17+
sources: "Sources",
18+
};
19+
1320
const Example = () => {
1421
const [selected, setSelected] = useState<string[]>([]);
1522
const handleSubmit = useCallback((value: string | string[]) => {
@@ -20,30 +27,43 @@ const Example = () => {
2027

2128
return (
2229
<div className="w-full max-w-2xl space-y-3">
23-
<Choice multiple onSubmit={handleSubmit} submitOnSelect={false}>
24-
<ChoiceQuestion>Which categories should we include?</ChoiceQuestion>
25-
<ChoiceOptions>
26-
<ChoiceOption description="Incidents and safety trends" value="crime">
27-
Crime
28-
</ChoiceOption>
29-
<ChoiceOption description="Permits and construction" value="permits">
30-
Permits
31-
</ChoiceOption>
32-
<ChoiceOption description="Neighborhood events" value="events">
33-
Events
34-
</ChoiceOption>
35-
<ChoiceOption description="Local headlines" value="news">
36-
News
37-
</ChoiceOption>
38-
</ChoiceOptions>
39-
<ChoiceSubmit />
40-
</Choice>
4130
{selected.length > 0 ? (
42-
<ChoiceStatus>Selected: {selected.join(", ")}</ChoiceStatus>
43-
) : (
4431
<ChoiceStatus>
45-
Select one or more categories, then confirm.
32+
Includes: {selected.map((v) => labels[v]).join(", ")}
4633
</ChoiceStatus>
34+
) : (
35+
<Choice multiple onSubmit={handleSubmit} submitOnSelect={false}>
36+
<ChoiceQuestion>
37+
What should the summary include?
38+
</ChoiceQuestion>
39+
<ChoiceOptions>
40+
<ChoiceOption
41+
description="Main takeaways and findings"
42+
value="key-points"
43+
>
44+
Key points
45+
</ChoiceOption>
46+
<ChoiceOption
47+
description="Next steps and follow-ups"
48+
value="action-items"
49+
>
50+
Action items
51+
</ChoiceOption>
52+
<ChoiceOption
53+
description="Key dates and milestones"
54+
value="timeline"
55+
>
56+
Timeline
57+
</ChoiceOption>
58+
<ChoiceOption
59+
description="References and citations"
60+
value="sources"
61+
>
62+
Sources
63+
</ChoiceOption>
64+
</ChoiceOptions>
65+
<ChoiceSubmit />
66+
</Choice>
4767
)}
4868
</div>
4969
);

packages/examples/src/choice.tsx

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import {
99
} from "@repo/elements/choice";
1010
import { useCallback, useState } from "react";
1111

12+
const labels: Record<string, string> = {
13+
brief: "Brief",
14+
standard: "Standard",
15+
detailed: "Detailed",
16+
};
17+
1218
const Example = () => {
1319
const [selected, setSelected] = useState<string>();
1420
const handleSubmit = useCallback((value: string | string[]) => {
@@ -19,27 +25,29 @@ const Example = () => {
1925

2026
return (
2127
<div className="w-full max-w-2xl space-y-3">
22-
<Choice onSubmit={handleSubmit}>
23-
<ChoiceQuestion>How should I scope this briefing?</ChoiceQuestion>
24-
<ChoiceOptions>
25-
<ChoiceOption
26-
description="Most recent 24 hours"
27-
value="last-24-hours"
28-
>
29-
Last 24 hours
30-
</ChoiceOption>
31-
<ChoiceOption description="Past 7 days" value="last-week">
32-
Last week
33-
</ChoiceOption>
34-
<ChoiceOption description="Past 30 days" value="last-month">
35-
Last month
36-
</ChoiceOption>
37-
</ChoiceOptions>
38-
</Choice>
3928
{selected ? (
40-
<ChoiceStatus>Selected: {selected}</ChoiceStatus>
29+
<ChoiceStatus>Response detail: {labels[selected]}</ChoiceStatus>
4130
) : (
42-
<ChoiceStatus>Choose one option to continue.</ChoiceStatus>
31+
<Choice onSubmit={handleSubmit}>
32+
<ChoiceQuestion>How detailed should the response be?</ChoiceQuestion>
33+
<ChoiceOptions>
34+
<ChoiceOption
35+
description="A few sentences at most"
36+
value="brief"
37+
>
38+
Brief
39+
</ChoiceOption>
40+
<ChoiceOption description="A balanced overview" value="standard">
41+
Standard
42+
</ChoiceOption>
43+
<ChoiceOption
44+
description="In-depth with examples"
45+
value="detailed"
46+
>
47+
Detailed
48+
</ChoiceOption>
49+
</ChoiceOptions>
50+
</Choice>
4351
)}
4452
</div>
4553
);

0 commit comments

Comments
 (0)