@vvedantb/ui 0.0.0-stage → 0.1.1
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.
- package/LICENSE +21 -0
- package/package.json +124 -4
- package/src/ai-elements/artifact.tsx +99 -0
- package/src/ai-elements/attachments.tsx +366 -0
- package/src/ai-elements/chain-of-thought.tsx +212 -0
- package/src/ai-elements/code-block.tsx +478 -0
- package/src/ai-elements/conversation.tsx +157 -0
- package/src/ai-elements/image.tsx +25 -0
- package/src/ai-elements/inline-citation.tsx +252 -0
- package/src/ai-elements/message.tsx +269 -0
- package/src/ai-elements/model-selector.tsx +211 -0
- package/src/ai-elements/prompt-input.tsx +1018 -0
- package/src/ai-elements/queue.tsx +251 -0
- package/src/ai-elements/reasoning.tsx +178 -0
- package/src/ai-elements/shimmer.tsx +56 -0
- package/src/ai-elements/sources.tsx +77 -0
- package/src/ai-elements/speech-input.tsx +290 -0
- package/src/ai-elements/suggestion.tsx +191 -0
- package/src/css.d.ts +2 -0
- package/src/index.ts +96 -0
- package/src/motion/presets.ts +35 -0
- package/src/motion/presets.vibot.ts +105 -0
- package/src/ui/_menu-classes.ts +30 -0
- package/src/ui/_menu-classes.vibot.ts +33 -0
- package/src/ui/accordion.tsx +81 -0
- package/src/ui/accordion.verve.tsx +56 -0
- package/src/ui/badge.tsx +34 -0
- package/src/ui/breadcrumb.tsx +83 -0
- package/src/ui/button-group.tsx +84 -0
- package/src/ui/button.tsx +57 -0
- package/src/ui/card.tsx +27 -0
- package/src/ui/carousel.tsx +211 -0
- package/src/ui/checkbox.tsx +27 -0
- package/src/ui/clear-input.tsx +170 -0
- package/src/ui/clear-input.vibot.tsx +76 -0
- package/src/ui/clearInputDissolve.ts +214 -0
- package/src/ui/collapsible.tsx +7 -0
- package/src/ui/command.tsx +148 -0
- package/src/ui/context-menu.tsx +173 -0
- package/src/ui/dialog.tsx +149 -0
- package/src/ui/dropdown-menu.tsx +178 -0
- package/src/ui/hover-card.tsx +31 -0
- package/src/ui/input-group.tsx +162 -0
- package/src/ui/input-group.verve.tsx +150 -0
- package/src/ui/input.tsx +21 -0
- package/src/ui/kbd.tsx +18 -0
- package/src/ui/label.tsx +20 -0
- package/src/ui/labeled-switch-row.tsx +53 -0
- package/src/ui/modalTransition.ts +85 -0
- package/src/ui/pagination.tsx +47 -0
- package/src/ui/popover.tsx +33 -0
- package/src/ui/progress.tsx +25 -0
- package/src/ui/radix-select.tsx +202 -0
- package/src/ui/scroll-area.tsx +55 -0
- package/src/ui/scroll-area.verve.tsx +43 -0
- package/src/ui/search-input.tsx +29 -0
- package/src/ui/select.tsx +163 -0
- package/src/ui/separator.tsx +28 -0
- package/src/ui/sheet.tsx +87 -0
- package/src/ui/skeleton.tsx +18 -0
- package/src/ui/sonner.css +16 -0
- package/src/ui/sonner.tsx +47 -0
- package/src/ui/spinner.tsx +27 -0
- package/src/ui/surface-classes.ts +55 -0
- package/src/ui/switch.tsx +26 -0
- package/src/ui/table.tsx +123 -0
- package/src/ui/tabs.tsx +140 -0
- package/src/ui/tabsSliding.ts +56 -0
- package/src/ui/textarea.tsx +21 -0
- package/src/ui/time-picker.tsx +170 -0
- package/src/ui/tooltip.tsx +25 -0
- package/src/ui/visually-hidden.tsx +3 -0
- package/src/utils/cn.ts +16 -0
- package/src/utils/ref.ts +10 -0
- package/README.md +0 -3
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type ReactNode } from "react";
|
|
4
|
+
import { motion, AnimatePresence } from "motion/react";
|
|
5
|
+
import { cn } from "../utils/cn";
|
|
6
|
+
import {
|
|
7
|
+
flatPanelSubtleClass,
|
|
8
|
+
flatPanelSubtleHoverClass,
|
|
9
|
+
} from "../ui/surface-classes";
|
|
10
|
+
import { Button } from "../ui/button";
|
|
11
|
+
import { Badge } from "../ui/badge";
|
|
12
|
+
import { ScrollArea } from "../ui/scroll-area";
|
|
13
|
+
import { Textarea } from "../ui/textarea";
|
|
14
|
+
import {
|
|
15
|
+
IconChevronDown,
|
|
16
|
+
IconEdit,
|
|
17
|
+
IconTrash,
|
|
18
|
+
IconCheck,
|
|
19
|
+
IconX,
|
|
20
|
+
IconStack2,
|
|
21
|
+
} from "@tabler/icons-react";
|
|
22
|
+
|
|
23
|
+
export interface QueueProps {
|
|
24
|
+
className?: string;
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
count: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function Queue({ className, children, count }: QueueProps) {
|
|
30
|
+
return (
|
|
31
|
+
<AnimatePresence initial={false}>
|
|
32
|
+
{count > 0 ? (
|
|
33
|
+
<motion.div
|
|
34
|
+
key="queue"
|
|
35
|
+
initial={{ height: 0, opacity: 0 }}
|
|
36
|
+
animate={{ height: "auto", opacity: 1 }}
|
|
37
|
+
exit={{ height: 0, opacity: 0 }}
|
|
38
|
+
transition={{ type: "spring", bounce: 0, duration: 0.35 }}
|
|
39
|
+
className={cn("w-full overflow-hidden", className)}
|
|
40
|
+
>
|
|
41
|
+
{children}
|
|
42
|
+
</motion.div>
|
|
43
|
+
) : null}
|
|
44
|
+
</AnimatePresence>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface QueueHeaderProps {
|
|
49
|
+
count: number;
|
|
50
|
+
isOpen: boolean;
|
|
51
|
+
onToggle: () => void;
|
|
52
|
+
className?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function QueueHeader({
|
|
56
|
+
count,
|
|
57
|
+
isOpen,
|
|
58
|
+
onToggle,
|
|
59
|
+
className,
|
|
60
|
+
}: QueueHeaderProps) {
|
|
61
|
+
return (
|
|
62
|
+
<button
|
|
63
|
+
type="button"
|
|
64
|
+
onClick={onToggle}
|
|
65
|
+
className={cn(
|
|
66
|
+
"flex items-center justify-between w-full",
|
|
67
|
+
"px-4 py-2.5",
|
|
68
|
+
flatPanelSubtleClass,
|
|
69
|
+
flatPanelSubtleHoverClass,
|
|
70
|
+
"rounded-t-2xl",
|
|
71
|
+
"group",
|
|
72
|
+
className,
|
|
73
|
+
)}
|
|
74
|
+
>
|
|
75
|
+
<div className="flex items-center gap-2">
|
|
76
|
+
<IconStack2 size={16} className="text-muted" />
|
|
77
|
+
<span className="text-sm font-medium text-foreground">
|
|
78
|
+
Queued Messages
|
|
79
|
+
</span>
|
|
80
|
+
<Badge variant="secondary" className="text-xs">
|
|
81
|
+
{count}
|
|
82
|
+
</Badge>
|
|
83
|
+
</div>
|
|
84
|
+
<IconChevronDown
|
|
85
|
+
size={16}
|
|
86
|
+
className={cn(
|
|
87
|
+
"text-muted transition-transform duration-200 ease-out",
|
|
88
|
+
isOpen && "rotate-180",
|
|
89
|
+
)}
|
|
90
|
+
/>
|
|
91
|
+
</button>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface QueueListProps {
|
|
96
|
+
className?: string;
|
|
97
|
+
children?: ReactNode;
|
|
98
|
+
open?: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function QueueList({
|
|
102
|
+
className,
|
|
103
|
+
children,
|
|
104
|
+
open = true,
|
|
105
|
+
}: QueueListProps) {
|
|
106
|
+
return (
|
|
107
|
+
<AnimatePresence initial={false}>
|
|
108
|
+
{open ? (
|
|
109
|
+
<motion.div
|
|
110
|
+
key="queue-list"
|
|
111
|
+
initial={{ height: 0, opacity: 0 }}
|
|
112
|
+
animate={{ height: "auto", opacity: 1 }}
|
|
113
|
+
exit={{ height: 0, opacity: 0 }}
|
|
114
|
+
transition={{ type: "spring", bounce: 0, duration: 0.35 }}
|
|
115
|
+
className="overflow-hidden"
|
|
116
|
+
>
|
|
117
|
+
<div className={cn(flatPanelSubtleClass, "rounded-b-2xl", className)}>
|
|
118
|
+
<ScrollArea className="max-h-[240px]">
|
|
119
|
+
<div className="p-2 space-y-1.5">{children}</div>
|
|
120
|
+
</ScrollArea>
|
|
121
|
+
</div>
|
|
122
|
+
</motion.div>
|
|
123
|
+
) : null}
|
|
124
|
+
</AnimatePresence>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface QueueItemProps {
|
|
129
|
+
text: string;
|
|
130
|
+
model?: string;
|
|
131
|
+
personalityName?: string;
|
|
132
|
+
personaName?: string;
|
|
133
|
+
personaImage?: string;
|
|
134
|
+
onEdit: (newText: string) => void;
|
|
135
|
+
onRemove: () => void;
|
|
136
|
+
className?: string;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function QueueItem({
|
|
140
|
+
text,
|
|
141
|
+
model,
|
|
142
|
+
personalityName,
|
|
143
|
+
personaName,
|
|
144
|
+
personaImage,
|
|
145
|
+
onEdit,
|
|
146
|
+
onRemove,
|
|
147
|
+
className,
|
|
148
|
+
}: QueueItemProps) {
|
|
149
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
150
|
+
const [editText, setEditText] = useState(text);
|
|
151
|
+
|
|
152
|
+
const handleSaveEdit = () => {
|
|
153
|
+
if (editText.trim()) {
|
|
154
|
+
onEdit(editText.trim());
|
|
155
|
+
setIsEditing(false);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const handleCancelEdit = () => {
|
|
160
|
+
setEditText(text);
|
|
161
|
+
setIsEditing(false);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div
|
|
166
|
+
className={cn(
|
|
167
|
+
"group relative",
|
|
168
|
+
"p-2.5 rounded-lg",
|
|
169
|
+
"transition-[background-color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary",
|
|
170
|
+
className,
|
|
171
|
+
)}
|
|
172
|
+
>
|
|
173
|
+
{isEditing ? (
|
|
174
|
+
<div className="flex flex-col gap-2">
|
|
175
|
+
<Textarea
|
|
176
|
+
value={editText}
|
|
177
|
+
onChange={(e) => setEditText(e.currentTarget.value)}
|
|
178
|
+
className="min-h-[60px] text-sm"
|
|
179
|
+
autoFocus
|
|
180
|
+
/>
|
|
181
|
+
<div className="flex justify-end gap-2">
|
|
182
|
+
<Button
|
|
183
|
+
size="sm"
|
|
184
|
+
variant="ghost"
|
|
185
|
+
onClick={handleCancelEdit}
|
|
186
|
+
className="h-7 px-2"
|
|
187
|
+
>
|
|
188
|
+
<IconX size={14} />
|
|
189
|
+
<span className="ml-1 text-xs">Cancel</span>
|
|
190
|
+
</Button>
|
|
191
|
+
<Button
|
|
192
|
+
size="sm"
|
|
193
|
+
onClick={handleSaveEdit}
|
|
194
|
+
disabled={!editText.trim()}
|
|
195
|
+
className="h-7 px-2"
|
|
196
|
+
>
|
|
197
|
+
<IconCheck size={14} />
|
|
198
|
+
<span className="ml-1 text-xs">Save</span>
|
|
199
|
+
</Button>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
) : (
|
|
203
|
+
<div className="flex items-center gap-2">
|
|
204
|
+
{personaImage && (
|
|
205
|
+
<img
|
|
206
|
+
src={personaImage}
|
|
207
|
+
alt={personaName || "Persona"}
|
|
208
|
+
className="size-5 rounded-full shrink-0"
|
|
209
|
+
/>
|
|
210
|
+
)}
|
|
211
|
+
<p className="text-sm text-foreground/90 flex-1 truncate">{text}</p>
|
|
212
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
213
|
+
{model && (
|
|
214
|
+
<Badge variant="outline" className="text-xs h-5 px-1.5">
|
|
215
|
+
{model.split("/").pop()}
|
|
216
|
+
</Badge>
|
|
217
|
+
)}
|
|
218
|
+
{personalityName && (
|
|
219
|
+
<Badge variant="outline" className="text-xs h-5 px-1.5">
|
|
220
|
+
{personalityName}
|
|
221
|
+
</Badge>
|
|
222
|
+
)}
|
|
223
|
+
{personaName && !personaImage && (
|
|
224
|
+
<Badge variant="outline" className="text-xs h-5 px-1.5">
|
|
225
|
+
{personaName}
|
|
226
|
+
</Badge>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
<div className="flex items-center gap-1 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
230
|
+
<Button
|
|
231
|
+
size="sm"
|
|
232
|
+
variant="ghost"
|
|
233
|
+
onClick={() => setIsEditing(true)}
|
|
234
|
+
className="h-6 w-6 p-0"
|
|
235
|
+
>
|
|
236
|
+
<IconEdit size={13} />
|
|
237
|
+
</Button>
|
|
238
|
+
<Button
|
|
239
|
+
size="sm"
|
|
240
|
+
variant="ghost"
|
|
241
|
+
onClick={onRemove}
|
|
242
|
+
className="h-6 w-6 p-0 text-danger hover:text-danger"
|
|
243
|
+
>
|
|
244
|
+
<IconTrash size={13} />
|
|
245
|
+
</Button>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
3
|
+
import { useControllableState } from "@radix-ui/react-use-controllable-state";
|
|
4
|
+
import {
|
|
5
|
+
Collapsible,
|
|
6
|
+
CollapsibleContent,
|
|
7
|
+
CollapsibleTrigger,
|
|
8
|
+
} from "../ui/collapsible";
|
|
9
|
+
import { cn } from "../utils/cn";
|
|
10
|
+
import { cjk } from "@streamdown/cjk";
|
|
11
|
+
import { code } from "@streamdown/code";
|
|
12
|
+
import { math } from "@streamdown/math";
|
|
13
|
+
import { mermaid } from "@streamdown/mermaid";
|
|
14
|
+
import { IconBrain, IconChevronDown } from "@tabler/icons-react";
|
|
15
|
+
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
|
16
|
+
import { Streamdown } from "streamdown";
|
|
17
|
+
import { Shimmer } from "./shimmer";
|
|
18
|
+
interface ReasoningContextValue {
|
|
19
|
+
isStreaming: boolean;
|
|
20
|
+
isOpen: boolean;
|
|
21
|
+
setIsOpen: (open: boolean) => void;
|
|
22
|
+
duration: number | undefined;
|
|
23
|
+
}
|
|
24
|
+
const ReasoningContext = createContext<ReasoningContextValue | null>(null);
|
|
25
|
+
export const useReasoning = () => {
|
|
26
|
+
const context = useContext(ReasoningContext);
|
|
27
|
+
if (!context) {
|
|
28
|
+
throw new Error("Reasoning components must be used within Reasoning");
|
|
29
|
+
}
|
|
30
|
+
return context;
|
|
31
|
+
};
|
|
32
|
+
export type ReasoningProps = ComponentProps<typeof Collapsible> & {
|
|
33
|
+
isStreaming?: boolean;
|
|
34
|
+
open?: boolean;
|
|
35
|
+
defaultOpen?: boolean;
|
|
36
|
+
onOpenChange?: (open: boolean) => void;
|
|
37
|
+
duration?: number;
|
|
38
|
+
};
|
|
39
|
+
const AUTO_CLOSE_DELAY = 1000;
|
|
40
|
+
const MS_IN_S = 1000;
|
|
41
|
+
export function Reasoning({
|
|
42
|
+
className,
|
|
43
|
+
isStreaming = false,
|
|
44
|
+
open,
|
|
45
|
+
defaultOpen,
|
|
46
|
+
onOpenChange,
|
|
47
|
+
duration: durationProp,
|
|
48
|
+
children,
|
|
49
|
+
...props
|
|
50
|
+
}: ReasoningProps) {
|
|
51
|
+
const resolvedDefaultOpen = defaultOpen ?? isStreaming;
|
|
52
|
+
// Track if defaultOpen was explicitly set to false (to prevent auto-open)
|
|
53
|
+
const isExplicitlyClosed = defaultOpen === false;
|
|
54
|
+
const [isOpen, setIsOpen] = useControllableState<boolean>({
|
|
55
|
+
defaultProp: resolvedDefaultOpen,
|
|
56
|
+
onChange: onOpenChange,
|
|
57
|
+
prop: open,
|
|
58
|
+
});
|
|
59
|
+
const [duration, setDuration] = useControllableState<number | undefined>({
|
|
60
|
+
defaultProp: undefined,
|
|
61
|
+
prop: durationProp,
|
|
62
|
+
});
|
|
63
|
+
const hasEverStreamedRef = useRef(isStreaming);
|
|
64
|
+
const [hasAutoClosed, setHasAutoClosed] = useState(false);
|
|
65
|
+
const startTimeRef = useRef<number | null>(null);
|
|
66
|
+
// Track when streaming starts and compute duration
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (isStreaming) {
|
|
69
|
+
hasEverStreamedRef.current = true;
|
|
70
|
+
if (startTimeRef.current === null) {
|
|
71
|
+
startTimeRef.current = Date.now();
|
|
72
|
+
}
|
|
73
|
+
} else if (startTimeRef.current !== null) {
|
|
74
|
+
setDuration(Math.ceil((Date.now() - startTimeRef.current) / MS_IN_S));
|
|
75
|
+
startTimeRef.current = null;
|
|
76
|
+
}
|
|
77
|
+
}, [isStreaming, setDuration]);
|
|
78
|
+
// Auto-open when streaming starts (unless explicitly closed)
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (isStreaming && !isOpen && !isExplicitlyClosed) {
|
|
81
|
+
setIsOpen(true);
|
|
82
|
+
}
|
|
83
|
+
}, [isStreaming, isOpen, setIsOpen, isExplicitlyClosed]);
|
|
84
|
+
// Auto-close when streaming ends (once only, and only if it ever streamed)
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (
|
|
87
|
+
hasEverStreamedRef.current &&
|
|
88
|
+
!isStreaming &&
|
|
89
|
+
isOpen &&
|
|
90
|
+
!hasAutoClosed
|
|
91
|
+
) {
|
|
92
|
+
const timer = setTimeout(() => {
|
|
93
|
+
setIsOpen(false);
|
|
94
|
+
setHasAutoClosed(true);
|
|
95
|
+
}, AUTO_CLOSE_DELAY);
|
|
96
|
+
return () => clearTimeout(timer);
|
|
97
|
+
}
|
|
98
|
+
}, [isStreaming, isOpen, setIsOpen, hasAutoClosed]);
|
|
99
|
+
const handleOpenChange = (newOpen: boolean) => {
|
|
100
|
+
setIsOpen(newOpen);
|
|
101
|
+
};
|
|
102
|
+
const contextValue = { duration, isOpen, isStreaming, setIsOpen };
|
|
103
|
+
return (
|
|
104
|
+
<ReasoningContext.Provider value={contextValue}>
|
|
105
|
+
<Collapsible
|
|
106
|
+
className={cn("not-prose mb-4", className)}
|
|
107
|
+
onOpenChange={handleOpenChange}
|
|
108
|
+
open={isOpen}
|
|
109
|
+
{...props}
|
|
110
|
+
>
|
|
111
|
+
{children}
|
|
112
|
+
</Collapsible>
|
|
113
|
+
</ReasoningContext.Provider>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
export type ReasoningTriggerProps = ComponentProps<
|
|
117
|
+
typeof CollapsibleTrigger
|
|
118
|
+
> & {
|
|
119
|
+
getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;
|
|
120
|
+
};
|
|
121
|
+
const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
|
|
122
|
+
if (isStreaming || duration === 0) {
|
|
123
|
+
return <Shimmer duration={1}>Thinking...</Shimmer>;
|
|
124
|
+
}
|
|
125
|
+
if (duration === undefined) {
|
|
126
|
+
return <p>Thought for a few seconds</p>;
|
|
127
|
+
}
|
|
128
|
+
return <p>Thought for {duration} seconds</p>;
|
|
129
|
+
};
|
|
130
|
+
export function ReasoningTrigger({
|
|
131
|
+
className,
|
|
132
|
+
children,
|
|
133
|
+
getThinkingMessage = defaultGetThinkingMessage,
|
|
134
|
+
...props
|
|
135
|
+
}: ReasoningTriggerProps) {
|
|
136
|
+
const { isStreaming, isOpen, duration } = useReasoning();
|
|
137
|
+
return (
|
|
138
|
+
<CollapsibleTrigger
|
|
139
|
+
className={cn(
|
|
140
|
+
"flex w-full items-center gap-2 py-1 text-sm text-muted transition-[color,transform] duration-150 ease-out active:scale-[0.99] hover:text-foreground",
|
|
141
|
+
className,
|
|
142
|
+
)}
|
|
143
|
+
{...props}
|
|
144
|
+
>
|
|
145
|
+
{children ?? (
|
|
146
|
+
<>
|
|
147
|
+
<IconBrain className="size-4" />
|
|
148
|
+
{getThinkingMessage(isStreaming, duration)}
|
|
149
|
+
<IconChevronDown
|
|
150
|
+
className={cn(
|
|
151
|
+
"size-4 transition-transform duration-150 ease-out",
|
|
152
|
+
isOpen ? "rotate-180" : "rotate-0",
|
|
153
|
+
)}
|
|
154
|
+
/>
|
|
155
|
+
</>
|
|
156
|
+
)}
|
|
157
|
+
</CollapsibleTrigger>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
export type ReasoningContentProps = ComponentProps<
|
|
161
|
+
typeof CollapsibleContent
|
|
162
|
+
> & {
|
|
163
|
+
children: string;
|
|
164
|
+
};
|
|
165
|
+
const streamdownPlugins = { cjk, code, math, mermaid };
|
|
166
|
+
export function ReasoningContent({
|
|
167
|
+
className,
|
|
168
|
+
children,
|
|
169
|
+
...props
|
|
170
|
+
}: ReasoningContentProps) {
|
|
171
|
+
return (
|
|
172
|
+
<CollapsibleContent className={cn("pt-2 pb-1", className)} {...props}>
|
|
173
|
+
<div className="rounded-lg bg-surface-secondary/40 p-3">
|
|
174
|
+
<Streamdown plugins={streamdownPlugins}>{children}</Streamdown>
|
|
175
|
+
</div>
|
|
176
|
+
</CollapsibleContent>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { type CSSProperties, type ElementType } from "react";
|
|
3
|
+
import { motion } from "motion/react";
|
|
4
|
+
import { cn } from "../utils/cn";
|
|
5
|
+
interface ShimmerCSSProperties extends CSSProperties {
|
|
6
|
+
"--spread"?: string;
|
|
7
|
+
"--bg"?: string;
|
|
8
|
+
}
|
|
9
|
+
interface ShimmerProps {
|
|
10
|
+
children: string;
|
|
11
|
+
as?: ElementType;
|
|
12
|
+
className?: string;
|
|
13
|
+
duration?: number;
|
|
14
|
+
spread?: number;
|
|
15
|
+
}
|
|
16
|
+
function Shimmer({
|
|
17
|
+
children,
|
|
18
|
+
as: Component = "span",
|
|
19
|
+
className,
|
|
20
|
+
duration = 2,
|
|
21
|
+
spread = 2,
|
|
22
|
+
}: ShimmerProps) {
|
|
23
|
+
const MotionComponent = motion.create(Component);
|
|
24
|
+
const totalSpread = spread * children.length;
|
|
25
|
+
const style = {
|
|
26
|
+
"--spread": `${totalSpread}px`,
|
|
27
|
+
"--bg": `linear-gradient(
|
|
28
|
+
90deg,
|
|
29
|
+
var(--muted) 0%,
|
|
30
|
+
var(--foreground) calc(50% - var(--spread)),
|
|
31
|
+
var(--muted) 50%,
|
|
32
|
+
var(--foreground) calc(50% + var(--spread)),
|
|
33
|
+
var(--muted) 100%
|
|
34
|
+
)`,
|
|
35
|
+
backgroundImage: "var(--bg)",
|
|
36
|
+
backgroundSize: "200% 100%",
|
|
37
|
+
} satisfies ShimmerCSSProperties;
|
|
38
|
+
return (
|
|
39
|
+
<MotionComponent
|
|
40
|
+
className={cn(
|
|
41
|
+
"inline-block bg-clip-text text-transparent [-webkit-text-fill-color:transparent]",
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
style={style}
|
|
45
|
+
animate={{ backgroundPosition: ["100% center", "0% center"] }}
|
|
46
|
+
transition={{
|
|
47
|
+
duration,
|
|
48
|
+
ease: "linear",
|
|
49
|
+
repeat: Number.POSITIVE_INFINITY,
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
{children}
|
|
53
|
+
</MotionComponent>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
export { Shimmer, type ShimmerProps };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps } from "react";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
Collapsible,
|
|
7
|
+
CollapsibleContent,
|
|
8
|
+
CollapsibleTrigger,
|
|
9
|
+
} from "../ui/collapsible";
|
|
10
|
+
import { cn } from "../utils/cn";
|
|
11
|
+
import { IconBook, IconChevronDown } from "@tabler/icons-react";
|
|
12
|
+
|
|
13
|
+
export type SourcesProps = ComponentProps<typeof Collapsible>;
|
|
14
|
+
|
|
15
|
+
export const Sources = ({ className, ...props }: SourcesProps) => (
|
|
16
|
+
<Collapsible className={cn("w-full", className)} {...props} />
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export type SourcesTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
|
|
20
|
+
count: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const SourcesTrigger = ({
|
|
24
|
+
className,
|
|
25
|
+
count,
|
|
26
|
+
children,
|
|
27
|
+
...props
|
|
28
|
+
}: SourcesTriggerProps) => (
|
|
29
|
+
<CollapsibleTrigger
|
|
30
|
+
className={cn(
|
|
31
|
+
"group inline-flex items-center gap-1.5 text-xs text-muted transition-[color,transform] duration-150 ease-out active:scale-[0.97] hover:text-foreground",
|
|
32
|
+
className,
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
36
|
+
{children ?? (
|
|
37
|
+
<>
|
|
38
|
+
<IconBook className="size-3.5" stroke={1.5} />
|
|
39
|
+
<span>Used {count} sources</span>
|
|
40
|
+
<IconChevronDown className="size-3.5 transition-transform duration-150 ease-out group-data-[state=open]:rotate-180" />
|
|
41
|
+
</>
|
|
42
|
+
)}
|
|
43
|
+
</CollapsibleTrigger>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
export type SourcesContentProps = ComponentProps<typeof CollapsibleContent>;
|
|
47
|
+
|
|
48
|
+
export const SourcesContent = ({
|
|
49
|
+
className,
|
|
50
|
+
...props
|
|
51
|
+
}: SourcesContentProps) => (
|
|
52
|
+
<CollapsibleContent className={cn("mt-2 space-y-2", className)} {...props} />
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
export type SourceProps = ComponentProps<"a">;
|
|
56
|
+
|
|
57
|
+
export const Source = ({
|
|
58
|
+
href,
|
|
59
|
+
title,
|
|
60
|
+
children,
|
|
61
|
+
className,
|
|
62
|
+
...props
|
|
63
|
+
}: SourceProps) => (
|
|
64
|
+
<a
|
|
65
|
+
className={cn(
|
|
66
|
+
"flex items-center justify-between gap-2 rounded-md px-3 py-2 text-xs text-muted transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.99] hover:bg-surface-secondary hover:text-foreground",
|
|
67
|
+
className,
|
|
68
|
+
)}
|
|
69
|
+
href={href}
|
|
70
|
+
rel="noreferrer"
|
|
71
|
+
target="_blank"
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
<span className="line-clamp-1">{children ?? title}</span>
|
|
75
|
+
<IconBook className="size-3.5 shrink-0" stroke={1.5} />
|
|
76
|
+
</a>
|
|
77
|
+
);
|