experimental-ash 0.33.1 → 0.34.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 (43) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/docs/public/auth-and-route-protection.md +18 -7
  3. package/dist/docs/public/channels/README.md +7 -3
  4. package/dist/docs/public/channels/slack.md +10 -4
  5. package/dist/src/cli/commands/channel-add-conflicts.d.ts +21 -0
  6. package/dist/src/cli/commands/channel-add-conflicts.js +1 -0
  7. package/dist/src/cli/commands/channels.d.ts +9 -1
  8. package/dist/src/cli/commands/channels.js +1 -3
  9. package/dist/src/cli/dev/repl.js +1 -1
  10. package/dist/src/cli/run.js +1 -1
  11. package/dist/src/compiler/compile-agent.js +1 -1
  12. package/dist/src/compiler/normalize-manifest.js +1 -1
  13. package/dist/src/internal/application/package.js +1 -1
  14. package/dist/src/internal/nitro/host/start-production-server.js +1 -1
  15. package/dist/src/node_modules/.pnpm/@clack_core@1.3.1/node_modules/@clack/core/dist/index.js +10 -0
  16. package/dist/src/node_modules/.pnpm/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/index.js +1 -0
  17. package/dist/src/node_modules/.pnpm/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/utils.js +1 -0
  18. package/dist/src/node_modules/.pnpm/fast-string-width@3.0.2/node_modules/fast-string-width/dist/index.js +1 -0
  19. package/dist/src/node_modules/.pnpm/fast-wrap-ansi@0.2.2/node_modules/fast-wrap-ansi/lib/main.js +5 -0
  20. package/dist/src/node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js +1 -0
  21. package/dist/src/node_modules/.pnpm/sisteransi@1.0.5/node_modules/sisteransi/src/index.js +1 -0
  22. package/dist/src/packages/ash-scaffold/src/channels.js +12 -2
  23. package/dist/src/packages/ash-scaffold/src/cli/channel-add-prompter.js +1 -0
  24. package/dist/src/packages/ash-scaffold/src/cli/channel-setup-prompter.js +1 -0
  25. package/dist/src/packages/ash-scaffold/src/cli/command-output.js +1 -0
  26. package/dist/src/packages/ash-scaffold/src/cli/index.js +1 -0
  27. package/dist/src/packages/ash-scaffold/src/cli/prompt-ui.js +3 -0
  28. package/dist/src/packages/ash-scaffold/src/cli/rail-log.js +2 -0
  29. package/dist/src/packages/ash-scaffold/src/primitives/detect-deployment.js +1 -0
  30. package/dist/src/packages/ash-scaffold/src/primitives/index.js +1 -0
  31. package/dist/src/packages/ash-scaffold/src/primitives/pnpm-invocation.js +1 -0
  32. package/dist/src/packages/ash-scaffold/src/primitives/process-output.js +1 -0
  33. package/dist/src/packages/ash-scaffold/src/primitives/run-pnpm.js +1 -0
  34. package/dist/src/packages/ash-scaffold/src/primitives/run-vercel.js +1 -0
  35. package/dist/src/packages/ash-scaffold/src/primitives/update-slack-channel.js +1 -0
  36. package/dist/src/packages/ash-scaffold/src/project.js +1 -1
  37. package/dist/src/packages/ash-scaffold/src/steps/deploy-to-vercel.js +1 -0
  38. package/dist/src/packages/ash-scaffold/src/steps/index.js +1 -0
  39. package/dist/src/packages/ash-scaffold/src/steps/run-add-to-agent.js +2 -0
  40. package/dist/src/packages/ash-scaffold/src/steps/setup-slackbot.js +1 -0
  41. package/dist/src/packages/ash-scaffold/src/web-template.js +4713 -0
  42. package/dist/src/public/next/server.js +1 -1
  43. package/package.json +1 -1
@@ -0,0 +1,4713 @@
1
+ const WEB_APP_TEMPLATE_FILES={"agent/channels/ash.ts":`import { ashChannel } from "experimental-ash/channels/ash";
2
+ import { type AuthFn, localDev, vercelOidc } from "experimental-ash/channels/auth";
3
+
4
+ // Replace with your real auth (Auth.js, Clerk, …): return a SessionAuthContext
5
+ // for signed-in users, or null to reject. Throws in production until you do.
6
+ function exampleProductionAuth(): AuthFn<Request> {
7
+ return () => {
8
+ if (process.env.VERCEL_ENV === "production") {
9
+ throw new Error(
10
+ "Configure production auth in agent/channels/ash.ts (e.g. Auth.js or Clerk).",
11
+ );
12
+ }
13
+ return null;
14
+ };
15
+ }
16
+
17
+ export default ashChannel({
18
+ auth: [
19
+ // Lets the Ash TUI and your Vercel deployments reach the deployed agent.
20
+ vercelOidc(),
21
+ // Open on localhost for \`ash dev\` and the REPL; ignored in production.
22
+ localDev(),
23
+ // Your end-user auth — replace the placeholder above.
24
+ exampleProductionAuth(),
25
+ ],
26
+ });
27
+ `,"app/_components/agent-chat.tsx":`"use client";
28
+
29
+ import { useAshAgent } from "experimental-ash/react";
30
+ import { AlertCircleIcon } from "lucide-react";
31
+ import { useState } from "react";
32
+ import {
33
+ Conversation,
34
+ ConversationContent,
35
+ ConversationScrollButton,
36
+ } from "@/components/ai-elements/conversation";
37
+ import {
38
+ PromptInput,
39
+ type PromptInputMessage,
40
+ PromptInputSubmit,
41
+ PromptInputTextarea,
42
+ } from "@/components/ai-elements/prompt-input";
43
+ import { cn } from "@/lib/utils";
44
+ import { AgentMessage } from "./agent-message";
45
+
46
+ const AGENT_NAME = "__ASH_INIT_APP_NAME__";
47
+
48
+ type AgentStatus = ReturnType<typeof useAshAgent>["status"];
49
+
50
+ export function AgentChat() {
51
+ const agent = useAshAgent();
52
+ const [inputText, setInputText] = useState("");
53
+ const isBusy = agent.status === "submitted" || agent.status === "streaming";
54
+ const isEmpty = agent.data.messages.length === 0;
55
+ const hasInputText = inputText.trim().length > 0;
56
+
57
+ const handleSubmit = async (message: PromptInputMessage) => {
58
+ const text = message.text.trim();
59
+ if (!text || isBusy) return;
60
+
61
+ setInputText("");
62
+ await agent.sendMessage(text);
63
+ };
64
+
65
+ const composer = (
66
+ <PromptInput onSubmit={handleSubmit}>
67
+ <PromptInputTextarea
68
+ onChange={(event) => setInputText(event.currentTarget.value)}
69
+ placeholder="Send a message…"
70
+ value={inputText}
71
+ />
72
+ <PromptInputSubmit
73
+ disabled={!isBusy && !hasInputText}
74
+ onStop={agent.stop}
75
+ status={agent.status}
76
+ />
77
+ </PromptInput>
78
+ );
79
+
80
+ return (
81
+ <main className="flex h-dvh flex-col overflow-hidden bg-background text-foreground">
82
+ {isEmpty ? null : (
83
+ <header className="flex h-14 shrink-0 items-center justify-center gap-2 pl-4 pr-2">
84
+ <span className="text-muted-foreground text-sm">{AGENT_NAME}</span>
85
+ <StatusDot status={agent.status} />
86
+ </header>
87
+ )}
88
+
89
+ {agent.error ? (
90
+ <div className="mx-auto w-full max-w-3xl shrink-0 px-4 pt-2 sm:px-6">
91
+ <div className="flex items-start gap-3 rounded-lg border border-destructive/30 bg-destructive/5 px-3 py-2.5 text-sm">
92
+ <AlertCircleIcon className="mt-0.5 size-4 shrink-0 text-destructive" />
93
+ <div>
94
+ <p className="font-medium">Request failed</p>
95
+ <p className="mt-0.5 text-muted-foreground">{agent.error.message}</p>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ ) : null}
100
+
101
+ {isEmpty ? null : (
102
+ <Conversation className="min-h-0 flex-1">
103
+ <ConversationContent className="mx-auto w-full max-w-3xl gap-6 px-4 py-6 sm:px-6">
104
+ {agent.data.messages.map((message, index) => (
105
+ <AgentMessage
106
+ canRespond={!isBusy}
107
+ isStreaming={
108
+ agent.status === "streaming" && index === agent.data.messages.length - 1
109
+ }
110
+ key={message.id}
111
+ message={message}
112
+ onInputResponses={(inputResponses) => agent.send({ inputResponses })}
113
+ />
114
+ ))}
115
+ </ConversationContent>
116
+ <ConversationScrollButton />
117
+ </Conversation>
118
+ )}
119
+
120
+ <div
121
+ className={cn(
122
+ "mx-auto w-full px-4 sm:px-6",
123
+ isEmpty
124
+ ? "flex max-w-xl flex-1 flex-col items-center justify-center gap-8 pb-[10vh]"
125
+ : "max-w-3xl shrink-0 pb-6",
126
+ )}
127
+ >
128
+ {isEmpty ? <h1 className="font-medium text-5xl tracking-tighter">{AGENT_NAME}</h1> : null}
129
+ <div className="w-full">{composer}</div>
130
+ </div>
131
+ </main>
132
+ );
133
+ }
134
+
135
+ function StatusDot({ status }: { readonly status: AgentStatus }) {
136
+ const isLive = status === "submitted" || status === "streaming";
137
+ const tone =
138
+ status === "error"
139
+ ? "bg-destructive"
140
+ : isLive
141
+ ? "bg-emerald-500"
142
+ : status === "ready"
143
+ ? "bg-muted-foreground"
144
+ : "bg-muted-foreground/50";
145
+
146
+ return (
147
+ <span className="relative flex size-1">
148
+ {isLive ? (
149
+ <span
150
+ className={cn(
151
+ "absolute inline-flex size-full animate-ping rounded-full opacity-75",
152
+ tone,
153
+ )}
154
+ />
155
+ ) : null}
156
+ <span className={cn("relative inline-flex size-1 rounded-full transition-colors", tone)} />
157
+ </span>
158
+ );
159
+ }
160
+ `,"app/_components/agent-message.tsx":`"use client";
161
+
162
+ import type { AshDynamicToolPart, AshMessage, AshMessagePart } from "experimental-ash/react";
163
+ import { Message, MessageContent, MessageResponse } from "@/components/ai-elements/message";
164
+ import { Reasoning, ReasoningContent, ReasoningTrigger } from "@/components/ai-elements/reasoning";
165
+ import {
166
+ Tool,
167
+ ToolContent,
168
+ ToolHeader,
169
+ ToolInput,
170
+ ToolOutput,
171
+ } from "@/components/ai-elements/tool";
172
+ import { Button } from "@/components/ui/button";
173
+
174
+ export type AgentInputResponse = {
175
+ readonly optionId?: string;
176
+ readonly requestId: string;
177
+ readonly text?: string;
178
+ };
179
+
180
+ export function AgentMessage({
181
+ canRespond,
182
+ isStreaming,
183
+ message,
184
+ onInputResponses,
185
+ }: {
186
+ readonly canRespond: boolean;
187
+ readonly isStreaming: boolean;
188
+ readonly message: AshMessage;
189
+ readonly onInputResponses: (responses: readonly AgentInputResponse[]) => void | Promise<void>;
190
+ }) {
191
+ const lastTextIndex = message.parts.reduce(
192
+ (last, part, index) => (part.type === "text" ? index : last),
193
+ -1,
194
+ );
195
+
196
+ return (
197
+ <Message
198
+ data-optimistic={message.metadata?.optimistic ? "true" : undefined}
199
+ from={message.role}
200
+ >
201
+ <MessageContent>
202
+ {message.parts.map((part, index) => (
203
+ <AgentMessagePart
204
+ canRespond={canRespond}
205
+ key={partKey(part, index)}
206
+ onInputResponses={onInputResponses}
207
+ part={part}
208
+ showCaret={isStreaming && message.role === "assistant" && index === lastTextIndex}
209
+ />
210
+ ))}
211
+ </MessageContent>
212
+ </Message>
213
+ );
214
+ }
215
+
216
+ function AgentMessagePart({
217
+ canRespond,
218
+ onInputResponses,
219
+ part,
220
+ showCaret,
221
+ }: {
222
+ readonly canRespond: boolean;
223
+ readonly onInputResponses: (responses: readonly AgentInputResponse[]) => void | Promise<void>;
224
+ readonly part: AshMessagePart;
225
+ readonly showCaret: boolean;
226
+ }) {
227
+ switch (part.type) {
228
+ case "step-start":
229
+ return null;
230
+ case "text":
231
+ return (
232
+ <MessageResponse caret="block" isAnimating={showCaret}>
233
+ {part.text}
234
+ </MessageResponse>
235
+ );
236
+ case "reasoning":
237
+ return (
238
+ <Reasoning defaultOpen isStreaming={part.state === "streaming"}>
239
+ <ReasoningTrigger />
240
+ <ReasoningContent>{part.text}</ReasoningContent>
241
+ </Reasoning>
242
+ );
243
+ case "dynamic-tool":
244
+ return (
245
+ <Tool
246
+ defaultOpen={part.state === "approval-requested" || part.state === "approval-responded"}
247
+ >
248
+ <ToolHeader
249
+ state={part.state}
250
+ title={part.toolName}
251
+ toolName={part.toolName}
252
+ type="dynamic-tool"
253
+ />
254
+ <ToolContent>
255
+ <ToolInput input={part.input} />
256
+ <InputRequestActions
257
+ canRespond={canRespond}
258
+ part={part}
259
+ onInputResponses={onInputResponses}
260
+ />
261
+ <ToolOutput errorText={part.errorText} output={part.output} />
262
+ </ToolContent>
263
+ </Tool>
264
+ );
265
+ }
266
+ }
267
+
268
+ function InputRequestActions({
269
+ canRespond,
270
+ onInputResponses,
271
+ part,
272
+ }: {
273
+ readonly canRespond: boolean;
274
+ readonly onInputResponses: (responses: readonly AgentInputResponse[]) => void | Promise<void>;
275
+ readonly part: AshDynamicToolPart;
276
+ }) {
277
+ const inputRequest = part.toolMetadata?.ash?.inputRequest;
278
+ if (!inputRequest) {
279
+ return null;
280
+ }
281
+
282
+ const inputResponse = part.toolMetadata?.ash?.inputResponse;
283
+ const selectedOption = inputRequest.options?.find(
284
+ (option) => option.id === inputResponse?.optionId,
285
+ );
286
+
287
+ return (
288
+ <div className="space-y-3 rounded-md border border-yellow-500/30 bg-yellow-500/5 p-3">
289
+ <p className="text-muted-foreground text-sm">{inputRequest.prompt}</p>
290
+ {inputResponse ? (
291
+ <p className="font-medium text-sm">
292
+ Responded: {selectedOption?.label ?? inputResponse.text ?? inputResponse.optionId}
293
+ </p>
294
+ ) : (
295
+ <div className="flex flex-wrap gap-2">
296
+ {inputRequest.options?.map((option) => (
297
+ <Button
298
+ disabled={!canRespond}
299
+ key={option.id}
300
+ onClick={() => {
301
+ void onInputResponses([
302
+ {
303
+ optionId: option.id,
304
+ requestId: inputRequest.requestId,
305
+ },
306
+ ]);
307
+ }}
308
+ size="sm"
309
+ type="button"
310
+ variant={option.style === "danger" ? "destructive" : "default"}
311
+ >
312
+ {option.label}
313
+ </Button>
314
+ ))}
315
+ </div>
316
+ )}
317
+ </div>
318
+ );
319
+ }
320
+
321
+ function partKey(part: AshMessagePart, index: number): string {
322
+ switch (part.type) {
323
+ case "dynamic-tool":
324
+ return part.toolCallId;
325
+ default:
326
+ return \`\${part.type}:\${index}\`;
327
+ }
328
+ }
329
+ `,"app/globals.css":`@import "tailwindcss";
330
+ @source "../node_modules/streamdown/dist/*.js";
331
+
332
+ @theme inline {
333
+ --color-background: var(--background);
334
+ --color-foreground: var(--foreground);
335
+ --color-card: var(--card);
336
+ --color-card-foreground: var(--card-foreground);
337
+ --color-popover: var(--popover);
338
+ --color-popover-foreground: var(--popover-foreground);
339
+ --color-primary: var(--primary);
340
+ --color-primary-foreground: var(--primary-foreground);
341
+ --color-secondary: var(--secondary);
342
+ --color-secondary-foreground: var(--secondary-foreground);
343
+ --color-muted: var(--muted);
344
+ --color-muted-foreground: var(--muted-foreground);
345
+ --color-accent: var(--accent);
346
+ --color-accent-foreground: var(--accent-foreground);
347
+ --color-destructive: var(--destructive);
348
+ --color-border: var(--border);
349
+ --color-input: var(--input);
350
+ --color-ring: var(--ring);
351
+ --radius-sm: calc(var(--radius) - 4px);
352
+ --radius-md: calc(var(--radius) - 2px);
353
+ --radius-lg: var(--radius);
354
+ --radius-xl: calc(var(--radius) + 4px);
355
+ --font-sans: "Geist", "Geist Fallback", ui-sans-serif, system-ui, sans-serif;
356
+ --font-mono: "Geist Mono", "Geist Mono Fallback", ui-monospace, monospace;
357
+ }
358
+
359
+ :root {
360
+ color-scheme: light;
361
+ /* Soft neutral page with white elevated surfaces so cards/composer pop. */
362
+ --background: oklch(0.971 0 0);
363
+ --foreground: oklch(0.16 0 0);
364
+ --card: oklch(1 0 0);
365
+ --card-foreground: oklch(0.16 0 0);
366
+ --popover: oklch(1 0 0);
367
+ --popover-foreground: oklch(0.16 0 0);
368
+ --primary: oklch(0.19 0 0);
369
+ --primary-foreground: oklch(0.985 0 0);
370
+ --secondary: oklch(0.94 0 0);
371
+ --secondary-foreground: oklch(0.19 0 0);
372
+ --muted: oklch(0.94 0 0);
373
+ --muted-foreground: oklch(0.6 0 0);
374
+ --accent: oklch(0.94 0 0);
375
+ --accent-foreground: oklch(0.19 0 0);
376
+ --destructive: oklch(0.577 0.245 27.325);
377
+ --border: oklch(0.916 0 0);
378
+ --input: oklch(0.916 0 0);
379
+ --ring: oklch(0.708 0 0);
380
+ --radius: 0.625rem;
381
+ }
382
+
383
+ @media (prefers-color-scheme: dark) {
384
+ :root {
385
+ color-scheme: dark;
386
+ --background: oklch(0.145 0 0);
387
+ --foreground: oklch(0.985 0 0);
388
+ --card: oklch(0.205 0 0);
389
+ --card-foreground: oklch(0.985 0 0);
390
+ --popover: oklch(0.205 0 0);
391
+ --popover-foreground: oklch(0.985 0 0);
392
+ --primary: oklch(0.922 0 0);
393
+ --primary-foreground: oklch(0.205 0 0);
394
+ --secondary: oklch(0.269 0 0);
395
+ --secondary-foreground: oklch(0.985 0 0);
396
+ --muted: oklch(0.269 0 0);
397
+ --muted-foreground: oklch(0.708 0 0);
398
+ --accent: oklch(0.269 0 0);
399
+ --accent-foreground: oklch(0.985 0 0);
400
+ --destructive: oklch(0.704 0.191 22.216);
401
+ --border: oklch(1 0 0 / 10%);
402
+ --input: oklch(1 0 0 / 15%);
403
+ --ring: oklch(0.556 0 0);
404
+ }
405
+ }
406
+
407
+ * {
408
+ border-color: var(--border);
409
+ }
410
+
411
+ html {
412
+ height: 100%;
413
+ }
414
+
415
+ body {
416
+ min-height: 100%;
417
+ margin: 0;
418
+ background: var(--background);
419
+ font-family: var(--font-sans);
420
+ }
421
+
422
+ button,
423
+ input,
424
+ textarea {
425
+ font: inherit;
426
+ }
427
+ `,"app/layout.tsx":`import type { Metadata } from "next";
428
+ import { Geist, Geist_Mono } from "next/font/google";
429
+ import type { ReactNode } from "react";
430
+ import { TooltipProvider } from "@/components/ui/tooltip";
431
+ import { cn } from "@/lib/utils";
432
+ import "./globals.css";
433
+
434
+ const sans = Geist({
435
+ variable: "--font-sans",
436
+ subsets: ["latin"],
437
+ weight: "variable",
438
+ display: "swap",
439
+ });
440
+
441
+ const mono = Geist_Mono({
442
+ variable: "--font-mono",
443
+ subsets: ["latin"],
444
+ weight: "variable",
445
+ display: "swap",
446
+ });
447
+
448
+ export const metadata: Metadata = {
449
+ title: "__ASH_INIT_APP_NAME__",
450
+ description: "A Next.js starter for Ash agents with AI Elements.",
451
+ };
452
+
453
+ export default function RootLayout({ children }: { readonly children: ReactNode }) {
454
+ return (
455
+ <html className={cn(sans.variable, mono.variable)} lang="en">
456
+ <body>
457
+ <TooltipProvider>{children}</TooltipProvider>
458
+ </body>
459
+ </html>
460
+ );
461
+ }
462
+ `,"app/page.tsx":`import { AgentChat } from "@/app/_components/agent-chat";
463
+
464
+ export default function Page() {
465
+ return <AgentChat />;
466
+ }
467
+ `,"components/ai-elements/chain-of-thought.tsx":`"use client";
468
+
469
+ import { useControllableState } from "@radix-ui/react-use-controllable-state";
470
+ import { Badge } from "@/components/ui/badge";
471
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
472
+ import { cn } from "@/lib/utils";
473
+ import type { LucideIcon } from "lucide-react";
474
+ import { BrainIcon, ChevronDownIcon, DotIcon } from "lucide-react";
475
+ import type { ComponentProps, ReactNode } from "react";
476
+ import { createContext, memo, useContext, useMemo } from "react";
477
+
478
+ interface ChainOfThoughtContextValue {
479
+ isOpen: boolean;
480
+ setIsOpen: (open: boolean) => void;
481
+ }
482
+
483
+ const ChainOfThoughtContext = createContext<ChainOfThoughtContextValue | null>(null);
484
+
485
+ const useChainOfThought = () => {
486
+ const context = useContext(ChainOfThoughtContext);
487
+ if (!context) {
488
+ throw new Error("ChainOfThought components must be used within ChainOfThought");
489
+ }
490
+ return context;
491
+ };
492
+
493
+ export type ChainOfThoughtProps = ComponentProps<"div"> & {
494
+ open?: boolean;
495
+ defaultOpen?: boolean;
496
+ onOpenChange?: (open: boolean) => void;
497
+ };
498
+
499
+ export const ChainOfThought = memo(
500
+ ({
501
+ className,
502
+ open,
503
+ defaultOpen = false,
504
+ onOpenChange,
505
+ children,
506
+ ...props
507
+ }: ChainOfThoughtProps) => {
508
+ const [isOpen, setIsOpen] = useControllableState({
509
+ defaultProp: defaultOpen,
510
+ onChange: onOpenChange,
511
+ prop: open,
512
+ });
513
+
514
+ const chainOfThoughtContext = useMemo(() => ({ isOpen, setIsOpen }), [isOpen, setIsOpen]);
515
+
516
+ return (
517
+ <ChainOfThoughtContext.Provider value={chainOfThoughtContext}>
518
+ <div className={cn("not-prose w-full space-y-4", className)} {...props}>
519
+ {children}
520
+ </div>
521
+ </ChainOfThoughtContext.Provider>
522
+ );
523
+ },
524
+ );
525
+
526
+ export type ChainOfThoughtHeaderProps = ComponentProps<typeof CollapsibleTrigger>;
527
+
528
+ export const ChainOfThoughtHeader = memo(
529
+ ({ className, children, ...props }: ChainOfThoughtHeaderProps) => {
530
+ const { isOpen, setIsOpen } = useChainOfThought();
531
+
532
+ return (
533
+ <Collapsible onOpenChange={setIsOpen} open={isOpen}>
534
+ <CollapsibleTrigger
535
+ className={cn(
536
+ "flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground",
537
+ className,
538
+ )}
539
+ {...props}
540
+ >
541
+ <BrainIcon className="size-4" />
542
+ <span className="flex-1 text-left">{children ?? "Chain of Thought"}</span>
543
+ <ChevronDownIcon
544
+ className={cn("size-4 transition-transform", isOpen ? "rotate-180" : "rotate-0")}
545
+ />
546
+ </CollapsibleTrigger>
547
+ </Collapsible>
548
+ );
549
+ },
550
+ );
551
+
552
+ export type ChainOfThoughtStepProps = ComponentProps<"div"> & {
553
+ icon?: LucideIcon;
554
+ label: ReactNode;
555
+ description?: ReactNode;
556
+ status?: "complete" | "active" | "pending";
557
+ };
558
+
559
+ const stepStatusStyles = {
560
+ active: "text-foreground",
561
+ complete: "text-muted-foreground",
562
+ pending: "text-muted-foreground/50",
563
+ };
564
+
565
+ export const ChainOfThoughtStep = memo(
566
+ ({
567
+ className,
568
+ icon: Icon = DotIcon,
569
+ label,
570
+ description,
571
+ status = "complete",
572
+ children,
573
+ ...props
574
+ }: ChainOfThoughtStepProps) => (
575
+ <div
576
+ className={cn(
577
+ "flex gap-2 text-sm",
578
+ stepStatusStyles[status],
579
+ "fade-in-0 slide-in-from-top-2 animate-in",
580
+ className,
581
+ )}
582
+ {...props}
583
+ >
584
+ <div className="relative mt-0.5">
585
+ <Icon className="size-4" />
586
+ <div className="absolute top-7 bottom-0 left-1/2 -mx-px w-px bg-border" />
587
+ </div>
588
+ <div className="flex-1 space-y-2 overflow-hidden">
589
+ <div>{label}</div>
590
+ {description && <div className="text-muted-foreground text-xs">{description}</div>}
591
+ {children}
592
+ </div>
593
+ </div>
594
+ ),
595
+ );
596
+
597
+ export type ChainOfThoughtSearchResultsProps = ComponentProps<"div">;
598
+
599
+ export const ChainOfThoughtSearchResults = memo(
600
+ ({ className, ...props }: ChainOfThoughtSearchResultsProps) => (
601
+ <div className={cn("flex flex-wrap items-center gap-2", className)} {...props} />
602
+ ),
603
+ );
604
+
605
+ export type ChainOfThoughtSearchResultProps = ComponentProps<typeof Badge>;
606
+
607
+ export const ChainOfThoughtSearchResult = memo(
608
+ ({ className, children, ...props }: ChainOfThoughtSearchResultProps) => (
609
+ <Badge
610
+ className={cn("gap-1 px-2 py-0.5 font-normal text-xs", className)}
611
+ variant="secondary"
612
+ {...props}
613
+ >
614
+ {children}
615
+ </Badge>
616
+ ),
617
+ );
618
+
619
+ export type ChainOfThoughtContentProps = ComponentProps<typeof CollapsibleContent>;
620
+
621
+ export const ChainOfThoughtContent = memo(
622
+ ({ className, children, ...props }: ChainOfThoughtContentProps) => {
623
+ const { isOpen } = useChainOfThought();
624
+
625
+ return (
626
+ <Collapsible open={isOpen}>
627
+ <CollapsibleContent
628
+ className={cn(
629
+ "mt-2 space-y-3",
630
+ "data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
631
+ className,
632
+ )}
633
+ {...props}
634
+ >
635
+ {children}
636
+ </CollapsibleContent>
637
+ </Collapsible>
638
+ );
639
+ },
640
+ );
641
+
642
+ export type ChainOfThoughtImageProps = ComponentProps<"div"> & {
643
+ caption?: string;
644
+ };
645
+
646
+ export const ChainOfThoughtImage = memo(
647
+ ({ className, children, caption, ...props }: ChainOfThoughtImageProps) => (
648
+ <div className={cn("mt-2 space-y-2", className)} {...props}>
649
+ <div className="relative flex max-h-[22rem] items-center justify-center overflow-hidden rounded-lg bg-muted p-3">
650
+ {children}
651
+ </div>
652
+ {caption && <p className="text-muted-foreground text-xs">{caption}</p>}
653
+ </div>
654
+ ),
655
+ );
656
+
657
+ ChainOfThought.displayName = "ChainOfThought";
658
+ ChainOfThoughtHeader.displayName = "ChainOfThoughtHeader";
659
+ ChainOfThoughtStep.displayName = "ChainOfThoughtStep";
660
+ ChainOfThoughtSearchResults.displayName = "ChainOfThoughtSearchResults";
661
+ ChainOfThoughtSearchResult.displayName = "ChainOfThoughtSearchResult";
662
+ ChainOfThoughtContent.displayName = "ChainOfThoughtContent";
663
+ ChainOfThoughtImage.displayName = "ChainOfThoughtImage";
664
+ `,"components/ai-elements/code-block.tsx":`"use client";
665
+
666
+ import { Button } from "@/components/ui/button";
667
+ import {
668
+ Select,
669
+ SelectContent,
670
+ SelectItem,
671
+ SelectTrigger,
672
+ SelectValue,
673
+ } from "@/components/ui/select";
674
+ import { cn } from "@/lib/utils";
675
+ import { CheckIcon, CopyIcon } from "lucide-react";
676
+ import type { ComponentProps, CSSProperties, HTMLAttributes } from "react";
677
+ import {
678
+ createContext,
679
+ memo,
680
+ useCallback,
681
+ useContext,
682
+ useEffect,
683
+ useMemo,
684
+ useRef,
685
+ useState,
686
+ } from "react";
687
+ import type { BundledLanguage, BundledTheme, HighlighterGeneric, ThemedToken } from "shiki";
688
+ import { createHighlighter } from "shiki";
689
+
690
+ // Shiki uses bitflags for font styles: 1=italic, 2=bold, 4=underline
691
+ // oxlint-disable-next-line eslint(no-bitwise)
692
+ const isItalic = (fontStyle: number | undefined) => fontStyle && fontStyle & 1;
693
+ // oxlint-disable-next-line eslint(no-bitwise)
694
+ const isBold = (fontStyle: number | undefined) => fontStyle && fontStyle & 2;
695
+ const isUnderline = (fontStyle: number | undefined) =>
696
+ // oxlint-disable-next-line eslint(no-bitwise)
697
+ fontStyle && fontStyle & 4;
698
+
699
+ // Transform tokens to include pre-computed keys to avoid noArrayIndexKey lint
700
+ interface KeyedToken {
701
+ token: ThemedToken;
702
+ key: string;
703
+ }
704
+ interface KeyedLine {
705
+ tokens: KeyedToken[];
706
+ key: string;
707
+ }
708
+
709
+ const addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] =>
710
+ lines.map((line, lineIdx) => ({
711
+ key: \`line-\${lineIdx}\`,
712
+ tokens: line.map((token, tokenIdx) => ({
713
+ key: \`line-\${lineIdx}-\${tokenIdx}\`,
714
+ token,
715
+ })),
716
+ }));
717
+
718
+ // Token rendering component
719
+ const TokenSpan = ({ token }: { token: ThemedToken }) => (
720
+ <span
721
+ className="dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)]"
722
+ style={
723
+ {
724
+ backgroundColor: token.bgColor,
725
+ color: token.color,
726
+ fontStyle: isItalic(token.fontStyle) ? "italic" : undefined,
727
+ fontWeight: isBold(token.fontStyle) ? "bold" : undefined,
728
+ textDecoration: isUnderline(token.fontStyle) ? "underline" : undefined,
729
+ ...token.htmlStyle,
730
+ } as CSSProperties
731
+ }
732
+ >
733
+ {token.content}
734
+ </span>
735
+ );
736
+
737
+ // Line number styles using CSS counters
738
+ const LINE_NUMBER_CLASSES = cn(
739
+ "block",
740
+ "before:content-[counter(line)]",
741
+ "before:inline-block",
742
+ "before:[counter-increment:line]",
743
+ "before:w-8",
744
+ "before:mr-4",
745
+ "before:text-right",
746
+ "before:text-muted-foreground/50",
747
+ "before:font-mono",
748
+ "before:select-none",
749
+ );
750
+
751
+ // Line rendering component
752
+ const LineSpan = ({
753
+ keyedLine,
754
+ showLineNumbers,
755
+ }: {
756
+ keyedLine: KeyedLine;
757
+ showLineNumbers: boolean;
758
+ }) => (
759
+ <span className={showLineNumbers ? LINE_NUMBER_CLASSES : "block"}>
760
+ {keyedLine.tokens.length === 0
761
+ ? "\\n"
762
+ : keyedLine.tokens.map(({ token, key }) => <TokenSpan key={key} token={token} />)}
763
+ </span>
764
+ );
765
+
766
+ // Types
767
+ type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
768
+ code: string;
769
+ language: BundledLanguage;
770
+ showLineNumbers?: boolean;
771
+ };
772
+
773
+ interface TokenizedCode {
774
+ tokens: ThemedToken[][];
775
+ fg: string;
776
+ bg: string;
777
+ }
778
+
779
+ interface CodeBlockContextType {
780
+ code: string;
781
+ }
782
+
783
+ // Context
784
+ const CodeBlockContext = createContext<CodeBlockContextType>({
785
+ code: "",
786
+ });
787
+
788
+ // Highlighter cache (singleton per language)
789
+ const highlighterCache = new Map<
790
+ string,
791
+ Promise<HighlighterGeneric<BundledLanguage, BundledTheme>>
792
+ >();
793
+
794
+ // Token cache
795
+ const tokensCache = new Map<string, TokenizedCode>();
796
+
797
+ // Subscribers for async token updates
798
+ const subscribers = new Map<string, Set<(result: TokenizedCode) => void>>();
799
+
800
+ const getTokensCacheKey = (code: string, language: BundledLanguage) => {
801
+ const start = code.slice(0, 100);
802
+ const end = code.length > 100 ? code.slice(-100) : "";
803
+ return \`\${language}:\${code.length}:\${start}:\${end}\`;
804
+ };
805
+
806
+ const getHighlighter = (
807
+ language: BundledLanguage,
808
+ ): Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> => {
809
+ const cached = highlighterCache.get(language);
810
+ if (cached) {
811
+ return cached;
812
+ }
813
+
814
+ const highlighterPromise = createHighlighter({
815
+ langs: [language],
816
+ themes: ["github-light", "github-dark"],
817
+ });
818
+
819
+ highlighterCache.set(language, highlighterPromise);
820
+ return highlighterPromise;
821
+ };
822
+
823
+ // Create raw tokens for immediate display while highlighting loads
824
+ const createRawTokens = (code: string): TokenizedCode => ({
825
+ bg: "transparent",
826
+ fg: "inherit",
827
+ tokens: code.split("\\n").map((line) =>
828
+ line === ""
829
+ ? []
830
+ : [
831
+ {
832
+ color: "inherit",
833
+ content: line,
834
+ } as ThemedToken,
835
+ ],
836
+ ),
837
+ });
838
+
839
+ // Synchronous highlight with callback for async results
840
+ export const highlightCode = (
841
+ code: string,
842
+ language: BundledLanguage,
843
+ // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks)
844
+ callback?: (result: TokenizedCode) => void,
845
+ ): TokenizedCode | null => {
846
+ const tokensCacheKey = getTokensCacheKey(code, language);
847
+
848
+ // Return cached result if available
849
+ const cached = tokensCache.get(tokensCacheKey);
850
+ if (cached) {
851
+ return cached;
852
+ }
853
+
854
+ // Subscribe callback if provided
855
+ if (callback) {
856
+ if (!subscribers.has(tokensCacheKey)) {
857
+ subscribers.set(tokensCacheKey, new Set());
858
+ }
859
+ subscribers.get(tokensCacheKey)?.add(callback);
860
+ }
861
+
862
+ // Start highlighting in background - fire-and-forget async pattern
863
+ getHighlighter(language)
864
+ // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then)
865
+ .then((highlighter) => {
866
+ const availableLangs = highlighter.getLoadedLanguages();
867
+ const langToUse = availableLangs.includes(language) ? language : "text";
868
+
869
+ const result = highlighter.codeToTokens(code, {
870
+ lang: langToUse,
871
+ themes: {
872
+ dark: "github-dark",
873
+ light: "github-light",
874
+ },
875
+ });
876
+
877
+ const tokenized: TokenizedCode = {
878
+ bg: result.bg ?? "transparent",
879
+ fg: result.fg ?? "inherit",
880
+ tokens: result.tokens,
881
+ };
882
+
883
+ // Cache the result
884
+ tokensCache.set(tokensCacheKey, tokenized);
885
+
886
+ // Notify all subscribers
887
+ const subs = subscribers.get(tokensCacheKey);
888
+ if (subs) {
889
+ for (const sub of subs) {
890
+ sub(tokenized);
891
+ }
892
+ subscribers.delete(tokensCacheKey);
893
+ }
894
+ })
895
+ // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then), eslint-plugin-promise(prefer-await-to-callbacks)
896
+ .catch((error) => {
897
+ console.error("Failed to highlight code:", error);
898
+ subscribers.delete(tokensCacheKey);
899
+ });
900
+
901
+ return null;
902
+ };
903
+
904
+ const CodeBlockBody = memo(
905
+ ({
906
+ tokenized,
907
+ showLineNumbers,
908
+ className,
909
+ }: {
910
+ tokenized: TokenizedCode;
911
+ showLineNumbers: boolean;
912
+ className?: string;
913
+ }) => {
914
+ const preStyle = useMemo(
915
+ () => ({
916
+ backgroundColor: tokenized.bg,
917
+ color: tokenized.fg,
918
+ }),
919
+ [tokenized.bg, tokenized.fg],
920
+ );
921
+
922
+ const keyedLines = useMemo(() => addKeysToTokens(tokenized.tokens), [tokenized.tokens]);
923
+
924
+ return (
925
+ <pre
926
+ className={cn(
927
+ "dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)] m-0 p-4 text-sm",
928
+ className,
929
+ )}
930
+ style={preStyle}
931
+ >
932
+ <code
933
+ className={cn(
934
+ "font-mono text-sm",
935
+ showLineNumbers && "[counter-increment:line_0] [counter-reset:line]",
936
+ )}
937
+ >
938
+ {keyedLines.map((keyedLine) => (
939
+ <LineSpan key={keyedLine.key} keyedLine={keyedLine} showLineNumbers={showLineNumbers} />
940
+ ))}
941
+ </code>
942
+ </pre>
943
+ );
944
+ },
945
+ (prevProps, nextProps) =>
946
+ prevProps.tokenized === nextProps.tokenized &&
947
+ prevProps.showLineNumbers === nextProps.showLineNumbers &&
948
+ prevProps.className === nextProps.className,
949
+ );
950
+
951
+ CodeBlockBody.displayName = "CodeBlockBody";
952
+
953
+ export const CodeBlockContainer = ({
954
+ className,
955
+ language,
956
+ style,
957
+ ...props
958
+ }: HTMLAttributes<HTMLDivElement> & { language: string }) => (
959
+ <div
960
+ className={cn(
961
+ "group relative w-full overflow-hidden rounded-md border bg-background text-foreground",
962
+ className,
963
+ )}
964
+ data-language={language}
965
+ style={{
966
+ containIntrinsicSize: "auto 200px",
967
+ contentVisibility: "auto",
968
+ ...style,
969
+ }}
970
+ {...props}
971
+ />
972
+ );
973
+
974
+ export const CodeBlockHeader = ({
975
+ children,
976
+ className,
977
+ ...props
978
+ }: HTMLAttributes<HTMLDivElement>) => (
979
+ <div
980
+ className={cn(
981
+ "flex items-center justify-between border-b bg-muted/80 px-3 py-2 text-muted-foreground text-xs",
982
+ className,
983
+ )}
984
+ {...props}
985
+ >
986
+ {children}
987
+ </div>
988
+ );
989
+
990
+ export const CodeBlockTitle = ({
991
+ children,
992
+ className,
993
+ ...props
994
+ }: HTMLAttributes<HTMLDivElement>) => (
995
+ <div className={cn("flex items-center gap-2", className)} {...props}>
996
+ {children}
997
+ </div>
998
+ );
999
+
1000
+ export const CodeBlockFilename = ({
1001
+ children,
1002
+ className,
1003
+ ...props
1004
+ }: HTMLAttributes<HTMLSpanElement>) => (
1005
+ <span className={cn("font-mono", className)} {...props}>
1006
+ {children}
1007
+ </span>
1008
+ );
1009
+
1010
+ export const CodeBlockActions = ({
1011
+ children,
1012
+ className,
1013
+ ...props
1014
+ }: HTMLAttributes<HTMLDivElement>) => (
1015
+ <div className={cn("-my-1 -mr-1 flex items-center gap-2", className)} {...props}>
1016
+ {children}
1017
+ </div>
1018
+ );
1019
+
1020
+ export const CodeBlockContent = ({
1021
+ code,
1022
+ language,
1023
+ showLineNumbers = false,
1024
+ }: {
1025
+ code: string;
1026
+ language: BundledLanguage;
1027
+ showLineNumbers?: boolean;
1028
+ }) => {
1029
+ // Memoized raw tokens for immediate display
1030
+ const rawTokens = useMemo(() => createRawTokens(code), [code]);
1031
+
1032
+ // Synchronous cache lookup — avoids setState in effect for cached results
1033
+ const syncTokens = useMemo(
1034
+ () => highlightCode(code, language) ?? rawTokens,
1035
+ [code, language, rawTokens],
1036
+ );
1037
+
1038
+ // Async highlighting result (populated after shiki loads)
1039
+ const [asyncTokens, setAsyncTokens] = useState<TokenizedCode | null>(null);
1040
+ const asyncKeyRef = useRef({ code, language });
1041
+
1042
+ // Invalidate stale async tokens synchronously during render
1043
+ if (asyncKeyRef.current.code !== code || asyncKeyRef.current.language !== language) {
1044
+ asyncKeyRef.current = { code, language };
1045
+ setAsyncTokens(null);
1046
+ }
1047
+
1048
+ useEffect(() => {
1049
+ let cancelled = false;
1050
+
1051
+ highlightCode(code, language, (result) => {
1052
+ if (!cancelled) {
1053
+ setAsyncTokens(result);
1054
+ }
1055
+ });
1056
+
1057
+ return () => {
1058
+ cancelled = true;
1059
+ };
1060
+ }, [code, language]);
1061
+
1062
+ const tokenized = asyncTokens ?? syncTokens;
1063
+
1064
+ return (
1065
+ <div className="relative overflow-auto">
1066
+ <CodeBlockBody showLineNumbers={showLineNumbers} tokenized={tokenized} />
1067
+ </div>
1068
+ );
1069
+ };
1070
+
1071
+ export const CodeBlock = ({
1072
+ code,
1073
+ language,
1074
+ showLineNumbers = false,
1075
+ className,
1076
+ children,
1077
+ ...props
1078
+ }: CodeBlockProps) => {
1079
+ const contextValue = useMemo(() => ({ code }), [code]);
1080
+
1081
+ return (
1082
+ <CodeBlockContext.Provider value={contextValue}>
1083
+ <CodeBlockContainer className={className} language={language} {...props}>
1084
+ {children}
1085
+ <CodeBlockContent code={code} language={language} showLineNumbers={showLineNumbers} />
1086
+ </CodeBlockContainer>
1087
+ </CodeBlockContext.Provider>
1088
+ );
1089
+ };
1090
+
1091
+ export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
1092
+ onCopy?: () => void;
1093
+ onError?: (error: Error) => void;
1094
+ timeout?: number;
1095
+ };
1096
+
1097
+ export const CodeBlockCopyButton = ({
1098
+ onCopy,
1099
+ onError,
1100
+ timeout = 2000,
1101
+ children,
1102
+ className,
1103
+ ...props
1104
+ }: CodeBlockCopyButtonProps) => {
1105
+ const [isCopied, setIsCopied] = useState(false);
1106
+ const timeoutRef = useRef<number>(0);
1107
+ const { code } = useContext(CodeBlockContext);
1108
+
1109
+ const copyToClipboard = useCallback(async () => {
1110
+ if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {
1111
+ onError?.(new Error("Clipboard API not available"));
1112
+ return;
1113
+ }
1114
+
1115
+ try {
1116
+ if (!isCopied) {
1117
+ await navigator.clipboard.writeText(code);
1118
+ setIsCopied(true);
1119
+ onCopy?.();
1120
+ timeoutRef.current = window.setTimeout(() => setIsCopied(false), timeout);
1121
+ }
1122
+ } catch (error) {
1123
+ onError?.(error as Error);
1124
+ }
1125
+ }, [code, onCopy, onError, timeout, isCopied]);
1126
+
1127
+ useEffect(
1128
+ () => () => {
1129
+ window.clearTimeout(timeoutRef.current);
1130
+ },
1131
+ [],
1132
+ );
1133
+
1134
+ const Icon = isCopied ? CheckIcon : CopyIcon;
1135
+
1136
+ return (
1137
+ <Button
1138
+ className={cn("shrink-0", className)}
1139
+ onClick={copyToClipboard}
1140
+ size="icon"
1141
+ variant="ghost"
1142
+ {...props}
1143
+ >
1144
+ {children ?? <Icon size={14} />}
1145
+ </Button>
1146
+ );
1147
+ };
1148
+
1149
+ export type CodeBlockLanguageSelectorProps = ComponentProps<typeof Select>;
1150
+
1151
+ export const CodeBlockLanguageSelector = (props: CodeBlockLanguageSelectorProps) => (
1152
+ <Select {...props} />
1153
+ );
1154
+
1155
+ export type CodeBlockLanguageSelectorTriggerProps = ComponentProps<typeof SelectTrigger>;
1156
+
1157
+ export const CodeBlockLanguageSelectorTrigger = ({
1158
+ className,
1159
+ ...props
1160
+ }: CodeBlockLanguageSelectorTriggerProps) => (
1161
+ <SelectTrigger
1162
+ className={cn("h-7 border-none bg-transparent px-2 text-xs shadow-none", className)}
1163
+ size="sm"
1164
+ {...props}
1165
+ />
1166
+ );
1167
+
1168
+ export type CodeBlockLanguageSelectorValueProps = ComponentProps<typeof SelectValue>;
1169
+
1170
+ export const CodeBlockLanguageSelectorValue = (props: CodeBlockLanguageSelectorValueProps) => (
1171
+ <SelectValue {...props} />
1172
+ );
1173
+
1174
+ export type CodeBlockLanguageSelectorContentProps = ComponentProps<typeof SelectContent>;
1175
+
1176
+ export const CodeBlockLanguageSelectorContent = ({
1177
+ align = "end",
1178
+ ...props
1179
+ }: CodeBlockLanguageSelectorContentProps) => <SelectContent align={align} {...props} />;
1180
+
1181
+ export type CodeBlockLanguageSelectorItemProps = ComponentProps<typeof SelectItem>;
1182
+
1183
+ export const CodeBlockLanguageSelectorItem = (props: CodeBlockLanguageSelectorItemProps) => (
1184
+ <SelectItem {...props} />
1185
+ );
1186
+ `,"components/ai-elements/conversation.tsx":`"use client";
1187
+
1188
+ import { Button } from "@/components/ui/button";
1189
+ import { cn } from "@/lib/utils";
1190
+ import type { UIMessage } from "ai";
1191
+ import { ArrowDownIcon, DownloadIcon } from "lucide-react";
1192
+ import type { ComponentProps } from "react";
1193
+ import { useCallback } from "react";
1194
+ import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
1195
+
1196
+ export type ConversationProps = ComponentProps<typeof StickToBottom>;
1197
+
1198
+ export const Conversation = ({ className, ...props }: ConversationProps) => (
1199
+ <StickToBottom
1200
+ className={cn("relative flex-1 overflow-y-hidden", className)}
1201
+ initial="smooth"
1202
+ resize="smooth"
1203
+ role="log"
1204
+ {...props}
1205
+ />
1206
+ );
1207
+
1208
+ export type ConversationContentProps = ComponentProps<typeof StickToBottom.Content>;
1209
+
1210
+ export const ConversationContent = ({ className, ...props }: ConversationContentProps) => (
1211
+ <StickToBottom.Content className={cn("flex flex-col gap-8 p-4", className)} {...props} />
1212
+ );
1213
+
1214
+ export type ConversationEmptyStateProps = ComponentProps<"div"> & {
1215
+ title?: string;
1216
+ description?: string;
1217
+ icon?: React.ReactNode;
1218
+ };
1219
+
1220
+ export const ConversationEmptyState = ({
1221
+ className,
1222
+ title = "No messages yet",
1223
+ description = "Start a conversation to see messages here",
1224
+ icon,
1225
+ children,
1226
+ ...props
1227
+ }: ConversationEmptyStateProps) => (
1228
+ <div
1229
+ className={cn(
1230
+ "flex size-full flex-col items-center justify-center gap-3 p-8 text-center",
1231
+ className,
1232
+ )}
1233
+ {...props}
1234
+ >
1235
+ {children ?? (
1236
+ <>
1237
+ {icon && <div className="text-muted-foreground">{icon}</div>}
1238
+ <div className="space-y-1">
1239
+ <h3 className="font-medium text-sm">{title}</h3>
1240
+ {description && <p className="text-muted-foreground text-sm">{description}</p>}
1241
+ </div>
1242
+ </>
1243
+ )}
1244
+ </div>
1245
+ );
1246
+
1247
+ export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
1248
+
1249
+ export const ConversationScrollButton = ({
1250
+ className,
1251
+ ...props
1252
+ }: ConversationScrollButtonProps) => {
1253
+ const { isAtBottom, scrollToBottom } = useStickToBottomContext();
1254
+
1255
+ const handleScrollToBottom = useCallback(() => {
1256
+ scrollToBottom();
1257
+ }, [scrollToBottom]);
1258
+
1259
+ return (
1260
+ !isAtBottom && (
1261
+ <Button
1262
+ className={cn(
1263
+ "absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full dark:bg-background dark:hover:bg-muted",
1264
+ className,
1265
+ )}
1266
+ onClick={handleScrollToBottom}
1267
+ size="icon"
1268
+ type="button"
1269
+ variant="outline"
1270
+ {...props}
1271
+ >
1272
+ <ArrowDownIcon className="size-4" />
1273
+ </Button>
1274
+ )
1275
+ );
1276
+ };
1277
+
1278
+ const getMessageText = (message: UIMessage): string =>
1279
+ message.parts
1280
+ .filter((part) => part.type === "text")
1281
+ .map((part) => part.text)
1282
+ .join("");
1283
+
1284
+ export type ConversationDownloadProps = Omit<ComponentProps<typeof Button>, "onClick"> & {
1285
+ messages: UIMessage[];
1286
+ filename?: string;
1287
+ formatMessage?: (message: UIMessage, index: number) => string;
1288
+ };
1289
+
1290
+ const defaultFormatMessage = (message: UIMessage): string => {
1291
+ const roleLabel = message.role.charAt(0).toUpperCase() + message.role.slice(1);
1292
+ return \`**\${roleLabel}:** \${getMessageText(message)}\`;
1293
+ };
1294
+
1295
+ export const messagesToMarkdown = (
1296
+ messages: UIMessage[],
1297
+ formatMessage: (message: UIMessage, index: number) => string = defaultFormatMessage,
1298
+ ): string => messages.map((msg, i) => formatMessage(msg, i)).join("\\n\\n");
1299
+
1300
+ export const ConversationDownload = ({
1301
+ messages,
1302
+ filename = "conversation.md",
1303
+ formatMessage = defaultFormatMessage,
1304
+ className,
1305
+ children,
1306
+ ...props
1307
+ }: ConversationDownloadProps) => {
1308
+ const handleDownload = useCallback(() => {
1309
+ const markdown = messagesToMarkdown(messages, formatMessage);
1310
+ const blob = new Blob([markdown], { type: "text/markdown" });
1311
+ const url = URL.createObjectURL(blob);
1312
+ const link = document.createElement("a");
1313
+ link.href = url;
1314
+ link.download = filename;
1315
+ document.body.append(link);
1316
+ link.click();
1317
+ link.remove();
1318
+ URL.revokeObjectURL(url);
1319
+ }, [messages, filename, formatMessage]);
1320
+
1321
+ return (
1322
+ <Button
1323
+ className={cn(
1324
+ "absolute top-4 right-4 rounded-full dark:bg-background dark:hover:bg-muted",
1325
+ className,
1326
+ )}
1327
+ onClick={handleDownload}
1328
+ size="icon"
1329
+ type="button"
1330
+ variant="outline"
1331
+ {...props}
1332
+ >
1333
+ {children ?? <DownloadIcon className="size-4" />}
1334
+ </Button>
1335
+ );
1336
+ };
1337
+ `,"components/ai-elements/message.tsx":`"use client";
1338
+
1339
+ import { Button } from "@/components/ui/button";
1340
+ import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group";
1341
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
1342
+ import { cn } from "@/lib/utils";
1343
+ import { cjk } from "@streamdown/cjk";
1344
+ import { code } from "@streamdown/code";
1345
+ import { math } from "@streamdown/math";
1346
+ import { mermaid } from "@streamdown/mermaid";
1347
+ import type { UIMessage } from "ai";
1348
+ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
1349
+ import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
1350
+ import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from "react";
1351
+ import { Streamdown } from "streamdown";
1352
+
1353
+ export type MessageProps = HTMLAttributes<HTMLDivElement> & {
1354
+ from: UIMessage["role"];
1355
+ };
1356
+
1357
+ export const Message = ({ className, from, ...props }: MessageProps) => (
1358
+ <div
1359
+ className={cn(
1360
+ "group flex w-full max-w-[95%] flex-col gap-2",
1361
+ from === "user" ? "is-user ml-auto justify-end" : "is-assistant",
1362
+ className,
1363
+ )}
1364
+ {...props}
1365
+ />
1366
+ );
1367
+
1368
+ export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
1369
+
1370
+ export const MessageContent = ({ children, className, ...props }: MessageContentProps) => (
1371
+ <div
1372
+ className={cn(
1373
+ "is-user:dark flex w-fit min-w-0 max-w-full flex-col gap-2 overflow-hidden text-sm",
1374
+ "group-[.is-user]:ml-auto group-[.is-user]:rounded-2xl group-[.is-user]:bg-primary group-[.is-user]:px-4 group-[.is-user]:py-2.5 group-[.is-user]:text-primary-foreground",
1375
+ "group-[.is-assistant]:w-full group-[.is-assistant]:text-foreground",
1376
+ "group-data-[optimistic=true]:opacity-70",
1377
+ className,
1378
+ )}
1379
+ {...props}
1380
+ >
1381
+ {children}
1382
+ </div>
1383
+ );
1384
+
1385
+ export type MessageActionsProps = ComponentProps<"div">;
1386
+
1387
+ export const MessageActions = ({ className, children, ...props }: MessageActionsProps) => (
1388
+ <div className={cn("flex items-center gap-1", className)} {...props}>
1389
+ {children}
1390
+ </div>
1391
+ );
1392
+
1393
+ export type MessageActionProps = ComponentProps<typeof Button> & {
1394
+ tooltip?: string;
1395
+ label?: string;
1396
+ };
1397
+
1398
+ export const MessageAction = ({
1399
+ tooltip,
1400
+ children,
1401
+ label,
1402
+ variant = "ghost",
1403
+ size = "icon-sm",
1404
+ ...props
1405
+ }: MessageActionProps) => {
1406
+ const button = (
1407
+ <Button size={size} type="button" variant={variant} {...props}>
1408
+ {children}
1409
+ <span className="sr-only">{label || tooltip}</span>
1410
+ </Button>
1411
+ );
1412
+
1413
+ if (tooltip) {
1414
+ return (
1415
+ <TooltipProvider>
1416
+ <Tooltip>
1417
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
1418
+ <TooltipContent>
1419
+ <p>{tooltip}</p>
1420
+ </TooltipContent>
1421
+ </Tooltip>
1422
+ </TooltipProvider>
1423
+ );
1424
+ }
1425
+
1426
+ return button;
1427
+ };
1428
+
1429
+ interface MessageBranchContextType {
1430
+ currentBranch: number;
1431
+ totalBranches: number;
1432
+ goToPrevious: () => void;
1433
+ goToNext: () => void;
1434
+ branches: ReactElement[];
1435
+ setBranches: (branches: ReactElement[]) => void;
1436
+ }
1437
+
1438
+ const MessageBranchContext = createContext<MessageBranchContextType | null>(null);
1439
+
1440
+ const useMessageBranch = () => {
1441
+ const context = useContext(MessageBranchContext);
1442
+
1443
+ if (!context) {
1444
+ throw new Error("MessageBranch components must be used within MessageBranch");
1445
+ }
1446
+
1447
+ return context;
1448
+ };
1449
+
1450
+ export type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {
1451
+ defaultBranch?: number;
1452
+ onBranchChange?: (branchIndex: number) => void;
1453
+ };
1454
+
1455
+ export const MessageBranch = ({
1456
+ defaultBranch = 0,
1457
+ onBranchChange,
1458
+ className,
1459
+ ...props
1460
+ }: MessageBranchProps) => {
1461
+ const [currentBranch, setCurrentBranch] = useState(defaultBranch);
1462
+ const [branches, setBranches] = useState<ReactElement[]>([]);
1463
+
1464
+ const handleBranchChange = useCallback(
1465
+ (newBranch: number) => {
1466
+ setCurrentBranch(newBranch);
1467
+ onBranchChange?.(newBranch);
1468
+ },
1469
+ [onBranchChange],
1470
+ );
1471
+
1472
+ const goToPrevious = useCallback(() => {
1473
+ const newBranch = currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
1474
+ handleBranchChange(newBranch);
1475
+ }, [currentBranch, branches.length, handleBranchChange]);
1476
+
1477
+ const goToNext = useCallback(() => {
1478
+ const newBranch = currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
1479
+ handleBranchChange(newBranch);
1480
+ }, [currentBranch, branches.length, handleBranchChange]);
1481
+
1482
+ const contextValue = useMemo<MessageBranchContextType>(
1483
+ () => ({
1484
+ branches,
1485
+ currentBranch,
1486
+ goToNext,
1487
+ goToPrevious,
1488
+ setBranches,
1489
+ totalBranches: branches.length,
1490
+ }),
1491
+ [branches, currentBranch, goToNext, goToPrevious],
1492
+ );
1493
+
1494
+ return (
1495
+ <MessageBranchContext.Provider value={contextValue}>
1496
+ <div className={cn("grid w-full gap-2 [&>div]:pb-0", className)} {...props} />
1497
+ </MessageBranchContext.Provider>
1498
+ );
1499
+ };
1500
+
1501
+ export type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;
1502
+
1503
+ export const MessageBranchContent = ({ children, ...props }: MessageBranchContentProps) => {
1504
+ const { currentBranch, setBranches, branches } = useMessageBranch();
1505
+ const childrenArray = useMemo(
1506
+ () => (Array.isArray(children) ? children : [children]),
1507
+ [children],
1508
+ );
1509
+
1510
+ // Use useEffect to update branches when they change
1511
+ useEffect(() => {
1512
+ if (branches.length !== childrenArray.length) {
1513
+ setBranches(childrenArray);
1514
+ }
1515
+ }, [childrenArray, branches, setBranches]);
1516
+
1517
+ return childrenArray.map((branch, index) => (
1518
+ <div
1519
+ className={cn(
1520
+ "grid gap-2 overflow-hidden [&>div]:pb-0",
1521
+ index === currentBranch ? "block" : "hidden",
1522
+ )}
1523
+ key={branch.key}
1524
+ {...props}
1525
+ >
1526
+ {branch}
1527
+ </div>
1528
+ ));
1529
+ };
1530
+
1531
+ export type MessageBranchSelectorProps = ComponentProps<typeof ButtonGroup>;
1532
+
1533
+ export const MessageBranchSelector = ({ className, ...props }: MessageBranchSelectorProps) => {
1534
+ const { totalBranches } = useMessageBranch();
1535
+
1536
+ // Don't render if there's only one branch
1537
+ if (totalBranches <= 1) {
1538
+ return null;
1539
+ }
1540
+
1541
+ return (
1542
+ <ButtonGroup
1543
+ className={cn(
1544
+ "[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md",
1545
+ className,
1546
+ )}
1547
+ orientation="horizontal"
1548
+ {...props}
1549
+ />
1550
+ );
1551
+ };
1552
+
1553
+ export type MessageBranchPreviousProps = ComponentProps<typeof Button>;
1554
+
1555
+ export const MessageBranchPrevious = ({ children, ...props }: MessageBranchPreviousProps) => {
1556
+ const { goToPrevious, totalBranches } = useMessageBranch();
1557
+
1558
+ return (
1559
+ <Button
1560
+ aria-label="Previous branch"
1561
+ disabled={totalBranches <= 1}
1562
+ onClick={goToPrevious}
1563
+ size="icon-sm"
1564
+ type="button"
1565
+ variant="ghost"
1566
+ {...props}
1567
+ >
1568
+ {children ?? <ChevronLeftIcon size={14} />}
1569
+ </Button>
1570
+ );
1571
+ };
1572
+
1573
+ export type MessageBranchNextProps = ComponentProps<typeof Button>;
1574
+
1575
+ export const MessageBranchNext = ({ children, ...props }: MessageBranchNextProps) => {
1576
+ const { goToNext, totalBranches } = useMessageBranch();
1577
+
1578
+ return (
1579
+ <Button
1580
+ aria-label="Next branch"
1581
+ disabled={totalBranches <= 1}
1582
+ onClick={goToNext}
1583
+ size="icon-sm"
1584
+ type="button"
1585
+ variant="ghost"
1586
+ {...props}
1587
+ >
1588
+ {children ?? <ChevronRightIcon size={14} />}
1589
+ </Button>
1590
+ );
1591
+ };
1592
+
1593
+ export type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;
1594
+
1595
+ export const MessageBranchPage = ({ className, ...props }: MessageBranchPageProps) => {
1596
+ const { currentBranch, totalBranches } = useMessageBranch();
1597
+
1598
+ return (
1599
+ <ButtonGroupText
1600
+ className={cn("border-none bg-transparent text-muted-foreground shadow-none", className)}
1601
+ {...props}
1602
+ >
1603
+ {currentBranch + 1} of {totalBranches}
1604
+ </ButtonGroupText>
1605
+ );
1606
+ };
1607
+
1608
+ export type MessageResponseProps = ComponentProps<typeof Streamdown>;
1609
+
1610
+ const streamdownPlugins = { cjk, code, math, mermaid };
1611
+
1612
+ export const MessageResponse = memo(
1613
+ ({ className, ...props }: MessageResponseProps) => (
1614
+ <Streamdown
1615
+ className={cn("size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0", className)}
1616
+ plugins={streamdownPlugins}
1617
+ {...props}
1618
+ />
1619
+ ),
1620
+ (prevProps, nextProps) =>
1621
+ prevProps.children === nextProps.children && nextProps.isAnimating === prevProps.isAnimating,
1622
+ );
1623
+
1624
+ MessageResponse.displayName = "MessageResponse";
1625
+
1626
+ export type MessageToolbarProps = ComponentProps<"div">;
1627
+
1628
+ export const MessageToolbar = ({ className, children, ...props }: MessageToolbarProps) => (
1629
+ <div className={cn("mt-4 flex w-full items-center justify-between gap-4", className)} {...props}>
1630
+ {children}
1631
+ </div>
1632
+ );
1633
+ `,"components/ai-elements/prompt-input.tsx":`"use client";
1634
+
1635
+ import {
1636
+ Command,
1637
+ CommandEmpty,
1638
+ CommandGroup,
1639
+ CommandInput,
1640
+ CommandItem,
1641
+ CommandList,
1642
+ CommandSeparator,
1643
+ } from "@/components/ui/command";
1644
+ import {
1645
+ DropdownMenu,
1646
+ DropdownMenuContent,
1647
+ DropdownMenuItem,
1648
+ DropdownMenuTrigger,
1649
+ } from "@/components/ui/dropdown-menu";
1650
+ import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
1651
+ import {
1652
+ InputGroup,
1653
+ InputGroupAddon,
1654
+ InputGroupButton,
1655
+ InputGroupTextarea,
1656
+ } from "@/components/ui/input-group";
1657
+ import {
1658
+ Select,
1659
+ SelectContent,
1660
+ SelectItem,
1661
+ SelectTrigger,
1662
+ SelectValue,
1663
+ } from "@/components/ui/select";
1664
+ import { Spinner } from "@/components/ui/spinner";
1665
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
1666
+ import { cn } from "@/lib/utils";
1667
+ import type { ChatStatus, FileUIPart, SourceDocumentUIPart } from "ai";
1668
+ import { ArrowUpIcon, ImageIcon, Monitor, PlusIcon, SquareIcon, XIcon } from "lucide-react";
1669
+ import { nanoid } from "nanoid";
1670
+ import type {
1671
+ ChangeEvent,
1672
+ ChangeEventHandler,
1673
+ ClipboardEventHandler,
1674
+ ComponentProps,
1675
+ FormEvent,
1676
+ FormEventHandler,
1677
+ HTMLAttributes,
1678
+ KeyboardEventHandler,
1679
+ PropsWithChildren,
1680
+ ReactNode,
1681
+ RefObject,
1682
+ } from "react";
1683
+ import {
1684
+ Children,
1685
+ createContext,
1686
+ useCallback,
1687
+ useContext,
1688
+ useEffect,
1689
+ useMemo,
1690
+ useRef,
1691
+ useState,
1692
+ } from "react";
1693
+
1694
+ // ============================================================================
1695
+ // Helpers
1696
+ // ============================================================================
1697
+
1698
+ const convertBlobUrlToDataUrl = async (url: string): Promise<string | null> => {
1699
+ try {
1700
+ const response = await fetch(url);
1701
+ const blob = await response.blob();
1702
+ // FileReader uses callback-based API, wrapping in Promise is necessary
1703
+ // oxlint-disable-next-line eslint-plugin-promise(avoid-new)
1704
+ return new Promise((resolve) => {
1705
+ const reader = new FileReader();
1706
+ // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener)
1707
+ reader.onloadend = () => resolve(reader.result as string);
1708
+ // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener)
1709
+ reader.onerror = () => resolve(null);
1710
+ reader.readAsDataURL(blob);
1711
+ });
1712
+ } catch {
1713
+ return null;
1714
+ }
1715
+ };
1716
+
1717
+ const captureScreenshot = async (): Promise<File | null> => {
1718
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getDisplayMedia) {
1719
+ return null;
1720
+ }
1721
+
1722
+ let stream: MediaStream | null = null;
1723
+ const video = document.createElement("video");
1724
+ video.muted = true;
1725
+ video.playsInline = true;
1726
+
1727
+ try {
1728
+ stream = await navigator.mediaDevices.getDisplayMedia({
1729
+ audio: false,
1730
+ video: true,
1731
+ });
1732
+
1733
+ video.srcObject = stream;
1734
+
1735
+ // Video element uses callback-based API, wrapping in Promise is necessary
1736
+ // oxlint-disable-next-line eslint-plugin-promise(avoid-new)
1737
+ await new Promise<void>((resolve, reject) => {
1738
+ // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener)
1739
+ video.onloadedmetadata = () => resolve();
1740
+ // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener)
1741
+ video.onerror = () => reject(new Error("Failed to load screen stream"));
1742
+ });
1743
+
1744
+ await video.play();
1745
+
1746
+ const width = video.videoWidth;
1747
+ const height = video.videoHeight;
1748
+ if (!width || !height) {
1749
+ return null;
1750
+ }
1751
+
1752
+ const canvas = document.createElement("canvas");
1753
+ canvas.width = width;
1754
+ canvas.height = height;
1755
+ const context = canvas.getContext("2d");
1756
+ if (!context) {
1757
+ return null;
1758
+ }
1759
+
1760
+ context.drawImage(video, 0, 0, width, height);
1761
+ // canvas.toBlob uses callback-based API, wrapping in Promise is necessary
1762
+ // oxlint-disable-next-line eslint-plugin-promise(avoid-new)
1763
+ const blob = await new Promise<Blob | null>((resolve) => {
1764
+ canvas.toBlob(resolve, "image/png");
1765
+ });
1766
+ if (!blob) {
1767
+ return null;
1768
+ }
1769
+
1770
+ const timestamp = new Date()
1771
+ .toISOString()
1772
+ .replaceAll(/[:.]/g, "-")
1773
+ .replace("T", "_")
1774
+ .replace("Z", "");
1775
+
1776
+ return new File([blob], \`screenshot-\${timestamp}.png\`, {
1777
+ lastModified: Date.now(),
1778
+ type: "image/png",
1779
+ });
1780
+ } finally {
1781
+ if (stream) {
1782
+ for (const track of stream.getTracks()) {
1783
+ track.stop();
1784
+ }
1785
+ }
1786
+ video.pause();
1787
+ video.srcObject = null;
1788
+ }
1789
+ };
1790
+
1791
+ // ============================================================================
1792
+ // Provider Context & Types
1793
+ // ============================================================================
1794
+
1795
+ export interface AttachmentsContext {
1796
+ files: (FileUIPart & { id: string })[];
1797
+ add: (files: File[] | FileList) => void;
1798
+ remove: (id: string) => void;
1799
+ clear: () => void;
1800
+ openFileDialog: () => void;
1801
+ fileInputRef: RefObject<HTMLInputElement | null>;
1802
+ }
1803
+
1804
+ export interface TextInputContext {
1805
+ value: string;
1806
+ setInput: (v: string) => void;
1807
+ clear: () => void;
1808
+ }
1809
+
1810
+ export interface PromptInputControllerProps {
1811
+ textInput: TextInputContext;
1812
+ attachments: AttachmentsContext;
1813
+ /** INTERNAL: Allows PromptInput to register its file textInput + "open" callback */
1814
+ __registerFileInput: (ref: RefObject<HTMLInputElement | null>, open: () => void) => void;
1815
+ }
1816
+
1817
+ const PromptInputController = createContext<PromptInputControllerProps | null>(null);
1818
+ const ProviderAttachmentsContext = createContext<AttachmentsContext | null>(null);
1819
+
1820
+ export const usePromptInputController = () => {
1821
+ const ctx = useContext(PromptInputController);
1822
+ if (!ctx) {
1823
+ throw new Error(
1824
+ "Wrap your component inside <PromptInputProvider> to use usePromptInputController().",
1825
+ );
1826
+ }
1827
+ return ctx;
1828
+ };
1829
+
1830
+ // Optional variants (do NOT throw). Useful for dual-mode components.
1831
+ const useOptionalPromptInputController = () => useContext(PromptInputController);
1832
+
1833
+ export const useProviderAttachments = () => {
1834
+ const ctx = useContext(ProviderAttachmentsContext);
1835
+ if (!ctx) {
1836
+ throw new Error(
1837
+ "Wrap your component inside <PromptInputProvider> to use useProviderAttachments().",
1838
+ );
1839
+ }
1840
+ return ctx;
1841
+ };
1842
+
1843
+ const useOptionalProviderAttachments = () => useContext(ProviderAttachmentsContext);
1844
+
1845
+ export type PromptInputProviderProps = PropsWithChildren<{
1846
+ initialInput?: string;
1847
+ }>;
1848
+
1849
+ /**
1850
+ * Optional global provider that lifts PromptInput state outside of PromptInput.
1851
+ * If you don't use it, PromptInput stays fully self-managed.
1852
+ */
1853
+ export const PromptInputProvider = ({
1854
+ initialInput: initialTextInput = "",
1855
+ children,
1856
+ }: PromptInputProviderProps) => {
1857
+ // ----- textInput state
1858
+ const [textInput, setTextInput] = useState(initialTextInput);
1859
+ const clearInput = useCallback(() => setTextInput(""), []);
1860
+
1861
+ // ----- attachments state (global when wrapped)
1862
+ const [attachmentFiles, setAttachmentFiles] = useState<(FileUIPart & { id: string })[]>([]);
1863
+ const fileInputRef = useRef<HTMLInputElement | null>(null);
1864
+ // oxlint-disable-next-line eslint(no-empty-function)
1865
+ const openRef = useRef<() => void>(() => {});
1866
+
1867
+ const add = useCallback((files: File[] | FileList) => {
1868
+ const incoming = [...files];
1869
+ if (incoming.length === 0) {
1870
+ return;
1871
+ }
1872
+
1873
+ setAttachmentFiles((prev) => [
1874
+ ...prev,
1875
+ ...incoming.map((file) => ({
1876
+ filename: file.name,
1877
+ id: nanoid(),
1878
+ mediaType: file.type,
1879
+ type: "file" as const,
1880
+ url: URL.createObjectURL(file),
1881
+ })),
1882
+ ]);
1883
+ }, []);
1884
+
1885
+ const remove = useCallback((id: string) => {
1886
+ setAttachmentFiles((prev) => {
1887
+ const found = prev.find((f) => f.id === id);
1888
+ if (found?.url) {
1889
+ URL.revokeObjectURL(found.url);
1890
+ }
1891
+ return prev.filter((f) => f.id !== id);
1892
+ });
1893
+ }, []);
1894
+
1895
+ const clear = useCallback(() => {
1896
+ setAttachmentFiles((prev) => {
1897
+ for (const f of prev) {
1898
+ if (f.url) {
1899
+ URL.revokeObjectURL(f.url);
1900
+ }
1901
+ }
1902
+ return [];
1903
+ });
1904
+ }, []);
1905
+
1906
+ // Keep a ref to attachments for cleanup on unmount (avoids stale closure)
1907
+ const attachmentsRef = useRef(attachmentFiles);
1908
+
1909
+ useEffect(() => {
1910
+ attachmentsRef.current = attachmentFiles;
1911
+ }, [attachmentFiles]);
1912
+
1913
+ // Cleanup blob URLs on unmount to prevent memory leaks
1914
+ useEffect(
1915
+ () => () => {
1916
+ for (const f of attachmentsRef.current) {
1917
+ if (f.url) {
1918
+ URL.revokeObjectURL(f.url);
1919
+ }
1920
+ }
1921
+ },
1922
+ [],
1923
+ );
1924
+
1925
+ const openFileDialog = useCallback(() => {
1926
+ openRef.current?.();
1927
+ }, []);
1928
+
1929
+ const attachments = useMemo<AttachmentsContext>(
1930
+ () => ({
1931
+ add,
1932
+ clear,
1933
+ fileInputRef,
1934
+ files: attachmentFiles,
1935
+ openFileDialog,
1936
+ remove,
1937
+ }),
1938
+ [attachmentFiles, add, remove, clear, openFileDialog],
1939
+ );
1940
+
1941
+ const __registerFileInput = useCallback(
1942
+ (ref: RefObject<HTMLInputElement | null>, open: () => void) => {
1943
+ fileInputRef.current = ref.current;
1944
+ openRef.current = open;
1945
+ },
1946
+ [],
1947
+ );
1948
+
1949
+ const controller = useMemo<PromptInputControllerProps>(
1950
+ () => ({
1951
+ __registerFileInput,
1952
+ attachments,
1953
+ textInput: {
1954
+ clear: clearInput,
1955
+ setInput: setTextInput,
1956
+ value: textInput,
1957
+ },
1958
+ }),
1959
+ [textInput, clearInput, attachments, __registerFileInput],
1960
+ );
1961
+
1962
+ return (
1963
+ <PromptInputController.Provider value={controller}>
1964
+ <ProviderAttachmentsContext.Provider value={attachments}>
1965
+ {children}
1966
+ </ProviderAttachmentsContext.Provider>
1967
+ </PromptInputController.Provider>
1968
+ );
1969
+ };
1970
+
1971
+ // ============================================================================
1972
+ // Component Context & Hooks
1973
+ // ============================================================================
1974
+
1975
+ const LocalAttachmentsContext = createContext<AttachmentsContext | null>(null);
1976
+
1977
+ export const usePromptInputAttachments = () => {
1978
+ // Prefer local context (inside PromptInput) as it has validation, fall back to provider
1979
+ const provider = useOptionalProviderAttachments();
1980
+ const local = useContext(LocalAttachmentsContext);
1981
+ const context = local ?? provider;
1982
+ if (!context) {
1983
+ throw new Error(
1984
+ "usePromptInputAttachments must be used within a PromptInput or PromptInputProvider",
1985
+ );
1986
+ }
1987
+ return context;
1988
+ };
1989
+
1990
+ // ============================================================================
1991
+ // Referenced Sources (Local to PromptInput)
1992
+ // ============================================================================
1993
+
1994
+ export interface ReferencedSourcesContext {
1995
+ sources: (SourceDocumentUIPart & { id: string })[];
1996
+ add: (sources: SourceDocumentUIPart[] | SourceDocumentUIPart) => void;
1997
+ remove: (id: string) => void;
1998
+ clear: () => void;
1999
+ }
2000
+
2001
+ export const LocalReferencedSourcesContext = createContext<ReferencedSourcesContext | null>(null);
2002
+
2003
+ export const usePromptInputReferencedSources = () => {
2004
+ const ctx = useContext(LocalReferencedSourcesContext);
2005
+ if (!ctx) {
2006
+ throw new Error(
2007
+ "usePromptInputReferencedSources must be used within a LocalReferencedSourcesContext.Provider",
2008
+ );
2009
+ }
2010
+ return ctx;
2011
+ };
2012
+
2013
+ export type PromptInputActionAddAttachmentsProps = ComponentProps<typeof DropdownMenuItem> & {
2014
+ label?: string;
2015
+ };
2016
+
2017
+ export const PromptInputActionAddAttachments = ({
2018
+ label = "Add photos or files",
2019
+ ...props
2020
+ }: PromptInputActionAddAttachmentsProps) => {
2021
+ const attachments = usePromptInputAttachments();
2022
+
2023
+ const handleSelect = useCallback(
2024
+ (e: Event) => {
2025
+ e.preventDefault();
2026
+ attachments.openFileDialog();
2027
+ },
2028
+ [attachments],
2029
+ );
2030
+
2031
+ return (
2032
+ <DropdownMenuItem {...props} onSelect={handleSelect}>
2033
+ <ImageIcon className="mr-2 size-4" /> {label}
2034
+ </DropdownMenuItem>
2035
+ );
2036
+ };
2037
+
2038
+ export type PromptInputActionAddScreenshotProps = ComponentProps<typeof DropdownMenuItem> & {
2039
+ label?: string;
2040
+ };
2041
+
2042
+ export const PromptInputActionAddScreenshot = ({
2043
+ label = "Take screenshot",
2044
+ onSelect,
2045
+ ...props
2046
+ }: PromptInputActionAddScreenshotProps) => {
2047
+ const attachments = usePromptInputAttachments();
2048
+
2049
+ const handleSelect = useCallback(
2050
+ async (event: Event) => {
2051
+ onSelect?.(event);
2052
+ if (event.defaultPrevented) {
2053
+ return;
2054
+ }
2055
+
2056
+ try {
2057
+ const screenshot = await captureScreenshot();
2058
+ if (screenshot) {
2059
+ attachments.add([screenshot]);
2060
+ }
2061
+ } catch (error) {
2062
+ if (
2063
+ error instanceof DOMException &&
2064
+ (error.name === "NotAllowedError" || error.name === "AbortError")
2065
+ ) {
2066
+ return;
2067
+ }
2068
+ throw error;
2069
+ }
2070
+ },
2071
+ [onSelect, attachments],
2072
+ );
2073
+
2074
+ return (
2075
+ <DropdownMenuItem {...props} onSelect={handleSelect}>
2076
+ <Monitor className="mr-2 size-4" />
2077
+ {label}
2078
+ </DropdownMenuItem>
2079
+ );
2080
+ };
2081
+
2082
+ export interface PromptInputMessage {
2083
+ text: string;
2084
+ files: FileUIPart[];
2085
+ }
2086
+
2087
+ export type PromptInputProps = Omit<HTMLAttributes<HTMLFormElement>, "onSubmit" | "onError"> & {
2088
+ // e.g., "image/*" or leave undefined for any
2089
+ accept?: string;
2090
+ multiple?: boolean;
2091
+ // When true, accepts drops anywhere on document. Default false (opt-in).
2092
+ globalDrop?: boolean;
2093
+ // Render a hidden input with given name and keep it in sync for native form posts. Default false.
2094
+ syncHiddenInput?: boolean;
2095
+ // Minimal constraints
2096
+ maxFiles?: number;
2097
+ // bytes
2098
+ maxFileSize?: number;
2099
+ onError?: (err: { code: "max_files" | "max_file_size" | "accept"; message: string }) => void;
2100
+ onSubmit: (
2101
+ message: PromptInputMessage,
2102
+ event: FormEvent<HTMLFormElement>,
2103
+ ) => void | Promise<void>;
2104
+ };
2105
+
2106
+ export const PromptInput = ({
2107
+ className,
2108
+ accept,
2109
+ multiple,
2110
+ globalDrop,
2111
+ syncHiddenInput,
2112
+ maxFiles,
2113
+ maxFileSize,
2114
+ onError,
2115
+ onSubmit,
2116
+ children,
2117
+ ...props
2118
+ }: PromptInputProps) => {
2119
+ // Try to use a provider controller if present
2120
+ const controller = useOptionalPromptInputController();
2121
+ const usingProvider = !!controller;
2122
+
2123
+ // Refs
2124
+ const inputRef = useRef<HTMLInputElement | null>(null);
2125
+ const formRef = useRef<HTMLFormElement | null>(null);
2126
+
2127
+ // ----- Local attachments (only used when no provider)
2128
+ const [items, setItems] = useState<(FileUIPart & { id: string })[]>([]);
2129
+ const files = usingProvider ? controller.attachments.files : items;
2130
+
2131
+ // ----- Local referenced sources (always local to PromptInput)
2132
+ const [referencedSources, setReferencedSources] = useState<
2133
+ (SourceDocumentUIPart & { id: string })[]
2134
+ >([]);
2135
+
2136
+ // Keep a ref to files for cleanup on unmount (avoids stale closure)
2137
+ const filesRef = useRef(files);
2138
+
2139
+ useEffect(() => {
2140
+ filesRef.current = files;
2141
+ }, [files]);
2142
+
2143
+ const openFileDialogLocal = useCallback(() => {
2144
+ inputRef.current?.click();
2145
+ }, []);
2146
+
2147
+ const matchesAccept = useCallback(
2148
+ (f: File) => {
2149
+ if (!accept || accept.trim() === "") {
2150
+ return true;
2151
+ }
2152
+
2153
+ const patterns = accept
2154
+ .split(",")
2155
+ .map((s) => s.trim())
2156
+ .filter(Boolean);
2157
+
2158
+ return patterns.some((pattern) => {
2159
+ if (pattern.endsWith("/*")) {
2160
+ // e.g: image/* -> image/
2161
+ const prefix = pattern.slice(0, -1);
2162
+ return f.type.startsWith(prefix);
2163
+ }
2164
+ return f.type === pattern;
2165
+ });
2166
+ },
2167
+ [accept],
2168
+ );
2169
+
2170
+ const addLocal = useCallback(
2171
+ (fileList: File[] | FileList) => {
2172
+ const incoming = [...fileList];
2173
+ const accepted = incoming.filter((f) => matchesAccept(f));
2174
+ if (incoming.length && accepted.length === 0) {
2175
+ onError?.({
2176
+ code: "accept",
2177
+ message: "No files match the accepted types.",
2178
+ });
2179
+ return;
2180
+ }
2181
+ const withinSize = (f: File) => (maxFileSize ? f.size <= maxFileSize : true);
2182
+ const sized = accepted.filter(withinSize);
2183
+ if (accepted.length > 0 && sized.length === 0) {
2184
+ onError?.({
2185
+ code: "max_file_size",
2186
+ message: "All files exceed the maximum size.",
2187
+ });
2188
+ return;
2189
+ }
2190
+
2191
+ setItems((prev) => {
2192
+ const capacity =
2193
+ typeof maxFiles === "number" ? Math.max(0, maxFiles - prev.length) : undefined;
2194
+ const capped = typeof capacity === "number" ? sized.slice(0, capacity) : sized;
2195
+ if (typeof capacity === "number" && sized.length > capacity) {
2196
+ onError?.({
2197
+ code: "max_files",
2198
+ message: "Too many files. Some were not added.",
2199
+ });
2200
+ }
2201
+ const next: (FileUIPart & { id: string })[] = [];
2202
+ for (const file of capped) {
2203
+ next.push({
2204
+ filename: file.name,
2205
+ id: nanoid(),
2206
+ mediaType: file.type,
2207
+ type: "file",
2208
+ url: URL.createObjectURL(file),
2209
+ });
2210
+ }
2211
+ return [...prev, ...next];
2212
+ });
2213
+ },
2214
+ [matchesAccept, maxFiles, maxFileSize, onError],
2215
+ );
2216
+
2217
+ const removeLocal = useCallback(
2218
+ (id: string) =>
2219
+ setItems((prev) => {
2220
+ const found = prev.find((file) => file.id === id);
2221
+ if (found?.url) {
2222
+ URL.revokeObjectURL(found.url);
2223
+ }
2224
+ return prev.filter((file) => file.id !== id);
2225
+ }),
2226
+ [],
2227
+ );
2228
+
2229
+ // Wrapper that validates files before calling provider's add
2230
+ const addWithProviderValidation = useCallback(
2231
+ (fileList: File[] | FileList) => {
2232
+ const incoming = [...fileList];
2233
+ const accepted = incoming.filter((f) => matchesAccept(f));
2234
+ if (incoming.length && accepted.length === 0) {
2235
+ onError?.({
2236
+ code: "accept",
2237
+ message: "No files match the accepted types.",
2238
+ });
2239
+ return;
2240
+ }
2241
+ const withinSize = (f: File) => (maxFileSize ? f.size <= maxFileSize : true);
2242
+ const sized = accepted.filter(withinSize);
2243
+ if (accepted.length > 0 && sized.length === 0) {
2244
+ onError?.({
2245
+ code: "max_file_size",
2246
+ message: "All files exceed the maximum size.",
2247
+ });
2248
+ return;
2249
+ }
2250
+
2251
+ const currentCount = files.length;
2252
+ const capacity =
2253
+ typeof maxFiles === "number" ? Math.max(0, maxFiles - currentCount) : undefined;
2254
+ const capped = typeof capacity === "number" ? sized.slice(0, capacity) : sized;
2255
+ if (typeof capacity === "number" && sized.length > capacity) {
2256
+ onError?.({
2257
+ code: "max_files",
2258
+ message: "Too many files. Some were not added.",
2259
+ });
2260
+ }
2261
+
2262
+ if (capped.length > 0) {
2263
+ controller?.attachments.add(capped);
2264
+ }
2265
+ },
2266
+ [matchesAccept, maxFileSize, maxFiles, onError, files.length, controller],
2267
+ );
2268
+
2269
+ const clearAttachments = useCallback(
2270
+ () =>
2271
+ usingProvider
2272
+ ? controller?.attachments.clear()
2273
+ : setItems((prev) => {
2274
+ for (const file of prev) {
2275
+ if (file.url) {
2276
+ URL.revokeObjectURL(file.url);
2277
+ }
2278
+ }
2279
+ return [];
2280
+ }),
2281
+ [usingProvider, controller],
2282
+ );
2283
+
2284
+ const clearReferencedSources = useCallback(() => setReferencedSources([]), []);
2285
+
2286
+ const add = usingProvider ? addWithProviderValidation : addLocal;
2287
+ const remove = usingProvider ? controller.attachments.remove : removeLocal;
2288
+ const openFileDialog = usingProvider
2289
+ ? controller.attachments.openFileDialog
2290
+ : openFileDialogLocal;
2291
+
2292
+ const clear = useCallback(() => {
2293
+ clearAttachments();
2294
+ clearReferencedSources();
2295
+ }, [clearAttachments, clearReferencedSources]);
2296
+
2297
+ // Let provider know about our hidden file input so external menus can call openFileDialog()
2298
+ useEffect(() => {
2299
+ if (!usingProvider) {
2300
+ return;
2301
+ }
2302
+ controller.__registerFileInput(inputRef, () => inputRef.current?.click());
2303
+ }, [usingProvider, controller]);
2304
+
2305
+ // Note: File input cannot be programmatically set for security reasons
2306
+ // The syncHiddenInput prop is no longer functional
2307
+ useEffect(() => {
2308
+ if (syncHiddenInput && inputRef.current && files.length === 0) {
2309
+ inputRef.current.value = "";
2310
+ }
2311
+ }, [files, syncHiddenInput]);
2312
+
2313
+ // Attach drop handlers on nearest form and document (opt-in)
2314
+ useEffect(() => {
2315
+ const form = formRef.current;
2316
+ if (!form) {
2317
+ return;
2318
+ }
2319
+ if (globalDrop) {
2320
+ // when global drop is on, let the document-level handler own drops
2321
+ return;
2322
+ }
2323
+
2324
+ const onDragOver = (e: DragEvent) => {
2325
+ if (e.dataTransfer?.types?.includes("Files")) {
2326
+ e.preventDefault();
2327
+ }
2328
+ };
2329
+ const onDrop = (e: DragEvent) => {
2330
+ if (e.dataTransfer?.types?.includes("Files")) {
2331
+ e.preventDefault();
2332
+ }
2333
+ if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
2334
+ add(e.dataTransfer.files);
2335
+ }
2336
+ };
2337
+ form.addEventListener("dragover", onDragOver);
2338
+ form.addEventListener("drop", onDrop);
2339
+ return () => {
2340
+ form.removeEventListener("dragover", onDragOver);
2341
+ form.removeEventListener("drop", onDrop);
2342
+ };
2343
+ }, [add, globalDrop]);
2344
+
2345
+ useEffect(() => {
2346
+ if (!globalDrop) {
2347
+ return;
2348
+ }
2349
+
2350
+ const onDragOver = (e: DragEvent) => {
2351
+ if (e.dataTransfer?.types?.includes("Files")) {
2352
+ e.preventDefault();
2353
+ }
2354
+ };
2355
+ const onDrop = (e: DragEvent) => {
2356
+ if (e.dataTransfer?.types?.includes("Files")) {
2357
+ e.preventDefault();
2358
+ }
2359
+ if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
2360
+ add(e.dataTransfer.files);
2361
+ }
2362
+ };
2363
+ document.addEventListener("dragover", onDragOver);
2364
+ document.addEventListener("drop", onDrop);
2365
+ return () => {
2366
+ document.removeEventListener("dragover", onDragOver);
2367
+ document.removeEventListener("drop", onDrop);
2368
+ };
2369
+ }, [add, globalDrop]);
2370
+
2371
+ useEffect(
2372
+ () => () => {
2373
+ if (!usingProvider) {
2374
+ for (const f of filesRef.current) {
2375
+ if (f.url) {
2376
+ URL.revokeObjectURL(f.url);
2377
+ }
2378
+ }
2379
+ }
2380
+ },
2381
+ [usingProvider],
2382
+ );
2383
+
2384
+ const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback(
2385
+ (event) => {
2386
+ if (event.currentTarget.files) {
2387
+ add(event.currentTarget.files);
2388
+ }
2389
+ // Reset input value to allow selecting files that were previously removed
2390
+ event.currentTarget.value = "";
2391
+ },
2392
+ [add],
2393
+ );
2394
+
2395
+ const attachmentsCtx = useMemo<AttachmentsContext>(
2396
+ () => ({
2397
+ add,
2398
+ clear: clearAttachments,
2399
+ fileInputRef: inputRef,
2400
+ files: files.map((item) => ({ ...item, id: item.id })),
2401
+ openFileDialog,
2402
+ remove,
2403
+ }),
2404
+ [files, add, remove, clearAttachments, openFileDialog],
2405
+ );
2406
+
2407
+ const refsCtx = useMemo<ReferencedSourcesContext>(
2408
+ () => ({
2409
+ add: (incoming: SourceDocumentUIPart[] | SourceDocumentUIPart) => {
2410
+ const array = Array.isArray(incoming) ? incoming : [incoming];
2411
+ setReferencedSources((prev) => [...prev, ...array.map((s) => ({ ...s, id: nanoid() }))]);
2412
+ },
2413
+ clear: clearReferencedSources,
2414
+ remove: (id: string) => {
2415
+ setReferencedSources((prev) => prev.filter((s) => s.id !== id));
2416
+ },
2417
+ sources: referencedSources,
2418
+ }),
2419
+ [referencedSources, clearReferencedSources],
2420
+ );
2421
+
2422
+ const handleSubmit: FormEventHandler<HTMLFormElement> = useCallback(
2423
+ async (event) => {
2424
+ event.preventDefault();
2425
+
2426
+ const form = event.currentTarget;
2427
+ const text = usingProvider
2428
+ ? controller.textInput.value
2429
+ : (() => {
2430
+ const formData = new FormData(form);
2431
+ return (formData.get("message") as string) || "";
2432
+ })();
2433
+
2434
+ // Reset form immediately after capturing text to avoid race condition
2435
+ // where user input during async blob conversion would be lost
2436
+ if (!usingProvider) {
2437
+ form.reset();
2438
+ }
2439
+
2440
+ try {
2441
+ // Convert blob URLs to data URLs asynchronously
2442
+ const convertedFiles: FileUIPart[] = await Promise.all(
2443
+ files.map(async ({ id: _id, ...item }) => {
2444
+ if (item.url?.startsWith("blob:")) {
2445
+ const dataUrl = await convertBlobUrlToDataUrl(item.url);
2446
+ // If conversion failed, keep the original blob URL
2447
+ return {
2448
+ ...item,
2449
+ url: dataUrl ?? item.url,
2450
+ };
2451
+ }
2452
+ return item;
2453
+ }),
2454
+ );
2455
+
2456
+ const result = onSubmit({ files: convertedFiles, text }, event);
2457
+
2458
+ // Handle both sync and async onSubmit
2459
+ if (result instanceof Promise) {
2460
+ try {
2461
+ await result;
2462
+ clear();
2463
+ if (usingProvider) {
2464
+ controller.textInput.clear();
2465
+ }
2466
+ } catch {
2467
+ // Don't clear on error - user may want to retry
2468
+ }
2469
+ } else {
2470
+ // Sync function completed without throwing, clear inputs
2471
+ clear();
2472
+ if (usingProvider) {
2473
+ controller.textInput.clear();
2474
+ }
2475
+ }
2476
+ } catch {
2477
+ // Don't clear on error - user may want to retry
2478
+ }
2479
+ },
2480
+ [usingProvider, controller, files, onSubmit, clear],
2481
+ );
2482
+
2483
+ // Render with or without local provider
2484
+ const inner = (
2485
+ <>
2486
+ <input
2487
+ accept={accept}
2488
+ aria-label="Upload files"
2489
+ className="hidden"
2490
+ multiple={multiple}
2491
+ onChange={handleChange}
2492
+ ref={inputRef}
2493
+ title="Upload files"
2494
+ type="file"
2495
+ />
2496
+ <form className="w-full" onSubmit={handleSubmit} ref={formRef} {...props}>
2497
+ <InputGroup
2498
+ className={cn(
2499
+ "overflow-hidden rounded-2xl bg-card shadow-sm",
2500
+ "focus-within:border-foreground has-[[data-slot=input-group-control]:focus-visible]:border-foreground",
2501
+ className,
2502
+ )}
2503
+ >
2504
+ {children}
2505
+ </InputGroup>
2506
+ </form>
2507
+ </>
2508
+ );
2509
+
2510
+ const withReferencedSources = (
2511
+ <LocalReferencedSourcesContext.Provider value={refsCtx}>
2512
+ {inner}
2513
+ </LocalReferencedSourcesContext.Provider>
2514
+ );
2515
+
2516
+ // Always provide LocalAttachmentsContext so children get validated add function
2517
+ return (
2518
+ <LocalAttachmentsContext.Provider value={attachmentsCtx}>
2519
+ {withReferencedSources}
2520
+ </LocalAttachmentsContext.Provider>
2521
+ );
2522
+ };
2523
+
2524
+ export type PromptInputBodyProps = HTMLAttributes<HTMLDivElement>;
2525
+
2526
+ export const PromptInputBody = ({ className, ...props }: PromptInputBodyProps) => (
2527
+ <div className={cn("contents", className)} {...props} />
2528
+ );
2529
+
2530
+ export type PromptInputTextareaProps = ComponentProps<typeof InputGroupTextarea>;
2531
+
2532
+ export const PromptInputTextarea = ({
2533
+ onChange,
2534
+ onKeyDown,
2535
+ className,
2536
+ placeholder = "What would you like to know?",
2537
+ ...props
2538
+ }: PromptInputTextareaProps) => {
2539
+ const controller = useOptionalPromptInputController();
2540
+ const attachments = usePromptInputAttachments();
2541
+ const [isComposing, setIsComposing] = useState(false);
2542
+
2543
+ const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = useCallback(
2544
+ (e) => {
2545
+ // Call the external onKeyDown handler first
2546
+ onKeyDown?.(e);
2547
+
2548
+ // If the external handler prevented default, don't run internal logic
2549
+ if (e.defaultPrevented) {
2550
+ return;
2551
+ }
2552
+
2553
+ if (e.key === "Enter") {
2554
+ if (isComposing || e.nativeEvent.isComposing) {
2555
+ return;
2556
+ }
2557
+ if (e.shiftKey) {
2558
+ return;
2559
+ }
2560
+ e.preventDefault();
2561
+
2562
+ // Check if the submit button is disabled before submitting
2563
+ const { form } = e.currentTarget;
2564
+ const submitButton = form?.querySelector(
2565
+ 'button[type="submit"]',
2566
+ ) as HTMLButtonElement | null;
2567
+ if (submitButton?.disabled) {
2568
+ return;
2569
+ }
2570
+
2571
+ form?.requestSubmit();
2572
+ }
2573
+
2574
+ // Remove last attachment when Backspace is pressed and textarea is empty
2575
+ if (e.key === "Backspace" && e.currentTarget.value === "" && attachments.files.length > 0) {
2576
+ e.preventDefault();
2577
+ const lastAttachment = attachments.files.at(-1);
2578
+ if (lastAttachment) {
2579
+ attachments.remove(lastAttachment.id);
2580
+ }
2581
+ }
2582
+ },
2583
+ [onKeyDown, isComposing, attachments],
2584
+ );
2585
+
2586
+ const handlePaste: ClipboardEventHandler<HTMLTextAreaElement> = useCallback(
2587
+ (event) => {
2588
+ const items = event.clipboardData?.items;
2589
+
2590
+ if (!items) {
2591
+ return;
2592
+ }
2593
+
2594
+ const files: File[] = [];
2595
+
2596
+ for (const item of items) {
2597
+ if (item.kind === "file") {
2598
+ const file = item.getAsFile();
2599
+ if (file) {
2600
+ files.push(file);
2601
+ }
2602
+ }
2603
+ }
2604
+
2605
+ if (files.length > 0) {
2606
+ event.preventDefault();
2607
+ attachments.add(files);
2608
+ }
2609
+ },
2610
+ [attachments],
2611
+ );
2612
+
2613
+ const handleCompositionEnd = useCallback(() => setIsComposing(false), []);
2614
+ const handleCompositionStart = useCallback(() => setIsComposing(true), []);
2615
+
2616
+ const controlledProps = controller
2617
+ ? {
2618
+ onChange: (e: ChangeEvent<HTMLTextAreaElement>) => {
2619
+ controller.textInput.setInput(e.currentTarget.value);
2620
+ onChange?.(e);
2621
+ },
2622
+ value: controller.textInput.value,
2623
+ }
2624
+ : {
2625
+ onChange,
2626
+ };
2627
+
2628
+ return (
2629
+ <InputGroupTextarea
2630
+ className={cn("field-sizing-content max-h-48 min-h-18", className)}
2631
+ name="message"
2632
+ onCompositionEnd={handleCompositionEnd}
2633
+ onCompositionStart={handleCompositionStart}
2634
+ onKeyDown={handleKeyDown}
2635
+ onPaste={handlePaste}
2636
+ placeholder={placeholder}
2637
+ {...props}
2638
+ {...controlledProps}
2639
+ />
2640
+ );
2641
+ };
2642
+
2643
+ export type PromptInputHeaderProps = Omit<ComponentProps<typeof InputGroupAddon>, "align">;
2644
+
2645
+ export const PromptInputHeader = ({ className, ...props }: PromptInputHeaderProps) => (
2646
+ <InputGroupAddon
2647
+ align="block-end"
2648
+ className={cn("order-first flex-wrap gap-1", className)}
2649
+ {...props}
2650
+ />
2651
+ );
2652
+
2653
+ export type PromptInputFooterProps = Omit<ComponentProps<typeof InputGroupAddon>, "align">;
2654
+
2655
+ export const PromptInputFooter = ({ className, ...props }: PromptInputFooterProps) => (
2656
+ <InputGroupAddon
2657
+ align="block-end"
2658
+ className={cn("justify-between gap-1", className)}
2659
+ {...props}
2660
+ />
2661
+ );
2662
+
2663
+ export type PromptInputToolsProps = HTMLAttributes<HTMLDivElement>;
2664
+
2665
+ export const PromptInputTools = ({ className, ...props }: PromptInputToolsProps) => (
2666
+ <div className={cn("flex min-w-0 items-center gap-1", className)} {...props} />
2667
+ );
2668
+
2669
+ export type PromptInputButtonTooltip =
2670
+ | string
2671
+ | {
2672
+ content: ReactNode;
2673
+ shortcut?: string;
2674
+ side?: ComponentProps<typeof TooltipContent>["side"];
2675
+ };
2676
+
2677
+ export type PromptInputButtonProps = ComponentProps<typeof InputGroupButton> & {
2678
+ tooltip?: PromptInputButtonTooltip;
2679
+ };
2680
+
2681
+ export const PromptInputButton = ({
2682
+ variant = "ghost",
2683
+ className,
2684
+ size,
2685
+ tooltip,
2686
+ ...props
2687
+ }: PromptInputButtonProps) => {
2688
+ const newSize = size ?? (Children.count(props.children) > 1 ? "sm" : "icon-sm");
2689
+
2690
+ const button = (
2691
+ <InputGroupButton
2692
+ className={cn(className)}
2693
+ size={newSize}
2694
+ type="button"
2695
+ variant={variant}
2696
+ {...props}
2697
+ />
2698
+ );
2699
+
2700
+ if (!tooltip) {
2701
+ return button;
2702
+ }
2703
+
2704
+ const tooltipContent = typeof tooltip === "string" ? tooltip : tooltip.content;
2705
+ const shortcut = typeof tooltip === "string" ? undefined : tooltip.shortcut;
2706
+ const side = typeof tooltip === "string" ? "top" : (tooltip.side ?? "top");
2707
+
2708
+ return (
2709
+ <Tooltip>
2710
+ <TooltipTrigger asChild>{button}</TooltipTrigger>
2711
+ <TooltipContent side={side}>
2712
+ {tooltipContent}
2713
+ {shortcut && <span className="ml-2 text-muted-foreground">{shortcut}</span>}
2714
+ </TooltipContent>
2715
+ </Tooltip>
2716
+ );
2717
+ };
2718
+
2719
+ export type PromptInputActionMenuProps = ComponentProps<typeof DropdownMenu>;
2720
+ export const PromptInputActionMenu = (props: PromptInputActionMenuProps) => (
2721
+ <DropdownMenu {...props} />
2722
+ );
2723
+
2724
+ export type PromptInputActionMenuTriggerProps = PromptInputButtonProps;
2725
+
2726
+ export const PromptInputActionMenuTrigger = ({
2727
+ className,
2728
+ children,
2729
+ ...props
2730
+ }: PromptInputActionMenuTriggerProps) => (
2731
+ <DropdownMenuTrigger asChild>
2732
+ <PromptInputButton className={className} {...props}>
2733
+ {children ?? <PlusIcon className="size-4" />}
2734
+ </PromptInputButton>
2735
+ </DropdownMenuTrigger>
2736
+ );
2737
+
2738
+ export type PromptInputActionMenuContentProps = ComponentProps<typeof DropdownMenuContent>;
2739
+ export const PromptInputActionMenuContent = ({
2740
+ className,
2741
+ ...props
2742
+ }: PromptInputActionMenuContentProps) => (
2743
+ <DropdownMenuContent align="start" className={cn(className)} {...props} />
2744
+ );
2745
+
2746
+ export type PromptInputActionMenuItemProps = ComponentProps<typeof DropdownMenuItem>;
2747
+ export const PromptInputActionMenuItem = ({
2748
+ className,
2749
+ ...props
2750
+ }: PromptInputActionMenuItemProps) => <DropdownMenuItem className={cn(className)} {...props} />;
2751
+
2752
+ // Note: Actions that perform side-effects (like opening a file dialog)
2753
+ // are provided in opt-in modules (e.g., prompt-input-attachments).
2754
+
2755
+ export type PromptInputSubmitProps = ComponentProps<typeof InputGroupButton> & {
2756
+ status?: ChatStatus;
2757
+ onStop?: () => void;
2758
+ };
2759
+
2760
+ export const PromptInputSubmit = ({
2761
+ className,
2762
+ variant = "default",
2763
+ size = "icon-sm",
2764
+ status,
2765
+ onStop,
2766
+ onClick,
2767
+ children,
2768
+ ...props
2769
+ }: PromptInputSubmitProps) => {
2770
+ const isGenerating = status === "submitted" || status === "streaming";
2771
+
2772
+ let Icon = <ArrowUpIcon className="size-4" />;
2773
+
2774
+ if (status === "submitted") {
2775
+ Icon = <Spinner />;
2776
+ } else if (status === "streaming") {
2777
+ Icon = <SquareIcon className="size-4" />;
2778
+ } else if (status === "error") {
2779
+ Icon = <XIcon className="size-4" />;
2780
+ }
2781
+
2782
+ const handleClick = useCallback(
2783
+ (e: React.MouseEvent<HTMLButtonElement>) => {
2784
+ if (isGenerating && onStop) {
2785
+ e.preventDefault();
2786
+ onStop();
2787
+ return;
2788
+ }
2789
+ onClick?.(e);
2790
+ },
2791
+ [isGenerating, onStop, onClick],
2792
+ );
2793
+
2794
+ return (
2795
+ <InputGroupButton
2796
+ aria-label={isGenerating ? "Stop" : "Submit"}
2797
+ className={cn("absolute right-2.5 bottom-2.5 rounded-full", className)}
2798
+ onClick={handleClick}
2799
+ size={size}
2800
+ type={isGenerating && onStop ? "button" : "submit"}
2801
+ variant={variant}
2802
+ {...props}
2803
+ >
2804
+ {children ?? Icon}
2805
+ </InputGroupButton>
2806
+ );
2807
+ };
2808
+
2809
+ export type PromptInputSelectProps = ComponentProps<typeof Select>;
2810
+
2811
+ export const PromptInputSelect = (props: PromptInputSelectProps) => <Select {...props} />;
2812
+
2813
+ export type PromptInputSelectTriggerProps = ComponentProps<typeof SelectTrigger>;
2814
+
2815
+ export const PromptInputSelectTrigger = ({
2816
+ className,
2817
+ ...props
2818
+ }: PromptInputSelectTriggerProps) => (
2819
+ <SelectTrigger
2820
+ className={cn(
2821
+ "border-none bg-transparent font-medium text-muted-foreground shadow-none transition-colors",
2822
+ "hover:bg-accent hover:text-foreground aria-expanded:bg-accent aria-expanded:text-foreground",
2823
+ className,
2824
+ )}
2825
+ {...props}
2826
+ />
2827
+ );
2828
+
2829
+ export type PromptInputSelectContentProps = ComponentProps<typeof SelectContent>;
2830
+
2831
+ export const PromptInputSelectContent = ({
2832
+ className,
2833
+ ...props
2834
+ }: PromptInputSelectContentProps) => <SelectContent className={cn(className)} {...props} />;
2835
+
2836
+ export type PromptInputSelectItemProps = ComponentProps<typeof SelectItem>;
2837
+
2838
+ export const PromptInputSelectItem = ({ className, ...props }: PromptInputSelectItemProps) => (
2839
+ <SelectItem className={cn(className)} {...props} />
2840
+ );
2841
+
2842
+ export type PromptInputSelectValueProps = ComponentProps<typeof SelectValue>;
2843
+
2844
+ export const PromptInputSelectValue = ({ className, ...props }: PromptInputSelectValueProps) => (
2845
+ <SelectValue className={cn(className)} {...props} />
2846
+ );
2847
+
2848
+ export type PromptInputHoverCardProps = ComponentProps<typeof HoverCard>;
2849
+
2850
+ export const PromptInputHoverCard = ({
2851
+ openDelay = 0,
2852
+ closeDelay = 0,
2853
+ ...props
2854
+ }: PromptInputHoverCardProps) => (
2855
+ <HoverCard closeDelay={closeDelay} openDelay={openDelay} {...props} />
2856
+ );
2857
+
2858
+ export type PromptInputHoverCardTriggerProps = ComponentProps<typeof HoverCardTrigger>;
2859
+
2860
+ export const PromptInputHoverCardTrigger = (props: PromptInputHoverCardTriggerProps) => (
2861
+ <HoverCardTrigger {...props} />
2862
+ );
2863
+
2864
+ export type PromptInputHoverCardContentProps = ComponentProps<typeof HoverCardContent>;
2865
+
2866
+ export const PromptInputHoverCardContent = ({
2867
+ align = "start",
2868
+ ...props
2869
+ }: PromptInputHoverCardContentProps) => <HoverCardContent align={align} {...props} />;
2870
+
2871
+ export type PromptInputTabsListProps = HTMLAttributes<HTMLDivElement>;
2872
+
2873
+ export const PromptInputTabsList = ({ className, ...props }: PromptInputTabsListProps) => (
2874
+ <div className={cn(className)} {...props} />
2875
+ );
2876
+
2877
+ export type PromptInputTabProps = HTMLAttributes<HTMLDivElement>;
2878
+
2879
+ export const PromptInputTab = ({ className, ...props }: PromptInputTabProps) => (
2880
+ <div className={cn(className)} {...props} />
2881
+ );
2882
+
2883
+ export type PromptInputTabLabelProps = HTMLAttributes<HTMLHeadingElement>;
2884
+
2885
+ export const PromptInputTabLabel = ({ className, ...props }: PromptInputTabLabelProps) => (
2886
+ // Content provided via children in props
2887
+ // oxlint-disable-next-line eslint-plugin-jsx-a11y(heading-has-content)
2888
+ <h3 className={cn("mb-2 px-3 font-medium text-muted-foreground text-xs", className)} {...props} />
2889
+ );
2890
+
2891
+ export type PromptInputTabBodyProps = HTMLAttributes<HTMLDivElement>;
2892
+
2893
+ export const PromptInputTabBody = ({ className, ...props }: PromptInputTabBodyProps) => (
2894
+ <div className={cn("space-y-1", className)} {...props} />
2895
+ );
2896
+
2897
+ export type PromptInputTabItemProps = HTMLAttributes<HTMLDivElement>;
2898
+
2899
+ export const PromptInputTabItem = ({ className, ...props }: PromptInputTabItemProps) => (
2900
+ <div
2901
+ className={cn("flex items-center gap-2 px-3 py-2 text-xs hover:bg-accent", className)}
2902
+ {...props}
2903
+ />
2904
+ );
2905
+
2906
+ export type PromptInputCommandProps = ComponentProps<typeof Command>;
2907
+
2908
+ export const PromptInputCommand = ({ className, ...props }: PromptInputCommandProps) => (
2909
+ <Command className={cn(className)} {...props} />
2910
+ );
2911
+
2912
+ export type PromptInputCommandInputProps = ComponentProps<typeof CommandInput>;
2913
+
2914
+ export const PromptInputCommandInput = ({ className, ...props }: PromptInputCommandInputProps) => (
2915
+ <CommandInput className={cn(className)} {...props} />
2916
+ );
2917
+
2918
+ export type PromptInputCommandListProps = ComponentProps<typeof CommandList>;
2919
+
2920
+ export const PromptInputCommandList = ({ className, ...props }: PromptInputCommandListProps) => (
2921
+ <CommandList className={cn(className)} {...props} />
2922
+ );
2923
+
2924
+ export type PromptInputCommandEmptyProps = ComponentProps<typeof CommandEmpty>;
2925
+
2926
+ export const PromptInputCommandEmpty = ({ className, ...props }: PromptInputCommandEmptyProps) => (
2927
+ <CommandEmpty className={cn(className)} {...props} />
2928
+ );
2929
+
2930
+ export type PromptInputCommandGroupProps = ComponentProps<typeof CommandGroup>;
2931
+
2932
+ export const PromptInputCommandGroup = ({ className, ...props }: PromptInputCommandGroupProps) => (
2933
+ <CommandGroup className={cn(className)} {...props} />
2934
+ );
2935
+
2936
+ export type PromptInputCommandItemProps = ComponentProps<typeof CommandItem>;
2937
+
2938
+ export const PromptInputCommandItem = ({ className, ...props }: PromptInputCommandItemProps) => (
2939
+ <CommandItem className={cn(className)} {...props} />
2940
+ );
2941
+
2942
+ export type PromptInputCommandSeparatorProps = ComponentProps<typeof CommandSeparator>;
2943
+
2944
+ export const PromptInputCommandSeparator = ({
2945
+ className,
2946
+ ...props
2947
+ }: PromptInputCommandSeparatorProps) => <CommandSeparator className={cn(className)} {...props} />;
2948
+ `,"components/ai-elements/reasoning.tsx":`"use client";
2949
+
2950
+ import { useControllableState } from "@radix-ui/react-use-controllable-state";
2951
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
2952
+ import { cn } from "@/lib/utils";
2953
+ import { cjk } from "@streamdown/cjk";
2954
+ import { code } from "@streamdown/code";
2955
+ import { math } from "@streamdown/math";
2956
+ import { mermaid } from "@streamdown/mermaid";
2957
+ import { BrainIcon, ChevronDownIcon } from "lucide-react";
2958
+ import type { ComponentProps, ReactNode } from "react";
2959
+ import {
2960
+ createContext,
2961
+ memo,
2962
+ useCallback,
2963
+ useContext,
2964
+ useEffect,
2965
+ useMemo,
2966
+ useRef,
2967
+ useState,
2968
+ } from "react";
2969
+ import { Streamdown } from "streamdown";
2970
+
2971
+ import { Shimmer } from "./shimmer";
2972
+
2973
+ interface ReasoningContextValue {
2974
+ isStreaming: boolean;
2975
+ isOpen: boolean;
2976
+ setIsOpen: (open: boolean) => void;
2977
+ duration: number | undefined;
2978
+ }
2979
+
2980
+ const ReasoningContext = createContext<ReasoningContextValue | null>(null);
2981
+
2982
+ export const useReasoning = () => {
2983
+ const context = useContext(ReasoningContext);
2984
+ if (!context) {
2985
+ throw new Error("Reasoning components must be used within Reasoning");
2986
+ }
2987
+ return context;
2988
+ };
2989
+
2990
+ export type ReasoningProps = ComponentProps<typeof Collapsible> & {
2991
+ isStreaming?: boolean;
2992
+ open?: boolean;
2993
+ defaultOpen?: boolean;
2994
+ onOpenChange?: (open: boolean) => void;
2995
+ duration?: number;
2996
+ };
2997
+
2998
+ const AUTO_CLOSE_DELAY = 1000;
2999
+ const MS_IN_S = 1000;
3000
+
3001
+ export const Reasoning = memo(
3002
+ ({
3003
+ className,
3004
+ isStreaming = false,
3005
+ open,
3006
+ defaultOpen,
3007
+ onOpenChange,
3008
+ duration: durationProp,
3009
+ children,
3010
+ ...props
3011
+ }: ReasoningProps) => {
3012
+ const resolvedDefaultOpen = defaultOpen ?? isStreaming;
3013
+ // Track if defaultOpen was explicitly set to false (to prevent auto-open)
3014
+ const isExplicitlyClosed = defaultOpen === false;
3015
+
3016
+ const [isOpen, setIsOpen] = useControllableState<boolean>({
3017
+ defaultProp: resolvedDefaultOpen,
3018
+ onChange: onOpenChange,
3019
+ prop: open,
3020
+ });
3021
+ const [duration, setDuration] = useControllableState<number | undefined>({
3022
+ defaultProp: undefined,
3023
+ prop: durationProp,
3024
+ });
3025
+
3026
+ const hasEverStreamedRef = useRef(isStreaming);
3027
+ const [hasAutoClosed, setHasAutoClosed] = useState(false);
3028
+ const startTimeRef = useRef<number | null>(null);
3029
+
3030
+ // Track when streaming starts and compute duration
3031
+ useEffect(() => {
3032
+ if (isStreaming) {
3033
+ hasEverStreamedRef.current = true;
3034
+ if (startTimeRef.current === null) {
3035
+ startTimeRef.current = Date.now();
3036
+ }
3037
+ } else if (startTimeRef.current !== null) {
3038
+ setDuration(Math.ceil((Date.now() - startTimeRef.current) / MS_IN_S));
3039
+ startTimeRef.current = null;
3040
+ }
3041
+ }, [isStreaming, setDuration]);
3042
+
3043
+ // Auto-open when streaming starts (unless explicitly closed)
3044
+ useEffect(() => {
3045
+ if (isStreaming && !isOpen && !isExplicitlyClosed) {
3046
+ setIsOpen(true);
3047
+ }
3048
+ }, [isStreaming, isOpen, setIsOpen, isExplicitlyClosed]);
3049
+
3050
+ // Auto-close when streaming ends (once only, and only if it ever streamed)
3051
+ useEffect(() => {
3052
+ if (hasEverStreamedRef.current && !isStreaming && isOpen && !hasAutoClosed) {
3053
+ const timer = setTimeout(() => {
3054
+ setIsOpen(false);
3055
+ setHasAutoClosed(true);
3056
+ }, AUTO_CLOSE_DELAY);
3057
+
3058
+ return () => clearTimeout(timer);
3059
+ }
3060
+ }, [isStreaming, isOpen, setIsOpen, hasAutoClosed]);
3061
+
3062
+ const handleOpenChange = useCallback(
3063
+ (newOpen: boolean) => {
3064
+ setIsOpen(newOpen);
3065
+ },
3066
+ [setIsOpen],
3067
+ );
3068
+
3069
+ const contextValue = useMemo(
3070
+ () => ({ duration, isOpen, isStreaming, setIsOpen }),
3071
+ [duration, isOpen, isStreaming, setIsOpen],
3072
+ );
3073
+
3074
+ return (
3075
+ <ReasoningContext.Provider value={contextValue}>
3076
+ <Collapsible
3077
+ className={cn("not-prose mb-4 w-full", className)}
3078
+ onOpenChange={handleOpenChange}
3079
+ open={isOpen}
3080
+ {...props}
3081
+ >
3082
+ {children}
3083
+ </Collapsible>
3084
+ </ReasoningContext.Provider>
3085
+ );
3086
+ },
3087
+ );
3088
+
3089
+ export type ReasoningTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
3090
+ getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;
3091
+ };
3092
+
3093
+ const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
3094
+ if (isStreaming || duration === 0) {
3095
+ return <Shimmer duration={1}>Thinking...</Shimmer>;
3096
+ }
3097
+ if (duration === undefined) {
3098
+ return <p>Thought for a few seconds</p>;
3099
+ }
3100
+ return <p>Thought for {duration} seconds</p>;
3101
+ };
3102
+
3103
+ export const ReasoningTrigger = memo(
3104
+ ({
3105
+ className,
3106
+ children,
3107
+ getThinkingMessage = defaultGetThinkingMessage,
3108
+ ...props
3109
+ }: ReasoningTriggerProps) => {
3110
+ const { isStreaming, isOpen, duration } = useReasoning();
3111
+
3112
+ return (
3113
+ <CollapsibleTrigger
3114
+ className={cn(
3115
+ "flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground",
3116
+ className,
3117
+ )}
3118
+ {...props}
3119
+ >
3120
+ {children ?? (
3121
+ <>
3122
+ <BrainIcon className="size-4" />
3123
+ {getThinkingMessage(isStreaming, duration)}
3124
+ <ChevronDownIcon
3125
+ className={cn("size-4 transition-transform", isOpen ? "rotate-180" : "rotate-0")}
3126
+ />
3127
+ </>
3128
+ )}
3129
+ </CollapsibleTrigger>
3130
+ );
3131
+ },
3132
+ );
3133
+
3134
+ export type ReasoningContentProps = ComponentProps<typeof CollapsibleContent> & {
3135
+ children: string;
3136
+ };
3137
+
3138
+ const streamdownPlugins = { cjk, code, math, mermaid };
3139
+
3140
+ export const ReasoningContent = memo(({ className, children, ...props }: ReasoningContentProps) => (
3141
+ <CollapsibleContent
3142
+ className={cn(
3143
+ "mt-4 text-sm",
3144
+ "data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-muted-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
3145
+ className,
3146
+ )}
3147
+ {...props}
3148
+ >
3149
+ <Streamdown plugins={streamdownPlugins}>{children}</Streamdown>
3150
+ </CollapsibleContent>
3151
+ ));
3152
+
3153
+ Reasoning.displayName = "Reasoning";
3154
+ ReasoningTrigger.displayName = "ReasoningTrigger";
3155
+ ReasoningContent.displayName = "ReasoningContent";
3156
+ `,"components/ai-elements/shimmer.tsx":`"use client";
3157
+
3158
+ import { cn } from "@/lib/utils";
3159
+ import type { MotionProps } from "motion/react";
3160
+ import { motion } from "motion/react";
3161
+ import type { CSSProperties, ElementType, JSX } from "react";
3162
+ import { memo, useMemo } from "react";
3163
+
3164
+ type MotionHTMLProps = MotionProps & Record<string, unknown>;
3165
+
3166
+ // Cache motion components at module level to avoid creating during render
3167
+ const motionComponentCache = new Map<
3168
+ keyof JSX.IntrinsicElements,
3169
+ React.ComponentType<MotionHTMLProps>
3170
+ >();
3171
+
3172
+ const getMotionComponent = (element: keyof JSX.IntrinsicElements) => {
3173
+ let component = motionComponentCache.get(element);
3174
+ if (!component) {
3175
+ component = motion.create(element);
3176
+ motionComponentCache.set(element, component);
3177
+ }
3178
+ return component;
3179
+ };
3180
+
3181
+ export interface TextShimmerProps {
3182
+ children: string;
3183
+ as?: ElementType;
3184
+ className?: string;
3185
+ duration?: number;
3186
+ spread?: number;
3187
+ }
3188
+
3189
+ const ShimmerComponent = ({
3190
+ children,
3191
+ as: Component = "p",
3192
+ className,
3193
+ duration = 2,
3194
+ spread = 2,
3195
+ }: TextShimmerProps) => {
3196
+ const MotionComponent = getMotionComponent(Component as keyof JSX.IntrinsicElements);
3197
+
3198
+ const dynamicSpread = useMemo(() => (children?.length ?? 0) * spread, [children, spread]);
3199
+
3200
+ return (
3201
+ <MotionComponent
3202
+ animate={{ backgroundPosition: "0% center" }}
3203
+ className={cn(
3204
+ "relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent",
3205
+ "[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--color-background),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]",
3206
+ className,
3207
+ )}
3208
+ initial={{ backgroundPosition: "100% center" }}
3209
+ style={
3210
+ {
3211
+ "--spread": \`\${dynamicSpread}px\`,
3212
+ backgroundImage:
3213
+ "var(--bg), linear-gradient(var(--color-muted-foreground), var(--color-muted-foreground))",
3214
+ } as CSSProperties
3215
+ }
3216
+ transition={{
3217
+ duration,
3218
+ ease: "linear",
3219
+ repeat: Number.POSITIVE_INFINITY,
3220
+ }}
3221
+ >
3222
+ {children}
3223
+ </MotionComponent>
3224
+ );
3225
+ };
3226
+
3227
+ export const Shimmer = memo(ShimmerComponent);
3228
+ `,"components/ai-elements/tool.tsx":`"use client";
3229
+
3230
+ import { Badge } from "@/components/ui/badge";
3231
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
3232
+ import { cn } from "@/lib/utils";
3233
+ import type { DynamicToolUIPart, ToolUIPart } from "ai";
3234
+ import {
3235
+ CheckCircleIcon,
3236
+ ChevronDownIcon,
3237
+ CircleIcon,
3238
+ ClockIcon,
3239
+ WrenchIcon,
3240
+ XCircleIcon,
3241
+ } from "lucide-react";
3242
+ import type { ComponentProps, ReactNode } from "react";
3243
+ import { isValidElement } from "react";
3244
+
3245
+ import { CodeBlock } from "./code-block";
3246
+
3247
+ export type ToolProps = ComponentProps<typeof Collapsible>;
3248
+
3249
+ export const Tool = ({ className, ...props }: ToolProps) => (
3250
+ <Collapsible
3251
+ className={cn("group not-prose mb-4 w-full rounded-md border", className)}
3252
+ {...props}
3253
+ />
3254
+ );
3255
+
3256
+ export type ToolPart = ToolUIPart | DynamicToolUIPart;
3257
+
3258
+ export type ToolHeaderProps = {
3259
+ title?: string;
3260
+ className?: string;
3261
+ } & (
3262
+ | { type: ToolUIPart["type"]; state: ToolUIPart["state"]; toolName?: never }
3263
+ | {
3264
+ type: DynamicToolUIPart["type"];
3265
+ state: DynamicToolUIPart["state"];
3266
+ toolName: string;
3267
+ }
3268
+ );
3269
+
3270
+ const statusLabels: Record<ToolPart["state"], string> = {
3271
+ "approval-requested": "Awaiting Approval",
3272
+ "approval-responded": "Responded",
3273
+ "input-available": "Running",
3274
+ "input-streaming": "Pending",
3275
+ "output-available": "Completed",
3276
+ "output-denied": "Denied",
3277
+ "output-error": "Error",
3278
+ };
3279
+
3280
+ const statusIcons: Record<ToolPart["state"], ReactNode> = {
3281
+ "approval-requested": <ClockIcon className="size-4 text-yellow-600" />,
3282
+ "approval-responded": <CheckCircleIcon className="size-4 text-blue-600" />,
3283
+ "input-available": <ClockIcon className="size-4 animate-pulse" />,
3284
+ "input-streaming": <CircleIcon className="size-4" />,
3285
+ "output-available": <CheckCircleIcon className="size-4 text-green-600" />,
3286
+ "output-denied": <XCircleIcon className="size-4 text-orange-600" />,
3287
+ "output-error": <XCircleIcon className="size-4 text-red-600" />,
3288
+ };
3289
+
3290
+ export const getStatusBadge = (status: ToolPart["state"]) => (
3291
+ <Badge className="gap-1.5 rounded-full text-xs" variant="secondary">
3292
+ {statusIcons[status]}
3293
+ {statusLabels[status]}
3294
+ </Badge>
3295
+ );
3296
+
3297
+ export const ToolHeader = ({
3298
+ className,
3299
+ title,
3300
+ type,
3301
+ state,
3302
+ toolName,
3303
+ ...props
3304
+ }: ToolHeaderProps) => {
3305
+ const derivedName = type === "dynamic-tool" ? toolName : type.split("-").slice(1).join("-");
3306
+
3307
+ return (
3308
+ <CollapsibleTrigger
3309
+ className={cn("flex w-full items-center justify-between gap-4 p-3", className)}
3310
+ {...props}
3311
+ >
3312
+ <div className="flex items-center gap-2">
3313
+ <WrenchIcon className="size-4 text-muted-foreground" />
3314
+ <span className="font-medium text-sm">{title ?? derivedName}</span>
3315
+ {getStatusBadge(state)}
3316
+ </div>
3317
+ <ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
3318
+ </CollapsibleTrigger>
3319
+ );
3320
+ };
3321
+
3322
+ export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;
3323
+
3324
+ export const ToolContent = ({ className, ...props }: ToolContentProps) => (
3325
+ <CollapsibleContent
3326
+ className={cn(
3327
+ "data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 space-y-4 p-4 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
3328
+ className,
3329
+ )}
3330
+ {...props}
3331
+ />
3332
+ );
3333
+
3334
+ export type ToolInputProps = ComponentProps<"div"> & {
3335
+ input: ToolPart["input"];
3336
+ };
3337
+
3338
+ export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
3339
+ <div className={cn("space-y-2 overflow-hidden", className)} {...props}>
3340
+ <h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
3341
+ Parameters
3342
+ </h4>
3343
+ <div className="rounded-md bg-muted/50">
3344
+ <CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
3345
+ </div>
3346
+ </div>
3347
+ );
3348
+
3349
+ export type ToolOutputProps = ComponentProps<"div"> & {
3350
+ output: ToolPart["output"];
3351
+ errorText: ToolPart["errorText"];
3352
+ };
3353
+
3354
+ export const ToolOutput = ({ className, output, errorText, ...props }: ToolOutputProps) => {
3355
+ if (!(output || errorText)) {
3356
+ return null;
3357
+ }
3358
+
3359
+ let Output = <div>{output as ReactNode}</div>;
3360
+
3361
+ if (typeof output === "object" && !isValidElement(output)) {
3362
+ Output = <CodeBlock code={JSON.stringify(output, null, 2)} language="json" />;
3363
+ } else if (typeof output === "string") {
3364
+ Output = <CodeBlock code={output} language="json" />;
3365
+ }
3366
+
3367
+ return (
3368
+ <div className={cn("space-y-2", className)} {...props}>
3369
+ <h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
3370
+ {errorText ? "Error" : "Result"}
3371
+ </h4>
3372
+ <div
3373
+ className={cn(
3374
+ "overflow-x-auto rounded-md text-xs [&_table]:w-full",
3375
+ errorText ? "bg-destructive/10 text-destructive" : "bg-muted/50 text-foreground",
3376
+ )}
3377
+ >
3378
+ {errorText && <div>{errorText}</div>}
3379
+ {Output}
3380
+ </div>
3381
+ </div>
3382
+ );
3383
+ };
3384
+ `,"components/ui/badge.tsx":`import * as React from "react";
3385
+ import { cva, type VariantProps } from "class-variance-authority";
3386
+ import { Slot } from "radix-ui";
3387
+
3388
+ import { cn } from "@/lib/utils";
3389
+
3390
+ const badgeVariants = cva(
3391
+ "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
3392
+ {
3393
+ variants: {
3394
+ variant: {
3395
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
3396
+ secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
3397
+ destructive:
3398
+ "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
3399
+ outline:
3400
+ "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
3401
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
3402
+ link: "text-primary underline-offset-4 [a&]:hover:underline",
3403
+ },
3404
+ },
3405
+ defaultVariants: {
3406
+ variant: "default",
3407
+ },
3408
+ },
3409
+ );
3410
+
3411
+ function Badge({
3412
+ className,
3413
+ variant = "default",
3414
+ asChild = false,
3415
+ ...props
3416
+ }: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
3417
+ const Comp = asChild ? Slot.Root : "span";
3418
+
3419
+ return (
3420
+ <Comp
3421
+ data-slot="badge"
3422
+ data-variant={variant}
3423
+ className={cn(badgeVariants({ variant }), className)}
3424
+ {...props}
3425
+ />
3426
+ );
3427
+ }
3428
+
3429
+ export { Badge, badgeVariants };
3430
+ `,"components/ui/button-group.tsx":`import { cva, type VariantProps } from "class-variance-authority";
3431
+ import { Slot } from "radix-ui";
3432
+
3433
+ import { cn } from "@/lib/utils";
3434
+ import { Separator } from "@/components/ui/separator";
3435
+
3436
+ const buttonGroupVariants = cva(
3437
+ "flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
3438
+ {
3439
+ variants: {
3440
+ orientation: {
3441
+ horizontal:
3442
+ "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
3443
+ vertical:
3444
+ "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
3445
+ },
3446
+ },
3447
+ defaultVariants: {
3448
+ orientation: "horizontal",
3449
+ },
3450
+ },
3451
+ );
3452
+
3453
+ function ButtonGroup({
3454
+ className,
3455
+ orientation,
3456
+ ...props
3457
+ }: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
3458
+ return (
3459
+ <div
3460
+ role="group"
3461
+ data-slot="button-group"
3462
+ data-orientation={orientation}
3463
+ className={cn(buttonGroupVariants({ orientation }), className)}
3464
+ {...props}
3465
+ />
3466
+ );
3467
+ }
3468
+
3469
+ function ButtonGroupText({
3470
+ className,
3471
+ asChild = false,
3472
+ ...props
3473
+ }: React.ComponentProps<"div"> & {
3474
+ asChild?: boolean;
3475
+ }) {
3476
+ const Comp = asChild ? Slot.Root : "div";
3477
+
3478
+ return (
3479
+ <Comp
3480
+ className={cn(
3481
+ "flex items-center gap-2 rounded-md border bg-muted px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
3482
+ className,
3483
+ )}
3484
+ {...props}
3485
+ />
3486
+ );
3487
+ }
3488
+
3489
+ function ButtonGroupSeparator({
3490
+ className,
3491
+ orientation = "vertical",
3492
+ ...props
3493
+ }: React.ComponentProps<typeof Separator>) {
3494
+ return (
3495
+ <Separator
3496
+ data-slot="button-group-separator"
3497
+ orientation={orientation}
3498
+ className={cn(
3499
+ "relative m-0! self-stretch bg-input data-[orientation=vertical]:h-auto",
3500
+ className,
3501
+ )}
3502
+ {...props}
3503
+ />
3504
+ );
3505
+ }
3506
+
3507
+ export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, buttonGroupVariants };
3508
+ `,"components/ui/button.tsx":`import * as React from "react";
3509
+ import { cva, type VariantProps } from "class-variance-authority";
3510
+ import { Slot } from "radix-ui";
3511
+
3512
+ import { cn } from "@/lib/utils";
3513
+
3514
+ const buttonVariants = cva(
3515
+ "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
3516
+ {
3517
+ variants: {
3518
+ variant: {
3519
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
3520
+ destructive:
3521
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
3522
+ outline:
3523
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
3524
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
3525
+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
3526
+ link: "text-primary underline-offset-4 hover:underline",
3527
+ },
3528
+ size: {
3529
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
3530
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
3531
+ sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
3532
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
3533
+ icon: "size-9",
3534
+ "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
3535
+ "icon-sm": "size-8",
3536
+ "icon-lg": "size-10",
3537
+ },
3538
+ },
3539
+ defaultVariants: {
3540
+ variant: "default",
3541
+ size: "default",
3542
+ },
3543
+ },
3544
+ );
3545
+
3546
+ function Button({
3547
+ className,
3548
+ variant = "default",
3549
+ size = "default",
3550
+ asChild = false,
3551
+ ...props
3552
+ }: React.ComponentProps<"button"> &
3553
+ VariantProps<typeof buttonVariants> & {
3554
+ asChild?: boolean;
3555
+ }) {
3556
+ const Comp = asChild ? Slot.Root : "button";
3557
+
3558
+ return (
3559
+ <Comp
3560
+ data-slot="button"
3561
+ data-variant={variant}
3562
+ data-size={size}
3563
+ className={cn(buttonVariants({ variant, size, className }))}
3564
+ {...props}
3565
+ />
3566
+ );
3567
+ }
3568
+
3569
+ export { Button, buttonVariants };
3570
+ `,"components/ui/collapsible.tsx":`"use client";
3571
+
3572
+ import { Collapsible as CollapsiblePrimitive } from "radix-ui";
3573
+
3574
+ function Collapsible({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
3575
+ return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
3576
+ }
3577
+
3578
+ function CollapsibleTrigger({
3579
+ ...props
3580
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
3581
+ return <CollapsiblePrimitive.CollapsibleTrigger data-slot="collapsible-trigger" {...props} />;
3582
+ }
3583
+
3584
+ function CollapsibleContent({
3585
+ ...props
3586
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
3587
+ return <CollapsiblePrimitive.CollapsibleContent data-slot="collapsible-content" {...props} />;
3588
+ }
3589
+
3590
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent };
3591
+ `,"components/ui/command.tsx":`"use client";
3592
+
3593
+ import * as React from "react";
3594
+ import { Command as CommandPrimitive } from "cmdk";
3595
+ import { SearchIcon } from "lucide-react";
3596
+
3597
+ import { cn } from "@/lib/utils";
3598
+ import {
3599
+ Dialog,
3600
+ DialogContent,
3601
+ DialogDescription,
3602
+ DialogHeader,
3603
+ DialogTitle,
3604
+ } from "@/components/ui/dialog";
3605
+
3606
+ function Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {
3607
+ return (
3608
+ <CommandPrimitive
3609
+ data-slot="command"
3610
+ className={cn(
3611
+ "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
3612
+ className,
3613
+ )}
3614
+ {...props}
3615
+ />
3616
+ );
3617
+ }
3618
+
3619
+ function CommandDialog({
3620
+ title = "Command Palette",
3621
+ description = "Search for a command to run...",
3622
+ children,
3623
+ className,
3624
+ showCloseButton = true,
3625
+ ...props
3626
+ }: React.ComponentProps<typeof Dialog> & {
3627
+ title?: string;
3628
+ description?: string;
3629
+ className?: string;
3630
+ showCloseButton?: boolean;
3631
+ }) {
3632
+ return (
3633
+ <Dialog {...props}>
3634
+ <DialogHeader className="sr-only">
3635
+ <DialogTitle>{title}</DialogTitle>
3636
+ <DialogDescription>{description}</DialogDescription>
3637
+ </DialogHeader>
3638
+ <DialogContent
3639
+ className={cn("overflow-hidden p-0", className)}
3640
+ showCloseButton={showCloseButton}
3641
+ >
3642
+ <Command className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
3643
+ {children}
3644
+ </Command>
3645
+ </DialogContent>
3646
+ </Dialog>
3647
+ );
3648
+ }
3649
+
3650
+ function CommandInput({
3651
+ className,
3652
+ ...props
3653
+ }: React.ComponentProps<typeof CommandPrimitive.Input>) {
3654
+ return (
3655
+ <div data-slot="command-input-wrapper" className="flex h-9 items-center gap-2 border-b px-3">
3656
+ <SearchIcon className="size-4 shrink-0 opacity-50" />
3657
+ <CommandPrimitive.Input
3658
+ data-slot="command-input"
3659
+ className={cn(
3660
+ "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
3661
+ className,
3662
+ )}
3663
+ {...props}
3664
+ />
3665
+ </div>
3666
+ );
3667
+ }
3668
+
3669
+ function CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) {
3670
+ return (
3671
+ <CommandPrimitive.List
3672
+ data-slot="command-list"
3673
+ className={cn("max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", className)}
3674
+ {...props}
3675
+ />
3676
+ );
3677
+ }
3678
+
3679
+ function CommandEmpty({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
3680
+ return (
3681
+ <CommandPrimitive.Empty
3682
+ data-slot="command-empty"
3683
+ className="py-6 text-center text-sm"
3684
+ {...props}
3685
+ />
3686
+ );
3687
+ }
3688
+
3689
+ function CommandGroup({
3690
+ className,
3691
+ ...props
3692
+ }: React.ComponentProps<typeof CommandPrimitive.Group>) {
3693
+ return (
3694
+ <CommandPrimitive.Group
3695
+ data-slot="command-group"
3696
+ className={cn(
3697
+ "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
3698
+ className,
3699
+ )}
3700
+ {...props}
3701
+ />
3702
+ );
3703
+ }
3704
+
3705
+ function CommandSeparator({
3706
+ className,
3707
+ ...props
3708
+ }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
3709
+ return (
3710
+ <CommandPrimitive.Separator
3711
+ data-slot="command-separator"
3712
+ className={cn("-mx-1 h-px bg-border", className)}
3713
+ {...props}
3714
+ />
3715
+ );
3716
+ }
3717
+
3718
+ function CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {
3719
+ return (
3720
+ <CommandPrimitive.Item
3721
+ data-slot="command-item"
3722
+ className={cn(
3723
+ "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
3724
+ className,
3725
+ )}
3726
+ {...props}
3727
+ />
3728
+ );
3729
+ }
3730
+
3731
+ function CommandShortcut({ className, ...props }: React.ComponentProps<"span">) {
3732
+ return (
3733
+ <span
3734
+ data-slot="command-shortcut"
3735
+ className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
3736
+ {...props}
3737
+ />
3738
+ );
3739
+ }
3740
+
3741
+ export {
3742
+ Command,
3743
+ CommandDialog,
3744
+ CommandInput,
3745
+ CommandList,
3746
+ CommandEmpty,
3747
+ CommandGroup,
3748
+ CommandItem,
3749
+ CommandShortcut,
3750
+ CommandSeparator,
3751
+ };
3752
+ `,"components/ui/dialog.tsx":`"use client";
3753
+
3754
+ import * as React from "react";
3755
+ import { XIcon } from "lucide-react";
3756
+ import { Dialog as DialogPrimitive } from "radix-ui";
3757
+
3758
+ import { cn } from "@/lib/utils";
3759
+ import { Button } from "@/components/ui/button";
3760
+
3761
+ function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
3762
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />;
3763
+ }
3764
+
3765
+ function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
3766
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
3767
+ }
3768
+
3769
+ function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
3770
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
3771
+ }
3772
+
3773
+ function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
3774
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
3775
+ }
3776
+
3777
+ function DialogOverlay({
3778
+ className,
3779
+ ...props
3780
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
3781
+ return (
3782
+ <DialogPrimitive.Overlay
3783
+ data-slot="dialog-overlay"
3784
+ className={cn(
3785
+ "fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
3786
+ className,
3787
+ )}
3788
+ {...props}
3789
+ />
3790
+ );
3791
+ }
3792
+
3793
+ function DialogContent({
3794
+ className,
3795
+ children,
3796
+ showCloseButton = true,
3797
+ ...props
3798
+ }: React.ComponentProps<typeof DialogPrimitive.Content> & {
3799
+ showCloseButton?: boolean;
3800
+ }) {
3801
+ return (
3802
+ <DialogPortal data-slot="dialog-portal">
3803
+ <DialogOverlay />
3804
+ <DialogPrimitive.Content
3805
+ data-slot="dialog-content"
3806
+ className={cn(
3807
+ "fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
3808
+ className,
3809
+ )}
3810
+ {...props}
3811
+ >
3812
+ {children}
3813
+ {showCloseButton && (
3814
+ <DialogPrimitive.Close
3815
+ data-slot="dialog-close"
3816
+ className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
3817
+ >
3818
+ <XIcon />
3819
+ <span className="sr-only">Close</span>
3820
+ </DialogPrimitive.Close>
3821
+ )}
3822
+ </DialogPrimitive.Content>
3823
+ </DialogPortal>
3824
+ );
3825
+ }
3826
+
3827
+ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
3828
+ return (
3829
+ <div
3830
+ data-slot="dialog-header"
3831
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
3832
+ {...props}
3833
+ />
3834
+ );
3835
+ }
3836
+
3837
+ function DialogFooter({
3838
+ className,
3839
+ showCloseButton = false,
3840
+ children,
3841
+ ...props
3842
+ }: React.ComponentProps<"div"> & {
3843
+ showCloseButton?: boolean;
3844
+ }) {
3845
+ return (
3846
+ <div
3847
+ data-slot="dialog-footer"
3848
+ className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
3849
+ {...props}
3850
+ >
3851
+ {children}
3852
+ {showCloseButton && (
3853
+ <DialogPrimitive.Close asChild>
3854
+ <Button variant="outline">Close</Button>
3855
+ </DialogPrimitive.Close>
3856
+ )}
3857
+ </div>
3858
+ );
3859
+ }
3860
+
3861
+ function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
3862
+ return (
3863
+ <DialogPrimitive.Title
3864
+ data-slot="dialog-title"
3865
+ className={cn("text-lg leading-none font-semibold", className)}
3866
+ {...props}
3867
+ />
3868
+ );
3869
+ }
3870
+
3871
+ function DialogDescription({
3872
+ className,
3873
+ ...props
3874
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
3875
+ return (
3876
+ <DialogPrimitive.Description
3877
+ data-slot="dialog-description"
3878
+ className={cn("text-sm text-muted-foreground", className)}
3879
+ {...props}
3880
+ />
3881
+ );
3882
+ }
3883
+
3884
+ export {
3885
+ Dialog,
3886
+ DialogClose,
3887
+ DialogContent,
3888
+ DialogDescription,
3889
+ DialogFooter,
3890
+ DialogHeader,
3891
+ DialogOverlay,
3892
+ DialogPortal,
3893
+ DialogTitle,
3894
+ DialogTrigger,
3895
+ };
3896
+ `,"components/ui/dropdown-menu.tsx":`"use client";
3897
+
3898
+ import * as React from "react";
3899
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
3900
+ import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
3901
+
3902
+ import { cn } from "@/lib/utils";
3903
+
3904
+ function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
3905
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
3906
+ }
3907
+
3908
+ function DropdownMenuPortal({
3909
+ ...props
3910
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
3911
+ return <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />;
3912
+ }
3913
+
3914
+ function DropdownMenuTrigger({
3915
+ ...props
3916
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
3917
+ return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
3918
+ }
3919
+
3920
+ function DropdownMenuContent({
3921
+ className,
3922
+ sideOffset = 4,
3923
+ ...props
3924
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
3925
+ return (
3926
+ <DropdownMenuPrimitive.Portal>
3927
+ <DropdownMenuPrimitive.Content
3928
+ data-slot="dropdown-menu-content"
3929
+ sideOffset={sideOffset}
3930
+ className={cn(
3931
+ "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
3932
+ className,
3933
+ )}
3934
+ {...props}
3935
+ />
3936
+ </DropdownMenuPrimitive.Portal>
3937
+ );
3938
+ }
3939
+
3940
+ function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
3941
+ return <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />;
3942
+ }
3943
+
3944
+ function DropdownMenuItem({
3945
+ className,
3946
+ inset,
3947
+ variant = "default",
3948
+ ...props
3949
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
3950
+ inset?: boolean;
3951
+ variant?: "default" | "destructive";
3952
+ }) {
3953
+ return (
3954
+ <DropdownMenuPrimitive.Item
3955
+ data-slot="dropdown-menu-item"
3956
+ data-inset={inset}
3957
+ data-variant={variant}
3958
+ className={cn(
3959
+ "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
3960
+ className,
3961
+ )}
3962
+ {...props}
3963
+ />
3964
+ );
3965
+ }
3966
+
3967
+ function DropdownMenuCheckboxItem({
3968
+ className,
3969
+ children,
3970
+ checked,
3971
+ ...props
3972
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
3973
+ return (
3974
+ <DropdownMenuPrimitive.CheckboxItem
3975
+ data-slot="dropdown-menu-checkbox-item"
3976
+ className={cn(
3977
+ "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
3978
+ className,
3979
+ )}
3980
+ checked={checked}
3981
+ {...props}
3982
+ >
3983
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
3984
+ <DropdownMenuPrimitive.ItemIndicator>
3985
+ <CheckIcon className="size-4" />
3986
+ </DropdownMenuPrimitive.ItemIndicator>
3987
+ </span>
3988
+ {children}
3989
+ </DropdownMenuPrimitive.CheckboxItem>
3990
+ );
3991
+ }
3992
+
3993
+ function DropdownMenuRadioGroup({
3994
+ ...props
3995
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
3996
+ return <DropdownMenuPrimitive.RadioGroup data-slot="dropdown-menu-radio-group" {...props} />;
3997
+ }
3998
+
3999
+ function DropdownMenuRadioItem({
4000
+ className,
4001
+ children,
4002
+ ...props
4003
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
4004
+ return (
4005
+ <DropdownMenuPrimitive.RadioItem
4006
+ data-slot="dropdown-menu-radio-item"
4007
+ className={cn(
4008
+ "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
4009
+ className,
4010
+ )}
4011
+ {...props}
4012
+ >
4013
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
4014
+ <DropdownMenuPrimitive.ItemIndicator>
4015
+ <CircleIcon className="size-2 fill-current" />
4016
+ </DropdownMenuPrimitive.ItemIndicator>
4017
+ </span>
4018
+ {children}
4019
+ </DropdownMenuPrimitive.RadioItem>
4020
+ );
4021
+ }
4022
+
4023
+ function DropdownMenuLabel({
4024
+ className,
4025
+ inset,
4026
+ ...props
4027
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
4028
+ inset?: boolean;
4029
+ }) {
4030
+ return (
4031
+ <DropdownMenuPrimitive.Label
4032
+ data-slot="dropdown-menu-label"
4033
+ data-inset={inset}
4034
+ className={cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
4035
+ {...props}
4036
+ />
4037
+ );
4038
+ }
4039
+
4040
+ function DropdownMenuSeparator({
4041
+ className,
4042
+ ...props
4043
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
4044
+ return (
4045
+ <DropdownMenuPrimitive.Separator
4046
+ data-slot="dropdown-menu-separator"
4047
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
4048
+ {...props}
4049
+ />
4050
+ );
4051
+ }
4052
+
4053
+ function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
4054
+ return (
4055
+ <span
4056
+ data-slot="dropdown-menu-shortcut"
4057
+ className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
4058
+ {...props}
4059
+ />
4060
+ );
4061
+ }
4062
+
4063
+ function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
4064
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
4065
+ }
4066
+
4067
+ function DropdownMenuSubTrigger({
4068
+ className,
4069
+ inset,
4070
+ children,
4071
+ ...props
4072
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
4073
+ inset?: boolean;
4074
+ }) {
4075
+ return (
4076
+ <DropdownMenuPrimitive.SubTrigger
4077
+ data-slot="dropdown-menu-sub-trigger"
4078
+ data-inset={inset}
4079
+ className={cn(
4080
+ "flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
4081
+ className,
4082
+ )}
4083
+ {...props}
4084
+ >
4085
+ {children}
4086
+ <ChevronRightIcon className="ml-auto size-4" />
4087
+ </DropdownMenuPrimitive.SubTrigger>
4088
+ );
4089
+ }
4090
+
4091
+ function DropdownMenuSubContent({
4092
+ className,
4093
+ ...props
4094
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
4095
+ return (
4096
+ <DropdownMenuPrimitive.SubContent
4097
+ data-slot="dropdown-menu-sub-content"
4098
+ className={cn(
4099
+ "z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
4100
+ className,
4101
+ )}
4102
+ {...props}
4103
+ />
4104
+ );
4105
+ }
4106
+
4107
+ export {
4108
+ DropdownMenu,
4109
+ DropdownMenuPortal,
4110
+ DropdownMenuTrigger,
4111
+ DropdownMenuContent,
4112
+ DropdownMenuGroup,
4113
+ DropdownMenuLabel,
4114
+ DropdownMenuItem,
4115
+ DropdownMenuCheckboxItem,
4116
+ DropdownMenuRadioGroup,
4117
+ DropdownMenuRadioItem,
4118
+ DropdownMenuSeparator,
4119
+ DropdownMenuShortcut,
4120
+ DropdownMenuSub,
4121
+ DropdownMenuSubTrigger,
4122
+ DropdownMenuSubContent,
4123
+ };
4124
+ `,"components/ui/hover-card.tsx":`"use client";
4125
+
4126
+ import * as React from "react";
4127
+ import { HoverCard as HoverCardPrimitive } from "radix-ui";
4128
+
4129
+ import { cn } from "@/lib/utils";
4130
+
4131
+ function HoverCard({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
4132
+ return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
4133
+ }
4134
+
4135
+ function HoverCardTrigger({ ...props }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
4136
+ return <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />;
4137
+ }
4138
+
4139
+ function HoverCardContent({
4140
+ className,
4141
+ align = "center",
4142
+ sideOffset = 4,
4143
+ ...props
4144
+ }: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
4145
+ return (
4146
+ <HoverCardPrimitive.Portal data-slot="hover-card-portal">
4147
+ <HoverCardPrimitive.Content
4148
+ data-slot="hover-card-content"
4149
+ align={align}
4150
+ sideOffset={sideOffset}
4151
+ className={cn(
4152
+ "z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
4153
+ className,
4154
+ )}
4155
+ {...props}
4156
+ />
4157
+ </HoverCardPrimitive.Portal>
4158
+ );
4159
+ }
4160
+
4161
+ export { HoverCard, HoverCardTrigger, HoverCardContent };
4162
+ `,"components/ui/input-group.tsx":`"use client";
4163
+
4164
+ import * as React from "react";
4165
+ import { cva, type VariantProps } from "class-variance-authority";
4166
+
4167
+ import { cn } from "@/lib/utils";
4168
+ import { Button } from "@/components/ui/button";
4169
+ import { Input } from "@/components/ui/input";
4170
+ import { Textarea } from "@/components/ui/textarea";
4171
+
4172
+ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
4173
+ return (
4174
+ <div
4175
+ data-slot="input-group"
4176
+ role="group"
4177
+ className={cn(
4178
+ "group/input-group relative flex w-full items-center rounded-md border border-input shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30",
4179
+ "h-9 min-w-0 has-[>textarea]:h-auto",
4180
+
4181
+ // Variants based on alignment.
4182
+ "has-[>[data-align=inline-start]]:[&>input]:pl-2",
4183
+ "has-[>[data-align=inline-end]]:[&>input]:pr-2",
4184
+ "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
4185
+ "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
4186
+
4187
+ // Focus state.
4188
+ "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50",
4189
+
4190
+ // Error state.
4191
+ "has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
4192
+
4193
+ className,
4194
+ )}
4195
+ {...props}
4196
+ />
4197
+ );
4198
+ }
4199
+
4200
+ const inputGroupAddonVariants = cva(
4201
+ "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
4202
+ {
4203
+ variants: {
4204
+ align: {
4205
+ "inline-start": "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
4206
+ "inline-end": "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
4207
+ "block-start":
4208
+ "order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3",
4209
+ "block-end":
4210
+ "order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3",
4211
+ },
4212
+ },
4213
+ defaultVariants: {
4214
+ align: "inline-start",
4215
+ },
4216
+ },
4217
+ );
4218
+
4219
+ function InputGroupAddon({
4220
+ className,
4221
+ align = "inline-start",
4222
+ ...props
4223
+ }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
4224
+ return (
4225
+ <div
4226
+ role="group"
4227
+ data-slot="input-group-addon"
4228
+ data-align={align}
4229
+ className={cn(inputGroupAddonVariants({ align }), className)}
4230
+ onClick={(e) => {
4231
+ if ((e.target as HTMLElement).closest("button")) {
4232
+ return;
4233
+ }
4234
+ e.currentTarget.parentElement?.querySelector("input")?.focus();
4235
+ }}
4236
+ {...props}
4237
+ />
4238
+ );
4239
+ }
4240
+
4241
+ const inputGroupButtonVariants = cva("flex items-center gap-2 text-sm shadow-none", {
4242
+ variants: {
4243
+ size: {
4244
+ xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
4245
+ sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
4246
+ "icon-xs": "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
4247
+ "icon-sm": "size-8 p-0 has-[>svg]:p-0",
4248
+ },
4249
+ },
4250
+ defaultVariants: {
4251
+ size: "xs",
4252
+ },
4253
+ });
4254
+
4255
+ function InputGroupButton({
4256
+ className,
4257
+ type = "button",
4258
+ variant = "ghost",
4259
+ size = "xs",
4260
+ ...props
4261
+ }: Omit<React.ComponentProps<typeof Button>, "size"> &
4262
+ VariantProps<typeof inputGroupButtonVariants>) {
4263
+ return (
4264
+ <Button
4265
+ type={type}
4266
+ data-size={size}
4267
+ variant={variant}
4268
+ className={cn(inputGroupButtonVariants({ size }), className)}
4269
+ {...props}
4270
+ />
4271
+ );
4272
+ }
4273
+
4274
+ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
4275
+ return (
4276
+ <span
4277
+ className={cn(
4278
+ "flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
4279
+ className,
4280
+ )}
4281
+ {...props}
4282
+ />
4283
+ );
4284
+ }
4285
+
4286
+ function InputGroupInput({ className, ...props }: React.ComponentProps<"input">) {
4287
+ return (
4288
+ <Input
4289
+ data-slot="input-group-control"
4290
+ className={cn(
4291
+ "flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
4292
+ className,
4293
+ )}
4294
+ {...props}
4295
+ />
4296
+ );
4297
+ }
4298
+
4299
+ function InputGroupTextarea({ className, ...props }: React.ComponentProps<"textarea">) {
4300
+ return (
4301
+ <Textarea
4302
+ data-slot="input-group-control"
4303
+ className={cn(
4304
+ "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
4305
+ className,
4306
+ )}
4307
+ {...props}
4308
+ />
4309
+ );
4310
+ }
4311
+
4312
+ export {
4313
+ InputGroup,
4314
+ InputGroupAddon,
4315
+ InputGroupButton,
4316
+ InputGroupText,
4317
+ InputGroupInput,
4318
+ InputGroupTextarea,
4319
+ };
4320
+ `,"components/ui/input.tsx":`import * as React from "react";
4321
+
4322
+ import { cn } from "@/lib/utils";
4323
+
4324
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
4325
+ return (
4326
+ <input
4327
+ type={type}
4328
+ data-slot="input"
4329
+ className={cn(
4330
+ "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
4331
+ "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
4332
+ "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
4333
+ className,
4334
+ )}
4335
+ {...props}
4336
+ />
4337
+ );
4338
+ }
4339
+
4340
+ export { Input };
4341
+ `,"components/ui/select.tsx":`"use client";
4342
+
4343
+ import * as React from "react";
4344
+ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
4345
+ import { Select as SelectPrimitive } from "radix-ui";
4346
+
4347
+ import { cn } from "@/lib/utils";
4348
+
4349
+ function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
4350
+ return <SelectPrimitive.Root data-slot="select" {...props} />;
4351
+ }
4352
+
4353
+ function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
4354
+ return <SelectPrimitive.Group data-slot="select-group" {...props} />;
4355
+ }
4356
+
4357
+ function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
4358
+ return <SelectPrimitive.Value data-slot="select-value" {...props} />;
4359
+ }
4360
+
4361
+ function SelectTrigger({
4362
+ className,
4363
+ size = "default",
4364
+ children,
4365
+ ...props
4366
+ }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
4367
+ size?: "sm" | "default";
4368
+ }) {
4369
+ return (
4370
+ <SelectPrimitive.Trigger
4371
+ data-slot="select-trigger"
4372
+ data-size={size}
4373
+ className={cn(
4374
+ "flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
4375
+ className,
4376
+ )}
4377
+ {...props}
4378
+ >
4379
+ {children}
4380
+ <SelectPrimitive.Icon asChild>
4381
+ <ChevronDownIcon className="size-4 opacity-50" />
4382
+ </SelectPrimitive.Icon>
4383
+ </SelectPrimitive.Trigger>
4384
+ );
4385
+ }
4386
+
4387
+ function SelectContent({
4388
+ className,
4389
+ children,
4390
+ position = "item-aligned",
4391
+ align = "center",
4392
+ ...props
4393
+ }: React.ComponentProps<typeof SelectPrimitive.Content>) {
4394
+ return (
4395
+ <SelectPrimitive.Portal>
4396
+ <SelectPrimitive.Content
4397
+ data-slot="select-content"
4398
+ className={cn(
4399
+ "relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
4400
+ position === "popper" &&
4401
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
4402
+ className,
4403
+ )}
4404
+ position={position}
4405
+ align={align}
4406
+ {...props}
4407
+ >
4408
+ <SelectScrollUpButton />
4409
+ <SelectPrimitive.Viewport
4410
+ className={cn(
4411
+ "p-1",
4412
+ position === "popper" &&
4413
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
4414
+ )}
4415
+ >
4416
+ {children}
4417
+ </SelectPrimitive.Viewport>
4418
+ <SelectScrollDownButton />
4419
+ </SelectPrimitive.Content>
4420
+ </SelectPrimitive.Portal>
4421
+ );
4422
+ }
4423
+
4424
+ function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
4425
+ return (
4426
+ <SelectPrimitive.Label
4427
+ data-slot="select-label"
4428
+ className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
4429
+ {...props}
4430
+ />
4431
+ );
4432
+ }
4433
+
4434
+ function SelectItem({
4435
+ className,
4436
+ children,
4437
+ ...props
4438
+ }: React.ComponentProps<typeof SelectPrimitive.Item>) {
4439
+ return (
4440
+ <SelectPrimitive.Item
4441
+ data-slot="select-item"
4442
+ className={cn(
4443
+ "relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
4444
+ className,
4445
+ )}
4446
+ {...props}
4447
+ >
4448
+ <span
4449
+ data-slot="select-item-indicator"
4450
+ className="absolute right-2 flex size-3.5 items-center justify-center"
4451
+ >
4452
+ <SelectPrimitive.ItemIndicator>
4453
+ <CheckIcon className="size-4" />
4454
+ </SelectPrimitive.ItemIndicator>
4455
+ </span>
4456
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
4457
+ </SelectPrimitive.Item>
4458
+ );
4459
+ }
4460
+
4461
+ function SelectSeparator({
4462
+ className,
4463
+ ...props
4464
+ }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
4465
+ return (
4466
+ <SelectPrimitive.Separator
4467
+ data-slot="select-separator"
4468
+ className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
4469
+ {...props}
4470
+ />
4471
+ );
4472
+ }
4473
+
4474
+ function SelectScrollUpButton({
4475
+ className,
4476
+ ...props
4477
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
4478
+ return (
4479
+ <SelectPrimitive.ScrollUpButton
4480
+ data-slot="select-scroll-up-button"
4481
+ className={cn("flex cursor-default items-center justify-center py-1", className)}
4482
+ {...props}
4483
+ >
4484
+ <ChevronUpIcon className="size-4" />
4485
+ </SelectPrimitive.ScrollUpButton>
4486
+ );
4487
+ }
4488
+
4489
+ function SelectScrollDownButton({
4490
+ className,
4491
+ ...props
4492
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
4493
+ return (
4494
+ <SelectPrimitive.ScrollDownButton
4495
+ data-slot="select-scroll-down-button"
4496
+ className={cn("flex cursor-default items-center justify-center py-1", className)}
4497
+ {...props}
4498
+ >
4499
+ <ChevronDownIcon className="size-4" />
4500
+ </SelectPrimitive.ScrollDownButton>
4501
+ );
4502
+ }
4503
+
4504
+ export {
4505
+ Select,
4506
+ SelectContent,
4507
+ SelectGroup,
4508
+ SelectItem,
4509
+ SelectLabel,
4510
+ SelectScrollDownButton,
4511
+ SelectScrollUpButton,
4512
+ SelectSeparator,
4513
+ SelectTrigger,
4514
+ SelectValue,
4515
+ };
4516
+ `,"components/ui/separator.tsx":`"use client";
4517
+
4518
+ import * as React from "react";
4519
+ import { Separator as SeparatorPrimitive } from "radix-ui";
4520
+
4521
+ import { cn } from "@/lib/utils";
4522
+
4523
+ function Separator({
4524
+ className,
4525
+ orientation = "horizontal",
4526
+ decorative = true,
4527
+ ...props
4528
+ }: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
4529
+ return (
4530
+ <SeparatorPrimitive.Root
4531
+ data-slot="separator"
4532
+ decorative={decorative}
4533
+ orientation={orientation}
4534
+ className={cn(
4535
+ "shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
4536
+ className,
4537
+ )}
4538
+ {...props}
4539
+ />
4540
+ );
4541
+ }
4542
+
4543
+ export { Separator };
4544
+ `,"components/ui/spinner.tsx":`import { Loader2Icon } from "lucide-react";
4545
+
4546
+ import { cn } from "@/lib/utils";
4547
+
4548
+ function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
4549
+ return (
4550
+ <Loader2Icon
4551
+ role="status"
4552
+ aria-label="Loading"
4553
+ className={cn("size-4 animate-spin", className)}
4554
+ {...props}
4555
+ />
4556
+ );
4557
+ }
4558
+
4559
+ export { Spinner };
4560
+ `,"components/ui/textarea.tsx":`import * as React from "react";
4561
+
4562
+ import { cn } from "@/lib/utils";
4563
+
4564
+ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
4565
+ return (
4566
+ <textarea
4567
+ data-slot="textarea"
4568
+ className={cn(
4569
+ "flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
4570
+ className,
4571
+ )}
4572
+ {...props}
4573
+ />
4574
+ );
4575
+ }
4576
+
4577
+ export { Textarea };
4578
+ `,"components/ui/tooltip.tsx":`"use client";
4579
+
4580
+ import * as React from "react";
4581
+ import { Tooltip as TooltipPrimitive } from "radix-ui";
4582
+
4583
+ import { cn } from "@/lib/utils";
4584
+
4585
+ function TooltipProvider({
4586
+ delayDuration = 0,
4587
+ ...props
4588
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
4589
+ return (
4590
+ <TooltipPrimitive.Provider
4591
+ data-slot="tooltip-provider"
4592
+ delayDuration={delayDuration}
4593
+ {...props}
4594
+ />
4595
+ );
4596
+ }
4597
+
4598
+ function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
4599
+ return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
4600
+ }
4601
+
4602
+ function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
4603
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
4604
+ }
4605
+
4606
+ function TooltipContent({
4607
+ className,
4608
+ sideOffset = 0,
4609
+ children,
4610
+ ...props
4611
+ }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
4612
+ return (
4613
+ <TooltipPrimitive.Portal>
4614
+ <TooltipPrimitive.Content
4615
+ data-slot="tooltip-content"
4616
+ sideOffset={sideOffset}
4617
+ className={cn(
4618
+ "z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
4619
+ className,
4620
+ )}
4621
+ {...props}
4622
+ >
4623
+ {children}
4624
+ <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
4625
+ </TooltipPrimitive.Content>
4626
+ </TooltipPrimitive.Portal>
4627
+ );
4628
+ }
4629
+
4630
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
4631
+ `,"components.json":`{
4632
+ "$schema": "https://ui.shadcn.com/schema.json",
4633
+ "style": "new-york",
4634
+ "rsc": true,
4635
+ "tsx": true,
4636
+ "tailwind": {
4637
+ "config": "",
4638
+ "css": "app/globals.css",
4639
+ "baseColor": "neutral",
4640
+ "cssVariables": true,
4641
+ "prefix": ""
4642
+ },
4643
+ "iconLibrary": "lucide",
4644
+ "aliases": {
4645
+ "components": "@/components",
4646
+ "utils": "@/lib/utils",
4647
+ "ui": "@/components/ui",
4648
+ "lib": "@/lib",
4649
+ "hooks": "@/hooks"
4650
+ },
4651
+ "registries": {}
4652
+ }
4653
+ `,"css.d.ts":`declare module "*.css";
4654
+ `,"lib/utils.ts":`import { clsx, type ClassValue } from "clsx";
4655
+ import { twMerge } from "tailwind-merge";
4656
+
4657
+ export function cn(...inputs: ClassValue[]): string {
4658
+ return twMerge(clsx(inputs));
4659
+ }
4660
+ `,"next-env.d.ts":`/// <reference types="next" />
4661
+ /// <reference types="next/image-types/global" />
4662
+ import "./.next/types/routes.d.ts";
4663
+
4664
+ // NOTE: This file should not be edited
4665
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
4666
+ `,"next.config.ts":`import type { NextConfig } from "next";
4667
+ import { withAsh } from "experimental-ash/next";
4668
+
4669
+ const nextConfig: NextConfig = {};
4670
+
4671
+ export default withAsh(nextConfig);
4672
+ `,"postcss.config.mjs":`const config = {
4673
+ plugins: {
4674
+ "@tailwindcss/postcss": {},
4675
+ },
4676
+ };
4677
+
4678
+ export default config;
4679
+ `,"tsconfig.json":`{
4680
+ "$schema": "https://json.schemastore.org/tsconfig",
4681
+ "compilerOptions": {
4682
+ "target": "ES2017",
4683
+ "lib": ["dom", "dom.iterable", "esnext"],
4684
+ "allowJs": true,
4685
+ "skipLibCheck": true,
4686
+ "strict": true,
4687
+ "noEmit": true,
4688
+ "esModuleInterop": true,
4689
+ "module": "esnext",
4690
+ "moduleResolution": "Bundler",
4691
+ "resolveJsonModule": true,
4692
+ "isolatedModules": true,
4693
+ "jsx": "react-jsx",
4694
+ "incremental": true,
4695
+ "plugins": [
4696
+ {
4697
+ "name": "next"
4698
+ }
4699
+ ],
4700
+ "paths": {
4701
+ "@/*": ["./*"]
4702
+ }
4703
+ },
4704
+ "include": [
4705
+ "next-env.d.ts",
4706
+ "**/*.ts",
4707
+ "**/*.tsx",
4708
+ ".next/types/**/*.ts",
4709
+ ".next/dev/types/**/*.ts"
4710
+ ],
4711
+ "exclude": ["node_modules"]
4712
+ }
4713
+ `},WEB_APP_TEMPLATE_PACKAGE_JSON={scripts:{build:`next build`,dev:`next dev`,start:`next start`,typecheck:`tsgo --noEmit -p tsconfig.json`},dependencies:{"@radix-ui/react-use-controllable-state":`1.2.2`,"@streamdown/cjk":`1.0.3`,"@streamdown/code":`1.1.1`,"@streamdown/math":`1.0.2`,"@streamdown/mermaid":`1.0.2`,"@tailwindcss/postcss":`4.3.0`,ai:`catalog:`,"class-variance-authority":`0.7.1`,clsx:`2.1.1`,cmdk:`1.1.1`,"experimental-ash":`workspace:*`,"lucide-react":`1.16.0`,motion:`12.40.0`,nanoid:`5.1.11`,next:`catalog:`,"radix-ui":`1.4.3`,react:`catalog:`,"react-dom":`catalog:`,shiki:`4.1.0`,streamdown:`catalog:`,"tailwind-merge":`3.6.0`,tailwindcss:`4.3.0`,"use-stick-to-bottom":`1.1.4`,zod:`catalog:`},devDependencies:{"@types/node":`catalog:`,"@types/react":`catalog:`,"@types/react-dom":`catalog:`}};export{WEB_APP_TEMPLATE_FILES,WEB_APP_TEMPLATE_PACKAGE_JSON};