@openconsole/shadcn 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +460 -380
  2. package/components/ai-elements/agent.tsx +141 -0
  3. package/components/ai-elements/artifact.tsx +148 -0
  4. package/components/ai-elements/attachments.tsx +426 -0
  5. package/components/ai-elements/audio-player.tsx +231 -0
  6. package/components/ai-elements/canvas.tsx +26 -0
  7. package/components/ai-elements/chain-of-thought.tsx +222 -0
  8. package/components/ai-elements/checkpoint.tsx +71 -0
  9. package/components/ai-elements/code-block.tsx +562 -0
  10. package/components/ai-elements/commit.tsx +458 -0
  11. package/components/ai-elements/confirmation.tsx +174 -0
  12. package/components/ai-elements/connection.tsx +28 -0
  13. package/components/ai-elements/context.tsx +409 -0
  14. package/components/ai-elements/controls.tsx +18 -0
  15. package/components/ai-elements/conversation.tsx +168 -0
  16. package/components/ai-elements/edge.tsx +143 -0
  17. package/components/ai-elements/environment-variables.tsx +324 -0
  18. package/components/ai-elements/file-tree.tsx +304 -0
  19. package/components/ai-elements/image.tsx +24 -0
  20. package/components/ai-elements/index.ts +51 -0
  21. package/components/ai-elements/inline-citation.tsx +296 -0
  22. package/components/ai-elements/jsx-preview.tsx +310 -0
  23. package/components/ai-elements/message.tsx +360 -0
  24. package/components/ai-elements/mic-selector.tsx +375 -0
  25. package/components/ai-elements/model-selector.tsx +213 -0
  26. package/components/ai-elements/node.tsx +71 -0
  27. package/components/ai-elements/open-in-chat.tsx +370 -0
  28. package/components/ai-elements/package-info.tsx +239 -0
  29. package/components/ai-elements/panel.tsx +15 -0
  30. package/components/ai-elements/persona.tsx +306 -0
  31. package/components/ai-elements/plan.tsx +147 -0
  32. package/components/ai-elements/prompt-input.tsx +1463 -0
  33. package/components/ai-elements/queue.tsx +274 -0
  34. package/components/ai-elements/reasoning.tsx +228 -0
  35. package/components/ai-elements/sandbox.tsx +132 -0
  36. package/components/ai-elements/schema-display.tsx +471 -0
  37. package/components/ai-elements/shimmer.tsx +77 -0
  38. package/components/ai-elements/snippet.tsx +145 -0
  39. package/components/ai-elements/sources.tsx +77 -0
  40. package/components/ai-elements/speech-input.tsx +323 -0
  41. package/components/ai-elements/stack-trace.tsx +528 -0
  42. package/components/ai-elements/suggestion.tsx +57 -0
  43. package/components/ai-elements/task.tsx +87 -0
  44. package/components/ai-elements/terminal.tsx +273 -0
  45. package/components/ai-elements/test-results.tsx +496 -0
  46. package/components/ai-elements/tool.tsx +173 -0
  47. package/components/ai-elements/toolbar.tsx +16 -0
  48. package/components/ai-elements/transcription.tsx +125 -0
  49. package/components/ai-elements/voice-selector.tsx +524 -0
  50. package/components/ai-elements/web-preview.tsx +281 -0
  51. package/components/index.ts +3 -0
  52. package/{accordion.tsx → components/ui/accordion.tsx} +66 -66
  53. package/{alert-dialog.tsx → components/ui/alert-dialog.tsx} +196 -196
  54. package/{alert.tsx → components/ui/alert.tsx} +66 -66
  55. package/{aspect-ratio.tsx → components/ui/aspect-ratio.tsx} +11 -11
  56. package/{avatar.tsx → components/ui/avatar.tsx} +53 -53
  57. package/{badge.tsx → components/ui/badge.tsx} +46 -46
  58. package/{breadcrumb.tsx → components/ui/breadcrumb.tsx} +109 -109
  59. package/{button-group.tsx → components/ui/button-group.tsx} +83 -83
  60. package/{button.tsx → components/ui/button.tsx} +60 -60
  61. package/{calendar.tsx → components/ui/calendar.tsx} +219 -219
  62. package/{card.tsx → components/ui/card.tsx} +92 -92
  63. package/{carousel.tsx → components/ui/carousel.tsx} +241 -241
  64. package/{chart.tsx → components/ui/chart.tsx} +374 -374
  65. package/{checkbox.tsx → components/ui/checkbox.tsx} +32 -32
  66. package/{collapsible.tsx → components/ui/collapsible.tsx} +33 -33
  67. package/{command.tsx → components/ui/command.tsx} +184 -184
  68. package/{context-menu.tsx → components/ui/context-menu.tsx} +252 -252
  69. package/{dialog.tsx → components/ui/dialog.tsx} +143 -143
  70. package/{direction.tsx → components/ui/direction.tsx} +22 -22
  71. package/{drawer.tsx → components/ui/drawer.tsx} +135 -135
  72. package/{dropdown-menu.tsx → components/ui/dropdown-menu.tsx} +257 -257
  73. package/{empty.tsx → components/ui/empty.tsx} +104 -104
  74. package/{field.tsx → components/ui/field.tsx} +248 -248
  75. package/{form.tsx → components/ui/form.tsx} +167 -167
  76. package/{hover-card.tsx → components/ui/hover-card.tsx} +44 -44
  77. package/{icon.tsx → components/ui/icon.tsx} +55 -55
  78. package/components/ui/index.ts +59 -0
  79. package/{input-group.tsx → components/ui/input-group.tsx} +170 -170
  80. package/{input-otp.tsx → components/ui/input-otp.tsx} +77 -77
  81. package/{input.tsx → components/ui/input.tsx} +21 -21
  82. package/{item.tsx → components/ui/item.tsx} +193 -193
  83. package/{kbd.tsx → components/ui/kbd.tsx} +28 -28
  84. package/{label.tsx → components/ui/label.tsx} +24 -24
  85. package/{menubar.tsx → components/ui/menubar.tsx} +276 -276
  86. package/{native-select.tsx → components/ui/native-select.tsx} +62 -62
  87. package/{navigation-menu.tsx → components/ui/navigation-menu.tsx} +168 -168
  88. package/{pagination.tsx → components/ui/pagination.tsx} +127 -127
  89. package/{popover.tsx → components/ui/popover.tsx} +89 -89
  90. package/{progress.tsx → components/ui/progress.tsx} +31 -31
  91. package/{radio-group.tsx → components/ui/radio-group.tsx} +45 -45
  92. package/{resizable.tsx → components/ui/resizable.tsx} +53 -53
  93. package/{scroll-area.tsx → components/ui/scroll-area.tsx} +58 -58
  94. package/{select.tsx → components/ui/select.tsx} +187 -187
  95. package/{separator.tsx → components/ui/separator.tsx} +28 -28
  96. package/{sheet.tsx → components/ui/sheet.tsx} +139 -139
  97. package/{sidebar.tsx → components/ui/sidebar.tsx} +724 -724
  98. package/{skeleton.tsx → components/ui/skeleton.tsx} +13 -13
  99. package/{slider.tsx → components/ui/slider.tsx} +63 -63
  100. package/{sonner.tsx → components/ui/sonner.tsx} +40 -40
  101. package/{spinner.tsx → components/ui/spinner.tsx} +16 -16
  102. package/{switch.tsx → components/ui/switch.tsx} +35 -35
  103. package/{table.tsx → components/ui/table.tsx} +116 -116
  104. package/{tabs.tsx → components/ui/tabs.tsx} +66 -66
  105. package/{textarea.tsx → components/ui/textarea.tsx} +18 -18
  106. package/{toggle-group.tsx → components/ui/toggle-group.tsx} +83 -83
  107. package/{toggle.tsx → components/ui/toggle.tsx} +47 -47
  108. package/{tooltip.tsx → components/ui/tooltip.tsx} +61 -61
  109. package/hooks/index.ts +1 -1
  110. package/hooks/use-mobile.ts +19 -19
  111. package/index.ts +3 -59
  112. package/lib/index.ts +1 -1
  113. package/lib/utils.ts +6 -6
  114. package/package.json +79 -1
  115. package/styles.css +124 -124
  116. package/tsconfig.json +0 -12
  117. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,310 @@
1
+ "use client";
2
+
3
+ import { cn } from "../../lib/utils";
4
+ import { AlertCircle } from "lucide-react";
5
+ import type { ComponentProps, ReactNode } from "react";
6
+ import {
7
+ createContext,
8
+ memo,
9
+ useCallback,
10
+ useContext,
11
+ useEffect,
12
+ useMemo,
13
+ useRef,
14
+ useState,
15
+ } from "react";
16
+ import type { TProps as JsxParserProps } from "react-jsx-parser";
17
+ import JsxParser from "react-jsx-parser";
18
+
19
+ interface JSXPreviewContextValue {
20
+ jsx: string;
21
+ processedJsx: string;
22
+ isStreaming: boolean;
23
+ error: Error | null;
24
+ setError: (error: Error | null) => void;
25
+ setLastGoodJsx: (jsx: string) => void;
26
+ components: JsxParserProps["components"];
27
+ bindings: JsxParserProps["bindings"];
28
+ onErrorProp?: (error: Error) => void;
29
+ }
30
+
31
+ const JSXPreviewContext = createContext<JSXPreviewContextValue | null>(null);
32
+
33
+ const TAG_REGEX = /<\/?([a-zA-Z][a-zA-Z0-9]*)\s*([^>]*?)(\/)?>/;
34
+
35
+ export const useJSXPreview = () => {
36
+ const context = useContext(JSXPreviewContext);
37
+ if (!context) {
38
+ throw new Error("JSXPreview components must be used within JSXPreview");
39
+ }
40
+ return context;
41
+ };
42
+
43
+ const matchJsxTag = (code: string) => {
44
+ if (code.trim() === "") {
45
+ return null;
46
+ }
47
+
48
+ const match = code.match(TAG_REGEX);
49
+
50
+ if (!match || match.index === undefined) {
51
+ return null;
52
+ }
53
+
54
+ const [fullMatch, tagName, attributes, selfClosing] = match;
55
+
56
+ let type: "self-closing" | "closing" | "opening";
57
+ if (selfClosing) {
58
+ type = "self-closing";
59
+ } else if (fullMatch.startsWith("</")) {
60
+ type = "closing";
61
+ } else {
62
+ type = "opening";
63
+ }
64
+
65
+ return {
66
+ attributes: attributes.trim(),
67
+ endIndex: match.index + fullMatch.length,
68
+ startIndex: match.index,
69
+ tag: fullMatch,
70
+ tagName,
71
+ type,
72
+ };
73
+ };
74
+
75
+ const stripIncompleteTag = (text: string) => {
76
+ // Find the last '<' that isn't part of a complete tag
77
+ const lastOpen = text.lastIndexOf("<");
78
+ if (lastOpen === -1) {
79
+ return text;
80
+ }
81
+
82
+ const afterOpen = text.slice(lastOpen);
83
+ // If there's no closing '>' after the last '<', it's an incomplete tag
84
+ if (!afterOpen.includes(">")) {
85
+ return text.slice(0, lastOpen);
86
+ }
87
+
88
+ return text;
89
+ };
90
+
91
+ const completeJsxTag = (code: string) => {
92
+ const stack: string[] = [];
93
+ let result = "";
94
+ let currentPosition = 0;
95
+
96
+ while (currentPosition < code.length) {
97
+ const match = matchJsxTag(code.slice(currentPosition));
98
+ if (!match) {
99
+ // No more tags found, strip any trailing incomplete tag
100
+ result += stripIncompleteTag(code.slice(currentPosition));
101
+ break;
102
+ }
103
+ const { tagName, type, endIndex } = match;
104
+
105
+ // Include any text content before this tag
106
+ result += code.slice(currentPosition, currentPosition + endIndex);
107
+
108
+ if (type === "opening") {
109
+ stack.push(tagName);
110
+ } else if (type === "closing") {
111
+ stack.pop();
112
+ }
113
+
114
+ currentPosition += endIndex;
115
+ }
116
+
117
+ return (
118
+ result +
119
+ [...stack]
120
+ .reverse()
121
+ .map((tag) => `</${tag}>`)
122
+ .join("")
123
+ );
124
+ };
125
+
126
+ export type JSXPreviewProps = ComponentProps<"div"> & {
127
+ jsx: string;
128
+ isStreaming?: boolean;
129
+ components?: JsxParserProps["components"];
130
+ bindings?: JsxParserProps["bindings"];
131
+ onError?: (error: Error) => void;
132
+ };
133
+
134
+ export const JSXPreview = memo(
135
+ ({
136
+ jsx,
137
+ isStreaming = false,
138
+ components,
139
+ bindings,
140
+ onError,
141
+ className,
142
+ children,
143
+ ...props
144
+ }: JSXPreviewProps) => {
145
+ const [prevJsx, setPrevJsx] = useState(jsx);
146
+ const [error, setError] = useState<Error | null>(null);
147
+ const [_lastGoodJsx, setLastGoodJsx] = useState("");
148
+
149
+ // Clear error when jsx changes (derived state pattern)
150
+ if (jsx !== prevJsx) {
151
+ setPrevJsx(jsx);
152
+ setError(null);
153
+ }
154
+
155
+ const processedJsx = useMemo(
156
+ () => (isStreaming ? completeJsxTag(jsx) : jsx),
157
+ [jsx, isStreaming]
158
+ );
159
+
160
+ const contextValue = useMemo(
161
+ () => ({
162
+ bindings,
163
+ components,
164
+ error,
165
+ isStreaming,
166
+ jsx,
167
+ onErrorProp: onError,
168
+ processedJsx,
169
+ setError,
170
+ setLastGoodJsx,
171
+ }),
172
+ [
173
+ bindings,
174
+ components,
175
+ error,
176
+ isStreaming,
177
+ jsx,
178
+ onError,
179
+ processedJsx,
180
+ setError,
181
+ ]
182
+ );
183
+
184
+ return (
185
+ <JSXPreviewContext.Provider value={contextValue}>
186
+ <div className={cn("relative", className)} {...props}>
187
+ {children}
188
+ </div>
189
+ </JSXPreviewContext.Provider>
190
+ );
191
+ }
192
+ );
193
+
194
+ JSXPreview.displayName = "JSXPreview";
195
+
196
+ export type JSXPreviewContentProps = Omit<ComponentProps<"div">, "children">;
197
+
198
+ export const JSXPreviewContent = memo(
199
+ ({ className, ...props }: JSXPreviewContentProps) => {
200
+ const {
201
+ processedJsx,
202
+ isStreaming,
203
+ components,
204
+ bindings,
205
+ setError,
206
+ setLastGoodJsx,
207
+ onErrorProp,
208
+ } = useJSXPreview();
209
+ const errorReportedRef = useRef<string | null>(null);
210
+ const lastGoodJsxRef = useRef("");
211
+ const [hadError, setHadError] = useState(false);
212
+
213
+ // Reset error tracking when jsx changes
214
+ useEffect(() => {
215
+ errorReportedRef.current = null;
216
+ setHadError(false);
217
+ }, [processedJsx]);
218
+
219
+ const handleError = useCallback(
220
+ (err: Error) => {
221
+ // Prevent duplicate error reports for the same jsx
222
+ if (errorReportedRef.current === processedJsx) {
223
+ return;
224
+ }
225
+ errorReportedRef.current = processedJsx;
226
+
227
+ // During streaming, suppress errors and fall back to last good JSX
228
+ if (isStreaming) {
229
+ setHadError(true);
230
+ return;
231
+ }
232
+
233
+ setError(err);
234
+ onErrorProp?.(err);
235
+ },
236
+ [processedJsx, isStreaming, onErrorProp, setError]
237
+ );
238
+
239
+ // Track the last JSX that rendered without error
240
+ useEffect(() => {
241
+ if (!errorReportedRef.current) {
242
+ lastGoodJsxRef.current = processedJsx;
243
+ setLastGoodJsx(processedJsx);
244
+ }
245
+ }, [processedJsx, setLastGoodJsx]);
246
+
247
+ // During streaming, if the current JSX errored, re-render with last good version
248
+ const displayJsx =
249
+ isStreaming && hadError ? lastGoodJsxRef.current : processedJsx;
250
+
251
+ return (
252
+ <div className={cn("jsx-preview-content", className)} {...props}>
253
+ <JsxParser
254
+ bindings={bindings}
255
+ components={components}
256
+ jsx={displayJsx}
257
+ onError={handleError}
258
+ renderInWrapper={false}
259
+ />
260
+ </div>
261
+ );
262
+ }
263
+ );
264
+
265
+ JSXPreviewContent.displayName = "JSXPreviewContent";
266
+
267
+ export type JSXPreviewErrorProps = ComponentProps<"div"> & {
268
+ children?: ReactNode | ((error: Error) => ReactNode);
269
+ };
270
+
271
+ const renderChildren = (
272
+ children: ReactNode | ((error: Error) => ReactNode),
273
+ error: Error
274
+ ): ReactNode => {
275
+ if (typeof children === "function") {
276
+ return children(error);
277
+ }
278
+ return children;
279
+ };
280
+
281
+ export const JSXPreviewError = memo(
282
+ ({ className, children, ...props }: JSXPreviewErrorProps) => {
283
+ const { error } = useJSXPreview();
284
+
285
+ if (!error) {
286
+ return null;
287
+ }
288
+
289
+ return (
290
+ <div
291
+ className={cn(
292
+ "flex items-center gap-2 rounded-md border border-destructive/50 bg-destructive/10 p-3 text-destructive text-sm",
293
+ className
294
+ )}
295
+ {...props}
296
+ >
297
+ {children ? (
298
+ renderChildren(children, error)
299
+ ) : (
300
+ <>
301
+ <AlertCircle className="size-4 shrink-0" />
302
+ <span>{error.message}</span>
303
+ </>
304
+ )}
305
+ </div>
306
+ );
307
+ }
308
+ );
309
+
310
+ JSXPreviewError.displayName = "JSXPreviewError";
@@ -0,0 +1,360 @@
1
+ "use client";
2
+
3
+ import { Button } from "../ui/button";
4
+ import {
5
+ ButtonGroup,
6
+ ButtonGroupText,
7
+ } from "../ui/button-group";
8
+ import {
9
+ Tooltip,
10
+ TooltipContent,
11
+ TooltipProvider,
12
+ TooltipTrigger,
13
+ } from "../ui/tooltip";
14
+ import { cn } from "../../lib/utils";
15
+ import { cjk } from "@streamdown/cjk";
16
+ import { code } from "@streamdown/code";
17
+ import { math } from "@streamdown/math";
18
+ import { mermaid } from "@streamdown/mermaid";
19
+ import type { UIMessage } from "ai";
20
+ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
21
+ import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
22
+ import {
23
+ createContext,
24
+ memo,
25
+ useCallback,
26
+ useContext,
27
+ useEffect,
28
+ useMemo,
29
+ useState,
30
+ } from "react";
31
+ import { Streamdown } from "streamdown";
32
+
33
+ export type MessageProps = HTMLAttributes<HTMLDivElement> & {
34
+ from: UIMessage["role"];
35
+ };
36
+
37
+ export const Message = ({ className, from, ...props }: MessageProps) => (
38
+ <div
39
+ className={cn(
40
+ "group flex w-full max-w-[95%] flex-col gap-2",
41
+ from === "user" ? "is-user ml-auto justify-end" : "is-assistant",
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ );
47
+
48
+ export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
49
+
50
+ export const MessageContent = ({
51
+ children,
52
+ className,
53
+ ...props
54
+ }: MessageContentProps) => (
55
+ <div
56
+ className={cn(
57
+ "is-user:dark flex w-fit min-w-0 max-w-full flex-col gap-2 overflow-hidden text-sm",
58
+ "group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:bg-secondary group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-foreground",
59
+ "group-[.is-assistant]:text-foreground",
60
+ className
61
+ )}
62
+ {...props}
63
+ >
64
+ {children}
65
+ </div>
66
+ );
67
+
68
+ export type MessageActionsProps = ComponentProps<"div">;
69
+
70
+ export const MessageActions = ({
71
+ className,
72
+ children,
73
+ ...props
74
+ }: MessageActionsProps) => (
75
+ <div className={cn("flex items-center gap-1", className)} {...props}>
76
+ {children}
77
+ </div>
78
+ );
79
+
80
+ export type MessageActionProps = ComponentProps<typeof Button> & {
81
+ tooltip?: string;
82
+ label?: string;
83
+ };
84
+
85
+ export const MessageAction = ({
86
+ tooltip,
87
+ children,
88
+ label,
89
+ variant = "ghost",
90
+ size = "icon-sm",
91
+ ...props
92
+ }: MessageActionProps) => {
93
+ const button = (
94
+ <Button size={size} type="button" variant={variant} {...props}>
95
+ {children}
96
+ <span className="sr-only">{label || tooltip}</span>
97
+ </Button>
98
+ );
99
+
100
+ if (tooltip) {
101
+ return (
102
+ <TooltipProvider>
103
+ <Tooltip>
104
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
105
+ <TooltipContent>
106
+ <p>{tooltip}</p>
107
+ </TooltipContent>
108
+ </Tooltip>
109
+ </TooltipProvider>
110
+ );
111
+ }
112
+
113
+ return button;
114
+ };
115
+
116
+ interface MessageBranchContextType {
117
+ currentBranch: number;
118
+ totalBranches: number;
119
+ goToPrevious: () => void;
120
+ goToNext: () => void;
121
+ branches: ReactElement[];
122
+ setBranches: (branches: ReactElement[]) => void;
123
+ }
124
+
125
+ const MessageBranchContext = createContext<MessageBranchContextType | null>(
126
+ null
127
+ );
128
+
129
+ const useMessageBranch = () => {
130
+ const context = useContext(MessageBranchContext);
131
+
132
+ if (!context) {
133
+ throw new Error(
134
+ "MessageBranch components must be used within MessageBranch"
135
+ );
136
+ }
137
+
138
+ return context;
139
+ };
140
+
141
+ export type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {
142
+ defaultBranch?: number;
143
+ onBranchChange?: (branchIndex: number) => void;
144
+ };
145
+
146
+ export const MessageBranch = ({
147
+ defaultBranch = 0,
148
+ onBranchChange,
149
+ className,
150
+ ...props
151
+ }: MessageBranchProps) => {
152
+ const [currentBranch, setCurrentBranch] = useState(defaultBranch);
153
+ const [branches, setBranches] = useState<ReactElement[]>([]);
154
+
155
+ const handleBranchChange = useCallback(
156
+ (newBranch: number) => {
157
+ setCurrentBranch(newBranch);
158
+ onBranchChange?.(newBranch);
159
+ },
160
+ [onBranchChange]
161
+ );
162
+
163
+ const goToPrevious = useCallback(() => {
164
+ const newBranch =
165
+ currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
166
+ handleBranchChange(newBranch);
167
+ }, [currentBranch, branches.length, handleBranchChange]);
168
+
169
+ const goToNext = useCallback(() => {
170
+ const newBranch =
171
+ currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
172
+ handleBranchChange(newBranch);
173
+ }, [currentBranch, branches.length, handleBranchChange]);
174
+
175
+ const contextValue = useMemo<MessageBranchContextType>(
176
+ () => ({
177
+ branches,
178
+ currentBranch,
179
+ goToNext,
180
+ goToPrevious,
181
+ setBranches,
182
+ totalBranches: branches.length,
183
+ }),
184
+ [branches, currentBranch, goToNext, goToPrevious]
185
+ );
186
+
187
+ return (
188
+ <MessageBranchContext.Provider value={contextValue}>
189
+ <div
190
+ className={cn("grid w-full gap-2 [&>div]:pb-0", className)}
191
+ {...props}
192
+ />
193
+ </MessageBranchContext.Provider>
194
+ );
195
+ };
196
+
197
+ export type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;
198
+
199
+ export const MessageBranchContent = ({
200
+ children,
201
+ ...props
202
+ }: MessageBranchContentProps) => {
203
+ const { currentBranch, setBranches, branches } = useMessageBranch();
204
+ const childrenArray = useMemo(
205
+ () => (Array.isArray(children) ? children : [children]),
206
+ [children]
207
+ );
208
+
209
+ // Use useEffect to update branches when they change
210
+ useEffect(() => {
211
+ if (branches.length !== childrenArray.length) {
212
+ setBranches(childrenArray);
213
+ }
214
+ }, [childrenArray, branches, setBranches]);
215
+
216
+ return childrenArray.map((branch, index) => (
217
+ <div
218
+ className={cn(
219
+ "grid gap-2 overflow-hidden [&>div]:pb-0",
220
+ index === currentBranch ? "block" : "hidden"
221
+ )}
222
+ key={branch.key}
223
+ {...props}
224
+ >
225
+ {branch}
226
+ </div>
227
+ ));
228
+ };
229
+
230
+ export type MessageBranchSelectorProps = ComponentProps<typeof ButtonGroup>;
231
+
232
+ export const MessageBranchSelector = ({
233
+ className,
234
+ ...props
235
+ }: MessageBranchSelectorProps) => {
236
+ const { totalBranches } = useMessageBranch();
237
+
238
+ // Don't render if there's only one branch
239
+ if (totalBranches <= 1) {
240
+ return null;
241
+ }
242
+
243
+ return (
244
+ <ButtonGroup
245
+ className={cn(
246
+ "[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md",
247
+ className
248
+ )}
249
+ orientation="horizontal"
250
+ {...props}
251
+ />
252
+ );
253
+ };
254
+
255
+ export type MessageBranchPreviousProps = ComponentProps<typeof Button>;
256
+
257
+ export const MessageBranchPrevious = ({
258
+ children,
259
+ ...props
260
+ }: MessageBranchPreviousProps) => {
261
+ const { goToPrevious, totalBranches } = useMessageBranch();
262
+
263
+ return (
264
+ <Button
265
+ aria-label="Previous branch"
266
+ disabled={totalBranches <= 1}
267
+ onClick={goToPrevious}
268
+ size="icon-sm"
269
+ type="button"
270
+ variant="ghost"
271
+ {...props}
272
+ >
273
+ {children ?? <ChevronLeftIcon size={14} />}
274
+ </Button>
275
+ );
276
+ };
277
+
278
+ export type MessageBranchNextProps = ComponentProps<typeof Button>;
279
+
280
+ export const MessageBranchNext = ({
281
+ children,
282
+ ...props
283
+ }: MessageBranchNextProps) => {
284
+ const { goToNext, totalBranches } = useMessageBranch();
285
+
286
+ return (
287
+ <Button
288
+ aria-label="Next branch"
289
+ disabled={totalBranches <= 1}
290
+ onClick={goToNext}
291
+ size="icon-sm"
292
+ type="button"
293
+ variant="ghost"
294
+ {...props}
295
+ >
296
+ {children ?? <ChevronRightIcon size={14} />}
297
+ </Button>
298
+ );
299
+ };
300
+
301
+ export type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;
302
+
303
+ export const MessageBranchPage = ({
304
+ className,
305
+ ...props
306
+ }: MessageBranchPageProps) => {
307
+ const { currentBranch, totalBranches } = useMessageBranch();
308
+
309
+ return (
310
+ <ButtonGroupText
311
+ className={cn(
312
+ "border-none bg-transparent text-muted-foreground shadow-none",
313
+ className
314
+ )}
315
+ {...props}
316
+ >
317
+ {currentBranch + 1} of {totalBranches}
318
+ </ButtonGroupText>
319
+ );
320
+ };
321
+
322
+ export type MessageResponseProps = ComponentProps<typeof Streamdown>;
323
+
324
+ const streamdownPlugins = { cjk, code, math, mermaid };
325
+
326
+ export const MessageResponse = memo(
327
+ ({ className, ...props }: MessageResponseProps) => (
328
+ <Streamdown
329
+ className={cn(
330
+ "size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
331
+ className
332
+ )}
333
+ plugins={streamdownPlugins}
334
+ {...props}
335
+ />
336
+ ),
337
+ (prevProps, nextProps) =>
338
+ prevProps.children === nextProps.children &&
339
+ nextProps.isAnimating === prevProps.isAnimating
340
+ );
341
+
342
+ MessageResponse.displayName = "MessageResponse";
343
+
344
+ export type MessageToolbarProps = ComponentProps<"div">;
345
+
346
+ export const MessageToolbar = ({
347
+ className,
348
+ children,
349
+ ...props
350
+ }: MessageToolbarProps) => (
351
+ <div
352
+ className={cn(
353
+ "mt-4 flex w-full items-center justify-between gap-4",
354
+ className
355
+ )}
356
+ {...props}
357
+ >
358
+ {children}
359
+ </div>
360
+ );