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.
- package/CHANGELOG.md +6 -0
- package/dist/docs/public/auth-and-route-protection.md +18 -7
- package/dist/docs/public/channels/README.md +7 -3
- package/dist/docs/public/channels/slack.md +10 -4
- package/dist/src/cli/commands/channel-add-conflicts.d.ts +21 -0
- package/dist/src/cli/commands/channel-add-conflicts.js +1 -0
- package/dist/src/cli/commands/channels.d.ts +9 -1
- package/dist/src/cli/commands/channels.js +1 -3
- package/dist/src/cli/dev/repl.js +1 -1
- package/dist/src/cli/run.js +1 -1
- package/dist/src/compiler/compile-agent.js +1 -1
- package/dist/src/compiler/normalize-manifest.js +1 -1
- package/dist/src/internal/application/package.js +1 -1
- package/dist/src/internal/nitro/host/start-production-server.js +1 -1
- package/dist/src/node_modules/.pnpm/@clack_core@1.3.1/node_modules/@clack/core/dist/index.js +10 -0
- package/dist/src/node_modules/.pnpm/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/index.js +1 -0
- package/dist/src/node_modules/.pnpm/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/utils.js +1 -0
- package/dist/src/node_modules/.pnpm/fast-string-width@3.0.2/node_modules/fast-string-width/dist/index.js +1 -0
- package/dist/src/node_modules/.pnpm/fast-wrap-ansi@0.2.2/node_modules/fast-wrap-ansi/lib/main.js +5 -0
- package/dist/src/node_modules/.pnpm/picocolors@1.1.1/node_modules/picocolors/picocolors.js +1 -0
- package/dist/src/node_modules/.pnpm/sisteransi@1.0.5/node_modules/sisteransi/src/index.js +1 -0
- package/dist/src/packages/ash-scaffold/src/channels.js +12 -2
- package/dist/src/packages/ash-scaffold/src/cli/channel-add-prompter.js +1 -0
- package/dist/src/packages/ash-scaffold/src/cli/channel-setup-prompter.js +1 -0
- package/dist/src/packages/ash-scaffold/src/cli/command-output.js +1 -0
- package/dist/src/packages/ash-scaffold/src/cli/index.js +1 -0
- package/dist/src/packages/ash-scaffold/src/cli/prompt-ui.js +3 -0
- package/dist/src/packages/ash-scaffold/src/cli/rail-log.js +2 -0
- package/dist/src/packages/ash-scaffold/src/primitives/detect-deployment.js +1 -0
- package/dist/src/packages/ash-scaffold/src/primitives/index.js +1 -0
- package/dist/src/packages/ash-scaffold/src/primitives/pnpm-invocation.js +1 -0
- package/dist/src/packages/ash-scaffold/src/primitives/process-output.js +1 -0
- package/dist/src/packages/ash-scaffold/src/primitives/run-pnpm.js +1 -0
- package/dist/src/packages/ash-scaffold/src/primitives/run-vercel.js +1 -0
- package/dist/src/packages/ash-scaffold/src/primitives/update-slack-channel.js +1 -0
- package/dist/src/packages/ash-scaffold/src/project.js +1 -1
- package/dist/src/packages/ash-scaffold/src/steps/deploy-to-vercel.js +1 -0
- package/dist/src/packages/ash-scaffold/src/steps/index.js +1 -0
- package/dist/src/packages/ash-scaffold/src/steps/run-add-to-agent.js +2 -0
- package/dist/src/packages/ash-scaffold/src/steps/setup-slackbot.js +1 -0
- package/dist/src/packages/ash-scaffold/src/web-template.js +4713 -0
- package/dist/src/public/next/server.js +1 -1
- 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};
|