@vvedantb/ui 0.0.0-stage → 0.1.0

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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +124 -4
  3. package/src/ai-elements/artifact.tsx +99 -0
  4. package/src/ai-elements/attachments.tsx +366 -0
  5. package/src/ai-elements/chain-of-thought.tsx +212 -0
  6. package/src/ai-elements/code-block.tsx +478 -0
  7. package/src/ai-elements/conversation.tsx +157 -0
  8. package/src/ai-elements/image.tsx +25 -0
  9. package/src/ai-elements/inline-citation.tsx +252 -0
  10. package/src/ai-elements/message.tsx +269 -0
  11. package/src/ai-elements/model-selector.tsx +211 -0
  12. package/src/ai-elements/prompt-input.tsx +1018 -0
  13. package/src/ai-elements/queue.tsx +251 -0
  14. package/src/ai-elements/reasoning.tsx +178 -0
  15. package/src/ai-elements/shimmer.tsx +56 -0
  16. package/src/ai-elements/sources.tsx +77 -0
  17. package/src/ai-elements/speech-input.tsx +290 -0
  18. package/src/ai-elements/suggestion.tsx +191 -0
  19. package/src/css.d.ts +2 -0
  20. package/src/index.ts +96 -0
  21. package/src/motion/presets.ts +35 -0
  22. package/src/motion/presets.vibot.ts +105 -0
  23. package/src/ui/_menu-classes.ts +30 -0
  24. package/src/ui/_menu-classes.vibot.ts +33 -0
  25. package/src/ui/accordion.tsx +81 -0
  26. package/src/ui/accordion.verve.tsx +56 -0
  27. package/src/ui/badge.tsx +34 -0
  28. package/src/ui/breadcrumb.tsx +83 -0
  29. package/src/ui/button-group.tsx +84 -0
  30. package/src/ui/button.tsx +57 -0
  31. package/src/ui/card.tsx +27 -0
  32. package/src/ui/carousel.tsx +211 -0
  33. package/src/ui/checkbox.tsx +27 -0
  34. package/src/ui/clear-input.tsx +170 -0
  35. package/src/ui/clear-input.vibot.tsx +76 -0
  36. package/src/ui/clearInputDissolve.ts +214 -0
  37. package/src/ui/collapsible.tsx +7 -0
  38. package/src/ui/command.tsx +148 -0
  39. package/src/ui/context-menu.tsx +173 -0
  40. package/src/ui/dialog.tsx +149 -0
  41. package/src/ui/dropdown-menu.tsx +178 -0
  42. package/src/ui/hover-card.tsx +31 -0
  43. package/src/ui/input-group.tsx +162 -0
  44. package/src/ui/input-group.verve.tsx +150 -0
  45. package/src/ui/input.tsx +21 -0
  46. package/src/ui/kbd.tsx +18 -0
  47. package/src/ui/label.tsx +20 -0
  48. package/src/ui/labeled-switch-row.tsx +53 -0
  49. package/src/ui/modalTransition.ts +85 -0
  50. package/src/ui/pagination.tsx +47 -0
  51. package/src/ui/popover.tsx +33 -0
  52. package/src/ui/progress.tsx +25 -0
  53. package/src/ui/radix-select.tsx +202 -0
  54. package/src/ui/scroll-area.tsx +55 -0
  55. package/src/ui/scroll-area.verve.tsx +43 -0
  56. package/src/ui/search-input.tsx +29 -0
  57. package/src/ui/select.tsx +163 -0
  58. package/src/ui/separator.tsx +28 -0
  59. package/src/ui/sheet.tsx +87 -0
  60. package/src/ui/skeleton.tsx +18 -0
  61. package/src/ui/sonner.css +16 -0
  62. package/src/ui/sonner.tsx +47 -0
  63. package/src/ui/spinner.tsx +27 -0
  64. package/src/ui/surface-classes.ts +55 -0
  65. package/src/ui/switch.tsx +26 -0
  66. package/src/ui/table.tsx +123 -0
  67. package/src/ui/tabs.tsx +140 -0
  68. package/src/ui/tabsSliding.ts +56 -0
  69. package/src/ui/textarea.tsx +21 -0
  70. package/src/ui/time-picker.tsx +170 -0
  71. package/src/ui/tooltip.tsx +25 -0
  72. package/src/ui/visually-hidden.tsx +3 -0
  73. package/src/utils/cn.ts +16 -0
  74. package/src/utils/ref.ts +10 -0
  75. package/README.md +0 -3
@@ -0,0 +1,157 @@
1
+ "use client";
2
+ import type { ComponentProps } from "react";
3
+ import { AnimatePresence, motion } from "motion/react";
4
+ import { IconArrowDown, IconDownload } from "@tabler/icons-react";
5
+ import { Button } from "../ui/button";
6
+ import { cn } from "../utils/cn";
7
+ import { flatInteractiveClass } from "../ui/surface-classes";
8
+ import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
9
+ import { fastTransition } from "../motion/presets.vibot";
10
+ export type ConversationProps = ComponentProps<typeof StickToBottom>;
11
+ export const Conversation = ({ className, ...props }: ConversationProps) => (
12
+ <StickToBottom
13
+ className={cn("relative flex-1 overflow-y-hidden", className)}
14
+ initial="instant"
15
+ resize="smooth"
16
+ role="log"
17
+ {...props}
18
+ />
19
+ );
20
+ export type ConversationContentProps = ComponentProps<
21
+ typeof StickToBottom.Content
22
+ >;
23
+ export const ConversationContent = ({
24
+ className,
25
+ ...props
26
+ }: ConversationContentProps) => (
27
+ <StickToBottom.Content
28
+ className={cn("flex flex-col gap-4 px-3 py-3", className)}
29
+ {...props}
30
+ />
31
+ );
32
+ export type ConversationEmptyStateProps = ComponentProps<"div"> & {
33
+ title?: string;
34
+ description?: string;
35
+ icon?: React.ReactNode;
36
+ };
37
+ export const ConversationEmptyState = ({
38
+ className,
39
+ title = "No messages yet",
40
+ description = "Start a conversation to see messages here",
41
+ icon,
42
+ children,
43
+ ...props
44
+ }: ConversationEmptyStateProps) => (
45
+ <div
46
+ className={cn(
47
+ "flex size-full flex-col items-center justify-center gap-3 p-8 text-center",
48
+ className,
49
+ )}
50
+ {...props}
51
+ >
52
+ {children ?? (
53
+ <>
54
+ {icon && <div className="text-muted">{icon}</div>}
55
+ <div className="space-y-1">
56
+ <h3 className="font-medium text-sm">{title}</h3>
57
+ {description && <p className="text-muted text-sm">{description}</p>}
58
+ </div>
59
+ </>
60
+ )}
61
+ </div>
62
+ );
63
+ export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
64
+ export const ConversationScrollButton = ({
65
+ className,
66
+ ...props
67
+ }: ConversationScrollButtonProps) => {
68
+ const { isAtBottom, scrollToBottom } = useStickToBottomContext();
69
+ const handleScrollToBottom = () => {
70
+ scrollToBottom();
71
+ };
72
+ return (
73
+ <AnimatePresence initial={false}>
74
+ {isAtBottom ? null : (
75
+ <motion.div
76
+ key="scroll-to-bottom"
77
+ className="absolute bottom-4 left-0 right-0 z-10 flex justify-center"
78
+ initial={{ opacity: 0, y: 8 }}
79
+ animate={{ opacity: 1, y: 0 }}
80
+ exit={{ opacity: 0, y: 8 }}
81
+ transition={fastTransition}
82
+ >
83
+ <Button
84
+ className={cn(flatInteractiveClass, "rounded-full", className)}
85
+ onClick={handleScrollToBottom}
86
+ size="icon"
87
+ type="button"
88
+ variant="outline"
89
+ {...props}
90
+ >
91
+ <IconArrowDown className="size-4" />
92
+ </Button>
93
+ </motion.div>
94
+ )}
95
+ </AnimatePresence>
96
+ );
97
+ };
98
+ export interface ConversationMessage {
99
+ role: "user" | "assistant" | "system" | "data" | "tool";
100
+ content: string;
101
+ }
102
+ export type ConversationDownloadProps = Omit<
103
+ ComponentProps<typeof Button>,
104
+ "onClick"
105
+ > & {
106
+ messages: ConversationMessage[];
107
+ filename?: string;
108
+ formatMessage?: (message: ConversationMessage, index: number) => string;
109
+ };
110
+ const defaultFormatMessage = (message: ConversationMessage): string => {
111
+ const roleLabel =
112
+ message.role.charAt(0).toUpperCase() + message.role.slice(1);
113
+ return `**${roleLabel}:** ${message.content}`;
114
+ };
115
+ export const messagesToMarkdown = (
116
+ messages: ConversationMessage[],
117
+ formatMessage: (
118
+ message: ConversationMessage,
119
+ index: number,
120
+ ) => string = defaultFormatMessage,
121
+ ): string => messages.map((msg, i) => formatMessage(msg, i)).join("\n\n");
122
+ export const ConversationDownload = ({
123
+ messages,
124
+ filename = "conversation.md",
125
+ formatMessage = defaultFormatMessage,
126
+ className,
127
+ children,
128
+ ...props
129
+ }: ConversationDownloadProps) => {
130
+ const handleDownload = () => {
131
+ const markdown = messagesToMarkdown(messages, formatMessage);
132
+ const blob = new Blob([markdown], { type: "text/markdown" });
133
+ const url = URL.createObjectURL(blob);
134
+ const link = document.createElement("a");
135
+ link.href = url;
136
+ link.download = filename;
137
+ document.body.append(link);
138
+ link.click();
139
+ link.remove();
140
+ URL.revokeObjectURL(url);
141
+ };
142
+ return (
143
+ <Button
144
+ className={cn(
145
+ cn(flatInteractiveClass, "absolute top-4 right-4 rounded-full"),
146
+ className,
147
+ )}
148
+ onClick={handleDownload}
149
+ size="icon"
150
+ type="button"
151
+ variant="outline"
152
+ {...props}
153
+ >
154
+ {children ?? <IconDownload className="size-4" />}
155
+ </Button>
156
+ );
157
+ };
@@ -0,0 +1,25 @@
1
+ import type { Experimental_GeneratedImage } from "ai";
2
+
3
+ import { cn } from "../utils/cn";
4
+
5
+ export type ImageProps = Experimental_GeneratedImage & {
6
+ className?: string;
7
+ alt?: string;
8
+ };
9
+
10
+ export const Image = ({
11
+ base64,
12
+ uint8Array: _uint8Array,
13
+ mediaType,
14
+ ...props
15
+ }: ImageProps) => (
16
+ <img
17
+ {...props}
18
+ alt={props.alt}
19
+ className={cn(
20
+ "h-auto max-w-full overflow-hidden rounded-md",
21
+ props.className,
22
+ )}
23
+ src={`data:${mediaType};base64,${base64}`}
24
+ />
25
+ );
@@ -0,0 +1,252 @@
1
+ "use client";
2
+ import type { CarouselApi } from "../ui/carousel";
3
+ import type { ComponentProps } from "react";
4
+ import { Badge } from "../ui/badge";
5
+ import { Carousel, CarouselContent, CarouselItem } from "../ui/carousel";
6
+ import {
7
+ HoverCard,
8
+ HoverCardContent,
9
+ HoverCardTrigger,
10
+ } from "../ui/hover-card";
11
+ import { cn } from "../utils/cn";
12
+ import { flatPanelSubtleClass } from "../ui/surface-classes";
13
+ import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react";
14
+ import { createContext, useContext, useEffect, useState } from "react";
15
+ export type InlineCitationProps = ComponentProps<"span">;
16
+ export const InlineCitation = ({
17
+ className,
18
+ ...props
19
+ }: InlineCitationProps) => (
20
+ <span
21
+ className={cn("group inline items-center gap-1", className)}
22
+ {...props}
23
+ />
24
+ );
25
+ export type InlineCitationTextProps = ComponentProps<"span">;
26
+ export const InlineCitationText = ({
27
+ className,
28
+ ...props
29
+ }: InlineCitationTextProps) => (
30
+ <span
31
+ className={cn(
32
+ "transition-colors group-hover:bg-surface-secondary",
33
+ className,
34
+ )}
35
+ {...props}
36
+ />
37
+ );
38
+ export type InlineCitationCardProps = ComponentProps<typeof HoverCard>;
39
+ export const InlineCitationCard = (props: InlineCitationCardProps) => (
40
+ <HoverCard closeDelay={0} openDelay={0} {...props} />
41
+ );
42
+ export type InlineCitationCardTriggerProps = ComponentProps<typeof Badge> & {
43
+ sources: string[];
44
+ };
45
+ export const InlineCitationCardTrigger = ({
46
+ sources,
47
+ className,
48
+ ...props
49
+ }: InlineCitationCardTriggerProps) => (
50
+ <HoverCardTrigger asChild>
51
+ <Badge
52
+ className={cn("ml-1 rounded-full", className)}
53
+ variant="secondary"
54
+ {...props}
55
+ >
56
+ {sources[0] ? (
57
+ <>
58
+ {new URL(sources[0]).hostname}{" "}
59
+ {sources.length > 1 && `+${sources.length - 1}`}
60
+ </>
61
+ ) : (
62
+ "unknown"
63
+ )}
64
+ </Badge>
65
+ </HoverCardTrigger>
66
+ );
67
+ export type InlineCitationCardBodyProps = ComponentProps<"div">;
68
+ export const InlineCitationCardBody = ({
69
+ className,
70
+ ...props
71
+ }: InlineCitationCardBodyProps) => (
72
+ <HoverCardContent className={cn("relative w-80 p-0", className)} {...props} />
73
+ );
74
+ const CarouselApiContext = createContext<CarouselApi | undefined>(undefined);
75
+ const useCarouselApi = () => {
76
+ const context = useContext(CarouselApiContext);
77
+ return context;
78
+ };
79
+ export type InlineCitationCarouselProps = ComponentProps<typeof Carousel>;
80
+ export const InlineCitationCarousel = ({
81
+ className,
82
+ children,
83
+ ...props
84
+ }: InlineCitationCarouselProps) => {
85
+ const [api, setApi] = useState<CarouselApi>();
86
+ return (
87
+ <CarouselApiContext.Provider value={api}>
88
+ <Carousel className={cn("w-full", className)} setApi={setApi} {...props}>
89
+ {children}
90
+ </Carousel>
91
+ </CarouselApiContext.Provider>
92
+ );
93
+ };
94
+ export type InlineCitationCarouselContentProps = ComponentProps<"div">;
95
+ export const InlineCitationCarouselContent = (
96
+ props: InlineCitationCarouselContentProps,
97
+ ) => <CarouselContent {...props} />;
98
+ export type InlineCitationCarouselItemProps = ComponentProps<"div">;
99
+ export const InlineCitationCarouselItem = ({
100
+ className,
101
+ ...props
102
+ }: InlineCitationCarouselItemProps) => (
103
+ <CarouselItem
104
+ className={cn("w-full space-y-2 p-4 pl-8", className)}
105
+ {...props}
106
+ />
107
+ );
108
+ export type InlineCitationCarouselHeaderProps = ComponentProps<"div">;
109
+ export const InlineCitationCarouselHeader = ({
110
+ className,
111
+ ...props
112
+ }: InlineCitationCarouselHeaderProps) => (
113
+ <div
114
+ className={cn(
115
+ cn(
116
+ flatPanelSubtleClass,
117
+ "flex items-center justify-between gap-2 rounded-t-md p-2",
118
+ ),
119
+ className,
120
+ )}
121
+ {...props}
122
+ />
123
+ );
124
+ export type InlineCitationCarouselIndexProps = ComponentProps<"div">;
125
+ export const InlineCitationCarouselIndex = ({
126
+ children,
127
+ className,
128
+ ...props
129
+ }: InlineCitationCarouselIndexProps) => {
130
+ const api = useCarouselApi();
131
+ const [current, setCurrent] = useState(0);
132
+ const [count, setCount] = useState(0);
133
+ useEffect(() => {
134
+ if (!api) {
135
+ return;
136
+ }
137
+ setCount(api.scrollSnapList().length);
138
+ setCurrent(api.selectedScrollSnap() + 1);
139
+ const handleSelect = () => {
140
+ setCurrent(api.selectedScrollSnap() + 1);
141
+ };
142
+ api.on("select", handleSelect);
143
+ return () => {
144
+ api.off("select", handleSelect);
145
+ };
146
+ }, [api]);
147
+ return (
148
+ <div
149
+ className={cn(
150
+ "flex flex-1 items-center justify-end px-3 py-1 text-muted text-xs",
151
+ className,
152
+ )}
153
+ {...props}
154
+ >
155
+ {children ?? `${current}/${count}`}
156
+ </div>
157
+ );
158
+ };
159
+ export type InlineCitationCarouselPrevProps = ComponentProps<"button">;
160
+ export const InlineCitationCarouselPrev = ({
161
+ className,
162
+ ...props
163
+ }: InlineCitationCarouselPrevProps) => {
164
+ const api = useCarouselApi();
165
+ const handleClick = () => {
166
+ if (api) {
167
+ api.scrollPrev();
168
+ }
169
+ };
170
+ return (
171
+ <button
172
+ aria-label="Previous"
173
+ className={cn(
174
+ "shrink-0 rounded-md p-0.5 transition-[background-color,transform] duration-150 ease-out active:scale-[0.97] hover:bg-surface-secondary",
175
+ className,
176
+ )}
177
+ onClick={handleClick}
178
+ type="button"
179
+ {...props}
180
+ >
181
+ <IconArrowLeft className="size-4 text-muted" />
182
+ </button>
183
+ );
184
+ };
185
+ export type InlineCitationCarouselNextProps = ComponentProps<"button">;
186
+ export const InlineCitationCarouselNext = ({
187
+ className,
188
+ ...props
189
+ }: InlineCitationCarouselNextProps) => {
190
+ const api = useCarouselApi();
191
+ const handleClick = () => {
192
+ if (api) {
193
+ api.scrollNext();
194
+ }
195
+ };
196
+ return (
197
+ <button
198
+ aria-label="Next"
199
+ className={cn(
200
+ "shrink-0 rounded-md p-0.5 transition-[background-color,transform] duration-150 ease-out active:scale-[0.97] hover:bg-surface-secondary",
201
+ className,
202
+ )}
203
+ onClick={handleClick}
204
+ type="button"
205
+ {...props}
206
+ >
207
+ <IconArrowRight className="size-4 text-muted" />
208
+ </button>
209
+ );
210
+ };
211
+ export type InlineCitationSourceProps = ComponentProps<"div"> & {
212
+ title?: string;
213
+ url?: string;
214
+ description?: string;
215
+ };
216
+ export const InlineCitationSource = ({
217
+ title,
218
+ url,
219
+ description,
220
+ className,
221
+ children,
222
+ ...props
223
+ }: InlineCitationSourceProps) => (
224
+ <div className={cn("space-y-1", className)} {...props}>
225
+ {title && (
226
+ <h4 className="truncate font-medium text-sm leading-tight">{title}</h4>
227
+ )}
228
+ {url && <p className="truncate break-all text-muted text-xs">{url}</p>}
229
+ {description && (
230
+ <p className="line-clamp-3 text-muted text-sm leading-relaxed">
231
+ {description}
232
+ </p>
233
+ )}
234
+ {children}
235
+ </div>
236
+ );
237
+ export type InlineCitationQuoteProps = ComponentProps<"blockquote">;
238
+ export const InlineCitationQuote = ({
239
+ children,
240
+ className,
241
+ ...props
242
+ }: InlineCitationQuoteProps) => (
243
+ <blockquote
244
+ className={cn(
245
+ "border-l-2 border-separator pl-3 text-muted text-sm italic",
246
+ className,
247
+ )}
248
+ {...props}
249
+ >
250
+ {children}
251
+ </blockquote>
252
+ );
@@ -0,0 +1,269 @@
1
+ "use client";
2
+ import type { UIMessage } from "ai";
3
+ import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
4
+ import { Button } from "../ui/button";
5
+ import { ButtonGroup, ButtonGroupText } from "../ui/button-group";
6
+ import {
7
+ Tooltip,
8
+ TooltipContent,
9
+ TooltipProvider,
10
+ TooltipTrigger,
11
+ } from "../ui/tooltip";
12
+ import { cn } from "../utils/cn";
13
+ import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
14
+ import { createContext, useContext, useEffect, useState } from "react";
15
+ export type MessageProps = HTMLAttributes<HTMLDivElement> & {
16
+ from: UIMessage["role"];
17
+ };
18
+ export const Message = ({ className, from, ...props }: MessageProps) => (
19
+ <div
20
+ className={cn(
21
+ "group flex w-full max-w-[95%] flex-col gap-2",
22
+ from === "user" ? "is-user ml-auto justify-end" : "is-assistant",
23
+ className,
24
+ )}
25
+ {...props}
26
+ />
27
+ );
28
+ export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
29
+ export const MessageContent = ({
30
+ children,
31
+ className,
32
+ ...props
33
+ }: MessageContentProps) => (
34
+ <div
35
+ className={cn(
36
+ "is-user:dark flex w-fit min-w-0 max-w-full flex-col gap-2 overflow-hidden text-sm",
37
+ "group-[.is-user]:ml-auto group-[.is-user]:rounded-2xl group-[.is-user]:rounded-br-md group-[.is-user]:bg-primary group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-primary-foreground",
38
+ "group-[.is-assistant]:px-0 group-[.is-assistant]:text-foreground",
39
+ className,
40
+ )}
41
+ {...props}
42
+ >
43
+ {children}
44
+ </div>
45
+ );
46
+ export type MessageActionsBarProps = ComponentProps<"div">;
47
+ export const MessageActionsBar = ({
48
+ className,
49
+ children,
50
+ ...props
51
+ }: MessageActionsBarProps) => (
52
+ <div className={cn("flex items-center gap-1", className)} {...props}>
53
+ {children}
54
+ </div>
55
+ );
56
+ export type MessageActionProps = ComponentProps<typeof Button> & {
57
+ tooltip?: string;
58
+ label?: string;
59
+ };
60
+ export const MessageAction = ({
61
+ tooltip,
62
+ children,
63
+ label,
64
+ variant = "ghost",
65
+ size = "icon-sm",
66
+ ...props
67
+ }: MessageActionProps) => {
68
+ const button = (
69
+ <Button size={size} type="button" variant={variant} {...props}>
70
+ {children}
71
+ <span className="sr-only">{label || tooltip}</span>
72
+ </Button>
73
+ );
74
+ if (tooltip) {
75
+ return (
76
+ <TooltipProvider>
77
+ <Tooltip>
78
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
79
+ <TooltipContent>
80
+ <p>{tooltip}</p>
81
+ </TooltipContent>
82
+ </Tooltip>
83
+ </TooltipProvider>
84
+ );
85
+ }
86
+ return button;
87
+ };
88
+ interface MessageBranchContextType {
89
+ currentBranch: number;
90
+ totalBranches: number;
91
+ goToPrevious: () => void;
92
+ goToNext: () => void;
93
+ branches: ReactElement[];
94
+ setBranches: (branches: ReactElement[]) => void;
95
+ }
96
+ const MessageBranchContext = createContext<MessageBranchContextType | null>(
97
+ null,
98
+ );
99
+ const useMessageBranch = () => {
100
+ const context = useContext(MessageBranchContext);
101
+ if (!context) {
102
+ throw new Error(
103
+ "MessageBranch components must be used within MessageBranch",
104
+ );
105
+ }
106
+ return context;
107
+ };
108
+ export type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {
109
+ defaultBranch?: number;
110
+ onBranchChange?: (branchIndex: number) => void;
111
+ };
112
+ export const MessageBranch = ({
113
+ defaultBranch = 0,
114
+ onBranchChange,
115
+ className,
116
+ ...props
117
+ }: MessageBranchProps) => {
118
+ const [currentBranch, setCurrentBranch] = useState(defaultBranch);
119
+ const [branches, setBranches] = useState<ReactElement[]>([]);
120
+ const handleBranchChange = (newBranch: number) => {
121
+ setCurrentBranch(newBranch);
122
+ onBranchChange?.(newBranch);
123
+ };
124
+ const goToPrevious = () => {
125
+ const newBranch =
126
+ currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
127
+ handleBranchChange(newBranch);
128
+ };
129
+ const goToNext = () => {
130
+ const newBranch =
131
+ currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
132
+ handleBranchChange(newBranch);
133
+ };
134
+ const contextValue = {
135
+ branches,
136
+ currentBranch,
137
+ goToNext,
138
+ goToPrevious,
139
+ setBranches,
140
+ totalBranches: branches.length,
141
+ };
142
+ return (
143
+ <MessageBranchContext.Provider value={contextValue}>
144
+ <div
145
+ className={cn("grid w-full gap-2 [&>div]:pb-0", className)}
146
+ {...props}
147
+ />
148
+ </MessageBranchContext.Provider>
149
+ );
150
+ };
151
+ export type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;
152
+ export const MessageBranchContent = ({
153
+ children,
154
+ ...props
155
+ }: MessageBranchContentProps) => {
156
+ const { currentBranch, setBranches, branches } = useMessageBranch();
157
+ const childrenArray = Array.isArray(children) ? children : [children];
158
+ useEffect(() => {
159
+ if (branches.length !== childrenArray.length) {
160
+ setBranches(childrenArray);
161
+ }
162
+ }, [childrenArray, branches, setBranches]);
163
+ return childrenArray.map((branch, index) => (
164
+ <div
165
+ className={cn(
166
+ "grid gap-2 overflow-hidden [&>div]:pb-0",
167
+ index === currentBranch ? "block" : "hidden",
168
+ )}
169
+ key={branch.key}
170
+ {...props}
171
+ >
172
+ {branch}
173
+ </div>
174
+ ));
175
+ };
176
+ export type MessageBranchSelectorProps = ComponentProps<typeof ButtonGroup>;
177
+ export const MessageBranchSelector = ({
178
+ className,
179
+ ...props
180
+ }: MessageBranchSelectorProps) => {
181
+ const { totalBranches } = useMessageBranch();
182
+ if (totalBranches <= 1) {
183
+ return null;
184
+ }
185
+ return (
186
+ <ButtonGroup
187
+ className={cn(
188
+ "[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md",
189
+ className,
190
+ )}
191
+ orientation="horizontal"
192
+ {...props}
193
+ />
194
+ );
195
+ };
196
+ export type MessageBranchPreviousProps = ComponentProps<typeof Button>;
197
+ export const MessageBranchPrevious = ({
198
+ children,
199
+ ...props
200
+ }: MessageBranchPreviousProps) => {
201
+ const { goToPrevious, totalBranches } = useMessageBranch();
202
+ return (
203
+ <Button
204
+ aria-label="Previous branch"
205
+ disabled={totalBranches <= 1}
206
+ onClick={goToPrevious}
207
+ size="icon-sm"
208
+ type="button"
209
+ variant="ghost"
210
+ {...props}
211
+ >
212
+ {children ?? <IconChevronLeft size={14} />}
213
+ </Button>
214
+ );
215
+ };
216
+ export type MessageBranchNextProps = ComponentProps<typeof Button>;
217
+ export const MessageBranchNext = ({
218
+ children,
219
+ ...props
220
+ }: MessageBranchNextProps) => {
221
+ const { goToNext, totalBranches } = useMessageBranch();
222
+ return (
223
+ <Button
224
+ aria-label="Next branch"
225
+ disabled={totalBranches <= 1}
226
+ onClick={goToNext}
227
+ size="icon-sm"
228
+ type="button"
229
+ variant="ghost"
230
+ {...props}
231
+ >
232
+ {children ?? <IconChevronRight size={14} />}
233
+ </Button>
234
+ );
235
+ };
236
+ export type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;
237
+ export const MessageBranchPage = ({
238
+ className,
239
+ ...props
240
+ }: MessageBranchPageProps) => {
241
+ const { currentBranch, totalBranches } = useMessageBranch();
242
+ return (
243
+ <ButtonGroupText
244
+ className={cn(
245
+ "border-none bg-transparent text-muted shadow-none",
246
+ className,
247
+ )}
248
+ {...props}
249
+ >
250
+ {currentBranch + 1} of {totalBranches}
251
+ </ButtonGroupText>
252
+ );
253
+ };
254
+ export type MessageToolbarProps = ComponentProps<"div">;
255
+ export const MessageToolbar = ({
256
+ className,
257
+ children,
258
+ ...props
259
+ }: MessageToolbarProps) => (
260
+ <div
261
+ className={cn(
262
+ "mt-4 flex w-full items-center justify-between gap-4",
263
+ className,
264
+ )}
265
+ {...props}
266
+ >
267
+ {children}
268
+ </div>
269
+ );