-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdefault.json
More file actions
546 lines (546 loc) · 488 KB
/
Copy pathdefault.json
File metadata and controls
546 lines (546 loc) · 488 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
{
"src": {
"directory": {
"lib": {
"directory": {
"shiki.ts": {
"file": {
"contents": "/**\n * Shiki highlighter — singleton with CDN-based language loading.\n *\n * Instead of importing from \"shiki\" (which bundles ALL ~200 language grammars\n * and ~50 themes as dynamic imports, producing 347 esbuild chunks), we use\n * `@shikijs/core` with:\n * - 2 themes imported statically (github-light, github-dark)\n * - Language grammars fetched from CDN on demand\n *\n * This reduces the chunk count from 347 → 0.\n *\n * @see https://github.com/taskade/taskcade/issues/26056\n */\n\nimport type { HighlighterCore } from \"@shikijs/core\";\nimport { createHighlighterCore } from \"@shikijs/core\";\nimport { createJavaScriptRegexEngine } from \"@shikijs/engine-javascript\";\nimport githubDark from \"@shikijs/themes/github-dark\";\nimport githubLight from \"@shikijs/themes/github-light\";\n\nexport type { ThemedToken } from \"@shikijs/core\";\n\nexport type BundledLanguage = string;\n\n// Keep in sync with the shiki version in package.json\nconst SHIKI_CDN_BASE = \"https://esm.sh/@shikijs/langs@4.0.2/\";\n\n// Singleton highlighter — created once, languages loaded incrementally\nlet highlighterPromise: Promise<HighlighterCore> | null = null;\n\nfunction getOrCreateHighlighter(): Promise<HighlighterCore> {\n if (!highlighterPromise) {\n highlighterPromise = createHighlighterCore({\n themes: [githubLight, githubDark],\n langs: [],\n engine: createJavaScriptRegexEngine(),\n });\n }\n return highlighterPromise;\n}\n\n// Track language loading state to deduplicate concurrent fetches\nconst loadedLangs = new Set<string>();\nconst loadingLangs = new Map<string, Promise<void>>();\n\nasync function ensureLanguage(\n highlighter: HighlighterCore,\n lang: string\n): Promise<void> {\n if (loadedLangs.has(lang)) return;\n\n // Deduplicate concurrent loads for the same language\n const existing = loadingLangs.get(lang);\n if (existing) return existing;\n\n const promise = (async () => {\n try {\n // Dynamic CDN import — intentionally left as a runtime fetch, not bundled\n const mod = await import(`${SHIKI_CDN_BASE}${lang}.mjs`);\n await highlighter.loadLanguage(mod.default ?? mod);\n } catch {\n // Language not available on CDN — will fall back to plaintext\n } finally {\n loadedLangs.add(lang); // Prevent retries, even on failure\n loadingLangs.delete(lang);\n }\n })();\n\n loadingLangs.set(lang, promise);\n return promise;\n}\n\n/**\n * Returns a Shiki highlighter with the requested language loaded.\n * The highlighter is a singleton; languages are loaded incrementally from CDN.\n */\nexport async function getHighlighter(\n language: string\n): Promise<HighlighterCore> {\n const highlighter = await getOrCreateHighlighter();\n await ensureLanguage(highlighter, language);\n return highlighter;\n}\n"
}
},
"utils.ts": {
"file": {
"contents": "import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"
}
},
"agent-chat": {
"directory": {
"v2": {
"directory": {
"index.ts": {
"file": {
"contents": "/**\n * Agent Chat SDK v2 for Taskade Genesis\n *\n * Uses the AI SDK (`@ai-sdk/react`) with the `/chat` endpoint.\n *\n * IMPORTANT: `useChat` requires a real `Chat` instance — it crashes if passed undefined.\n * Always guard with a conditional render so `useChat` is only called after `chat` is created.\n *\n * @example\n * ```typescript\n * import { useChat } from '@ai-sdk/react';\n * import { createConversation, createAgentChat } from '@/lib/agent-chat/v2';\n * import { useState } from 'react';\n * import { ulid } from 'ulidx';\n *\n * function ChatComponent() {\n * const [chat, setChat] = useState<ReturnType<typeof createAgentChat> | null>(null);\n *\n * const handleStartChat = async () => {\n * const { conversationId } = await createConversation(agentId);\n * setChat(createAgentChat(agentId, conversationId));\n * };\n *\n * if (!chat) return <button onClick={handleStartChat}>Start Chat</button>;\n * return <ActiveChat chat={chat} />;\n * }\n *\n * function ActiveChat({ chat }: { chat: ReturnType<typeof createAgentChat> }) {\n * const { messages, status } = useChat({ chat, id: chat.id });\n *\n * const handleSend = async (text: string) => {\n * await chat.sendMessage({\n * id: ulid(),\n * role: 'user',\n * parts: [{ type: 'text', text }],\n * });\n * };\n *\n * return (\n * <div>\n * {messages.map(msg => (\n * <div key={msg.id}>\n * {msg.role === 'user' ? 'You: ' : 'Agent: '}\n * {msg.parts.filter(p => p.type === 'text').map(p => p.text).join('')}\n * </div>\n * ))}\n * <button onClick={() => handleSend('Hello!')}>Send</button>\n * </div>\n * );\n * }\n * ```\n */\n\nexport type { ClientOptions, CreateConversationResponse } from './client';\nexport { createConversation } from './client';\nexport { createAgentChat } from './createAgentChat';\n"
}
},
"README.md": {
"file": {
"contents": "# Agent Chat SDK v2\n\nSDK for building AI Agent Chat interfaces in Taskade Genesis apps.\nBuilt on the AI SDK (`@ai-sdk/react`), using the `/chat` endpoint.\n\n## Quick Start\n\n```typescript\nimport { useChat } from '@ai-sdk/react';\nimport { createConversation, createAgentChat } from '@/lib/agent-chat/v2';\nimport { isToolUIPart } from 'ai';\nimport type { UIMessage } from 'ai';\nimport { useState } from 'react';\nimport { ulid } from 'ulidx';\n\n// IMPORTANT: useChat requires a real Chat instance — it crashes if passed undefined.\n// Split into two components so useChat is only called after chat is created.\n\nfunction ChatComponent() {\n const [chat, setChat] = useState<ReturnType<typeof createAgentChat> | null>(null);\n\n const handleStartChat = async () => {\n const { conversationId } = await createConversation(agentId);\n setChat(createAgentChat(agentId, conversationId));\n };\n\n if (!chat) return <button onClick={handleStartChat}>Start Chat</button>;\n return <ActiveChat chat={chat} />;\n}\n\nfunction ActiveChat({ chat }: { chat: ReturnType<typeof createAgentChat> }) {\n const { messages, status, addToolApprovalResponse } = useChat({ chat, id: chat.id });\n const isSending = status === 'submitted' || status === 'streaming';\n\n const handleSend = async (text: string) => {\n await chat.sendMessage({\n id: ulid(),\n role: 'user',\n parts: [{ type: 'text', text }],\n });\n };\n\n return (\n <div>\n {messages.map(msg => (\n <div key={msg.id}>\n <strong>{msg.role === 'user' ? 'You' : 'Agent'}:</strong>\n {msg.parts.map((part, i) => {\n // Always render all part types — the agent may use tools even if none\n // are configured yet. Omitting this causes tool calls to be silently dropped.\n if (part.type === 'text') {\n return <span key={i}>{part.text}</span>;\n }\n if (isToolUIPart(part)) {\n return (\n <div key={i}>\n <em>Tool: {part.toolName} [{part.state}]</em>\n {part.state === 'approval-requested' && part.approval != null && (\n <>\n <button onClick={() => addToolApprovalResponse({ id: part.approval.id, approved: true })}>\n Approve\n </button>\n <button onClick={() => addToolApprovalResponse({ id: part.approval.id, approved: false })}>\n Deny\n </button>\n </>\n )}\n </div>\n );\n }\n return null;\n })}\n </div>\n ))}\n <button onClick={() => handleSend('Hello!')} disabled={isSending}>Send</button>\n </div>\n );\n}\n```\n\n## Pre-built AI Elements UI (`@/components/ai-elements/`)\n\nPre-built, styled React components for chat interfaces are available via AI Elements.\nUse these instead of building chat UI from scratch:\n\n| Component | Import | Purpose |\n|-----------|--------|---------|\n| `Conversation` | `@/components/ai-elements/conversation` | Scrollable chat container with auto-stick-to-bottom |\n| `Message` | `@/components/ai-elements/message` | Message bubble with role-based styling + markdown |\n| `PromptInput` | `@/components/ai-elements/prompt-input` | Chat input form with file attachments + submit |\n| `Suggestion` | `@/components/ai-elements/suggestion` | Quick-reply suggestion pills |\n| `Reasoning` | `@/components/ai-elements/reasoning` | Collapsible thinking/reasoning display |\n| `CodeBlock` | `@/components/ai-elements/code-block` | Syntax-highlighted code with copy button |\n| `Tool` | `@/components/ai-elements/tool` | Collapsible tool call display with status |\n| `Confirmation` | `@/components/ai-elements/confirmation` | Tool call approval UI with approve/reject slots |\n| `Shimmer` | `@/components/ai-elements/shimmer` | Animated shimmer text — usage: `<Shimmer>Loading...</Shimmer>` (children must be a string) |\n\nSee `@/components/ai-elements` for the full list of available components that you may use to build your chat UI.\n\n\n### Full Example with AI Elements\n\n```typescript\nimport { useChat } from '@ai-sdk/react';\nimport { createConversation, createAgentChat } from '@/lib/agent-chat/v2';\nimport { Conversation, ConversationContent, ConversationScrollButton } from '@/components/ai-elements/conversation';\nimport { Message, MessageContent, MessageResponse } from '@/components/ai-elements/message';\nimport { Tool, ToolHeader, ToolContent, ToolInput, ToolOutput } from '@/components/ai-elements/tool';\nimport {\n Confirmation,\n ConfirmationTitle,\n ConfirmationRequest,\n ConfirmationAccepted,\n ConfirmationRejected,\n ConfirmationActions,\n ConfirmationAction,\n} from '@/components/ai-elements/confirmation';\nimport { PromptInput, PromptInputTextarea, PromptInputFooter, PromptInputSubmit } from '@/components/ai-elements/prompt-input';\nimport { Suggestions, Suggestion } from '@/components/ai-elements/suggestion';\nimport { isToolUIPart } from 'ai';\nimport type { UIMessage } from 'ai';\nimport { useState } from 'react';\nimport { ulid } from 'ulidx';\n\nfunction ChatPage() {\n const [chat, setChat] = useState<ReturnType<typeof createAgentChat> | null>(null);\n\n const handleStartChat = async () => {\n const { conversationId } = await createConversation(agentId);\n setChat(createAgentChat(agentId, conversationId));\n };\n\n if (!chat) return <button onClick={handleStartChat}>Start Chat</button>;\n return <ActiveChat chat={chat} />;\n}\n\nfunction ActiveChat({ chat }: { chat: ReturnType<typeof createAgentChat> }) {\n const { messages, status, addToolApprovalResponse } = useChat({ chat, id: chat.id });\n\n const handleSend = async (text: string) => {\n await chat.sendMessage({\n id: ulid(),\n role: 'user',\n parts: [{ type: 'text', text }],\n });\n };\n\n const hasMessages = messages.length > 0;\n\n return (\n <div className=\"flex h-screen flex-col\">\n <Conversation>\n <ConversationContent>\n {messages.map((msg) => (\n <Message key={msg.id} from={msg.role}>\n <MessageContent>\n <MessageParts message={msg} onApprove={addToolApprovalResponse} />\n </MessageContent>\n </Message>\n ))}\n </ConversationContent>\n <ConversationScrollButton />\n </Conversation>\n\n {!hasMessages && (\n <Suggestions>\n <Suggestion suggestion=\"What can you help me with?\" onClick={handleSend} />\n <Suggestion suggestion=\"Tell me about this app\" onClick={handleSend} />\n </Suggestions>\n )}\n\n <PromptInput onSubmit={({ text }) => handleSend(text)}>\n <PromptInputTextarea />\n <PromptInputFooter>\n <PromptInputSubmit status={status} />\n </PromptInputFooter>\n </PromptInput>\n </div>\n );\n}\n\n// Always handle all part types — the agent may call tools even if none are configured\n// yet. Omitting isToolUIPart handling causes tool calls to be silently dropped.\nfunction MessageParts({\n message,\n onApprove,\n}: {\n message: UIMessage;\n onApprove: ReturnType<typeof useChat>['addToolApprovalResponse'];\n}) {\n return (\n <>\n {message.parts.map((part, i) => {\n const key = `${message.id}-${i}`;\n\n if (part.type === 'text') {\n return message.role === 'user' ? (\n <p key={key}>{part.text}</p>\n ) : (\n <MessageResponse key={key}>{part.text}</MessageResponse>\n );\n }\n\n if (isToolUIPart(part)) {\n return (\n <Tool key={key}>\n <ToolHeader type={part.type} state={part.state} />\n <ToolContent>\n <ToolInput input={part.input} />\n <Confirmation approval={part.approval} state={part.state}>\n <ConfirmationRequest>\n <ConfirmationTitle>Allow this tool to run?</ConfirmationTitle>\n </ConfirmationRequest>\n <ConfirmationAccepted>Approved</ConfirmationAccepted>\n <ConfirmationRejected>Rejected</ConfirmationRejected>\n <ConfirmationActions>\n <ConfirmationAction\n variant=\"outline\"\n onClick={() =>\n part.approval != null && onApprove({ id: part.approval.id, approved: false })\n }\n >\n Deny\n </ConfirmationAction>\n <ConfirmationAction\n onClick={() =>\n part.approval != null && onApprove({ id: part.approval.id, approved: true })\n }\n >\n Approve\n </ConfirmationAction>\n </ConfirmationActions>\n </Confirmation>\n <ToolOutput output={part.output} errorText={part.errorText} />\n </ToolContent>\n </Tool>\n );\n }\n\n return null;\n })}\n </>\n );\n}\n```\n\n## Tool Call Approval\n\nBoth examples above already include full approval handling — it is part of the standard\n`MessageParts` pattern and should always be present, even if the agent has no tools\nconfigured today. Tool call parts will simply never appear in that case; the code is inert.\n\nThe approval flow is managed by two pieces:\n\n- **`Confirmation` + sub-components** (`@/components/ai-elements/confirmation`) — conditional\n rendering driven by `part.state` and `part.approval`. No handler logic lives inside them.\n- **`addToolApprovalResponse`** (from `useChat`) — call with `{ id: part.approval.id, approved }`.\n The `id` must be the tool part’s **`approval.id`**, not `toolCallId`; wrong `id` updates nothing.\n `createAgentChat` then automatically sends the next request to the server.\n\n### Approval state lifecycle\n\n| State | What's visible |\n|---|---|\n| `approval-requested` | `<ConfirmationRequest>` + `<ConfirmationActions>` (approve/deny buttons) |\n| `approval-responded` | `<ConfirmationAccepted>` or `<ConfirmationRejected>` based on decision |\n| `output-available` | Tool completed — `<ToolOutput>` shows result |\n| `output-denied` | Tool was denied — `<ConfirmationRejected>` stays visible |\n\nOnce `addToolApprovalResponse` is called, `createAgentChat` automatically sends the next\nrequest to the server — no manual trigger required.\n\n## API\n\n**`createConversation(agentId, options?)`**\nCreates a new public conversation. Returns `{ ok, conversationId }`.\n\n**`createAgentChat(agentId, conversationId, options?)`**\nCreates a `Chat` instance configured for the agent. Use with `useChat` from `@ai-sdk/react`.\n\n**`useChat({ chat, id })`** (from `@ai-sdk/react`)\nStandard AI SDK hook. Returns `{ messages, status, error, addToolApprovalResponse }`.\n\n**`addToolApprovalResponse({ id, approved, reason? })`** (from `useChat`)\nSubmits an approve (`true`) or deny (`false`) decision. **`id` is `toolUIPart.approval.id`**, not `toolCallId`.\nThe conversation automatically resumes after the response is submitted.\n\n## Sending Messages\n\n```typescript\nimport { ulid } from 'ulidx';\n\nawait chat.sendMessage({\n id: ulid(),\n role: 'user',\n parts: [{ type: 'text', text: 'Hello!' }],\n});\n```\n\n## Message Format\n\nMessages use the AI SDK `UIMessage` type:\n\n```typescript\nimport { isToolUIPart } from 'ai';\n\nmsg.parts.filter(p => p.type === 'text').map(p => p.text) // Text\nmsg.parts.filter(isToolUIPart) // Tool calls\n```\n\n## Requirements\n\n- Agent must have **public visibility** enabled before creating a conversation\n- `useChat` must receive a real `Chat` instance, never `undefined`\n"
}
},
"client.ts": {
"file": {
"contents": "/**\n * API Response types\n */\nexport interface CreateConversationResponse {\n ok: boolean;\n conversationId: string;\n}\n\n/**\n * Configuration for API client\n */\nexport interface ClientOptions {\n /** Base URL for API requests (defaults to relative paths) */\n baseUrl?: string;\n}\n\nfunction isEmptyString(value: string | null | undefined): boolean {\n return value == null || value.trim().length === 0;\n}\n\n/**\n * Creates a new public agent conversation\n *\n * @param agentId - The agent ID\n * @param options - Optional client configuration\n * @returns Promise resolving to conversation ID\n * @throws Error if conversation creation fails\n *\n * @example\n * ```typescript\n * const { conversationId } = await createConversation('agent-456');\n * ```\n */\nexport async function createConversation(\n agentId: string,\n options?: ClientOptions,\n): Promise<CreateConversationResponse> {\n if (isEmptyString(agentId)) {\n throw new Error('Agent ID cannot be empty');\n }\n\n const baseUrl = options?.baseUrl ?? '';\n const url = `${baseUrl}/api/taskade/agents/${encodeURIComponent(agentId)}/public-conversations`;\n\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n\n const contentType = response.headers.get('content-type') || '';\n const responseText = await response.text().catch(() => '');\n\n if (!response.ok) {\n throw new Error(\n `Failed to create conversation: ${response.status} ${responseText || 'Unknown error'}`,\n );\n }\n\n if (!contentType.includes('application/json')) {\n throw new Error(\n `Invalid response format: expected JSON, got ${contentType}. Response: ${responseText.substring(0, 100)}`,\n );\n }\n\n try {\n const data = JSON.parse(responseText);\n return data as CreateConversationResponse;\n } catch (err) {\n throw new Error(\n `Failed to parse JSON response: ${err instanceof Error ? err.message : 'Unknown error'}. Response: ${responseText.substring(0, 200)}`,\n );\n }\n}\n"
}
},
"createAgentChat.ts": {
"file": {
"contents": "import { Chat } from '@ai-sdk/react';\nimport {\n DefaultChatTransport,\n lastAssistantMessageIsCompleteWithApprovalResponses,\n lastAssistantMessageIsCompleteWithToolCalls,\n UIMessage,\n isToolUIPart,\n} from 'ai';\nimport { ulid } from 'ulidx';\n\nimport type { ClientOptions } from './client';\n\nexport type ExtractInputMessagesResult = {\n history: UIMessage[];\n messages: UIMessage[];\n};\n\n/**\n * Generic version of the TAA helper:\n * - Separates \"history\" from the latest actionable message(s) to send to the server.\n * - Handles agentic tool-call loops where the last assistant message contains tool parts.\n */\nfunction extractInputMessages(messages: UIMessage[]): ExtractInputMessagesResult {\n if (messages.length === 0) {\n return { history: messages, messages: [] };\n }\n\n const lastMessageIndex = messages.length - 1;\n const lastMessage = messages[lastMessageIndex];\n if (lastMessage == null) {\n return { history: messages, messages: [] };\n }\n\n if (lastMessage.role === 'user') {\n const history = messages.slice(0, lastMessageIndex);\n return { history, messages: [lastMessage] };\n }\n\n if (lastMessage.role === 'assistant') {\n const parts = lastMessage.parts;\n if (parts != null && parts.length > 0) {\n const lastPart = parts[parts.length - 1];\n if (lastPart != null && isToolUIPart(lastPart)) {\n if (lastPart.state === 'output-available' || lastPart.state === 'output-error') {\n const history = messages.slice(0, lastMessageIndex);\n return { history, messages: [lastMessage] };\n }\n }\n }\n }\n\n const history = messages.slice(0, lastMessageIndex);\n return { history, messages: [lastMessage] };\n}\n\nconst MAX_HISTORY_MESSAGES = 6;\n\n/**\n * Creates a Chat instance configured for a Taskade agent public conversation.\n *\n * Use with `useChat` from `@ai-sdk/react` to build chat interfaces.\n *\n * IMPORTANT: `useChat` requires a real `Chat` instance — it crashes if passed undefined.\n * Always guard with a conditional render so `useChat` is only called after `chat` is created.\n *\n * @param agentId - The agent ID\n * @param conversationId - The conversation ID (from createConversation)\n * @param options - Optional client configuration\n * @returns A Chat instance ready to use with useChat\n *\n * Tool approval: `useChat`'s `addToolApprovalResponse` expects `{ id, approved }` where `id` is\n * `toolUIPart.approval.id` — **not** `toolCallId`. Passing `toolCallId` will not update any part.\n *\n * @example\n * ```typescript\n * import { useChat } from '@ai-sdk/react';\n * import { createConversation, createAgentChat } from '@/lib/agent-chat/v2';\n *\n * function ChatComponent() {\n * const [chat, setChat] = useState<ReturnType<typeof createAgentChat> | null>(null);\n *\n * const handleStartChat = async () => {\n * const { conversationId } = await createConversation(agentId);\n * setChat(createAgentChat(agentId, conversationId));\n * };\n *\n * if (!chat) return <button onClick={handleStartChat}>Start Chat</button>;\n * return <ActiveChat chat={chat} />;\n * }\n *\n * function ActiveChat({ chat }: { chat: ReturnType<typeof createAgentChat> }) {\n * const { messages, status } = useChat({ chat, id: chat.id });\n * // ...\n * }\n * ```\n */\nexport function createAgentChat(\n agentId: string,\n conversationId: string,\n options?: ClientOptions,\n): Chat<UIMessage> {\n const baseUrl = options?.baseUrl ?? '';\n const api = `${baseUrl}/api/taskade/agents/${encodeURIComponent(agentId)}/public-conversations/${encodeURIComponent(conversationId)}/chat`;\n\n const chatState = new Chat<UIMessage>({\n messages: [],\n transport: new DefaultChatTransport({\n api,\n prepareSendMessagesRequest: (opts) => {\n const { history, messages } = extractInputMessages(opts.messages);\n\n const maxHistory = Math.max(0, MAX_HISTORY_MESSAGES - messages.length);\n const trimmedHistory = maxHistory === 0 ? [] : history.slice(-maxHistory);\n\n return {\n body: {\n messages,\n history: trimmedHistory,\n },\n };\n },\n }),\n id: conversationId,\n generateId: ulid,\n sendAutomaticallyWhen: (options) => {\n const shouldSendAutomatically =\n lastAssistantMessageIsCompleteWithToolCalls(options) ||\n lastAssistantMessageIsCompleteWithApprovalResponses(options);\n if (!shouldSendAutomatically) {\n return false;\n }\n if (chatState.error != null) {\n return false;\n }\n return true;\n },\n });\n\n return chatState;\n}\n"
}
}
}
},
"hooks.ts": {
"file": {
"contents": "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport {\n type ClientOptions,\n createConversation as createConversationApi,\n sendMessage as sendMessageApi,\n} from './client';\nimport { AgentChatStream } from './stream';\nimport type { ErrorEvent, MessageState, StreamOptions } from './types';\n\n/**\n * Options for useAgentChat hook\n */\nexport interface UseAgentChatOptions extends StreamOptions {\n /** Auto-connect stream on mount (default: true) */\n autoConnect?: boolean;\n}\n\n/**\n * Return type for useAgentChat hook\n */\nexport interface UseAgentChatReturn {\n /** Send a message to the conversation */\n sendMessage: (text: string) => Promise<void>;\n /** Array of messages (sorted by creation order) */\n messages: MessageState[];\n /** Whether stream is currently connected */\n isConnected: boolean;\n /** Current error, if any */\n error: Error | null;\n /** Current conversation ID */\n conversationId: string | null;\n /** Create a new conversation (stream stays open, switches to new conversation) */\n createConversation: () => Promise<string>;\n /** Switch to a different conversation (stream stays open) */\n switchConversation: (conversationId: string) => void;\n /** Manually connect the stream (useful when autoConnect is false) */\n connect: () => void;\n}\n\n/**\n * React hook for managing agent chat conversation\n *\n * Requires a conversationId to be provided. The stream is opened when conversationId is available\n * and stays open throughout the conversation lifecycle.\n *\n * @param agentId - The agent ID\n * @param conversationId - The conversation ID (required - must be created manually)\n * @param options - Configuration options\n * @returns Chat state and methods\n *\n * @example\n * ```typescript\n * function ChatComponent() {\n * const [conversationId, setConversationId] = useState<string | null>(null);\n * const { sendMessage, messages, isConnected } = useAgentChat('agent-456', conversationId);\n *\n * // Create conversation manually\n * const handleStartChat = async () => {\n * const { conversationId: newId } = await createConversation('agent-456');\n * setConversationId(newId);\n * };\n *\n * return (\n * <div>\n * {!conversationId && <button onClick={handleStartChat}>Start Chat</button>}\n * {messages.map(msg => (\n * <div key={msg.id}>{msg.content}</div>\n * ))}\n * <button onClick={() => sendMessage('Hello!')}>Send</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useAgentChat(\n agentId: string,\n conversationId: string | null,\n options?: UseAgentChatOptions,\n): UseAgentChatReturn {\n const [messagesMap, setMessagesMap] = useState<Map<string, MessageState>>(new Map());\n const [isConnected, setIsConnected] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [currentConversationId, setCurrentConversationId] = useState<string | null>(conversationId);\n const streamRef = useRef<AgentChatStream | null>(null);\n const listenersRef = useRef<Array<() => void>>([]);\n const streamAgentIdRef = useRef<string | null>(null);\n const currentConversationIdRef = useRef<string | null>(conversationId);\n const previousConversationIdRef = useRef<string | null>(conversationId);\n\n // Sync currentConversationId with prop when prop changes externally\n // This ensures the hook's internal state stays in sync with external prop updates\n useEffect(() => {\n const previousId = previousConversationIdRef.current;\n const newId = conversationId;\n\n // Clear messages when conversationId changes to a different non-null value\n // This handles the case where the prop changes externally (not via createConversation/switchConversation)\n if (previousId !== newId && newId != null && previousId != null) {\n setMessagesMap(new Map());\n // Also clear stream's message states if stream exists\n if (streamRef.current) {\n streamRef.current.clearMessages();\n }\n }\n\n setCurrentConversationId(newId);\n currentConversationIdRef.current = newId;\n previousConversationIdRef.current = newId;\n }, [conversationId]);\n\n // Cleanup on component unmount\n useEffect(() => {\n return () => {\n if (streamRef.current) {\n streamRef.current.disconnect();\n streamRef.current = null;\n streamAgentIdRef.current = null;\n }\n };\n }, []);\n\n // Convert messages map to sorted array\n const messages = useMemo(() => {\n const allMessages = Array.from(messagesMap.values());\n // Sort by message ID (ULID) - ULIDs are lexicographically sortable and encode timestamp\n // This ensures chronological order regardless of message role\n allMessages.sort((a, b) => {\n return a.id.localeCompare(b.id);\n });\n return allMessages;\n }, [messagesMap]);\n\n // Initialize stream when conversationId is available\n // Uses currentConversationId to ensure we're working with the latest state\n // (which may have been updated by createConversation/switchConversation)\n useEffect(() => {\n // Don't initialize if no conversationId\n if (!currentConversationId) {\n // Clean up existing stream if conversationId is removed\n if (streamRef.current) {\n streamRef.current.clearMessages(); // Clear stream's message states\n streamRef.current.disconnect();\n streamRef.current = null;\n streamAgentIdRef.current = null;\n setIsConnected(false);\n }\n // Clear messages when conversation is removed\n setMessagesMap(new Map());\n currentConversationIdRef.current = null;\n return;\n }\n\n // Update ref to track current conversation ID\n currentConversationIdRef.current = currentConversationId;\n\n // Clean up previous listeners before setting up new ones\n // This ensures we don't accumulate duplicate listeners when reusing the stream\n listenersRef.current.forEach((unsub) => unsub());\n listenersRef.current = [];\n\n const unsubscribeFunctions: Array<() => void> = [];\n let isMounted = true;\n\n // Register cleanup function first to ensure it's available immediately\n // This prevents race conditions where component unmounts before cleanup is registered\n const cleanup = () => {\n isMounted = false;\n unsubscribeFunctions.forEach((unsub) => unsub());\n // Note: We don't disconnect the stream here because:\n // 1. The stream may be reused in the next effect run (when switching conversations)\n // 2. Disconnection is handled when conversationId becomes null (early return above)\n // 3. On component unmount, React will call cleanup, but the stream will be garbage collected\n // when the ref is cleared by the early return path if conversationId becomes null\n };\n\n try {\n // Ensure we have valid agentId before creating stream\n // Note: currentConversationId is already validated above (early return if falsy)\n if (!agentId) {\n throw new Error(`Invalid parameters: agentId is required`);\n }\n\n // Helper function to update messages from stream state\n // Defined inline here since it only uses refs and setState (both stable)\n const updateMessages = () => {\n const currentStream = streamRef.current;\n if (currentStream) {\n setMessagesMap((prev) => {\n const next = new Map(prev);\n // Add/update stream messages (assistant responses)\n for (const [id, streamMsg] of currentStream.messages) {\n // Only update if this is an assistant message (or doesn't exist yet)\n // Preserve user messages - they should never be overwritten by stream\n const existing = prev.get(id);\n if (!existing || existing.role === 'assistant') {\n // Merge with existing to preserve content if stream message is missing it\n // Stream message should have latest content, but fallback to existing as safety\n const mergedMsg: MessageState = {\n ...existing,\n ...streamMsg,\n id, // Ensure ID is preserved\n role: 'assistant' as const,\n // Prefer stream message content if it exists and is non-empty, otherwise preserve existing\n content:\n typeof streamMsg.content === 'string' && streamMsg.content.length > 0\n ? streamMsg.content\n : existing?.content || '',\n };\n next.set(id, mergedMsg);\n }\n }\n return next;\n });\n }\n };\n\n let stream: AgentChatStream;\n\n // Check if we need to recreate the stream (agentId changed or stream doesn't exist)\n const existingStream = streamRef.current;\n const agentIdChanged = existingStream && streamAgentIdRef.current !== agentId;\n\n if (agentIdChanged && existingStream) {\n // AgentId changed - preserve messages from old stream before disconnecting\n // The hook's messagesMap should already have all messages, but we ensure\n // any messages in the stream's state are preserved in the hook's state\n setMessagesMap((prev) => {\n const next = new Map(prev);\n // Preserve any messages from the old stream that aren't already in the map\n for (const [id, streamMsg] of existingStream.messages) {\n if (!next.has(id)) {\n next.set(id, streamMsg);\n } else {\n // If message exists, preserve user messages and merge assistant messages\n const existing = prev.get(id);\n if (existing?.role === 'user') {\n // Keep user message as-is\n continue;\n }\n // Merge assistant messages\n if (existing?.role === 'assistant' || !existing) {\n next.set(id, {\n ...existing,\n ...streamMsg,\n id,\n role: 'assistant' as const,\n });\n }\n }\n }\n return next;\n });\n // Now disconnect the old stream\n existingStream.disconnect();\n streamRef.current = null;\n streamAgentIdRef.current = null;\n }\n\n // If stream exists and agentId hasn't changed, update its conversation ID and reuse it\n // We still need to re-register event listeners to ensure fresh closures\n if (streamRef.current && !agentIdChanged) {\n stream = streamRef.current;\n // Update conversation ID if needed - this handles disconnection/reconnection\n stream.setConversationId(currentConversationId);\n } else {\n // Create new stream\n const streamOptions: StreamOptions = {\n baseUrl: options?.baseUrl,\n autoReconnect: options?.autoReconnect ?? true,\n reconnectDelay: options?.reconnectDelay,\n onError: (err) => {\n if (isMounted) {\n setError(err);\n }\n options?.onError?.(err);\n },\n };\n stream = new AgentChatStream(agentId, currentConversationId, streamOptions);\n streamRef.current = stream;\n streamAgentIdRef.current = agentId;\n }\n\n // Always set up event listeners to ensure fresh closures\n // Even if stream exists, we need to re-register listeners with current closures\n // (isMounted, updateMessages, etc.)\n\n // Subscribe to events\n // Note: We always re-register listeners even if stream exists to ensure fresh closures\n const unsubscribeOpen = stream.on('open', () => {\n if (isMounted) {\n setIsConnected(true);\n setError(null);\n }\n });\n unsubscribeFunctions.push(unsubscribeOpen);\n\n const unsubscribeClose = stream.on('close', () => {\n if (isMounted) {\n setIsConnected(false);\n }\n });\n unsubscribeFunctions.push(unsubscribeClose);\n\n const unsubscribeTextDelta = stream.on('text-delta', () => {\n if (isMounted) {\n updateMessages();\n }\n });\n unsubscribeFunctions.push(unsubscribeTextDelta);\n\n const unsubscribeFinish = stream.on('finish', () => {\n if (isMounted) {\n updateMessages();\n }\n });\n unsubscribeFunctions.push(unsubscribeFinish);\n\n const unsubscribeError = stream.on('error', (event: ErrorEvent) => {\n if (isMounted) {\n setError(new Error(event.errorText));\n }\n });\n unsubscribeFunctions.push(unsubscribeError);\n\n // Store unsubscribe functions for cleanup on next effect run\n listenersRef.current = unsubscribeFunctions;\n\n // Connect stream if autoConnect is enabled (default: true)\n if (options?.autoConnect !== false) {\n stream.connect();\n }\n } catch (err) {\n if (isMounted) {\n const initError = err instanceof Error ? err : new Error('Failed to initialize chat');\n setError(initError);\n console.error('[useAgentChat] Initialization error:', initError);\n }\n }\n\n // Return cleanup function\n return cleanup;\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n agentId,\n currentConversationId, // Use currentConversationId instead of prop to react to internal updates\n options?.baseUrl,\n options?.autoReconnect,\n options?.reconnectDelay,\n options?.autoConnect,\n options?.onError,\n // Note: updateMessages is defined inline in the effect and only uses refs/setState (stable)\n ]);\n\n // Send message function\n const sendMessage = useCallback(\n async (text: string) => {\n // Use currentConversationId to ensure we're using the latest state\n const convoId = currentConversationId;\n if (!convoId) {\n throw new Error('No conversation available. Create a conversation first.');\n }\n\n const clientOptions: ClientOptions = {\n baseUrl: options?.baseUrl,\n };\n\n try {\n setError(null);\n const response = await sendMessageApi(agentId, convoId, text, clientOptions);\n\n // Check if conversation is still active before adding message\n // This prevents race condition where conversation switches while API call is pending\n // Use ref to check latest conversation ID (closure value might be stale)\n if (currentConversationIdRef.current !== convoId) {\n // Conversation changed during API call, don't add message (it belongs to old conversation)\n return;\n }\n\n // Add user message with the messageId from the API response\n const userMessage: MessageState = {\n id: response.messageId,\n content: text,\n isComplete: true,\n role: 'user',\n };\n // Add user message to messages map\n setMessagesMap((prev) => {\n const next = new Map(prev);\n next.set(response.messageId, userMessage);\n return next;\n });\n } catch (err) {\n const sendError = err instanceof Error ? err : new Error('Failed to send message');\n setError(sendError);\n throw sendError;\n }\n },\n [agentId, currentConversationId, options?.baseUrl],\n );\n\n /**\n * Create a new conversation\n *\n * Creates a new conversation and switches the stream to it. The hook's internal state\n * is updated automatically. If you're managing conversationId as a prop, you should\n * also update it to keep it in sync:\n *\n * @example\n * ```typescript\n * const [conversationId, setConversationId] = useState<string | null>(null);\n * const { createConversation } = useAgentChat(agentId, conversationId);\n *\n * const handleNewChat = async () => {\n * const newId = await createConversation();\n * setConversationId(newId); // Update prop to keep it in sync\n * };\n * ```\n *\n * @returns The new conversation ID\n */\n const createConversation = useCallback(async (): Promise<string> => {\n const clientOptions: ClientOptions = {\n baseUrl: options?.baseUrl,\n };\n const { conversationId: newConvoId } = await createConversationApi(agentId, clientOptions);\n\n // Update internal state first - this will trigger useEffect to reinitialize stream\n setCurrentConversationId(newConvoId);\n currentConversationIdRef.current = newConvoId;\n // Clear messages when switching to new conversation\n setMessagesMap(new Map());\n\n // If stream exists, update it to the new conversation ID\n // This handles the case where stream was already initialized\n if (streamRef.current) {\n streamRef.current.setConversationId(newConvoId);\n }\n\n return newConvoId;\n }, [agentId, options?.baseUrl]);\n\n /**\n * Switch to a different conversation\n *\n * Switches the stream to an existing conversation. The hook's internal state\n * is updated automatically. If you're managing conversationId as a prop, you should\n * also update it to keep it in sync:\n *\n * @example\n * ```typescript\n * const [conversationId, setConversationId] = useState<string | null>(null);\n * const { switchConversation } = useAgentChat(agentId, conversationId);\n *\n * const handleSwitchChat = (existingConvoId: string) => {\n * switchConversation(existingConvoId);\n * setConversationId(existingConvoId); // Update prop to keep it in sync\n * };\n * ```\n *\n * @param convoId - The conversation ID to switch to\n */\n const switchConversation = useCallback((convoId: string) => {\n // Update internal state first - this will trigger useEffect to reinitialize stream\n setCurrentConversationId(convoId);\n currentConversationIdRef.current = convoId;\n // Clear messages when switching conversations\n setMessagesMap(new Map());\n\n // If stream exists, update it to the new conversation ID\n // This handles the case where stream was already initialized\n if (streamRef.current) {\n streamRef.current.setConversationId(convoId);\n }\n }, []);\n\n /**\n * Manually connect the stream\n *\n * Useful when `autoConnect` is set to `false`. The stream must have a valid\n * conversationId before connecting.\n *\n * @example\n * ```typescript\n * const { connect, conversationId } = useAgentChat(agentId, conversationId, {\n * autoConnect: false,\n * });\n *\n * // Connect manually after conversation is created\n * const handleStartChat = async () => {\n * const { conversationId: newId } = await createConversation(agentId);\n * setConversationId(newId);\n * // Stream will be initialized but not connected due to autoConnect: false\n * // Manually connect it\n * connect();\n * };\n * ```\n */\n const connect = useCallback(() => {\n if (!currentConversationId) {\n throw new Error('Cannot connect: no conversation available. Create a conversation first.');\n }\n\n if (!streamRef.current) {\n throw new Error('Cannot connect: stream not initialized. Ensure conversationId is provided.');\n }\n\n streamRef.current.connect();\n }, [currentConversationId]);\n\n return {\n sendMessage,\n messages,\n isConnected,\n error,\n conversationId: currentConversationId,\n createConversation,\n switchConversation,\n connect,\n };\n}\n"
}
},
"index.ts": {
"file": {
"contents": "/**\n * Agent Chat SDK for Taskade Genesis (Legacy)\n *\n * @deprecated Use `@/lib/agent-chat/v2` instead for new code.\n *\n * Low-level SDK for building AI Agent Chat interfaces in React applications.\n * Provides API client, SSE stream management, and optional React hooks.\n *\n * @example\n * ```typescript\n * // React hook usage\n * import { useAgentChat, createConversation } from '@/lib/agent-chat';\n * import { useState } from 'react';\n *\n * function ChatComponent() {\n * const [conversationId, setConversationId] = useState<string | null>(null);\n * const { sendMessage, messages, isConnected } = useAgentChat(agentId, conversationId);\n *\n * const handleStartChat = async () => {\n * const { conversationId: newId } = await createConversation(agentId);\n * setConversationId(newId);\n * };\n *\n * return (\n * <div>\n * {!conversationId && <button onClick={handleStartChat}>Start Chat</button>}\n * {messages.map(msg => (\n * <div key={msg.id}>\n * {msg.role === 'user' ? 'You: ' : 'Agent: '}\n * {msg.content}\n * </div>\n * ))}\n * <button onClick={() => sendMessage('Hello!')}>Send</button>\n * </div>\n * );\n * }\n * ```\n */\n\n// Core API client (for advanced usage)\nexport type { ClientOptions } from './client';\nexport { createConversation, sendMessage } from './client';\n\n// Stream manager (for advanced usage)\nexport { AgentChatStream } from './stream';\n\n// Types\nexport type {\n CreateConversationResponse,\n ErrorEvent,\n ErrorHandler,\n FinishEvent,\n FinishHandler,\n MessageState,\n SendMessageResponse,\n StartEvent,\n StreamEvent,\n StreamEventHandler,\n StreamOptions,\n TextDeltaEvent,\n TextDeltaHandler,\n TextEndEvent,\n TextStartEvent,\n ToolCallEndEvent,\n ToolCallState,\n ToolInputAvailableEvent,\n ToolInputDeltaEvent,\n ToolInputStartEvent,\n ToolOutputAvailableEvent,\n} from './types';\n\n// React hook (main API)\nexport type { UseAgentChatOptions, UseAgentChatReturn } from './hooks';\nexport { useAgentChat } from './hooks';\n"
}
},
"types.ts": {
"file": {
"contents": "import { z } from 'zod';\n\n/**\n * SSE Event schemas matching backend AgentPublicConversationMessageStreamResponseSchema\n */\nexport const StreamEventSchema = z.union([\n z.object({\n type: z.literal('start'),\n messageId: z.string(),\n }),\n z.object({\n type: z.literal('text-start'),\n id: z.string(),\n }),\n z.object({\n type: z.literal('text-delta'),\n id: z.string(),\n delta: z.string(),\n }),\n z.object({\n type: z.literal('text-end'),\n id: z.string(),\n }),\n z.object({\n type: z.literal('tool-input-start'),\n toolCallId: z.string(),\n toolName: z.string(),\n messageId: z.string().optional(),\n }),\n z.object({\n type: z.literal('tool-input-delta'),\n toolCallId: z.string(),\n inputTextDelta: z.string(),\n messageId: z.string().optional(),\n }),\n z.object({\n type: z.literal('tool-input-available'),\n toolCallId: z.string(),\n input: z.unknown(),\n messageId: z.string().optional(),\n }),\n z.object({\n type: z.literal('tool-output-available'),\n toolCallId: z.string(),\n output: z.unknown(),\n messageId: z.string().optional(),\n }),\n z.object({\n type: z.literal('tool-call-end'),\n toolCallId: z.string(),\n messageId: z.string().optional(),\n }),\n z.object({\n type: z.literal('finish'),\n }),\n z.object({\n type: z.literal('error'),\n errorText: z.string(),\n }),\n]);\n\nexport type StreamEvent = z.infer<typeof StreamEventSchema>;\n\n/**\n * Specific event types for type narrowing\n */\nexport type StartEvent = Extract<StreamEvent, { type: 'start' }>;\nexport type TextStartEvent = Extract<StreamEvent, { type: 'text-start' }>;\nexport type TextDeltaEvent = Extract<StreamEvent, { type: 'text-delta' }>;\nexport type TextEndEvent = Extract<StreamEvent, { type: 'text-end' }>;\nexport type ToolInputStartEvent = Extract<StreamEvent, { type: 'tool-input-start' }>;\nexport type ToolInputDeltaEvent = Extract<StreamEvent, { type: 'tool-input-delta' }>;\nexport type ToolInputAvailableEvent = Extract<StreamEvent, { type: 'tool-input-available' }>;\nexport type ToolOutputAvailableEvent = Extract<StreamEvent, { type: 'tool-output-available' }>;\nexport type ToolCallEndEvent = Extract<StreamEvent, { type: 'tool-call-end' }>;\nexport type FinishEvent = Extract<StreamEvent, { type: 'finish' }>;\nexport type ErrorEvent = Extract<StreamEvent, { type: 'error' }>;\n\n/**\n * API Response types\n */\nexport interface CreateConversationResponse {\n ok: boolean;\n conversationId: string;\n}\n\nexport interface SendMessageResponse {\n ok: boolean;\n messageId: string;\n}\n\n/**\n * Configuration options for stream\n */\nexport interface StreamOptions {\n /** Base URL for API requests (defaults to relative paths) */\n baseUrl?: string;\n /** Automatically reconnect on disconnect (default: true) */\n autoReconnect?: boolean;\n /** Delay in ms before reconnecting (default: 1000) */\n reconnectDelay?: number;\n /**\n * Callback for stream errors\n * @default Logs to console.error\n * @remarks In production, provide your own error handler to properly handle errors\n */\n onError?: (error: Error) => void;\n}\n\n/**\n * Accumulated message state\n */\nexport interface MessageState {\n id: string;\n content: string;\n isComplete: boolean;\n role: 'user' | 'assistant';\n toolCalls?: ToolCallState[];\n}\n\n/**\n * Tool call state\n */\nexport interface ToolCallState {\n toolCallId: string;\n toolName: string;\n input?: unknown;\n output?: unknown;\n isComplete: boolean;\n}\n\n/**\n * Event handler types\n */\nexport type StreamEventHandler = (event: StreamEvent) => void;\nexport type TextDeltaHandler = (event: TextDeltaEvent) => void;\nexport type FinishHandler = (event: FinishEvent) => void;\nexport type ErrorHandler = (event: ErrorEvent) => void;\n"
}
},
"README.md": {
"file": {
"contents": "# Agent Chat SDK (Legacy)\n\n> **DEPRECATED**: Do not use this SDK for new code. Use `@/lib/agent-chat/v2` instead — see `src/lib/agent-chat/v2/README.md` for docs.\n\n---\n\nSimple SDK for building AI Agent Chat interfaces in Taskade Genesis apps.\n\n**Key Features:**\n- Manual conversation creation (consumer must create conversation before use)\n- Stream opens when conversationId is provided\n- Stream stays open throughout (handles reconnection automatically)\n- Supports creating/switching conversations\n\n## Quick Start\n\n```typescript\nimport { useAgentChat, createConversation } from '@/lib/agent-chat';\nimport { useState } from 'react';\n\nfunction ChatComponent() {\n const [conversationId, setConversationId] = useState<string | null>(null);\n const { sendMessage, messages, isConnected } = useAgentChat(agentId, conversationId);\n\n // Create conversation manually\n const handleStartChat = async () => {\n const { conversationId: newId } = await createConversation(agentId);\n setConversationId(newId);\n };\n \n return (\n <div>\n {!conversationId && <button onClick={handleStartChat}>Start Chat</button>}\n {messages.map(msg => (\n <div key={msg.id}>\n {msg.role === 'user' ? 'You: ' : 'Agent: '}\n {msg.content}\n {msg.toolCalls && msg.toolCalls.length > 0 && (\n <div>Tool calls: {msg.toolCalls.length}</div>\n )}\n </div>\n ))}\n <button onClick={() => sendMessage('Hello!')}>Send</button>\n </div>\n );\n}\n```\n\n## Multiple Conversations\n\n```typescript\nconst [conversationId, setConversationId] = useState<string | null>(null);\nconst {\n sendMessage,\n messages,\n createConversation: createNewConversation, // Create new conversation\n switchConversation, // Switch to existing conversation\n} = useAgentChat(agentId, conversationId);\n\n// Create new chat\nconst handleNewChat = async () => {\n const newConvoId = await createNewConversation();\n setConversationId(newConvoId);\n};\n\n// Switch to different conversation\nconst handleSwitchChat = (existingConvoId: string) => {\n switchConversation(existingConvoId);\n setConversationId(existingConvoId);\n};\n```\n\n## API Reference\n\n**`useAgentChat(agentId, conversationId)`**\n- `agentId` - The agent ID (required)\n- `conversationId` - The conversation ID (required, pass `null` if not yet created)\n- `sendMessage(text)` - Send message to current conversation (requires conversationId)\n- `messages` - Array of messages (MessageState[])\n- `isConnected` - Stream connection status\n- `conversationId` - Current conversation ID (from hook return)\n- `createConversation()` - Create new conversation (returns ID)\n- `switchConversation(id)` - Switch to different conversation\n- `error` - Current error, if any\n\n**Note:** The hook will only connect the stream when `conversationId` is provided (not `null`). You must create a conversation manually before the stream can connect.\n\n**Advanced (low-level):**\n- `createConversation(agentId)` - Direct API call\n- `sendMessage(agentId, conversationId, text)` - Direct API call\n- `AgentChatStream` - Stream manager class\n\nSee `index.ts` for full type exports.\n\n## Requirements\n\n- Agent must have **public visibility** enabled before creating conversation\n- Stream stays open permanently (never close after first response)\n- Text deltas are automatically accumulated (append, never replace)\n"
}
},
"client.ts": {
"file": {
"contents": "import type { CreateConversationResponse, SendMessageResponse } from './types';\n\n/**\n * Configuration for API client\n */\nexport interface ClientOptions {\n /** Base URL for API requests (defaults to relative paths) */\n baseUrl?: string;\n}\n\n/**\n * Checks if a string is null, undefined, or empty after trimming\n */\nfunction isEmptyString(value: string | null | undefined): boolean {\n return value == null || value.trim().length === 0;\n}\n\n/**\n * Creates a new public agent conversation\n *\n * @param agentId - The agent ID\n * @param options - Optional client configuration\n * @returns Promise resolving to conversation ID\n * @throws Error if conversation creation fails\n *\n * @example\n * ```typescript\n * const { conversationId } = await createConversation('agent-456');\n * ```\n */\nexport async function createConversation(\n agentId: string,\n options?: ClientOptions,\n): Promise<CreateConversationResponse> {\n if (isEmptyString(agentId)) {\n throw new Error('Agent ID cannot be empty');\n }\n\n const baseUrl = options?.baseUrl ?? '';\n const url = `${baseUrl}/api/taskade/agents/${encodeURIComponent(agentId)}/public-conversations`;\n\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n\n // Read response body once\n const contentType = response.headers.get('content-type') || '';\n const responseText = await response.text().catch(() => '');\n\n if (!response.ok) {\n throw new Error(\n `Failed to create conversation: ${response.status} ${responseText || 'Unknown error'}`,\n );\n }\n\n if (!contentType.includes('application/json')) {\n throw new Error(\n `Invalid response format: expected JSON, got ${contentType}. Response: ${responseText.substring(\n 0,\n 100,\n )}`,\n );\n }\n\n try {\n const data = JSON.parse(responseText);\n return data as CreateConversationResponse;\n } catch (err) {\n throw new Error(\n `Failed to parse JSON response: ${\n err instanceof Error ? err.message : 'Unknown error'\n }. Response: ${responseText.substring(0, 200)}`,\n );\n }\n}\n\n/**\n * Sends a message to an existing conversation\n *\n * @param agentId - The agent ID\n * @param conversationId - The conversation ID\n * @param text - The message text to send\n * @param options - Optional client configuration\n * @returns Promise resolving when message is sent\n * @throws Error if message sending fails (e.g., conversation not idle, conversation ended)\n *\n * @example\n * ```typescript\n * await sendMessage('agent-456', 'convo-789', 'Hello!');\n * ```\n */\nexport async function sendMessage(\n agentId: string,\n conversationId: string,\n text: string,\n options?: ClientOptions,\n): Promise<SendMessageResponse> {\n if (isEmptyString(agentId)) {\n throw new Error('Agent ID cannot be empty');\n }\n\n if (isEmptyString(conversationId)) {\n throw new Error('Conversation ID cannot be empty');\n }\n\n const trimmedText = text.trim();\n if (isEmptyString(trimmedText)) {\n throw new Error('Message text cannot be empty');\n }\n\n const baseUrl = options?.baseUrl ?? '';\n const url = `${baseUrl}/api/taskade/agents/${encodeURIComponent(\n agentId,\n )}/public-conversations/${encodeURIComponent(conversationId)}/messages`;\n\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({ text: trimmedText }),\n });\n\n // Read response body once\n const contentType = response.headers.get('content-type') || '';\n const responseText = await response.text().catch(() => '');\n\n if (!response.ok) {\n // Parse error message if available\n let errorMessage = `Failed to send message: ${response.status}`;\n try {\n const errorData = JSON.parse(responseText);\n if (errorData.message) {\n errorMessage = errorData.message;\n }\n } catch {\n // Use default error message with response text\n if (responseText) {\n errorMessage = `${errorMessage}: ${responseText.substring(0, 100)}`;\n }\n }\n throw new Error(errorMessage);\n }\n\n if (!contentType.includes('application/json')) {\n throw new Error(\n `Invalid response format: expected JSON, got ${contentType}. Response: ${responseText.substring(\n 0,\n 100,\n )}`,\n );\n }\n\n try {\n const data = JSON.parse(responseText);\n return data as SendMessageResponse;\n } catch (err) {\n throw new Error(\n `Failed to parse JSON response: ${\n err instanceof Error ? err.message : 'Unknown error'\n }. Response: ${responseText.substring(0, 200)}`,\n );\n }\n}\n"
}
},
"stream.ts": {
"file": {
"contents": "import type {\n ErrorEvent,\n FinishEvent,\n MessageState,\n StreamEvent,\n StreamEventHandler,\n StreamOptions,\n TextDeltaEvent,\n} from './types';\nimport { StreamEventSchema } from './types';\n\n/**\n * Event emitter interface for stream events\n */\ntype EventMap = {\n event: StreamEventHandler;\n 'text-delta': (event: TextDeltaEvent) => void;\n finish: (event: FinishEvent) => void;\n error: (event: ErrorEvent) => void;\n open: () => void;\n close: () => void;\n};\n\n/**\n * Manages SSE connection for agent conversation streaming\n *\n * Handles connection lifecycle, event parsing, and message state accumulation.\n * The stream stays open permanently and handles all messages in the conversation.\n *\n * **Memory Management**: The `messageStates` Map accumulates messages throughout the conversation.\n * For long-running conversations, call `clearMessages()` when switching conversations\n * or create a new stream instance for new conversations.\n *\n * @example\n * ```typescript\n * const stream = new AgentChatStream('agent-456', 'convo-789');\n * stream.on('text-delta', ({ id, delta }) => {\n * // Append delta to message content\n * });\n * stream.connect();\n * ```\n */\nexport class AgentChatStream {\n private agentId: string;\n private conversationId: string;\n private options: Required<StreamOptions>;\n private eventSource: EventSource | null = null;\n private listeners: Map<keyof EventMap, Set<EventMap[keyof EventMap]>> = new Map();\n private reconnectTimeout: ReturnType<typeof setTimeout> | null = null;\n private messageStates: Map<string, MessageState> = new Map();\n private currentMessageId: string | null = null;\n private isConnecting = false;\n\n constructor(agentId: string, conversationId: string, options?: StreamOptions) {\n this.agentId = agentId;\n this.conversationId = conversationId;\n this.options = {\n baseUrl: options?.baseUrl ?? '',\n autoReconnect: options?.autoReconnect ?? true,\n reconnectDelay: options?.reconnectDelay ?? 1000,\n onError:\n options?.onError ??\n ((error: Error) => {\n // Log errors by default to help with debugging\n // In production, consumers should provide their own error handler\n console.error('[AgentChatStream] Unhandled error:', error);\n }),\n };\n }\n\n /**\n * Clear all accumulated message states\n * Useful when switching conversations or resetting the stream state\n */\n clearMessages(): void {\n this.messageStates.clear();\n this.currentMessageId = null;\n }\n\n /**\n * Update the conversation ID for this stream\n * Useful when switching to a new conversation while keeping stream open\n * Automatically clears message states when switching conversations\n */\n setConversationId(conversationId: string): void {\n if (this.conversationId === conversationId) {\n return;\n }\n // Track both connected and connecting states to handle race conditions\n const wasConnected = this.isConnected;\n const wasConnecting = this.isConnecting;\n this.disconnect();\n this.conversationId = conversationId;\n // Clear message state when switching conversations\n this.clearMessages();\n // Reconnect if we were connected or in the process of connecting\n if (wasConnected || wasConnecting) {\n this.connect();\n }\n }\n\n /**\n * Opens SSE connection to stream endpoint\n * Stream stays open permanently - never close after first response\n */\n connect(): void {\n // Check isConnecting first since it's set synchronously before any async work\n // This prevents race conditions where connect() is called multiple times\n // before eventSource is assigned\n if (this.isConnecting) {\n return; // Already connecting\n }\n\n if (this.eventSource?.readyState === EventSource.OPEN) {\n return; // Already connected\n }\n\n this.isConnecting = true;\n this.disconnect(); // Clean up any existing connection\n\n const baseUrl = this.options.baseUrl;\n const url = `${baseUrl}/api/taskade/agents/${encodeURIComponent(\n this.agentId,\n )}/public-conversations/${encodeURIComponent(this.conversationId)}/stream`;\n\n // Validate required parameters\n if (!this.agentId || !this.conversationId) {\n const error = new Error(\n `Missing required parameters: agentId=${this.agentId || 'null'}, conversationId=${\n this.conversationId || 'null'\n }`,\n );\n this.options.onError(error);\n this.emit('error', {\n type: 'error',\n errorText: error.message,\n });\n this.isConnecting = false;\n return;\n }\n\n try {\n this.eventSource = new EventSource(url);\n\n this.eventSource.onopen = () => {\n this.isConnecting = false;\n this.emit('open');\n };\n\n this.eventSource.onmessage = (e) => {\n try {\n const data = JSON.parse(e.data);\n const event = StreamEventSchema.parse(data);\n this.handleEvent(event);\n } catch (error) {\n const parseError = error instanceof Error ? error : new Error('Failed to parse event');\n this.options.onError(parseError);\n this.emit('error', {\n type: 'error',\n errorText: `Parse error: ${parseError.message}`,\n });\n }\n };\n\n this.eventSource.onerror = () => {\n this.isConnecting = false;\n\n // EventSource doesn't provide detailed error info, but we can check readyState\n if (this.eventSource?.readyState === EventSource.CLOSED) {\n this.emit('close');\n\n // Auto-reconnect if enabled\n if (this.options.autoReconnect) {\n this.scheduleReconnect();\n }\n } else if (this.eventSource?.readyState === EventSource.CONNECTING) {\n // Still connecting, might be a temporary issue\n // Don't emit error yet, wait for connection to complete or fail\n } else {\n // Connection error\n const error = new Error('EventSource connection error');\n this.options.onError(error);\n this.emit('error', {\n type: 'error',\n errorText: 'Stream connection error',\n });\n }\n };\n } catch (error) {\n // EventSource constructor threw an error (e.g., invalid URL)\n this.isConnecting = false;\n const constructorError =\n error instanceof Error ? error : new Error('Failed to create EventSource');\n this.options.onError(constructorError);\n this.emit('error', {\n type: 'error',\n errorText: `Failed to create connection: ${constructorError.message}`,\n });\n }\n }\n\n /**\n * Closes SSE connection\n */\n disconnect(): void {\n if (this.reconnectTimeout) {\n clearTimeout(this.reconnectTimeout);\n this.reconnectTimeout = null;\n }\n\n if (this.eventSource) {\n this.eventSource.close();\n this.eventSource = null;\n }\n\n this.isConnecting = false;\n }\n\n /**\n * Subscribe to stream events\n *\n * @param event - Event type to listen for\n * @param handler - Callback function\n * @returns Unsubscribe function\n */\n on<K extends keyof EventMap>(event: K, handler: EventMap[K]): () => void {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set());\n }\n this.listeners.get(event)!.add(handler);\n\n // Return unsubscribe function\n return () => {\n this.listeners.get(event)?.delete(handler);\n };\n }\n\n /**\n * Unsubscribe from stream events\n */\n off<K extends keyof EventMap>(event: K, handler: EventMap[K]): void {\n this.listeners.get(event)?.delete(handler);\n }\n\n /**\n * Get current connection state\n */\n get isConnected(): boolean {\n // Check readyState directly - more reliable across browsers\n return this.eventSource?.readyState === EventSource.OPEN;\n }\n\n /**\n * Get accumulated message states\n */\n get messages(): Map<string, MessageState> {\n return new Map(this.messageStates);\n }\n\n /**\n * Get message state by ID\n */\n getMessage(id: string): MessageState | undefined {\n return this.messageStates.get(id);\n }\n\n /**\n * Handle incoming SSE event\n */\n private handleEvent(event: StreamEvent): void {\n // Emit generic event\n this.emit('event', event);\n\n switch (event.type) {\n case 'start':\n this.currentMessageId = event.messageId;\n // Always create/update message on start to ensure it exists\n if (!this.messageStates.has(event.messageId)) {\n this.messageStates.set(event.messageId, {\n id: event.messageId,\n content: '',\n isComplete: false,\n role: 'assistant',\n });\n }\n break;\n\n case 'text-start':\n // event.id is a text segment ID, but text belongs to the current message (from 'start' event)\n // Ensure the current message exists\n if (this.currentMessageId) {\n if (!this.messageStates.has(this.currentMessageId)) {\n this.messageStates.set(this.currentMessageId, {\n id: this.currentMessageId,\n content: '',\n isComplete: false,\n role: 'assistant',\n });\n }\n }\n break;\n\n case 'text-delta': {\n // event.id is a text segment ID, but text belongs to the current message (from 'start' event)\n // Append delta to the current message's content\n if (this.currentMessageId) {\n const existingState = this.messageStates.get(this.currentMessageId);\n if (existingState) {\n // Create new object with updated content to ensure change detection\n const updatedState: MessageState = {\n ...existingState,\n content: existingState.content + event.delta,\n };\n this.messageStates.set(this.currentMessageId, updatedState);\n } else {\n // Create message if it doesn't exist (shouldn't happen, but handle gracefully)\n this.messageStates.set(this.currentMessageId, {\n id: this.currentMessageId,\n content: event.delta,\n isComplete: false,\n role: 'assistant',\n });\n }\n }\n this.emit('text-delta', event);\n break;\n }\n\n case 'text-end': {\n // event.id is a text segment ID, but text belongs to the current message (from 'start' event)\n // Text part is complete, but don't mark entire message as complete (message completes on 'finish' event)\n break;\n }\n\n case 'tool-input-start': {\n // Use messageId from event if available (for child segments), otherwise fall back to currentMessageId\n const messageId =\n 'messageId' in event && event.messageId ? event.messageId : this.currentMessageId;\n const toolMessage = messageId ? this.messageStates.get(messageId) : undefined;\n if (toolMessage) {\n const updatedState: MessageState = {\n ...toolMessage,\n toolCalls: [\n ...(toolMessage.toolCalls ?? []),\n {\n toolCallId: event.toolCallId,\n toolName: event.toolName,\n isComplete: false,\n },\n ],\n };\n this.messageStates.set(toolMessage.id, updatedState);\n }\n break;\n }\n\n case 'tool-input-delta': {\n // Use messageId from event if available (for child segments), otherwise fall back to currentMessageId\n const messageId =\n 'messageId' in event && event.messageId ? event.messageId : this.currentMessageId;\n const toolMessage = messageId ? this.messageStates.get(messageId) : undefined;\n if (toolMessage?.toolCalls) {\n const toolCallIndex = toolMessage.toolCalls.findIndex(\n (tc) => tc.toolCallId === event.toolCallId,\n );\n if (toolCallIndex !== -1) {\n const existingToolCall = toolMessage.toolCalls[toolCallIndex];\n // Accumulate inputTextDelta into input (stored as string during streaming)\n // When tool-input-available arrives, it will replace this with the parsed object\n const currentInput =\n typeof existingToolCall.input === 'string' ? existingToolCall.input : '';\n const updatedState: MessageState = {\n ...toolMessage,\n toolCalls: toolMessage.toolCalls.map((tc, index) =>\n index === toolCallIndex\n ? { ...tc, input: currentInput + event.inputTextDelta }\n : tc,\n ),\n };\n this.messageStates.set(toolMessage.id, updatedState);\n }\n }\n break;\n }\n\n case 'tool-input-available': {\n // Use messageId from event if available (for child segments), otherwise fall back to currentMessageId\n const messageId =\n 'messageId' in event && event.messageId ? event.messageId : this.currentMessageId;\n const toolMessage = messageId ? this.messageStates.get(messageId) : undefined;\n if (toolMessage?.toolCalls) {\n const toolCallIndex = toolMessage.toolCalls.findIndex(\n (tc) => tc.toolCallId === event.toolCallId,\n );\n if (toolCallIndex !== -1) {\n const updatedState: MessageState = {\n ...toolMessage,\n toolCalls: toolMessage.toolCalls.map((tc, index) =>\n index === toolCallIndex ? { ...tc, input: event.input } : tc,\n ),\n };\n this.messageStates.set(toolMessage.id, updatedState);\n }\n }\n break;\n }\n\n case 'tool-output-available': {\n // Use messageId from event if available (for child segments), otherwise fall back to currentMessageId\n const messageId =\n 'messageId' in event && event.messageId ? event.messageId : this.currentMessageId;\n const toolMessage = messageId ? this.messageStates.get(messageId) : undefined;\n if (toolMessage?.toolCalls) {\n const toolCallIndex = toolMessage.toolCalls.findIndex(\n (tc) => tc.toolCallId === event.toolCallId,\n );\n if (toolCallIndex !== -1) {\n const updatedState: MessageState = {\n ...toolMessage,\n toolCalls: toolMessage.toolCalls.map((tc, index) =>\n index === toolCallIndex ? { ...tc, output: event.output } : tc,\n ),\n };\n this.messageStates.set(toolMessage.id, updatedState);\n }\n }\n break;\n }\n\n case 'tool-call-end': {\n // Use messageId from event if available (for child segments), otherwise fall back to currentMessageId\n const messageId =\n 'messageId' in event && event.messageId ? event.messageId : this.currentMessageId;\n const toolMessage = messageId ? this.messageStates.get(messageId) : undefined;\n if (toolMessage?.toolCalls) {\n const toolCallIndex = toolMessage.toolCalls.findIndex(\n (tc) => tc.toolCallId === event.toolCallId,\n );\n if (toolCallIndex !== -1) {\n const updatedState: MessageState = {\n ...toolMessage,\n toolCalls: toolMessage.toolCalls.map((tc, index) =>\n index === toolCallIndex ? { ...tc, isComplete: true } : tc,\n ),\n };\n this.messageStates.set(toolMessage.id, updatedState);\n }\n }\n break;\n }\n\n case 'finish':\n if (this.currentMessageId) {\n const finishedMessage = this.messageStates.get(this.currentMessageId);\n if (finishedMessage) {\n const updatedState: MessageState = {\n ...finishedMessage,\n isComplete: true,\n };\n this.messageStates.set(this.currentMessageId, updatedState);\n }\n }\n this.emit('finish', event);\n break;\n\n case 'error':\n this.emit('error', event);\n this.options.onError(new Error(event.errorText));\n break;\n }\n }\n\n /**\n * Emit event to all listeners\n */\n private emit<K extends keyof EventMap>(event: K, ...args: Parameters<EventMap[K]>): void {\n const handlers = this.listeners.get(event);\n if (handlers) {\n handlers.forEach((handler) => {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (handler as (...args: any[]) => void)(...args);\n } catch (error) {\n this.options.onError(error instanceof Error ? error : new Error('Handler error'));\n }\n });\n }\n }\n\n /**\n * Schedule reconnection attempt\n */\n private scheduleReconnect(): void {\n if (this.reconnectTimeout) {\n return; // Already scheduled\n }\n\n this.reconnectTimeout = setTimeout(() => {\n this.reconnectTimeout = null;\n if (this.options.autoReconnect) {\n this.connect();\n }\n }, this.options.reconnectDelay);\n }\n}\n"
}
}
}
},
"genesis.tsx": {
"file": {
"contents": "import type {\n LogFunction,\n LoggerEntryInput,\n SpaceAppLogLifecycleData,\n} from '@taskade/parade-shared';\nimport * as React from 'react';\nimport { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';\n\n// ---------------------------------------------------------------------------\n// Lifecycle logger (injected by Director Preview via window global)\n// ---------------------------------------------------------------------------\n\ninterface GenesisLogger {\n log: LogFunction;\n}\ndeclare global {\n interface Window {\n __TASKADE_APP_LIFECYCLE_LOGGER__?: GenesisLogger;\n }\n}\n\n/**\n * Returns the Taskade lifecycle logger if running inside Director Preview.\n * The logger is injected into `window.__TASKADE_APP_LIFECYCLE_LOGGER__` by\n * the preview inject script. Returns `null` in published mode.\n */\nexport function getGenesisAppLifecycleLogger(): GenesisLogger | null {\n if (typeof window === 'undefined') {\n return null;\n }\n return window.__TASKADE_APP_LIFECYCLE_LOGGER__ ?? null;\n}\n\n/**\n * Report a runtime error to the parent frame via postMessage.\n * In preview mode this triggers the \"Fix with AI\" popup.\n * In published mode this is a no-op (logger not available).\n */\nexport function reportGenesisError(\n code: 'error.boundary',\n error: unknown,\n componentStack?: string | null,\n) {\n getGenesisAppLifecycleLogger()?.log({\n level: 'error',\n message: 'Runtime Error',\n data: {\n code,\n message: error instanceof Error ? error.message : String(error),\n stack: [error instanceof Error ? error.stack : undefined, componentStack]\n .filter(Boolean)\n .join('\\n'),\n } satisfies SpaceAppLogLifecycleData,\n } satisfies LoggerEntryInput);\n}\n\n// ---------------------------------------------------------------------------\n// Error boundary fallback UI (inline styles for resilience)\n// ---------------------------------------------------------------------------\n\n/** Fallback UI shown when the ErrorBoundary catches a render error. */\nfunction ErrorFallback({ error }: { error: Error }) {\n return (\n <div\n style={{\n padding: '2rem',\n fontFamily: 'system-ui, sans-serif',\n maxWidth: 600,\n margin: '0 auto',\n }}\n >\n <h2 style={{ margin: '0 0 0.5rem', fontSize: '1.25rem' }}>Something went wrong</h2>\n <p style={{ color: '#999', margin: '0 0 1rem', fontSize: '0.95rem' }}>\n The app encountered an error. Try refreshing the page.\n </p>\n <pre\n style={{\n background: '#f5f5f5',\n padding: '1rem',\n borderRadius: '8px',\n overflow: 'auto',\n fontSize: '0.8rem',\n color: '#dc2626',\n margin: 0,\n whiteSpace: 'pre-wrap',\n wordBreak: 'break-word',\n }}\n >\n {String(error)}\n </pre>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// GenesisRoot\n// ---------------------------------------------------------------------------\n\n/**\n * Genesis root wrapper — must be provided by the base template, not LLM-generated.\n *\n * Wraps children in an ErrorBoundary that:\n * 1. Catches render-phase errors and shows {@link ErrorFallback} instead of a blank page.\n * 2. Reports the error to the parent frame via {@link reportGenesisError} so\n * Director Preview can show the \"Fix with AI\" popup.\n */\nexport function GenesisRoot({ children }: { children: React.ReactNode }) {\n return (\n <ReactErrorBoundary\n FallbackComponent={ErrorFallback}\n onError={(error, info) => {\n console.error('[Genesis] Uncaught render error:', error, info);\n reportGenesisError('error.boundary', error, info.componentStack);\n }}\n >\n {children}\n </ReactErrorBoundary>\n );\n}\n"
}
},
"theme-bridge.ts": {
"file": {
"contents": "/**\n * Listen for TASKADE_THEME_UPDATE messages from the parent Taskade 01KP2NYSV3FEZ09WSYFBGKCM6C\n * and apply CSS variable overrides to the document root in real-time.\n * Also responds to TASKADE_THEME_READ requests with current CSS variable values.\n *\n * Theme overrides are injected via a <style> element rather than inline styles\n * so that both :root (light) and .dark selectors are properly handled.\n */\n\nconst THEME_STYLE_ID = 'taskade-theme-overrides';\n\n/**\n * Parse an HSL string \"H S% L%\" into its numeric components.\n */\nfunction parseHsl(hsl: string): { h: number; s: number; l: number } | null {\n const parts = hsl.trim().split(/\\s+/);\n if (parts.length < 3) return null;\n const h = parseFloat(parts[0]!);\n const s = parseFloat(parts[1]!);\n const l = parseFloat(parts[2]!);\n if (isNaN(h) || isNaN(s) || isNaN(l)) return null;\n return { h, s, l };\n}\n\n/**\n * Derive a dark-mode HSL value from a light-mode HSL value based on the\n * CSS variable role. Returns a new \"H S% L%\" string.\n */\nfunction deriveDarkValue(key: string, hsl: string): string {\n const parsed = parseHsl(hsl);\n if (parsed == null) return hsl;\n const { h, s, l } = parsed;\n\n // Non-color variables — pass through unchanged\n if (key === '--radius') return hsl;\n\n // Derive dark-mode lightness based on the variable's role\n let darkL: number;\n let darkS = s;\n\n if (['--background', '--card', '--popover'].includes(key)) {\n // Light backgrounds → very dark\n darkL = Math.max(3, Math.min(8, 100 - l));\n } else if (\n ['--foreground', '--card-foreground', '--popover-foreground'].includes(key)\n ) {\n // Dark foregrounds → very light\n darkL = Math.min(95, Math.max(85, 100 - l));\n } else if (key === '--primary') {\n // Invert: dark primary in light → light primary in dark\n darkL = l <= 50 ? Math.min(98, 100 - l) : Math.max(2, 100 - l);\n } else if (key === '--primary-foreground') {\n darkL = l > 50 ? Math.max(2, 100 - l) : Math.min(98, 100 - l);\n } else if (\n ['--accent', '--muted', '--border', '--input', '--ring'].includes(key)\n ) {\n // Light accents/muted → dark equivalents\n darkL = Math.max(10, Math.min(20, 100 - l));\n darkS = Math.min(s, 50);\n } else if (key === '--accent-foreground' || key === '--muted-foreground') {\n darkL = Math.min(98, Math.max(55, 100 - l));\n } else if (\n key === '--secondary' ||\n key === '--secondary-foreground'\n ) {\n darkL = l > 50 ? Math.max(8, 100 - l) : Math.min(98, 100 - l);\n } else if (key === '--destructive') {\n darkL = Math.max(25, l * 0.65);\n darkS = Math.min(s, 70);\n } else if (key === '--destructive-foreground') {\n darkL = Math.min(98, Math.max(90, 100 - l));\n } else {\n // Default: invert lightness\n darkL = 100 - l;\n }\n\n return `${h} ${darkS}% ${Math.round(darkL)}%`;\n}\n\n/**\n * Build CSS text for the given vars targeting a specific selector.\n */\nfunction buildCssBlock(\n selector: string,\n vars: Record<string, string>,\n): string {\n const declarations = Object.entries(vars)\n .map(([key, value]) => ` ${key}: ${value};`)\n .join('\\n');\n return `${selector} {\\n${declarations}\\n}`;\n}\n\nexport function setupThemeBridge() {\n const appliedKeys = new Set<string>();\n window.addEventListener('message', (event) => {\n // Only accept messages from the parent frame\n if (event.source !== window.parent) return;\n\n if (event.data?.type === 'TASKADE_THEME_UPDATE') {\n const vars: Record<string, string> | undefined = event.data.data?.vars;\n if (vars == null) return;\n\n // Snapshot all previously applied keys so we can clear their inline\n // styles even if they are being removed in this update.\n const previousKeys = new Set(appliedKeys);\n\n const lightVars: Record<string, string> = {};\n const darkVars: Record<string, string> = {};\n\n // Reset tracking — will be rebuilt from current vars\n appliedKeys.clear();\n\n for (const [key, value] of Object.entries(vars)) {\n if (value === '') continue;\n lightVars[key] = value;\n darkVars[key] = deriveDarkValue(key, value);\n appliedKeys.add(key);\n }\n\n // Clear any previously-set inline styles from older bridge versions\n // (covers both current and removed keys)\n for (const key of previousKeys) {\n document.documentElement.style.removeProperty(key);\n }\n\n // Inject or update the <style> element\n let styleEl = document.getElementById(\n THEME_STYLE_ID,\n ) as HTMLStyleElement | null;\n\n if (Object.keys(lightVars).length === 0) {\n // No vars — remove the style element entirely\n styleEl?.remove();\n } else {\n if (styleEl == null) {\n styleEl = document.createElement('style');\n styleEl.id = THEME_STYLE_ID;\n document.head.appendChild(styleEl);\n }\n styleEl.textContent = [\n buildCssBlock(':root', lightVars),\n buildCssBlock('.dark', darkVars),\n ].join('\\n\\n');\n }\n\n window.parent.postMessage({ type: 'TASKADE_THEME_APPLIED' }, '*');\n return;\n }\n\n if (event.data?.type === 'TASKADE_THEME_READ') {\n const keys: string[] | undefined = event.data.data?.keys;\n if (keys == null) return;\n\n const computed = getComputedStyle(document.documentElement);\n const vars: Record<string, string> = {};\n for (const key of keys) {\n const val = computed.getPropertyValue(key).trim();\n if (val) vars[key] = val;\n }\n\n window.parent.postMessage(\n { type: 'TASKADE_THEME_CURRENT', data: { vars } },\n '*',\n );\n return;\n }\n });\n}\n"
}
},
"genesis-auth.tsx": {
"file": {
"contents": "import type * as React from 'react';\nimport { AuthProvider } from 'react-oidc-context';\n\n/**\n * Pre-built Genesis OIDC auth wrapper.\n *\n * Fetches the discovery document at `/_genesis/auth/.well-known/openid-configuration`\n * and infers all other config (endpoints, supported scopes, etc.) automatically.\n *\n * Trade-off: small initial load for the discovery fetch, acceptable for\n * the reliability gain of not hardcoding any OIDC config.\n *\n * Usage in App.tsx:\n * ```tsx\n * import { GenesisAuth } from '@/lib/genesis-auth';\n *\n * function App() {\n * return (\n * <GenesisAuth>\n * <ProtectedApp />\n * </GenesisAuth>\n * );\n * }\n * ```\n *\n * Access user profile after login:\n * ```tsx\n * import { useAuth } from 'react-oidc-context';\n *\n * function Profile() {\n * const auth = useAuth();\n * const { email, name, preferred_username, sub } = auth.user?.profile ?? {};\n * // ...\n * }\n * ```\n */\nexport function GenesisAuth({ children }: { children: React.ReactNode }) {\n const origin = window.location.origin;\n return (\n <AuthProvider\n authority={`${origin}/_genesis/auth`}\n client_id=\"default\"\n redirect_uri={`${origin}/`}\n scope=\"openid profile email\"\n >\n {children}\n </AuthProvider>\n );\n}\n"
}
},
"leaflet-setup.ts": {
"file": {
"contents": "import * as L from \"leaflet\";\nimport \"leaflet/dist/leaflet.css\";\nimport iconUrl from \"leaflet/dist/images/marker-icon.png\";\nimport iconRetinaUrl from \"leaflet/dist/images/marker-icon-2x.png\";\nimport shadowUrl from \"leaflet/dist/images/marker-shadow.png\";\n\n// Fix Leaflet default marker icons in Vite/bundler environments.\n// Import this once in any component that uses markers:\n// import '@/lib/leaflet-setup'\n\ndelete (L.Icon.Default.prototype as any)._getIconUrl\n\nL.Icon.Default.mergeOptions({\n iconRetinaUrl,\n iconUrl,\n shadowUrl,\n})"
}
},
"streamdown-code.ts": {
"file": {
"contents": "/**\n * Custom Streamdown code-highlighter plugin using @shikijs/core with CDN.\n *\n * Drop-in replacement for `@streamdown/code` that avoids bundling all ~200\n * shiki language grammars (which creates 347 esbuild chunks and crashes the\n * preview server). Instead, languages are loaded on demand from CDN.\n *\n * Uses the same singleton highlighter as `@/lib/shiki`.\n *\n * @see https://github.com/taskade/taskcade/issues/26056\n */\n\nimport type { HighlighterCore } from \"@shikijs/core\";\nimport { getHighlighter } from \"./shiki\";\n\n// Re-use the theme names from the shared shiki singleton\nconst THEMES = [\"github-light\", \"github-dark\"] as const;\n\n// Cache: key → tokens result\nconst cache = new Map<\n string,\n { bg?: string; fg?: string; tokens: unknown[][] }\n>();\n\n// In-flight subscribers for async results\nconst subscribers = new Map<\n string,\n Set<(result: { bg?: string; fg?: string; tokens: unknown[][] }) => void>\n>();\n\nfunction cacheKey(\n code: string,\n language: string,\n themes: [string, string]\n): string {\n const start = code.slice(0, 100);\n const end = code.length > 100 ? code.slice(-100) : \"\";\n return `${language}:${themes[0]}:${themes[1]}:${code.length}:${start}:${end}`;\n}\n\nfunction themeNames(\n themes: [unknown, unknown]\n): [string, string] {\n const name = (t: unknown) =>\n typeof t === \"string\" ? t : (t as { name?: string })?.name ?? \"custom\";\n return [name(themes[0]), name(themes[1])];\n}\n\n/**\n * CDN-based code highlighter plugin for Streamdown.\n *\n * Implements the `CodeHighlighterPlugin` interface expected by Streamdown.\n */\nexport const code = {\n name: \"shiki\" as const,\n type: \"code-highlighter\" as const,\n\n supportsLanguage(_language: string): boolean {\n // Accept all languages — unknown ones will fall back to plaintext\n // after the CDN fetch fails.\n return true;\n },\n\n getSupportedLanguages(): string[] {\n // Return empty — languages are loaded dynamically from CDN\n return [];\n },\n\n getThemes(): [string, string] {\n return [THEMES[0], THEMES[1]];\n },\n\n highlight(\n options: {\n code: string;\n language: string;\n themes: [unknown, unknown];\n },\n callback?: (result: {\n bg?: string;\n fg?: string;\n tokens: unknown[][];\n }) => void\n ): { bg?: string; fg?: string; tokens: unknown[][] } | null {\n const { code: src, language } = options;\n const names = themeNames(options.themes);\n const key = cacheKey(src, language, names);\n\n // Return cached result if available\n const cached = cache.get(key);\n if (cached) return cached;\n\n // Register callback for async notification\n if (callback) {\n if (!subscribers.has(key)) {\n subscribers.set(key, new Set());\n }\n subscribers.get(key)!.add(callback);\n }\n\n // Fire-and-forget async highlight\n getHighlighter(language)\n .then((highlighter: HighlighterCore) => {\n const langToUse = highlighter.getLoadedLanguages().includes(language)\n ? language\n : \"text\";\n\n const result = highlighter.codeToTokens(src, {\n lang: langToUse,\n themes: {\n light: names[0],\n dark: names[1],\n },\n });\n\n const tokenized = {\n bg: result.bg ?? undefined,\n fg: result.fg ?? undefined,\n tokens: result.tokens,\n };\n\n cache.set(key, tokenized);\n\n // Notify subscribers\n const subs = subscribers.get(key);\n if (subs) {\n for (const sub of subs) {\n sub(tokenized);\n }\n subscribers.delete(key);\n }\n })\n .catch((error: unknown) => {\n console.error(\"[streamdown-code] Failed to highlight:\", error);\n subscribers.delete(key);\n });\n\n // Not ready yet — Streamdown will show unhighlighted code\n return null;\n },\n};\n"
}
},
"streamdown-mermaid.ts": {
"file": {
"contents": "/**\n * Custom Streamdown diagram plugin that loads Mermaid from CDN.\n *\n * Drop-in replacement for `@streamdown/mermaid` that avoids bundling the\n * full mermaid library (~83 esbuild chunks for all diagram types).\n * Instead, mermaid is loaded on demand from esm.sh CDN.\n *\n * @see https://github.com/taskade/taskcade/issues/26056\n */\n\nconst MERMAID_VERSION = \"11.14.0\";\nconst MERMAID_CDN_URL = `https://esm.sh/mermaid@${MERMAID_VERSION}`;\n\ninterface MermaidConfig {\n startOnLoad?: boolean;\n theme?: string;\n securityLevel?: string;\n fontFamily?: string;\n suppressErrorRendering?: boolean;\n [key: string]: unknown;\n}\n\ninterface MermaidInstance {\n initialize: (config: MermaidConfig) => void;\n render: (\n id: string,\n source: string\n ) => Promise<{\n svg: string;\n }>;\n}\n\nconst DEFAULT_CONFIG: MermaidConfig = {\n startOnLoad: false,\n theme: \"default\",\n securityLevel: \"strict\",\n fontFamily: \"monospace\",\n suppressErrorRendering: true,\n};\n\n// Singleton: CDN module promise\nlet mermaidModulePromise: Promise<{ default: MermaidInstance }> | null = null;\nlet initialized = false;\n\nfunction getMermaidModule(): Promise<{ default: MermaidInstance }> {\n if (!mermaidModulePromise) {\n mermaidModulePromise = import(\n /* @vite-ignore */ MERMAID_CDN_URL\n ) as Promise<{ default: MermaidInstance }>;\n }\n return mermaidModulePromise;\n}\n\n/**\n * CDN-based Mermaid diagram plugin for Streamdown.\n *\n * Implements the `DiagramPlugin` interface expected by Streamdown.\n */\nexport const mermaid = {\n name: \"mermaid\" as const,\n type: \"diagram\" as const,\n language: \"mermaid\",\n\n getMermaid(config?: MermaidConfig): MermaidInstance {\n const mergedConfig = { ...DEFAULT_CONFIG, ...config };\n\n return {\n initialize(overrides: MermaidConfig) {\n // Will be applied when the CDN module loads\n Object.assign(mergedConfig, overrides);\n initialized = false; // Force re-init with new config\n },\n\n async render(id: string, source: string): Promise<{ svg: string }> {\n const mod = await getMermaidModule();\n const m = mod.default;\n\n if (!initialized) {\n m.initialize(mergedConfig);\n initialized = true;\n }\n\n return m.render(id, source);\n },\n };\n },\n};\n"
}
}
}
},
"data": {
"directory": {
"workspace.ts": {
"file": {
"contents": "// Cortex — Workspace DNA constants\n// All agents, projects, and automations in the space\n\nexport const AGENTS = [\n {\n id: '01KP2NYSV39GD3AVDGD4AYNA48',\n name: 'Strategist',\n emoji: '🧭',\n color: 'from-violet-500/20 to-purple-600/20',\n border: 'border-violet-500/30',\n badge: 'bg-violet-500/10 text-violet-400',\n description: 'Surfaces tradeoffs you haven\\'t named. Turns false binaries into real options.',\n role: 'Options · Tradeoffs · Conditions',\n taskadeUrl: 'https://staging.taskade.dev',\n },\n {\n id: '01KP2NYSV3Z89PXY39TNKRY8TR',\n name: 'Critic',\n emoji: '⚔️',\n color: 'from-red-500/20 to-rose-600/20',\n border: 'border-red-500/30',\n badge: 'bg-red-500/10 text-red-400',\n description: 'Argues against every plan. Finds the hole before the market does.',\n role: 'Pressure Test · Steel Man · Gaps',\n taskadeUrl: 'https://staging.taskade.dev',\n },\n {\n id: '01KP2NYSV3MX2MZNYJSFWT0R34',\n name: 'Researcher',\n emoji: '🔬',\n color: 'from-cyan-500/20 to-blue-600/20',\n border: 'border-cyan-500/30',\n badge: 'bg-cyan-500/10 text-cyan-400',\n description: 'Verifies claims with evidence. Never confuses confidence with correctness.',\n role: 'Evidence · Claims · Sources',\n taskadeUrl: 'https://staging.taskade.dev',\n },\n {\n id: '01KP2NYSV3FEZ09WSYFBGKCM6C',\n name: 'Editor',\n emoji: '✍️',\n color: 'from-[#ffcf40]/20 to-[#e6b800]/20',\n border: 'border-[#ffcf40]/30',\n badge: 'bg-[#ffcf40]/10 text-[#ffcf40]',\n description: 'Cuts until only signal remains. Makes every word earn its place.',\n role: 'Clarity · Precision · Voice',\n taskadeUrl: 'https://staging.taskade.dev',\n },\n {\n id: '01KP2NYSV3MB5JRCTY72P77C3F',\n name: 'Builder',\n emoji: '🔧',\n color: 'from-emerald-500/20 to-green-600/20',\n border: 'border-emerald-500/30',\n badge: 'bg-emerald-500/10 text-emerald-400',\n description: 'Turns vague direction into executable specs. Scoped, sequenced, ready to ship.',\n role: 'Spec · Scope · Sequence',\n taskadeUrl: 'https://staging.taskade.dev',\n },\n] as const;\n\nexport const PROJECTS = [\n {\n id: 'V1LWQVLhmHB4LcNM',\n name: 'Welcome to Cortex',\n category: 'Core',\n emoji: '🏠',\n description: 'The front door. Start here.',\n taskadeUrl: 'https://staging.taskade.dev/d/V1LWQVLhmHB4LcNM',\n lastModified: '2026-04-12',\n },\n {\n id: 'jSuYHAuWfhAFWANm',\n name: 'Company Context',\n category: 'Core',\n emoji: '🏛️',\n description: 'Operating principles, weekly priorities, and institutional memory.',\n taskadeUrl: 'https://staging.taskade.dev/d/jSuYHAuWfhAFWANm',\n lastModified: '2026-04-12',\n },\n {\n id: 'R5cAQa4pU2hA7jMr',\n name: 'Decision Log',\n category: 'Core',\n emoji: '📋',\n description: 'Every major decision, its reasoning, and outcome.',\n taskadeUrl: 'https://staging.taskade.dev/d/R5cAQa4pU2hA7jMr',\n lastModified: '2026-04-12',\n },\n {\n id: '9T9rrVHjc59ayGQh',\n name: 'Library — Frameworks',\n category: 'Library',\n emoji: '🧠',\n description: 'Mental models and decision-making frameworks.',\n taskadeUrl: 'https://staging.taskade.dev/d/9T9rrVHjc59ayGQh',\n lastModified: '2026-04-10',\n },\n {\n id: 'x4Wxw3NruLd78emT',\n name: 'Library — References',\n category: 'Library',\n emoji: '📚',\n description: 'Essential reading, blog posts, tools, and key metric definitions.',\n taskadeUrl: 'https://staging.taskade.dev/d/x4Wxw3NruLd78emT',\n lastModified: '2026-04-10',\n },\n {\n id: 'sw2xtkqZyHhEVucy',\n name: 'Playbook — Launch',\n category: 'Playbooks',\n emoji: '🚀',\n description: 'How to ship. The complete launch sequence.',\n taskadeUrl: 'https://staging.taskade.dev/d/sw2xtkqZyHhEVucy',\n lastModified: '2026-04-08',\n },\n {\n id: 'hqkA7LQQKjR9dbYt',\n name: 'Playbook — Fundraise',\n category: 'Playbooks',\n emoji: '💰',\n description: 'Investor prep, pitch narrative, and term sheet playbook.',\n taskadeUrl: 'https://staging.taskade.dev/d/hqkA7LQQKjR9dbYt',\n lastModified: '2026-04-08',\n },\n {\n id: 'Cb9tCSuN4Mzmkk4e',\n name: 'Playbook — Pricing',\n category: 'Playbooks',\n emoji: '💳',\n description: 'Pricing strategy, packaging, and model decisions.',\n taskadeUrl: 'https://staging.taskade.dev/d/Cb9tCSuN4Mzmkk4e',\n lastModified: '2026-04-07',\n },\n {\n id: 'q7jth2d1k2GSAALQ',\n name: 'Playbook — Hiring',\n category: 'Playbooks',\n emoji: '🧑💼',\n description: 'Who to hire, when, and how to run a great interview process.',\n taskadeUrl: 'https://staging.taskade.dev/d/q7jth2d1k2GSAALQ',\n lastModified: '2026-04-06',\n },\n {\n id: 'DibevYJnsNjUxmJq',\n name: 'Playbook — Support',\n category: 'Playbooks',\n emoji: '🎧',\n description: 'Customer support escalation paths and response templates.',\n taskadeUrl: 'https://staging.taskade.dev/d/DibevYJnsNjUxmJq',\n lastModified: '2026-04-05',\n },\n] as const;\n\nexport const AUTOMATIONS = [\n {\n id: '01KP2NYSV32Y1Y298SYBDK79DR',\n name: 'Decision Council',\n emoji: '⚖️',\n trigger: 'Webhook',\n description: '4 agents pressure-test every major decision before you commit.',\n agents: ['Strategist', 'Critic', 'Researcher', 'Builder'],\n taskadeUrl: 'https://staging.taskade.dev/flows/01KP2NYSV32Y1Y298SYBDK79DR',\n color: 'from-violet-500/10 to-purple-500/10',\n border: 'border-violet-500/20',\n },\n {\n id: '01KP2NYSV3WV1N5ST447YFWRSK',\n name: 'Inbox Triage',\n emoji: '📥',\n trigger: 'Webhook',\n description: 'Researcher classifies incoming signals and appends to Inbox Triage.',\n agents: ['Researcher'],\n taskadeUrl: 'https://staging.taskade.dev/flows/01KP2NYSV3WV1N5ST447YFWRSK',\n color: 'from-cyan-500/10 to-blue-500/10',\n border: 'border-cyan-500/20',\n },\n {\n id: '01KP2NYSV3S7JMHRKS9E2GKV1M',\n name: 'Incident Response',\n emoji: '🚨',\n trigger: 'Webhook',\n description: 'Builder specs the next 4 hours of response when an incident fires.',\n agents: ['Builder'],\n taskadeUrl: 'https://staging.taskade.dev/flows/01KP2NYSV3S7JMHRKS9E2GKV1M',\n color: 'from-red-500/10 to-rose-500/10',\n border: 'border-red-500/20',\n },\n {\n id: '01KP2NYSV3842BNHGYHEVKVTPS',\n name: 'Weekly Review',\n emoji: '📊',\n trigger: 'Friday 6 PM',\n description: 'Editor and Strategist close the week with a retrospective.',\n agents: ['Editor', 'Strategist'],\n taskadeUrl: 'https://staging.taskade.dev/flows/01KP2NYSV3842BNHGYHEVKVTPS',\n color: 'from-[#ffcf40]/10 to-[#e6b800]/10',\n border: 'border-[#ffcf40]/20',\n },\n {\n id: '01KP2NYSV3FEK74Y3NNJW6QJW6',\n name: 'Monday Planning',\n emoji: '🗓️',\n trigger: 'Monday 8 AM',\n description: 'Strategist maps the one thing that matters most this week.',\n agents: ['Strategist'],\n taskadeUrl: 'https://staging.taskade.dev/flows/01KP2NYSV3FEK74Y3NNJW6QJW6',\n color: 'from-emerald-500/10 to-green-500/10',\n border: 'border-emerald-500/20',\n },\n {\n id: '01KP2NYSV3C5H36KCB9M0N20JA',\n name: 'Daily Standup',\n emoji: '☀️',\n trigger: 'Daily 9 AM',\n description: 'Morning briefing from the Builder on today\\'s priorities.',\n agents: ['Builder'],\n taskadeUrl: 'https://staging.taskade.dev/flows/01KP2NYSV3C5H36KCB9M0N20JA',\n color: 'from-sky-500/10 to-blue-500/10',\n border: 'border-sky-500/20',\n },\n] as const;\n\nexport type Agent = typeof AGENTS[number];\nexport type Project = typeof PROJECTS[number];\nexport type Automation = typeof AUTOMATIONS[number];\n"
}
}
}
},
"hooks": {
"directory": {
"use-theme.ts": {
"file": {
"contents": "import { useTheme } from \"next-themes\"\n\n/**\n * Compatibility shim: LLMs often generate `import { useTheme } from '@/hooks/use-theme'`.\n * This re-exports next-themes which is the actual theme provider.\n *\n * Preferred usage: import { useTheme } from \"next-themes\" directly.\n */\nexport { useTheme }\n"
}
},
"use-toast.ts": {
"file": {
"contents": "import { toast } from \"sonner\"\n\n/**\n * Compatibility shim: LLMs often generate shadcn/ui's useToast pattern.\n * This bridges to Sonner which is the actual toast library in base-template-v2.\n *\n * Preferred usage: import { toast } from \"sonner\" directly.\n */\nexport function useToast() {\n return {\n toast,\n dismiss: toast.dismiss,\n }\n}\n\nexport { toast }\n"
}
},
"use-mobile.ts": {
"file": {
"contents": "import * as React from \"react\"\n\nconst MOBILE_BREAKPOINT = 768\n\nexport function useIsMobile() {\n const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)\n\n React.useEffect(() => {\n const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)\n const onChange = () => {\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)\n }\n mql.addEventListener(\"change\", onChange)\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)\n return () => mql.removeEventListener(\"change\", onChange)\n }, [])\n\n return !!isMobile\n}\n"
}
}
}
},
"pages": {
"directory": {
"Council.tsx": {
"file": {
"contents": "import * as React from 'react';\nimport { useChat } from '@ai-sdk/react';\nimport { createConversation, createAgentChat } from '@/lib/agent-chat/v2';\nimport { isToolUIPart } from 'ai';\nimport type { UIMessage } from 'ai';\nimport { ulid } from 'ulidx';\nimport { cn } from '@/lib/utils';\nimport ReactMarkdown from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\nimport { Scale, ChevronRight, Loader2, AlertCircle, CheckCircle2, Send } from 'lucide-react';\n\n// The 4 council agents that respond to the webhook\nconst COUNCIL_MEMBERS = [\n {\n id: '01KP2NYSV39GD3AVDGD4AYNA48',\n name: 'Strategist',\n emoji: '🧭',\n role: 'Counsel on a Decision',\n color: 'from-violet-500/10 to-purple-500/10',\n border: 'border-violet-500/30',\n headerBg: 'bg-violet-500/10',\n accent: 'text-violet-400',\n dot: 'bg-violet-400',\n },\n {\n id: '01KP2NYSV3Z89PXY39TNKRY8TR',\n name: 'Critic',\n emoji: '⚔️',\n role: 'Argue Against This',\n color: 'from-red-500/10 to-rose-500/10',\n border: 'border-red-500/30',\n headerBg: 'bg-red-500/10',\n accent: 'text-red-400',\n dot: 'bg-red-400',\n },\n {\n id: '01KP2NYSV3MX2MZNYJSFWT0R34',\n name: 'Researcher',\n emoji: '🔬',\n role: 'Verify the Claim',\n color: 'from-cyan-500/10 to-blue-500/10',\n border: 'border-cyan-500/30',\n headerBg: 'bg-cyan-500/10',\n accent: 'text-cyan-400',\n dot: 'bg-cyan-400',\n },\n {\n id: '01KP2NYSV3MB5JRCTY72P77C3F',\n name: 'Builder',\n emoji: '🔧',\n role: 'Spec It',\n color: 'from-emerald-500/10 to-green-500/10',\n border: 'border-emerald-500/30',\n headerBg: 'bg-emerald-500/10',\n accent: 'text-emerald-400',\n dot: 'bg-emerald-400',\n },\n] as const;\n\ntype AgentState =\n | { status: 'idle' }\n | { status: 'loading' }\n | { status: 'streaming'; chat: ReturnType<typeof createAgentChat> }\n | { status: 'done'; text: string }\n | { status: 'error'; message: string };\n\n// Individual streaming panel per agent\nfunction AgentPanel({\n member,\n prompt,\n active,\n}: {\n member: typeof COUNCIL_MEMBERS[number];\n prompt: string;\n active: boolean;\n}) {\n const [chat, setChat] = React.useState<ReturnType<typeof createAgentChat> | null>(null);\n const [agentState, setAgentState] = React.useState<AgentState>({ status: 'idle' });\n\n React.useEffect(() => {\n if (!active || !prompt) return;\n setAgentState({ status: 'loading' });\n setChat(null);\n\n let cancelled = false;\n (async () => {\n try {\n const { conversationId } = await createConversation(member.id);\n if (cancelled) return;\n const newChat = createAgentChat(member.id, conversationId);\n if (cancelled) return;\n setChat(newChat);\n setAgentState({ status: 'streaming', chat: newChat });\n } catch (e) {\n if (!cancelled) {\n setAgentState({ status: 'error', message: String(e) });\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [active, prompt, member.id]);\n\n if (!active || agentState.status === 'idle') {\n return (\n <div\n className={cn(\n 'rounded-xl border bg-gradient-to-br p-3 sm:p-4',\n member.color,\n member.border,\n )}\n >\n <AgentPanelHeader member={member} status=\"idle\" />\n <div className=\"mt-2 sm:mt-3 text-[10px] sm:text-xs text-muted-foreground italic\">Awaiting the question…</div>\n </div>\n );\n }\n\n if (agentState.status === 'loading') {\n return (\n <div className={cn('rounded-xl border bg-gradient-to-br p-3 sm:p-4', member.color, member.border)}>\n <AgentPanelHeader member={member} status=\"loading\" />\n <div className=\"mt-2 sm:mt-3 flex items-center gap-2 text-[10px] sm:text-xs text-muted-foreground\">\n <Loader2 className=\"w-3 h-3 animate-spin\" />\n Assembling…\n </div>\n </div>\n );\n }\n\n if (agentState.status === 'error') {\n return (\n <div className={cn('rounded-xl border bg-gradient-to-br p-3 sm:p-4', member.color, member.border)}>\n <AgentPanelHeader member={member} status=\"error\" />\n <div className=\"mt-2 sm:mt-3 flex items-center gap-2 text-[10px] sm:text-xs text-red-400\">\n <AlertCircle className=\"w-3 h-3\" />\n {agentState.message}\n </div>\n </div>\n );\n }\n\n if (!chat) return null;\n\n return (\n <div className={cn('rounded-xl border bg-gradient-to-br p-3 sm:p-4', member.color, member.border)}>\n <AgentPanelHeader member={member} status=\"streaming\" />\n <ActiveAgentChat chat={chat} member={member} prompt={prompt} />\n </div>\n );\n}\n\nfunction AgentPanelHeader({\n member,\n status,\n}: {\n member: typeof COUNCIL_MEMBERS[number];\n status: 'idle' | 'loading' | 'streaming' | 'done' | 'error';\n}) {\n return (\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-1.5 sm:gap-2\">\n <span className=\"text-sm sm:text-lg\">{member.emoji}</span>\n <div>\n <div className={cn('text-xs sm:text-sm font-semibold', member.accent)}>{member.name}</div>\n <div className=\"text-[10px] sm:text-xs text-muted-foreground\">{member.role}</div>\n </div>\n </div>\n <div className=\"flex items-center gap-1.5\">\n {status === 'idle' && (\n <div className=\"w-1.5 h-1.5 rounded-full bg-muted-foreground/40\" />\n )}\n {status === 'loading' && (\n <Loader2 className=\"w-3 h-3 text-muted-foreground animate-spin\" />\n )}\n {status === 'streaming' && (\n <div className={cn('w-1.5 h-1.5 rounded-full animate-pulse', member.dot)} />\n )}\n {status === 'done' && (\n <CheckCircle2 className=\"w-3.5 h-3.5 text-emerald-400\" />\n )}\n {status === 'error' && (\n <AlertCircle className=\"w-3.5 h-3.5 text-red-400\" />\n )}\n </div>\n </div>\n );\n}\n\nfunction ActiveAgentChat({\n chat,\n member,\n prompt,\n}: {\n chat: ReturnType<typeof createAgentChat>;\n member: typeof COUNCIL_MEMBERS[number];\n prompt: string;\n}) {\n const { messages, status, addToolApprovalResponse } = useChat({ chat, id: chat.id });\n const [sent, setSent] = React.useState(false);\n\n React.useEffect(() => {\n if (sent || !prompt) return;\n setSent(true);\n chat.sendMessage({\n id: ulid(),\n role: 'user',\n parts: [{ type: 'text', text: prompt }],\n });\n }, [chat, prompt, sent]);\n\n const isStreaming = status === 'submitted' || status === 'streaming';\n const assistantMessages = messages.filter((m) => m.role === 'assistant');\n\n return (\n <div className=\"mt-2 sm:mt-3\">\n {isStreaming && assistantMessages.length === 0 && (\n <div className=\"flex items-center gap-2 text-[10px] sm:text-xs text-muted-foreground\">\n <div className=\"flex gap-0.5\">\n <div className={cn('w-1 h-1 rounded-full animate-bounce', member.dot)} style={{ animationDelay: '0ms' }} />\n <div className={cn('w-1 h-1 rounded-full animate-bounce', member.dot)} style={{ animationDelay: '150ms' }} />\n <div className={cn('w-1 h-1 rounded-full animate-bounce', member.dot)} style={{ animationDelay: '300ms' }} />\n </div>\n Thinking…\n </div>\n )}\n {assistantMessages.map((msg) => (\n <MessageContent key={msg.id} message={msg} onApprove={addToolApprovalResponse} />\n ))}\n </div>\n );\n}\n\nfunction MessageContent({\n message,\n onApprove,\n}: {\n message: UIMessage;\n onApprove: ReturnType<typeof useChat>['addToolApprovalResponse'];\n}) {\n return (\n <div className=\"space-y-2\">\n {message.parts.map((part, i) => {\n const key = `${message.id}-${i}`;\n if (part.type === 'text') {\n return (\n <div key={key} className=\"prose prose-sm prose-invert max-w-none text-foreground/90 text-[11px] sm:text-xs leading-relaxed\">\n <ReactMarkdown remarkPlugins={[remarkGfm]}>{part.text}</ReactMarkdown>\n </div>\n );\n }\n if (isToolUIPart(part)) {\n return (\n <div key={key} className=\"text-[10px] sm:text-xs text-muted-foreground italic flex items-center gap-1\">\n <span>Tool: {part.toolName}</span>\n <span className=\"opacity-60\">[{part.state}]</span>\n {part.state === 'approval-requested' && part.approval != null && (\n <span className=\"flex gap-1 ml-2\">\n <button\n onClick={() => part.approval != null && onApprove({ id: part.approval.id, approved: true })}\n className=\"px-2 py-0.5 rounded bg-emerald-500/20 text-emerald-400 hover:bg-emerald-500/30 transition-colors\"\n >\n Approve\n </button>\n <button\n onClick={() => part.approval != null && onApprove({ id: part.approval.id, approved: false })}\n className=\"px-2 py-0.5 rounded bg-red-500/20 text-red-400 hover:bg-red-500/30 transition-colors\"\n >\n Deny\n </button>\n </span>\n )}\n </div>\n );\n }\n return null;\n })}\n </div>\n );\n}\n\nfunction useIsMobile(breakpoint = 640) {\n const [isMobile, setIsMobile] = React.useState(() => window.innerWidth < breakpoint);\n React.useEffect(() => {\n const mq = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);\n const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);\n mq.addEventListener('change', handler);\n setIsMobile(mq.matches);\n return () => mq.removeEventListener('change', handler);\n }, [breakpoint]);\n return isMobile;\n}\n\nexport function Council() {\n const [decision, setDecision] = React.useState('');\n const [context, setContext] = React.useState('');\n const isMobile = useIsMobile();\n const [convened, setConvened] = React.useState(false);\n const [activePrompt, setActivePrompt] = React.useState('');\n const [webhookFired, setWebhookFired] = React.useState(false);\n const [webhookError, setWebhookError] = React.useState<string | null>(null);\n const resultsRef = React.useRef<HTMLDivElement>(null);\n\n const canConvene = decision.trim().length > 5;\n\n async function handleConvene() {\n if (!canConvene) return;\n\n const prompt = `Decision: ${decision.trim()}\\n\\nContext: ${context.trim() || 'No additional context provided.'}`;\n setActivePrompt(prompt);\n setConvened(true);\n setWebhookFired(false);\n setWebhookError(null);\n\n // Fire the Decision Council webhook (for Decision Log persistence)\n try {\n await fetch('/api/taskade/webhooks/01KP2NYSV32Y1Y298SYBDK79DR/run', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ decision: decision.trim(), context: context.trim() }),\n });\n setWebhookFired(true);\n } catch {\n setWebhookError('Decision Log sync failed — live responses still active.');\n }\n\n setTimeout(() => {\n resultsRef.current?.scrollIntoView({ behavior: 'smooth' });\n }, 300);\n }\n\n function handleReset() {\n setConvened(false);\n setActivePrompt('');\n setDecision('');\n setContext('');\n setWebhookFired(false);\n setWebhookError(null);\n }\n\n return (\n <div className=\"px-4 py-4 sm:px-6 sm:py-8 max-w-7xl mx-auto\">\n {/* Header */}\n <div className=\"mb-5 sm:mb-8\">\n <div className=\"flex items-center gap-2 sm:gap-3 mb-1.5 sm:mb-2\">\n <div className=\"p-1.5 sm:p-2 rounded-xl bg-teal-500/10 border border-teal-500/20\">\n <Scale className=\"w-4 h-4 sm:w-5 sm:h-5 text-teal-500\" />\n </div>\n <h1 className=\"text-lg sm:text-2xl font-bold text-foreground\">Decision Council</h1>\n </div>\n <p className=\"text-muted-foreground text-xs sm:text-sm max-w-xl\">\n Type a decision. The council assembles — 4 agents pressure-test it from every angle before you commit.\n </p>\n </div>\n\n {/* Input form */}\n <div className=\"p-4 sm:p-6 rounded-2xl border border-border bg-card/60 backdrop-blur-sm mb-5 sm:mb-8\">\n <div className=\"space-y-3 sm:space-y-4\">\n <div>\n <label className=\"block text-[10px] sm:text-xs font-semibold text-muted-foreground uppercase tracking-widest mb-1.5 sm:mb-2\">\n The Decision *\n </label>\n <textarea\n value={decision}\n onChange={(e) => setDecision(e.target.value)}\n placeholder={isMobile ? \"e.g. We should raise a seed round now instead of staying default-alive.\" : \"e.g. We should raise a $3M seed round now instead of staying default-alive for another 18 months.\"}\n rows={isMobile ? 2 : 3}\n disabled={convened}\n className=\"w-full px-3 py-2.5 sm:px-4 sm:py-3 rounded-xl bg-background border border-border text-xs sm:text-sm text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-2 focus:ring-teal-500/40 focus:border-teal-500/40 resize-none disabled:opacity-60 transition-colors\"\n />\n </div>\n <div>\n <label className=\"block text-[10px] sm:text-xs font-semibold text-muted-foreground uppercase tracking-widest mb-1.5 sm:mb-2\">\n Context <span className=\"text-muted-foreground/50 font-normal normal-case\">(optional)</span>\n </label>\n <textarea\n value={context}\n onChange={(e) => setContext(e.target.value)}\n placeholder={isMobile ? \"e.g. $45k MRR, 15% MoM growth, 18 months runway.\" : \"e.g. We're at $45k MRR, growing 15% MoM, 18 months runway. Main competitor just raised $10M.\"}\n rows={2}\n disabled={convened}\n className=\"w-full px-3 py-2.5 sm:px-4 sm:py-3 rounded-xl bg-background border border-border text-xs sm:text-sm text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-2 focus:ring-teal-500/40 focus:border-teal-500/40 resize-none disabled:opacity-60 transition-colors\"\n />\n </div>\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n {webhookFired && (\n <>\n <CheckCircle2 className=\"w-3.5 h-3.5 text-emerald-400\" />\n <span className=\"text-emerald-400\">Logged to Decision Log</span>\n </>\n )}\n {webhookError && (\n <>\n <AlertCircle className=\"w-3.5 h-3.5 text-[#ffcf40]\" />\n <span className=\"text-[#ffcf40]\">{webhookError}</span>\n </>\n )}\n </div>\n <div className=\"flex gap-3\">\n {convened && (\n <button\n onClick={handleReset}\n className=\"px-3 py-1.5 sm:px-4 sm:py-2 rounded-xl text-xs sm:text-sm text-muted-foreground hover:text-foreground border border-border hover:border-border/80 transition-colors\"\n >\n New Decision\n </button>\n )}\n <button\n onClick={handleConvene}\n disabled={!canConvene || convened}\n className={cn(\n 'flex items-center gap-1.5 sm:gap-2 px-3.5 py-2 sm:px-5 sm:py-2.5 rounded-xl text-xs sm:text-sm font-semibold transition-all duration-200',\n canConvene && !convened\n ? 'bg-teal-600 hover:bg-teal-500 text-white shadow-lg shadow-teal-500/20 hover:shadow-teal-500/30 hover:-translate-y-0.5'\n : convened && webhookFired\n ? 'bg-emerald-500/15 text-emerald-400 border border-emerald-500/25 cursor-default'\n : 'bg-muted text-muted-foreground cursor-not-allowed',\n )}\n >\n {convened ? (\n webhookFired ? (\n <>\n <CheckCircle2 className=\"w-3.5 h-3.5 sm:w-4 sm:h-4\" />\n Council convened\n </>\n ) : (\n <>\n <Loader2 className=\"w-3.5 h-3.5 sm:w-4 sm:h-4 animate-spin\" />\n Council in session\n </>\n )\n ) : (\n <>\n <Send className=\"w-4 h-4\" />\n Convene Council\n <ChevronRight className=\"w-3.5 h-3.5\" />\n </>\n )}\n </button>\n </div>\n </div>\n </div>\n </div>\n\n {/* Council members (always visible, activate on convene) */}\n <div ref={resultsRef}>\n {convened && (\n <div className=\"mb-3 sm:mb-4 flex items-center gap-2 text-[10px] sm:text-xs text-muted-foreground\">\n {webhookFired ? (\n <>\n <CheckCircle2 className=\"w-3.5 h-3.5 text-emerald-400\" />\n <span className=\"text-emerald-400\">Council has spoken — responses below.</span>\n </>\n ) : (\n <>\n <div className=\"flex gap-1\">\n {COUNCIL_MEMBERS.map((m) => (\n <div key={m.id} className={cn('w-1.5 h-1.5 rounded-full animate-pulse', m.dot)} />\n ))}\n </div>\n The council is deliberating…\n </>\n )}\n </div>\n )}\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-4\">\n {COUNCIL_MEMBERS.map((member) => (\n <AgentPanel\n key={member.id}\n member={member}\n prompt={activePrompt}\n active={convened}\n />\n ))}\n </div>\n </div>\n </div>\n );\n}\n"
}
},
"Journal.tsx": {
"file": {
"contents": "import * as React from 'react';\nimport { cn } from '@/lib/utils';\nimport ReactMarkdown from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\nimport {\n BookOpen,\n Search,\n ExternalLink,\n Loader2,\n AlertCircle,\n ChevronDown,\n ChevronRight,\n FileText,\n} from 'lucide-react';\n\ninterface ProjectNode {\n id: string;\n parentId: string | null;\n fieldValues: {\n '/text'?: string;\n '/attributes/note'?: string;\n };\n}\n\ninterface DecisionEntry {\n id: string;\n title: string;\n children: ProjectNode[];\n}\n\nfunction buildTree(nodes: ProjectNode[]): { sections: DecisionEntry[] } {\n // Find top-level h2 nodes (section headings) — parentId is null or root\n const rootIds = new Set(nodes.filter((n) => n.parentId === null).map((n) => n.id));\n const childrenOf = (parentId: string) => nodes.filter((n) => n.parentId === parentId);\n\n const sections: DecisionEntry[] = [];\n\n // Find all nodes that could be decision entries (those under the \"Decisions\" heading)\n for (const rootNode of nodes.filter((n) => n.parentId === null)) {\n const title = rootNode.fieldValues['/text'] ?? '';\n if (!title) continue;\n sections.push({\n id: rootNode.id,\n title,\n children: childrenOf(rootNode.id),\n });\n }\n\n return { sections };\n}\n\nfunction nodeText(node: ProjectNode): string {\n return node.fieldValues['/text'] ?? '';\n}\n\nfunction EntryCard({ entry, query }: { entry: DecisionEntry; query: string }) {\n const [expanded, setExpanded] = React.useState(false);\n\n // Highlight match\n const highlight = (text: string) => {\n if (!query) return text;\n const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')})`, 'gi');\n const parts = text.split(regex);\n return parts.map((part, i) =>\n regex.test(part) ? (\n <mark key={i} className=\"bg-[#ffcf40]/25 text-[#ffcf40] dark:text-[#ffcf40] rounded px-0.5\">\n {part}\n </mark>\n ) : (\n part\n ),\n );\n };\n\n const isWide = entry.title.includes('Decision Council') || entry.title.includes('War Room');\n const contentText = entry.children.map((c) => nodeText(c)).join(' ');\n\n return (\n <div className=\"rounded-xl border border-border bg-card/60 overflow-hidden hover:border-border/80 transition-colors\">\n <button\n onClick={() => setExpanded((e) => !e)}\n className=\"w-full flex items-start gap-2 sm:gap-3 p-3 sm:p-4 text-left hover:bg-accent/30 transition-colors\"\n >\n <div className=\"flex-shrink-0 mt-0.5\">\n {expanded ? (\n <ChevronDown className=\"w-3.5 h-3.5 sm:w-4 sm:h-4 text-muted-foreground\" />\n ) : (\n <ChevronRight className=\"w-3.5 h-3.5 sm:w-4 sm:h-4 text-muted-foreground\" />\n )}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"font-medium text-xs sm:text-sm text-foreground mb-1\">\n {highlight(entry.title)}\n </div>\n {!expanded && contentText && (\n <div className=\"text-[11px] sm:text-xs text-muted-foreground line-clamp-2\">\n {contentText.substring(0, 160)}…\n </div>\n )}\n {isWide && (\n <div className=\"mt-1\">\n <span className=\"text-[10px] sm:text-xs px-1.5 py-0.5 rounded bg-teal-500/10 text-teal-400 border border-teal-500/20\">\n Council\n </span>\n </div>\n )}\n </div>\n </button>\n\n {expanded && (\n <div className=\"px-3 pb-3 sm:px-4 sm:pb-4 border-t border-border/50\">\n <div className=\"mt-2 sm:mt-3 space-y-1.5\">\n {entry.children.map((child) => {\n const text = nodeText(child);\n if (!text) return null;\n return (\n <div key={child.id} className=\"text-[11px] sm:text-xs text-muted-foreground leading-relaxed\">\n {highlight(text)}\n </div>\n );\n })}\n </div>\n </div>\n )}\n </div>\n );\n}\n\nfunction SkeletonCard() {\n return (\n <div className=\"rounded-xl border border-border bg-card/60 p-4 space-y-2 animate-pulse\">\n <div className=\"h-4 bg-muted rounded w-3/4\" />\n <div className=\"h-3 bg-muted rounded w-full\" />\n <div className=\"h-3 bg-muted rounded w-5/6\" />\n </div>\n );\n}\n\nexport function Journal() {\n const [nodes, setNodes] = React.useState<ProjectNode[]>([]);\n const [loading, setLoading] = React.useState(true);\n const [error, setError] = React.useState<string | null>(null);\n const [query, setQuery] = React.useState('');\n\n React.useEffect(() => {\n setLoading(true);\n fetch('/api/taskade/projects/R5cAQa4pU2hA7jMr/nodes')\n .then((r) => r.json())\n .then((data) => {\n if (data.ok) {\n setNodes(data.payload.nodes);\n } else {\n setError('Failed to load Decision Log.');\n }\n })\n .catch(() => setError('Network error loading Decision Log.'))\n .finally(() => setLoading(false));\n }, []);\n\n // Build flat list of all non-root entries that have content\n const entries: DecisionEntry[] = React.useMemo(() => {\n if (!nodes.length) return [];\n const topLevel = nodes.filter((n) => n.parentId === null);\n const childrenOf = (id: string) => nodes.filter((n) => n.parentId === id);\n\n const result: DecisionEntry[] = [];\n for (const top of topLevel) {\n const title = top.fieldValues['/text'] ?? '';\n if (!title) continue;\n // Each top-level node + its children as one entry\n result.push({ id: top.id, title, children: childrenOf(top.id) });\n }\n return result;\n }, [nodes]);\n\n const filtered = React.useMemo(() => {\n if (!query.trim()) return entries;\n const q = query.toLowerCase();\n return entries.filter((e) => {\n const titleMatch = e.title.toLowerCase().includes(q);\n const childMatch = e.children.some((c) =>\n (c.fieldValues['/text'] ?? '').toLowerCase().includes(q),\n );\n return titleMatch || childMatch;\n });\n }, [entries, query]);\n\n return (\n <div className=\"px-4 py-4 sm:px-6 sm:py-8 max-w-4xl mx-auto\">\n {/* Header */}\n <div className=\"mb-5 sm:mb-8\">\n <div className=\"flex items-center gap-2 sm:gap-3 mb-1.5 sm:mb-2\">\n <div className=\"p-1.5 sm:p-2 rounded-xl bg-[#ffcf40]/10 border border-[#ffcf40]/20\">\n <BookOpen className=\"w-4 h-4 sm:w-5 sm:h-5 text-[#ffcf40]\" />\n </div>\n <h1 className=\"text-lg sm:text-2xl font-bold text-foreground\">Decision Journal</h1>\n <a\n href=\"https://staging.taskade.dev/d/R5cAQa4pU2hA7jMr\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"ml-auto flex items-center gap-1 text-[10px] sm:text-xs text-muted-foreground hover:text-foreground transition-colors\"\n >\n <ExternalLink className=\"w-3 h-3 sm:w-3.5 sm:h-3.5\" />\n <span className=\"hidden sm:inline\">Open in Taskade</span>\n <span className=\"sm:hidden\">Open</span>\n </a>\n </div>\n <p className=\"text-muted-foreground text-xs sm:text-sm\">\n Every major decision, its reasoning, and outcome — read-only, searchable.\n </p>\n </div>\n\n {/* Search */}\n <div className=\"relative mb-4 sm:mb-6\">\n <Search className=\"absolute left-3 sm:left-3.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 sm:w-4 sm:h-4 text-muted-foreground pointer-events-none\" />\n <input\n type=\"text\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search decisions, context, outcomes…\"\n className=\"w-full pl-9 sm:pl-10 pr-4 py-2 sm:py-2.5 rounded-xl bg-card border border-border text-xs sm:text-sm text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-2 focus:ring-[#ffcf40]/40 focus:border-[#ffcf40]/40 transition-colors\"\n />\n </div>\n\n {/* Results count */}\n {!loading && !error && query && (\n <div className=\"mb-3 sm:mb-4 text-[10px] sm:text-xs text-muted-foreground\">\n {filtered.length} result{filtered.length !== 1 ? 's' : ''} for "{query}"\n </div>\n )}\n\n {/* Content */}\n {loading && (\n <div className=\"space-y-3\">\n {Array.from({ length: 5 }).map((_, i) => (\n <SkeletonCard key={i} />\n ))}\n </div>\n )}\n\n {error && (\n <div className=\"flex items-center gap-2 p-3 sm:p-4 rounded-xl border border-red-500/20 bg-red-500/5 text-red-400 text-xs sm:text-sm\">\n <AlertCircle className=\"w-4 h-4 flex-shrink-0\" />\n {error}\n </div>\n )}\n\n {!loading && !error && filtered.length === 0 && (\n <div className=\"text-center py-12 sm:py-16\">\n <FileText className=\"w-8 h-8 sm:w-10 sm:h-10 text-muted-foreground/30 mx-auto mb-3\" />\n <p className=\"text-muted-foreground text-xs sm:text-sm\">\n {query ? 'No decisions match that search.' : 'No decisions logged yet.'}\n </p>\n {query && (\n <button\n onClick={() => setQuery('')}\n className=\"mt-2 text-[10px] sm:text-xs text-teal-400 hover:text-teal-300 transition-colors\"\n >\n Clear search\n </button>\n )}\n </div>\n )}\n\n {!loading && !error && filtered.length > 0 && (\n <div className=\"space-y-3\">\n {filtered.map((entry) => (\n <EntryCard key={entry.id} entry={entry} query={query} />\n ))}\n </div>\n )}\n </div>\n );\n}\n"
}
},
"Library.tsx": {
"file": {
"contents": "import * as React from 'react';\nimport { cn } from '@/lib/utils';\nimport {\n Library as LibraryIcon,\n Brain,\n BookMarked,\n Search,\n Copy,\n Check,\n ExternalLink,\n Loader2,\n AlertCircle,\n ChevronRight,\n ChevronDown,\n} from 'lucide-react';\n\ninterface ProjectNode {\n id: string;\n parentId: string | null;\n fieldValues: {\n '/text'?: string;\n '/attributes/note'?: string;\n };\n}\n\ninterface TreeNode {\n id: string;\n text: string;\n note?: string;\n children: TreeNode[];\n}\n\nfunction buildTree(nodes: ProjectNode[]): TreeNode[] {\n const map = new Map<string, TreeNode>();\n const roots: TreeNode[] = [];\n\n for (const n of nodes) {\n map.set(n.id, {\n id: n.id,\n text: n.fieldValues['/text'] ?? '',\n note: n.fieldValues['/attributes/note'],\n children: [],\n });\n }\n\n for (const n of nodes) {\n const node = map.get(n.id)!;\n if (n.parentId === null) {\n roots.push(node);\n } else {\n map.get(n.parentId)?.children.push(node);\n }\n }\n\n return roots;\n}\n\nfunction collectText(node: TreeNode): string {\n const lines: string[] = [node.text];\n for (const child of node.children) {\n lines.push(collectText(child));\n }\n return lines.join('\\n');\n}\n\nfunction CopyButton({ text }: { text: string }) {\n const [copied, setCopied] = React.useState(false);\n\n async function handleCopy() {\n try {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n } catch {\n // silent\n }\n }\n\n return (\n <button\n onClick={handleCopy}\n className={cn(\n 'flex items-center gap-1 px-2 py-0.5 sm:px-2.5 sm:py-1 rounded-lg text-[10px] sm:text-xs font-medium transition-all duration-150',\n copied\n ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'\n : 'bg-muted/60 text-muted-foreground hover:text-foreground hover:bg-muted border border-transparent hover:border-border/50',\n )}\n title=\"Copy to clipboard\"\n >\n {copied ? (\n <>\n <Check className=\"w-3 h-3\" />\n Copied\n </>\n ) : (\n <>\n <Copy className=\"w-3 h-3\" />\n Copy\n </>\n )}\n </button>\n );\n}\n\nfunction TreeNodeRow({\n node,\n depth,\n query,\n}: {\n node: TreeNode;\n depth: number;\n query: string;\n}) {\n const [expanded, setExpanded] = React.useState(depth < 1);\n const isSection = depth === 0;\n const hasChildren = node.children.length > 0;\n const copyText = collectText(node);\n\n const highlight = (text: string) => {\n if (!query) return <>{text}</>;\n const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')})`, 'gi');\n const parts = text.split(regex);\n return (\n <>\n {parts.map((part, i) =>\n regex.test(part) ? (\n <mark key={i} className=\"bg-[#ffcf40]/25 text-[#ffcf40] dark:text-[#ffcf40] rounded px-0.5\">\n {part}\n </mark>\n ) : (\n <React.Fragment key={i}>{part}</React.Fragment>\n ),\n )}\n </>\n );\n };\n\n if (!node.text) return null;\n\n if (isSection) {\n return (\n <div className=\"rounded-xl border border-border bg-card/60 overflow-hidden mb-3\">\n <button\n onClick={() => setExpanded((e) => !e)}\n className=\"w-full flex items-center gap-2 sm:gap-3 px-3 py-2.5 sm:px-4 sm:py-3 text-left hover:bg-accent/30 transition-colors\"\n >\n {expanded ? (\n <ChevronDown className=\"w-3.5 h-3.5 sm:w-4 sm:h-4 text-muted-foreground flex-shrink-0\" />\n ) : (\n <ChevronRight className=\"w-3.5 h-3.5 sm:w-4 sm:h-4 text-muted-foreground flex-shrink-0\" />\n )}\n <span className=\"font-semibold text-xs sm:text-sm text-foreground flex-1\">{highlight(node.text)}</span>\n <span className=\"text-[10px] sm:text-xs text-muted-foreground\">{node.children.length} items</span>\n </button>\n {expanded && node.children.length > 0 && (\n <div className=\"border-t border-border/50\">\n {node.children.map((child) => (\n <TreeNodeRow key={child.id} node={child} depth={1} query={query} />\n ))}\n </div>\n )}\n </div>\n );\n }\n\n // Leaf / framework entry\n return (\n <div className=\"group px-3 py-2.5 sm:px-4 sm:py-3 border-b border-border/30 last:border-0 hover:bg-accent/20 transition-colors\">\n <div className=\"flex items-start justify-between gap-2 sm:gap-3\">\n <div className=\"flex-1 min-w-0\">\n <div className=\"text-xs sm:text-sm font-medium text-foreground mb-1\">{highlight(node.text)}</div>\n {node.note && (\n <div className=\"text-[11px] sm:text-xs text-muted-foreground leading-relaxed\">{highlight(node.note)}</div>\n )}\n {hasChildren && (\n <div className=\"mt-1.5 sm:mt-2 space-y-1\">\n {node.children.map((child) => (\n <div key={child.id} className=\"text-[11px] sm:text-xs text-muted-foreground/80 pl-2.5 sm:pl-3 border-l border-border/40 leading-relaxed\">\n {highlight(child.text)}\n {child.children.map((gc) => (\n <div key={gc.id} className=\"mt-0.5 pl-2.5 sm:pl-3 border-l border-border/30 text-muted-foreground/60\">\n {highlight(gc.text)}\n </div>\n ))}\n </div>\n ))}\n </div>\n )}\n </div>\n <div className=\"flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity\">\n <CopyButton text={copyText} />\n </div>\n </div>\n </div>\n );\n}\n\nfunction ProjectSection({\n projectId,\n title,\n icon: Icon,\n externalUrl,\n accentClass,\n query,\n}: {\n projectId: string;\n title: string;\n icon: React.ElementType;\n externalUrl: string;\n accentClass: string;\n query: string;\n}) {\n const [nodes, setNodes] = React.useState<ProjectNode[]>([]);\n const [loading, setLoading] = React.useState(true);\n const [error, setError] = React.useState<string | null>(null);\n\n React.useEffect(() => {\n setLoading(true);\n fetch(`/api/taskade/projects/${projectId}/nodes`)\n .then((r) => r.json())\n .then((data) => {\n if (data.ok) setNodes(data.payload.nodes);\n else setError('Failed to load.');\n })\n .catch(() => setError('Network error.'))\n .finally(() => setLoading(false));\n }, [projectId]);\n\n const tree = React.useMemo(() => buildTree(nodes), [nodes]);\n\n const filteredTree = React.useMemo(() => {\n if (!query.trim()) return tree;\n const q = query.toLowerCase();\n function nodeMatches(n: TreeNode): boolean {\n if (n.text.toLowerCase().includes(q)) return true;\n if ((n.note ?? '').toLowerCase().includes(q)) return true;\n return n.children.some(nodeMatches);\n }\n return tree.filter(nodeMatches);\n }, [tree, query]);\n\n return (\n <div>\n <div className=\"flex items-center justify-between mb-3 sm:mb-4\">\n <div className=\"flex items-center gap-2\">\n <div className={cn('p-1 sm:p-1.5 rounded-lg', accentClass)}>\n <Icon className=\"w-3.5 h-3.5 sm:w-4 sm:h-4\" />\n </div>\n <h2 className=\"font-semibold text-sm sm:text-base text-foreground\">{title}</h2>\n </div>\n <a\n href={externalUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"flex items-center gap-1 text-[10px] sm:text-xs text-muted-foreground hover:text-foreground transition-colors\"\n >\n <ExternalLink className=\"w-3 h-3\" />\n Open\n </a>\n </div>\n\n {loading && (\n <div className=\"space-y-2\">\n {Array.from({ length: 3 }).map((_, i) => (\n <div key={i} className=\"h-12 rounded-xl bg-card animate-pulse border border-border\" />\n ))}\n </div>\n )}\n\n {error && (\n <div className=\"flex items-center gap-2 p-3 rounded-xl border border-red-500/20 bg-red-500/5 text-red-400 text-[11px] sm:text-xs\">\n <AlertCircle className=\"w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0\" />\n {error}\n </div>\n )}\n\n {!loading && !error && filteredTree.length === 0 && (\n <div className=\"py-6 sm:py-8 text-center text-xs sm:text-sm text-muted-foreground\">\n {query ? 'No results match.' : 'No content found.'}\n </div>\n )}\n\n {!loading && !error && filteredTree.length > 0 && (\n <div>\n {filteredTree.map((node) => (\n <TreeNodeRow key={node.id} node={node} depth={0} query={query} />\n ))}\n </div>\n )}\n </div>\n );\n}\n\nexport function Library() {\n const [query, setQuery] = React.useState('');\n const [tab, setTab] = React.useState<'frameworks' | 'references'>('frameworks');\n\n return (\n <div className=\"px-4 py-4 sm:px-6 sm:py-8 max-w-4xl mx-auto\">\n {/* Header */}\n <div className=\"mb-5 sm:mb-8\">\n <div className=\"flex items-center gap-2 sm:gap-3 mb-1.5 sm:mb-2\">\n <div className=\"p-1.5 sm:p-2 rounded-xl bg-cyan-500/10 border border-cyan-500/20\">\n <LibraryIcon className=\"w-4 h-4 sm:w-5 sm:h-5 text-cyan-400\" />\n </div>\n <h1 className=\"text-lg sm:text-2xl font-bold text-foreground\">Library</h1>\n </div>\n <p className=\"text-muted-foreground text-xs sm:text-sm\">\n Mental models, frameworks, essential reading, and key references — with quick-copy.\n </p>\n </div>\n\n {/* Search */}\n <div className=\"relative mb-4 sm:mb-6\">\n <Search className=\"absolute left-3 sm:left-3.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 sm:w-4 sm:h-4 text-muted-foreground pointer-events-none\" />\n <input\n type=\"text\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder=\"Search frameworks, books, tools, metrics…\"\n className=\"w-full pl-9 sm:pl-10 pr-4 py-2 sm:py-2.5 rounded-xl bg-card border border-border text-xs sm:text-sm text-foreground placeholder:text-muted-foreground/50 focus:outline-none focus:ring-2 focus:ring-cyan-500/40 focus:border-cyan-500/40 transition-colors\"\n />\n </div>\n\n {/* Tabs */}\n <div className=\"flex gap-1 p-1 rounded-xl bg-muted mb-4 sm:mb-6 w-fit\">\n {(\n [\n { key: 'frameworks', label: 'Mental Models', icon: Brain },\n { key: 'references', label: 'References', icon: BookMarked },\n ] as const\n ).map(({ key, label, icon: Icon }) => (\n <button\n key={key}\n onClick={() => setTab(key)}\n className={cn(\n 'flex items-center gap-1.5 sm:gap-2 px-3 py-1.5 sm:px-4 sm:py-2 rounded-lg text-xs sm:text-sm font-medium transition-all duration-150',\n tab === key\n ? 'bg-background text-foreground shadow-sm'\n : 'text-muted-foreground hover:text-foreground',\n )}\n >\n <Icon className=\"w-3 h-3 sm:w-3.5 sm:h-3.5\" />\n {label}\n </button>\n ))}\n </div>\n\n {/* Content */}\n {tab === 'frameworks' && (\n <ProjectSection\n projectId=\"9T9rrVHjc59ayGQh\"\n title=\"Mental Models & Frameworks\"\n icon={Brain}\n externalUrl=\"https://staging.taskade.dev/d/9T9rrVHjc59ayGQh\"\n accentClass=\"bg-teal-500/10 text-teal-500\"\n query={query}\n />\n )}\n {tab === 'references' && (\n <ProjectSection\n projectId=\"x4Wxw3NruLd78emT\"\n title=\"Library — References\"\n icon={BookMarked}\n externalUrl=\"https://staging.taskade.dev/d/x4Wxw3NruLd78emT\"\n accentClass=\"bg-cyan-500/10 text-cyan-400\"\n query={query}\n />\n )}\n </div>\n );\n}\n"
}
},
"Dashboard.tsx": {
"file": {
"contents": "import * as React from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { cn } from '@/lib/utils';\nimport { AGENTS, PROJECTS, AUTOMATIONS } from '@/data/workspace';\nimport {\n ExternalLink,\n Clock,\n Webhook,\n CalendarClock,\n ArrowRight,\n Layers,\n Bot,\n Workflow,\n} from 'lucide-react';\n\nfunction SectionHeader({\n icon: Icon,\n title,\n count,\n action,\n onAction,\n}: {\n icon: React.ElementType;\n title: string;\n count: number;\n action?: string;\n onAction?: () => void;\n}) {\n return (\n <div className=\"flex items-center justify-between mb-4\">\n <div className=\"flex items-center gap-2.5\">\n <div className=\"p-1.5 rounded-md bg-muted\">\n <Icon className=\"w-4 h-4 text-muted-foreground\" />\n </div>\n <h2 className=\"font-semibold text-foreground\">{title}</h2>\n <span className=\"text-xs text-muted-foreground bg-muted px-2 py-0.5 rounded-full\">\n {count}\n </span>\n </div>\n {action && onAction && (\n <button\n onClick={onAction}\n className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors\"\n >\n {action} <ArrowRight className=\"w-3 h-3\" />\n </button>\n )}\n </div>\n );\n}\n\nfunction AgentCard({ agent }: { agent: typeof AGENTS[number] }) {\n const navigate = useNavigate();\n const isCouncilAgent =\n agent.name === 'Strategist' ||\n agent.name === 'Critic' ||\n agent.name === 'Researcher' ||\n agent.name === 'Builder';\n\n return (\n <button\n onClick={() => isCouncilAgent && navigate('/council')}\n className={cn(\n 'group relative text-left p-4 rounded-xl border bg-card dark:bg-gradient-to-br transition-all duration-200 hover:shadow-lg hover:-translate-y-0.5',\n agent.color,\n agent.border,\n isCouncilAgent ? 'cursor-pointer' : 'cursor-default',\n )}\n >\n <div className=\"flex items-start justify-between mb-2 sm:mb-3\">\n <div className=\"text-lg sm:text-2xl\">{agent.emoji}</div>\n <span className={cn('text-[10px] sm:text-xs px-1.5 sm:px-2 py-0.5 rounded-full font-medium', agent.badge)}>\n Agent\n </span>\n </div>\n <div className=\"font-semibold text-foreground text-xs sm:text-sm mb-0.5 sm:mb-1\">{agent.name}</div>\n <div className=\"text-[10px] sm:text-xs text-muted-foreground mb-1.5 sm:mb-2 leading-relaxed line-clamp-2\">\n {agent.description}\n </div>\n <div className=\"text-[10px] sm:text-xs text-muted-foreground/70 font-mono\">{agent.role}</div>\n {isCouncilAgent && (\n <div className=\"absolute inset-0 rounded-xl flex items-center justify-center bg-background/80 backdrop-blur-sm opacity-0 group-hover:opacity-100 transition-opacity\">\n <span className=\"text-xs font-medium text-foreground flex items-center gap-1\">\n Open Council <ArrowRight className=\"w-3 h-3\" />\n </span>\n </div>\n )}\n </button>\n );\n}\n\nconst CATEGORY_ORDER = ['Core', 'Library', 'Playbooks'] as const;\nconst CATEGORY_COLORS: Record<string, string> = {\n Core: 'bg-teal-500/10 text-teal-500 border-teal-500/20',\n Library: 'bg-cyan-500/10 text-cyan-400 border-cyan-500/20',\n Playbooks: 'bg-[#ffcf40]/10 text-[#ffcf40] border-[#ffcf40]/20',\n};\n\nfunction ProjectCard({ project }: { project: typeof PROJECTS[number] }) {\n const catColor = CATEGORY_COLORS[project.category] ?? 'bg-muted text-muted-foreground';\n\n return (\n <a\n href={project.taskadeUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"group flex items-center gap-3 p-3 rounded-lg border border-border bg-card hover:border-border/80 hover:bg-accent/50 transition-all duration-150\"\n >\n <div className=\"text-base sm:text-xl flex-shrink-0\">{project.emoji}</div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-1.5 sm:gap-2 mb-0.5\">\n <span className=\"text-xs sm:text-sm font-medium text-foreground truncate\">{project.name}</span>\n <span className={cn('text-[10px] sm:text-xs px-1 sm:px-1.5 py-0.5 rounded border flex-shrink-0', catColor)}>\n {project.category}\n </span>\n </div>\n <div className=\"text-[10px] sm:text-xs text-muted-foreground truncate\">{project.description}</div>\n </div>\n <div className=\"hidden sm:flex items-center gap-1.5 flex-shrink-0 text-xs text-muted-foreground/60\">\n <Clock className=\"w-3 h-3\" />\n <span>{project.lastModified}</span>\n <ExternalLink className=\"w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity ml-1\" />\n </div>\n </a>\n );\n}\n\nfunction AutomationCard({ automation }: { automation: typeof AUTOMATIONS[number] }) {\n const isWebhook = automation.trigger === 'Webhook';\n const navigate = useNavigate();\n const isCouncil = automation.id === '01KP2NYSV32Y1Y298SYBDK79DR';\n\n return (\n <button\n onClick={() => (isCouncil ? navigate('/council') : window.open(automation.taskadeUrl, '_blank'))}\n className={cn(\n 'group text-left w-full p-4 rounded-xl border bg-card dark:bg-gradient-to-br transition-all duration-200 hover:shadow-md hover:-translate-y-0.5',\n automation.color,\n automation.border,\n )}\n >\n <div className=\"flex items-start justify-between mb-1.5 sm:mb-2\">\n <span className=\"text-base sm:text-xl\">{automation.emoji}</span>\n <div className=\"flex items-center gap-1 sm:gap-1.5\">\n {isWebhook ? (\n <Webhook className=\"w-3 h-3 sm:w-3.5 sm:h-3.5 text-muted-foreground\" />\n ) : (\n <CalendarClock className=\"w-3 h-3 sm:w-3.5 sm:h-3.5 text-muted-foreground\" />\n )}\n <span className=\"text-[10px] sm:text-xs text-muted-foreground\">{automation.trigger}</span>\n </div>\n </div>\n <div className=\"font-semibold text-xs sm:text-sm text-foreground mb-0.5 sm:mb-1\">{automation.name}</div>\n <div className=\"text-[10px] sm:text-xs text-muted-foreground leading-relaxed mb-2 sm:mb-3 line-clamp-2\">\n {automation.description}\n </div>\n <div className=\"flex flex-wrap gap-1\">\n {automation.agents.map((a) => (\n <span\n key={a}\n className=\"text-[10px] sm:text-xs px-1 sm:px-1.5 py-0.5 rounded bg-background/60 text-muted-foreground border border-border/50\"\n >\n {a}\n </span>\n ))}\n </div>\n </button>\n );\n}\n\nexport function Dashboard() {\n const navigate = useNavigate();\n\n const projectsByCategory = CATEGORY_ORDER.map((cat) => ({\n category: cat,\n projects: PROJECTS.filter((p) => p.category === cat),\n }));\n\n return (\n <div className=\"px-6 py-4 sm:py-8 max-w-7xl mx-auto\">\n {/* Hero */}\n <div className=\"mb-6 sm:mb-10\">\n <h1 className=\"text-3xl font-bold text-foreground tracking-tight mb-2\">\n Good morning, Cortex.\n </h1>\n <p className=\"text-muted-foreground text-xs sm:text-base\">\n {AGENTS.length} agents · {PROJECTS.length} projects · {AUTOMATIONS.length} automations — all alive.\n </p>\n </div>\n\n {/* Stats row */}\n <div className=\"grid grid-cols-3 gap-2 sm:gap-4 mb-6 sm:mb-10\">\n {[\n { icon: Bot, label: 'Agents', value: AGENTS.length, color: 'text-teal-500' },\n { icon: Layers, label: 'Projects', value: PROJECTS.length, color: 'text-cyan-400' },\n { icon: Workflow, label: 'Automations', value: AUTOMATIONS.length, color: 'text-emerald-400' },\n ].map(({ icon: Icon, label, value, color }) => (\n <div key={label} className=\"p-2.5 sm:p-4 rounded-xl border border-border bg-card flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-4 min-w-0\">\n <div className=\"flex items-center gap-2 sm:gap-4\">\n <div className={cn('p-1.5 sm:p-2.5 rounded-lg bg-muted flex-shrink-0', color)}>\n <Icon className=\"w-4 h-4 sm:w-5 sm:h-5\" />\n </div>\n <div className=\"text-lg sm:text-2xl font-bold text-foreground\">{value}</div>\n </div>\n <div className=\"text-[10px] sm:text-xs text-muted-foreground truncate\">{label}</div>\n </div>\n ))}\n </div>\n\n {/* Agents */}\n <section className=\"mb-10\">\n <SectionHeader\n icon={Bot}\n title=\"Agents\"\n count={AGENTS.length}\n action=\"Open Council\"\n onAction={() => navigate('/council')}\n />\n <div className=\"grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3\">\n {AGENTS.map((agent) => (\n <AgentCard key={agent.id} agent={agent} />\n ))}\n </div>\n </section>\n\n {/* Projects */}\n <section className=\"mb-10\">\n <SectionHeader icon={Layers} title=\"Projects\" count={PROJECTS.length} />\n <div className=\"space-y-6\">\n {projectsByCategory.map(({ category, projects }) => (\n <div key={category}>\n <div className=\"text-xs font-semibold text-muted-foreground uppercase tracking-widest mb-2 ml-1\">\n {category}\n </div>\n <div className=\"space-y-1.5\">\n {projects.map((p) => (\n <ProjectCard key={p.id} project={p} />\n ))}\n </div>\n </div>\n ))}\n </div>\n </section>\n\n {/* Automations */}\n <section>\n <SectionHeader\n icon={Workflow}\n title=\"Automations\"\n count={AUTOMATIONS.length}\n />\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3\">\n {AUTOMATIONS.map((a) => (\n <AutomationCard key={a.id} automation={a} />\n ))}\n </div>\n </section>\n </div>\n );\n}\n"
}
}
}
},
"App.tsx": {
"file": {
"contents": "import * as React from 'react';\nimport { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';\nimport { ThemeProvider } from 'next-themes';\nimport { GenesisAuth } from '@/lib/genesis-auth';\nimport { Layout } from '@/components/Layout';\nimport { Dashboard } from '@/pages/Dashboard';\nimport { Council } from '@/pages/Council';\nimport { Journal } from '@/pages/Journal';\nimport { Library } from '@/pages/Library';\n\nconst App: React.FC = function () {\n return (\n <ThemeProvider attribute=\"class\" defaultTheme=\"dark\" enableSystem>\n <GenesisAuth>\n <BrowserRouter>\n <Layout>\n <Routes>\n <Route path=\"/\" element={<Dashboard />} />\n <Route path=\"/council\" element={<Council />} />\n <Route path=\"/journal\" element={<Journal />} />\n <Route path=\"/library\" element={<Library />} />\n <Route path=\"*\" element={<Navigate to=\"/\" replace />} />\n </Routes>\n </Layout>\n </BrowserRouter>\n </GenesisAuth>\n </ThemeProvider>\n );\n};\n\nexport default App;\n"
}
},
"main.tsx": {
"file": {
"contents": "import './index.css';\nimport './lib/leaflet-setup';\n\n// Load IBM Plex fonts from Google Fonts\nconst fontLink = document.createElement('link');\nfontLink.rel = 'stylesheet';\nfontLink.href = 'https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400;1,500&family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Serif:ital,wght@0,400;0,500;0,600;1,400;1,500&display=swap';\ndocument.head.appendChild(fontLink);\n\nimport { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\n\nimport App from './App.jsx';\nimport { GenesisRoot } from './lib/genesis.jsx';\nimport { setupThemeBridge } from './lib/theme-bridge';\n\nsetupThemeBridge();\n\ncreateRoot(document.getElementById('root')!).render(\n <StrictMode>\n <GenesisRoot>\n <App />\n </GenesisRoot>\n </StrictMode>,\n);\n"
}
},
"index.css": {
"file": {
"contents": "/* Tailwind CSS v3 */\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n :root {\n --background: 37 29% 95%;\n --foreground: 222.2 47.4% 11.2%;\n --muted: 36 20% 91%;\n --muted-foreground: 215.4 16.3% 46.9%;\n --popover: 0 0% 100%;\n --popover-foreground: 222.2 47.4% 11.2%;\n --border: 36 15% 88%;\n --input: 36 15% 88%;\n --card: 0 0% 100%;\n --card-foreground: 222.2 47.4% 11.2%;\n --primary: 185 79% 40%;\n --primary-foreground: 0 0% 100%;\n --secondary: 36 20% 91%;\n --secondary-foreground: 222.2 47.4% 11.2%;\n --accent: 36 20% 91%;\n --accent-foreground: 222.2 47.4% 11.2%;\n --destructive: 0 100% 50%;\n --destructive-foreground: 210 40% 98%;\n --ring: 185 79% 40%;\n --radius: 0.5rem;\n }\n\n .dark {\n --background: 224 71% 4%;\n --foreground: 213 31% 91%;\n --muted: 223 47% 11%;\n --muted-foreground: 215.4 16.3% 56.9%;\n --accent: 216 34% 17%;\n --accent-foreground: 210 40% 98%;\n --popover: 224 71% 4%;\n --popover-foreground: 215 20.2% 65.1%;\n --border: 216 34% 17%;\n --input: 216 34% 17%;\n --card: 224 71% 4%;\n --card-foreground: 213 31% 91%;\n --primary: 185 79% 40%;\n --primary-foreground: 0 0% 100%;\n --secondary: 222.2 47.4% 11.2%;\n --secondary-foreground: 210 40% 98%;\n --destructive: 0 63% 31%;\n --destructive-foreground: 210 40% 98%;\n --ring: 185 79% 40%;\n }\n}\n\n@layer base {\n * {\n @apply border-border;\n }\n body {\n font-family: 'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif;\n @apply antialiased bg-background text-foreground;\n }\n code, pre, kbd, samp, .font-mono {\n font-family: 'IBM Plex Mono', ui-monospace, SFMono-Regular, monospace;\n }\n blockquote, .font-serif {\n font-family: 'IBM Plex Serif', ui-serif, Georgia, serif;\n }\n}\n\n:root {\n /* #1a1a2e navigation panel */\n --sidebar: hsl(240 27% 14%);\n --sidebar-foreground: hsl(240 10% 85%);\n --sidebar-primary: hsl(185 79% 40%);\n --sidebar-primary-foreground: hsl(0 0% 100%);\n --sidebar-accent: hsl(240 20% 20%);\n --sidebar-accent-foreground: hsl(240 10% 95%);\n --sidebar-border: hsl(240 18% 22%);\n --sidebar-ring: hsl(185 79% 40%);\n}\n\n.dark {\n /* #1a1a2e navigation panel */\n --sidebar: hsl(240 27% 14%);\n --sidebar-foreground: hsl(240 10% 85%);\n --sidebar-primary: hsl(185 79% 40%);\n --sidebar-primary-foreground: hsl(0 0% 100%);\n --sidebar-accent: hsl(240 20% 20%);\n --sidebar-accent-foreground: hsl(240 10% 95%);\n --sidebar-border: hsl(240 18% 22%);\n --sidebar-ring: hsl(185 79% 40%);\n}\n\n\n@layer base {\n * {\n @apply border-border outline-ring/50;\n }\n}\n\n/* Full screen spinner styles */\n.genesis__spinner-container {\n position: fixed;\n inset: 0;\n background: #000000;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 9999;\n}\n\n.genesis__spinner-ring {\n width: 20px;\n height: 20px;\n border: 1.5px solid transparent;\n border-top: 1.5px solid rgba(255, 255, 255, 0.6);\n border-radius: 50%;\n animation: genesis__spin 1s linear infinite;\n}\n\n@keyframes genesis__spin {\n 0% { transform: rotate(0deg); }\n 100% { transform: rotate(360deg); }\n}\n\n/* Respect reduced motion preferences */\n@media (prefers-reduced-motion: reduce) {\n .genesis__spinner-ring {\n animation: none;\n border-top-color: rgba(255, 255, 255, 0.9);\n }\n}\n"
}
},
"components": {
"directory": {
"ui": {
"directory": {
"kbd.tsx": {
"file": {
"contents": "import { cn } from \"@/lib/utils\"\n\nfunction Kbd({ className, ...props }: React.ComponentProps<\"kbd\">) {\n return (\n <kbd\n data-slot=\"kbd\"\n className={cn(\n \"bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none\",\n \"[&_svg:not([class*='size-'])]:size-3\",\n \"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction KbdGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <kbd\n data-slot=\"kbd-group\"\n className={cn(\"inline-flex items-center gap-1\", className)}\n {...props}\n />\n )\n}\n\nexport { Kbd, KbdGroup }\n"
}
},
"card.tsx": {
"file": {
"contents": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Card({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card\"\n className={cn(\n \"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CardHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-header\"\n className={cn(\n \"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CardTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-title\"\n className={cn(\"leading-none font-semibold\", className)}\n {...props}\n />\n )\n}\n\nfunction CardDescription({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-description\"\n className={cn(\"text-muted-foreground text-sm\", className)}\n {...props}\n />\n )\n}\n\nfunction CardAction({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-action\"\n className={cn(\n \"col-start-2 row-span-2 row-start-1 self-start justify-self-end\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CardContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-content\"\n className={cn(\"px-6\", className)}\n {...props}\n />\n )\n}\n\nfunction CardFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"card-footer\"\n className={cn(\"flex items-center px-6 [.border-t]:pt-6\", className)}\n {...props}\n />\n )\n}\n\nexport {\n Card,\n CardHeader,\n CardFooter,\n CardTitle,\n CardAction,\n CardDescription,\n CardContent,\n}\n"
}
},
"form.tsx": {
"file": {
"contents": "\"use client\"\n\nimport * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport {\n Controller,\n FormProvider,\n useFormContext,\n useFormState,\n type ControllerProps,\n type FieldPath,\n type FieldValues,\n} from \"react-hook-form\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Label } from \"@/components/ui/label\"\n\nconst Form = FormProvider\n\ntype FormFieldContextValue<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n> = {\n name: TName\n}\n\nconst FormFieldContext = React.createContext<FormFieldContextValue>(\n {} as FormFieldContextValue\n)\n\nconst FormField = <\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n>({\n ...props\n}: ControllerProps<TFieldValues, TName>) => {\n return (\n <FormFieldContext.Provider value={{ name: props.name }}>\n <Controller {...props} />\n </FormFieldContext.Provider>\n )\n}\n\nconst useFormField = () => {\n const fieldContext = React.useContext(FormFieldContext)\n const itemContext = React.useContext(FormItemContext)\n const { getFieldState } = useFormContext()\n const formState = useFormState({ name: fieldContext.name })\n const fieldState = getFieldState(fieldContext.name, formState)\n\n if (!fieldContext) {\n throw new Error(\"useFormField should be used within <FormField>\")\n }\n\n const { id } = itemContext\n\n return {\n id,\n name: fieldContext.name,\n formItemId: `${id}-form-item`,\n formDescriptionId: `${id}-form-item-description`,\n formMessageId: `${id}-form-item-message`,\n ...fieldState,\n }\n}\n\ntype FormItemContextValue = {\n id: string\n}\n\nconst FormItemContext = React.createContext<FormItemContextValue>(\n {} as FormItemContextValue\n)\n\nfunction FormItem({ className, ...props }: React.ComponentProps<\"div\">) {\n const id = React.useId()\n\n return (\n <FormItemContext.Provider value={{ id }}>\n <div\n data-slot=\"form-item\"\n className={cn(\"grid gap-2\", className)}\n {...props}\n />\n </FormItemContext.Provider>\n )\n}\n\nfunction FormLabel({\n className,\n ...props\n}: React.ComponentProps<typeof LabelPrimitive.Root>) {\n const { error, formItemId } = useFormField()\n\n return (\n <Label\n data-slot=\"form-label\"\n data-error={!!error}\n className={cn(\"data-[error=true]:text-destructive\", className)}\n htmlFor={formItemId}\n {...props}\n />\n )\n}\n\nfunction FormControl({ ...props }: React.ComponentProps<typeof Slot>) {\n const { error, formItemId, formDescriptionId, formMessageId } = useFormField()\n\n return (\n <Slot\n data-slot=\"form-control\"\n id={formItemId}\n aria-describedby={\n !error\n ? `${formDescriptionId}`\n : `${formDescriptionId} ${formMessageId}`\n }\n aria-invalid={!!error}\n {...props}\n />\n )\n}\n\nfunction FormDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n const { formDescriptionId } = useFormField()\n\n return (\n <p\n data-slot=\"form-description\"\n id={formDescriptionId}\n className={cn(\"text-muted-foreground text-sm\", className)}\n {...props}\n />\n )\n}\n\nfunction FormMessage({ className, ...props }: React.ComponentProps<\"p\">) {\n const { error, formMessageId } = useFormField()\n const body = error ? String(error?.message ?? \"\") : props.children\n\n if (!body) {\n return null\n }\n\n return (\n <p\n data-slot=\"form-message\"\n id={formMessageId}\n className={cn(\"text-destructive text-sm\", className)}\n {...props}\n >\n {body}\n </p>\n )\n}\n\nexport {\n useFormField,\n Form,\n FormItem,\n FormLabel,\n FormControl,\n FormDescription,\n FormMessage,\n FormField,\n}\n"
}
},
"item.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Separator } from \"@/components/ui/separator\"\n\nfunction ItemGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n role=\"list\"\n data-slot=\"item-group\"\n className={cn(\"group/item-group flex flex-col\", className)}\n {...props}\n />\n )\n}\n\nfunction ItemSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof Separator>) {\n return (\n <Separator\n data-slot=\"item-separator\"\n orientation=\"horizontal\"\n className={cn(\"my-0\", className)}\n {...props}\n />\n )\n}\n\nconst itemVariants = cva(\n \"group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n {\n variants: {\n variant: {\n default: \"bg-transparent\",\n outline: \"border-border\",\n muted: \"bg-muted/50\",\n },\n size: {\n default: \"p-4 gap-4 \",\n sm: \"py-3 px-4 gap-2.5\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\nfunction Item({\n className,\n variant = \"default\",\n size = \"default\",\n asChild = false,\n ...props\n}: React.ComponentProps<\"div\"> &\n VariantProps<typeof itemVariants> & { asChild?: boolean }) {\n const Comp = asChild ? Slot : \"div\"\n return (\n <Comp\n data-slot=\"item\"\n data-variant={variant}\n data-size={size}\n className={cn(itemVariants({ variant, size, className }))}\n {...props}\n />\n )\n}\n\nconst itemMediaVariants = cva(\n \"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5\",\n {\n variants: {\n variant: {\n default: \"bg-transparent\",\n icon: \"size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4\",\n image:\n \"size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction ItemMedia({\n className,\n variant = \"default\",\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof itemMediaVariants>) {\n return (\n <div\n data-slot=\"item-media\"\n data-variant={variant}\n className={cn(itemMediaVariants({ variant, className }))}\n {...props}\n />\n )\n}\n\nfunction ItemContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"item-content\"\n className={cn(\n \"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ItemTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"item-title\"\n className={cn(\n \"flex w-fit items-center gap-2 text-sm leading-snug font-medium\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ItemDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n <p\n data-slot=\"item-description\"\n className={cn(\n \"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance\",\n \"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ItemActions({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"item-actions\"\n className={cn(\"flex items-center gap-2\", className)}\n {...props}\n />\n )\n}\n\nfunction ItemHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"item-header\"\n className={cn(\n \"flex basis-full items-center justify-between gap-2\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ItemFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"item-footer\"\n className={cn(\n \"flex basis-full items-center justify-between gap-2\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport {\n Item,\n ItemMedia,\n ItemContent,\n ItemActions,\n ItemGroup,\n ItemSeparator,\n ItemTitle,\n ItemDescription,\n ItemHeader,\n ItemFooter,\n}\n"
}
},
"tabs.tsx": {
"file": {
"contents": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Tabs({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Root>) {\n return (\n <TabsPrimitive.Root\n data-slot=\"tabs\"\n className={cn(\"flex flex-col gap-2\", className)}\n {...props}\n />\n )\n}\n\nfunction TabsList({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.List>) {\n return (\n <TabsPrimitive.List\n data-slot=\"tabs-list\"\n className={cn(\n \"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TabsTrigger({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {\n return (\n <TabsPrimitive.Trigger\n data-slot=\"tabs-trigger\"\n className={cn(\n \"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TabsContent({\n className,\n ...props\n}: React.ComponentProps<typeof TabsPrimitive.Content>) {\n return (\n <TabsPrimitive.Content\n data-slot=\"tabs-content\"\n className={cn(\"flex-1 outline-none\", className)}\n {...props}\n />\n )\n}\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent }\n"
}
},
"alert.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst alertVariants = cva(\n \"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current\",\n {\n variants: {\n variant: {\n default: \"bg-card text-card-foreground\",\n destructive:\n \"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction Alert({\n className,\n variant,\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof alertVariants>) {\n return (\n <div\n data-slot=\"alert\"\n role=\"alert\"\n className={cn(alertVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nfunction AlertTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-title\"\n className={cn(\n \"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction AlertDescription({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-description\"\n className={cn(\n \"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport { Alert, AlertTitle, AlertDescription }\n"
}
},
"badge.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst badgeVariants = cva(\n \"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden\",\n {\n variants: {\n variant: {\n default:\n \"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90\",\n secondary:\n \"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90\",\n destructive:\n \"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n outline:\n \"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction Badge({\n className,\n variant,\n asChild = false,\n ...props\n}: React.ComponentProps<\"span\"> &\n VariantProps<typeof badgeVariants> & { asChild?: boolean }) {\n const Comp = asChild ? Slot : \"span\"\n\n return (\n <Comp\n data-slot=\"badge\"\n className={cn(badgeVariants({ variant }), className)}\n {...props}\n />\n )\n}\n\nexport { Badge, badgeVariants }\n"
}
},
"chart.tsx": {
"file": {
"contents": "\"use client\"\n\nimport * as React from \"react\"\nimport * as RechartsPrimitive from \"recharts\"\n\nimport { cn } from \"@/lib/utils\"\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = { light: \"\", dark: \".dark\" } as const\n\nexport type ChartConfig = {\n [k in string]: {\n label?: React.ReactNode\n icon?: React.ComponentType\n } & (\n | { color?: string; theme?: never }\n | { color?: never; theme: Record<keyof typeof THEMES, string> }\n )\n}\n\ntype ChartContextProps = {\n config: ChartConfig\n}\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null)\n\nfunction useChart() {\n const context = React.useContext(ChartContext)\n\n if (!context) {\n throw new Error(\"useChart must be used within a <ChartContainer />\")\n }\n\n return context\n}\n\nfunction ChartContainer({\n id,\n className,\n children,\n config,\n ...props\n}: React.ComponentProps<\"div\"> & {\n config: ChartConfig\n children: React.ComponentProps<\n typeof RechartsPrimitive.ResponsiveContainer\n >[\"children\"]\n}) {\n const uniqueId = React.useId()\n const chartId = `chart-${id || uniqueId.replace(/:/g, \"\")}`\n\n return (\n <ChartContext.Provider value={{ config }}>\n <div\n data-slot=\"chart\"\n data-chart={chartId}\n className={cn(\n \"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden\",\n className\n )}\n {...props}\n >\n <ChartStyle id={chartId} config={config} />\n <RechartsPrimitive.ResponsiveContainer>\n {children}\n </RechartsPrimitive.ResponsiveContainer>\n </div>\n </ChartContext.Provider>\n )\n}\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n const colorConfig = Object.entries(config).filter(\n ([, config]) => config.theme || config.color\n )\n\n if (!colorConfig.length) {\n return null\n }\n\n return (\n <style\n dangerouslySetInnerHTML={{\n __html: Object.entries(THEMES)\n .map(\n ([theme, prefix]) => `\n${prefix} [data-chart=${id}] {\n${colorConfig\n .map(([key, itemConfig]) => {\n const color =\n itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||\n itemConfig.color\n return color ? ` --color-${key}: ${color};` : null\n })\n .join(\"\\n\")}\n}\n`\n )\n .join(\"\\n\"),\n }}\n />\n )\n}\n\nconst ChartTooltip = RechartsPrimitive.Tooltip\n\nfunction ChartTooltipContent({\n active,\n payload,\n className,\n indicator = \"dot\",\n hideLabel = false,\n hideIndicator = false,\n label,\n labelFormatter,\n labelClassName,\n formatter,\n color,\n nameKey,\n labelKey,\n}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &\n React.ComponentProps<\"div\"> & {\n hideLabel?: boolean\n hideIndicator?: boolean\n indicator?: \"line\" | \"dot\" | \"dashed\"\n nameKey?: string\n labelKey?: string\n }) {\n const { config } = useChart()\n\n const tooltipLabel = React.useMemo(() => {\n if (hideLabel || !payload?.length) {\n return null\n }\n\n const [item] = payload\n const key = `${labelKey || item?.dataKey || item?.name || \"value\"}`\n const itemConfig = getPayloadConfigFromPayload(config, item, key)\n const value =\n !labelKey && typeof label === \"string\"\n ? config[label as keyof typeof config]?.label || label\n : itemConfig?.label\n\n if (labelFormatter) {\n return (\n <div className={cn(\"font-medium\", labelClassName)}>\n {labelFormatter(value, payload)}\n </div>\n )\n }\n\n if (!value) {\n return null\n }\n\n return <div className={cn(\"font-medium\", labelClassName)}>{value}</div>\n }, [\n label,\n labelFormatter,\n payload,\n hideLabel,\n labelClassName,\n config,\n labelKey,\n ])\n\n if (!active || !payload?.length) {\n return null\n }\n\n const nestLabel = payload.length === 1 && indicator !== \"dot\"\n\n return (\n <div\n className={cn(\n \"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl\",\n className\n )}\n >\n {!nestLabel ? tooltipLabel : null}\n <div className=\"grid gap-1.5\">\n {payload\n .filter((item) => item.type !== \"none\")\n .map((item, index) => {\n const key = `${nameKey || item.name || item.dataKey || \"value\"}`\n const itemConfig = getPayloadConfigFromPayload(config, item, key)\n const indicatorColor = color || item.payload.fill || item.color\n\n return (\n <div\n key={item.dataKey}\n className={cn(\n \"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5\",\n indicator === \"dot\" && \"items-center\"\n )}\n >\n {formatter && item?.value !== undefined && item.name ? (\n formatter(item.value, item.name, item, index, item.payload)\n ) : (\n <>\n {itemConfig?.icon ? (\n <itemConfig.icon />\n ) : (\n !hideIndicator && (\n <div\n className={cn(\n \"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)\",\n {\n \"h-2.5 w-2.5\": indicator === \"dot\",\n \"w-1\": indicator === \"line\",\n \"w-0 border-[1.5px] border-dashed bg-transparent\":\n indicator === \"dashed\",\n \"my-0.5\": nestLabel && indicator === \"dashed\",\n }\n )}\n style={\n {\n \"--color-bg\": indicatorColor,\n \"--color-border\": indicatorColor,\n } as React.CSSProperties\n }\n />\n )\n )}\n <div\n className={cn(\n \"flex flex-1 justify-between leading-none\",\n nestLabel ? \"items-end\" : \"items-center\"\n )}\n >\n <div className=\"grid gap-1.5\">\n {nestLabel ? tooltipLabel : null}\n <span className=\"text-muted-foreground\">\n {itemConfig?.label || item.name}\n </span>\n </div>\n {item.value && (\n <span className=\"text-foreground font-mono font-medium tabular-nums\">\n {item.value.toLocaleString()}\n </span>\n )}\n </div>\n </>\n )}\n </div>\n )\n })}\n </div>\n </div>\n )\n}\n\nconst ChartLegend = RechartsPrimitive.Legend\n\nfunction ChartLegendContent({\n className,\n hideIcon = false,\n payload,\n verticalAlign = \"bottom\",\n nameKey,\n}: React.ComponentProps<\"div\"> &\n Pick<RechartsPrimitive.LegendProps, \"payload\" | \"verticalAlign\"> & {\n hideIcon?: boolean\n nameKey?: string\n }) {\n const { config } = useChart()\n\n if (!payload?.length) {\n return null\n }\n\n return (\n <div\n className={cn(\n \"flex items-center justify-center gap-4\",\n verticalAlign === \"top\" ? \"pb-3\" : \"pt-3\",\n className\n )}\n >\n {payload\n .filter((item) => item.type !== \"none\")\n .map((item) => {\n const key = `${nameKey || item.dataKey || \"value\"}`\n const itemConfig = getPayloadConfigFromPayload(config, item, key)\n\n return (\n <div\n key={item.value}\n className={cn(\n \"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3\"\n )}\n >\n {itemConfig?.icon && !hideIcon ? (\n <itemConfig.icon />\n ) : (\n <div\n className=\"h-2 w-2 shrink-0 rounded-[2px]\"\n style={{\n backgroundColor: item.color,\n }}\n />\n )}\n {itemConfig?.label}\n </div>\n )\n })}\n </div>\n )\n}\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(\n config: ChartConfig,\n payload: unknown,\n key: string\n) {\n if (typeof payload !== \"object\" || payload === null) {\n return undefined\n }\n\n const payloadPayload =\n \"payload\" in payload &&\n typeof payload.payload === \"object\" &&\n payload.payload !== null\n ? payload.payload\n : undefined\n\n let configLabelKey: string = key\n\n if (\n key in payload &&\n typeof payload[key as keyof typeof payload] === \"string\"\n ) {\n configLabelKey = payload[key as keyof typeof payload] as string\n } else if (\n payloadPayload &&\n key in payloadPayload &&\n typeof payloadPayload[key as keyof typeof payloadPayload] === \"string\"\n ) {\n configLabelKey = payloadPayload[\n key as keyof typeof payloadPayload\n ] as string\n }\n\n return configLabelKey in config\n ? config[configLabelKey]\n : config[key as keyof typeof config]\n}\n\nexport {\n ChartContainer,\n ChartTooltip,\n ChartTooltipContent,\n ChartLegend,\n ChartLegendContent,\n ChartStyle,\n}\n"
}
},
"empty.tsx": {
"file": {
"contents": "import { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Empty({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"empty\"\n className={cn(\n \"flex min-w-0 flex-1 flex-col items-center justify-center gap-6 rounded-lg border-dashed p-6 text-center text-balance md:p-12\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction EmptyHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"empty-header\"\n className={cn(\n \"flex max-w-sm flex-col items-center gap-2 text-center\",\n className\n )}\n {...props}\n />\n )\n}\n\nconst emptyMediaVariants = cva(\n \"flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0\",\n {\n variants: {\n variant: {\n default: \"bg-transparent\",\n icon: \"bg-muted text-foreground flex size-10 shrink-0 items-center justify-center rounded-lg [&_svg:not([class*='size-'])]:size-6\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n },\n }\n)\n\nfunction EmptyMedia({\n className,\n variant = \"default\",\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof emptyMediaVariants>) {\n return (\n <div\n data-slot=\"empty-icon\"\n data-variant={variant}\n className={cn(emptyMediaVariants({ variant, className }))}\n {...props}\n />\n )\n}\n\nfunction EmptyTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"empty-title\"\n className={cn(\"text-lg font-medium tracking-tight\", className)}\n {...props}\n />\n )\n}\n\nfunction EmptyDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n <div\n data-slot=\"empty-description\"\n className={cn(\n \"text-muted-foreground [&>a:hover]:text-primary text-sm/relaxed [&>a]:underline [&>a]:underline-offset-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction EmptyContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"empty-content\"\n className={cn(\n \"flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport {\n Empty,\n EmptyHeader,\n EmptyTitle,\n EmptyDescription,\n EmptyContent,\n EmptyMedia,\n}\n"
}
},
"field.tsx": {
"file": {
"contents": "import { useMemo } from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Label } from \"@/components/ui/label\"\nimport { Separator } from \"@/components/ui/separator\"\n\nfunction FieldSet({ className, ...props }: React.ComponentProps<\"fieldset\">) {\n return (\n <fieldset\n data-slot=\"field-set\"\n className={cn(\n \"flex flex-col gap-6\",\n \"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldLegend({\n className,\n variant = \"legend\",\n ...props\n}: React.ComponentProps<\"legend\"> & { variant?: \"legend\" | \"label\" }) {\n return (\n <legend\n data-slot=\"field-legend\"\n data-variant={variant}\n className={cn(\n \"mb-3 font-medium\",\n \"data-[variant=legend]:text-base\",\n \"data-[variant=label]:text-sm\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"field-group\"\n className={cn(\n \"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nconst fieldVariants = cva(\n \"group/field flex w-full gap-3 data-[invalid=true]:text-destructive\",\n {\n variants: {\n orientation: {\n vertical: [\"flex-col [&>*]:w-full [&>.sr-only]:w-auto\"],\n horizontal: [\n \"flex-row items-center\",\n \"[&>[data-slot=field-label]]:flex-auto\",\n \"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px\",\n ],\n responsive: [\n \"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto\",\n \"@md/field-group:[&>[data-slot=field-label]]:flex-auto\",\n \"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px\",\n ],\n },\n },\n defaultVariants: {\n orientation: \"vertical\",\n },\n }\n)\n\nfunction Field({\n className,\n orientation = \"vertical\",\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof fieldVariants>) {\n return (\n <div\n role=\"group\"\n data-slot=\"field\"\n data-orientation={orientation}\n className={cn(fieldVariants({ orientation }), className)}\n {...props}\n />\n )\n}\n\nfunction FieldContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"field-content\"\n className={cn(\n \"group/field-content flex flex-1 flex-col gap-1.5 leading-snug\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldLabel({\n className,\n ...props\n}: React.ComponentProps<typeof Label>) {\n return (\n <Label\n data-slot=\"field-label\"\n className={cn(\n \"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50\",\n \"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4\",\n \"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"field-label\"\n className={cn(\n \"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n return (\n <p\n data-slot=\"field-description\"\n className={cn(\n \"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance\",\n \"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5\",\n \"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction FieldSeparator({\n children,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n children?: React.ReactNode\n}) {\n return (\n <div\n data-slot=\"field-separator\"\n data-content={!!children}\n className={cn(\n \"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2\",\n className\n )}\n {...props}\n >\n <Separator className=\"absolute inset-0 top-1/2\" />\n {children && (\n <span\n className=\"bg-background text-muted-foreground relative mx-auto block w-fit px-2\"\n data-slot=\"field-separator-content\"\n >\n {children}\n </span>\n )}\n </div>\n )\n}\n\nfunction FieldError({\n className,\n children,\n errors,\n ...props\n}: React.ComponentProps<\"div\"> & {\n errors?: Array<{ message?: string } | undefined>\n}) {\n const content = useMemo(() => {\n if (children) {\n return children\n }\n\n if (!errors) {\n return null\n }\n\n if (errors?.length === 1 && errors[0]?.message) {\n return errors[0].message\n }\n\n return (\n <ul className=\"ml-4 flex list-disc flex-col gap-1\">\n {errors.map(\n (error, index) =>\n error?.message && <li key={index}>{error.message}</li>\n )}\n </ul>\n )\n }, [children, errors])\n\n if (!content) {\n return null\n }\n\n return (\n <div\n role=\"alert\"\n data-slot=\"field-error\"\n className={cn(\"text-destructive text-sm font-normal\", className)}\n {...props}\n >\n {content}\n </div>\n )\n}\n\nexport {\n Field,\n FieldLabel,\n FieldDescription,\n FieldError,\n FieldGroup,\n FieldLegend,\n FieldSeparator,\n FieldSet,\n FieldContent,\n FieldTitle,\n}\n"
}
},
"input.tsx": {
"file": {
"contents": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Input({ className, type, ...props }: React.ComponentProps<\"input\">) {\n return (\n <input\n type={type}\n data-slot=\"input\"\n className={cn(\n \"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n \"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]\",\n \"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport { Input }\n"
}
},
"label.tsx": {
"file": {
"contents": "\"use client\"\n\nimport * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Label({\n className,\n ...props\n}: React.ComponentProps<typeof LabelPrimitive.Root>) {\n return (\n <LabelPrimitive.Root\n data-slot=\"label\"\n className={cn(\n \"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport { Label }\n"
}
},
"sheet.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\"\nimport { XIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {\n return <SheetPrimitive.Root data-slot=\"sheet\" {...props} />\n}\n\nfunction SheetTrigger({\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {\n return <SheetPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />\n}\n\nfunction SheetClose({\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Close>) {\n return <SheetPrimitive.Close data-slot=\"sheet-close\" {...props} />\n}\n\nfunction SheetPortal({\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Portal>) {\n return <SheetPrimitive.Portal data-slot=\"sheet-portal\" {...props} />\n}\n\nfunction SheetOverlay({\n className,\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {\n return (\n <SheetPrimitive.Overlay\n data-slot=\"sheet-overlay\"\n className={cn(\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction SheetContent({\n className,\n children,\n side = \"right\",\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Content> & {\n side?: \"top\" | \"right\" | \"bottom\" | \"left\"\n}) {\n return (\n <SheetPortal>\n <SheetOverlay />\n <SheetPrimitive.Content\n data-slot=\"sheet-content\"\n className={cn(\n \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500\",\n side === \"right\" &&\n \"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm\",\n side === \"left\" &&\n \"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm\",\n side === \"top\" &&\n \"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b\",\n side === \"bottom\" &&\n \"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t\",\n className\n )}\n {...props}\n >\n {children}\n <SheetPrimitive.Close className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none\">\n <XIcon className=\"size-4\" />\n <span className=\"sr-only\">Close</span>\n </SheetPrimitive.Close>\n </SheetPrimitive.Content>\n </SheetPortal>\n )\n}\n\nfunction SheetHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sheet-header\"\n className={cn(\"flex flex-col gap-1.5 p-4\", className)}\n {...props}\n />\n )\n}\n\nfunction SheetFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sheet-footer\"\n className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\n {...props}\n />\n )\n}\n\nfunction SheetTitle({\n className,\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Title>) {\n return (\n <SheetPrimitive.Title\n data-slot=\"sheet-title\"\n className={cn(\"text-foreground font-semibold\", className)}\n {...props}\n />\n )\n}\n\nfunction SheetDescription({\n className,\n ...props\n}: React.ComponentProps<typeof SheetPrimitive.Description>) {\n return (\n <SheetPrimitive.Description\n data-slot=\"sheet-description\"\n className={cn(\"text-muted-foreground text-sm\", className)}\n {...props}\n />\n )\n}\n\nexport {\n Sheet,\n SheetTrigger,\n SheetClose,\n SheetContent,\n SheetHeader,\n SheetFooter,\n SheetTitle,\n SheetDescription,\n}\n"
}
},
"table.tsx": {
"file": {
"contents": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Table({ className, ...props }: React.ComponentProps<\"table\">) {\n return (\n <div\n data-slot=\"table-container\"\n className=\"relative w-full overflow-x-auto\"\n >\n <table\n data-slot=\"table\"\n className={cn(\"w-full caption-bottom text-sm\", className)}\n {...props}\n />\n </div>\n )\n}\n\nfunction TableHeader({ className, ...props }: React.ComponentProps<\"thead\">) {\n return (\n <thead\n data-slot=\"table-header\"\n className={cn(\"[&_tr]:border-b\", className)}\n {...props}\n />\n )\n}\n\nfunction TableBody({ className, ...props }: React.ComponentProps<\"tbody\">) {\n return (\n <tbody\n data-slot=\"table-body\"\n className={cn(\"[&_tr:last-child]:border-0\", className)}\n {...props}\n />\n )\n}\n\nfunction TableFooter({ className, ...props }: React.ComponentProps<\"tfoot\">) {\n return (\n <tfoot\n data-slot=\"table-footer\"\n className={cn(\n \"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TableRow({ className, ...props }: React.ComponentProps<\"tr\">) {\n return (\n <tr\n data-slot=\"table-row\"\n className={cn(\n \"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TableHead({ className, ...props }: React.ComponentProps<\"th\">) {\n return (\n <th\n data-slot=\"table-head\"\n className={cn(\n \"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TableCell({ className, ...props }: React.ComponentProps<\"td\">) {\n return (\n <td\n data-slot=\"table-cell\"\n className={cn(\n \"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction TableCaption({\n className,\n ...props\n}: React.ComponentProps<\"caption\">) {\n return (\n <caption\n data-slot=\"table-caption\"\n className={cn(\"text-muted-foreground mt-4 text-sm\", className)}\n {...props}\n />\n )\n}\n\nexport {\n Table,\n TableHeader,\n TableBody,\n TableFooter,\n TableHead,\n TableRow,\n TableCell,\n TableCaption,\n}\n"
}
},
"avatar.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Avatar({\n className,\n ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Root>) {\n return (\n <AvatarPrimitive.Root\n data-slot=\"avatar\"\n className={cn(\n \"relative flex size-8 shrink-0 overflow-hidden rounded-full\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction AvatarImage({\n className,\n ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Image>) {\n return (\n <AvatarPrimitive.Image\n data-slot=\"avatar-image\"\n className={cn(\"aspect-square size-full\", className)}\n {...props}\n />\n )\n}\n\nfunction AvatarFallback({\n className,\n ...props\n}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {\n return (\n <AvatarPrimitive.Fallback\n data-slot=\"avatar-fallback\"\n className={cn(\n \"bg-muted flex size-full items-center justify-center rounded-full\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport { Avatar, AvatarImage, AvatarFallback }\n"
}
},
"button.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\n {\n variants: {\n variant: {\n default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n destructive:\n \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60\",\n outline:\n \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n ghost:\n \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n sm: \"h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5\",\n lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n icon: \"size-9\",\n \"icon-sm\": \"size-8\",\n \"icon-lg\": \"size-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\nfunction Button({\n className,\n variant,\n size,\n asChild = false,\n ...props\n}: React.ComponentProps<\"button\"> &\n VariantProps<typeof buttonVariants> & {\n asChild?: boolean\n }) {\n const Comp = asChild ? Slot : \"button\"\n\n return (\n <Comp\n data-slot=\"button\"\n className={cn(buttonVariants({ variant, size, className }))}\n {...props}\n />\n )\n}\n\nexport { Button, buttonVariants }\n"
}
},
"dialog.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { XIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Dialog({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Root>) {\n return <DialogPrimitive.Root data-slot=\"dialog\" {...props} />\n}\n\nfunction DialogTrigger({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {\n return <DialogPrimitive.Trigger data-slot=\"dialog-trigger\" {...props} />\n}\n\nfunction DialogPortal({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Portal>) {\n return <DialogPrimitive.Portal data-slot=\"dialog-portal\" {...props} />\n}\n\nfunction DialogClose({\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Close>) {\n return <DialogPrimitive.Close data-slot=\"dialog-close\" {...props} />\n}\n\nfunction DialogOverlay({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {\n return (\n <DialogPrimitive.Overlay\n data-slot=\"dialog-overlay\"\n className={cn(\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction DialogContent({\n className,\n children,\n showCloseButton = true,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Content> & {\n showCloseButton?: boolean\n}) {\n return (\n <DialogPortal data-slot=\"dialog-portal\">\n <DialogOverlay />\n <DialogPrimitive.Content\n data-slot=\"dialog-content\"\n className={cn(\n \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 p-6 shadow-lg duration-200 sm:max-w-lg\",\n className\n )}\n {...props}\n >\n {children}\n {showCloseButton && (\n <DialogPrimitive.Close\n data-slot=\"dialog-close\"\n className=\"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\"\n >\n <XIcon />\n <span className=\"sr-only\">Close</span>\n </DialogPrimitive.Close>\n )}\n </DialogPrimitive.Content>\n </DialogPortal>\n )\n}\n\nfunction DialogHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-header\"\n className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n {...props}\n />\n )\n}\n\nfunction DialogFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"dialog-footer\"\n className={cn(\n \"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction DialogTitle({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Title>) {\n return (\n <DialogPrimitive.Title\n data-slot=\"dialog-title\"\n className={cn(\"text-lg leading-none font-semibold\", className)}\n {...props}\n />\n )\n}\n\nfunction DialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof DialogPrimitive.Description>) {\n return (\n <DialogPrimitive.Description\n data-slot=\"dialog-description\"\n className={cn(\"text-muted-foreground text-sm\", className)}\n {...props}\n />\n )\n}\n\nexport {\n Dialog,\n DialogClose,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogOverlay,\n DialogPortal,\n DialogTitle,\n DialogTrigger,\n}\n"
}
},
"drawer.tsx": {
"file": {
"contents": "\"use client\"\n\nimport * as React from \"react\"\nimport { Drawer as DrawerPrimitive } from \"vaul\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Drawer({\n ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Root>) {\n return <DrawerPrimitive.Root data-slot=\"drawer\" {...props} />\n}\n\nfunction DrawerTrigger({\n ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {\n return <DrawerPrimitive.Trigger data-slot=\"drawer-trigger\" {...props} />\n}\n\nfunction DrawerPortal({\n ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {\n return <DrawerPrimitive.Portal data-slot=\"drawer-portal\" {...props} />\n}\n\nfunction DrawerClose({\n ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Close>) {\n return <DrawerPrimitive.Close data-slot=\"drawer-close\" {...props} />\n}\n\nfunction DrawerOverlay({\n className,\n ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {\n return (\n <DrawerPrimitive.Overlay\n data-slot=\"drawer-overlay\"\n className={cn(\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction DrawerContent({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Content>) {\n return (\n <DrawerPortal data-slot=\"drawer-portal\">\n <DrawerOverlay />\n <DrawerPrimitive.Content\n data-slot=\"drawer-content\"\n className={cn(\n \"group/drawer-content bg-background fixed z-50 flex h-auto flex-col\",\n \"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b\",\n \"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t\",\n \"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm\",\n \"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm\",\n className\n )}\n {...props}\n >\n <div className=\"bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block\" />\n {children}\n </DrawerPrimitive.Content>\n </DrawerPortal>\n )\n}\n\nfunction DrawerHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"drawer-header\"\n className={cn(\n \"flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction DrawerFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"drawer-footer\"\n className={cn(\"mt-auto flex flex-col gap-2 p-4\", className)}\n {...props}\n />\n )\n}\n\nfunction DrawerTitle({\n className,\n ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Title>) {\n return (\n <DrawerPrimitive.Title\n data-slot=\"drawer-title\"\n className={cn(\"text-foreground font-semibold\", className)}\n {...props}\n />\n )\n}\n\nfunction DrawerDescription({\n className,\n ...props\n}: React.ComponentProps<typeof DrawerPrimitive.Description>) {\n return (\n <DrawerPrimitive.Description\n data-slot=\"drawer-description\"\n className={cn(\"text-muted-foreground text-sm\", className)}\n {...props}\n />\n )\n}\n\nexport {\n Drawer,\n DrawerPortal,\n DrawerOverlay,\n DrawerTrigger,\n DrawerClose,\n DrawerContent,\n DrawerHeader,\n DrawerFooter,\n DrawerTitle,\n DrawerDescription,\n}\n"
}
},
"select.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { CheckIcon, ChevronDownIcon, ChevronUpIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Select({\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Root>) {\n return <SelectPrimitive.Root data-slot=\"select\" {...props} />\n}\n\nfunction SelectGroup({\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Group>) {\n return <SelectPrimitive.Group data-slot=\"select-group\" {...props} />\n}\n\nfunction SelectValue({\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Value>) {\n return <SelectPrimitive.Value data-slot=\"select-value\" {...props} />\n}\n\nfunction SelectTrigger({\n className,\n size = \"default\",\n children,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {\n size?: \"sm\" | \"default\"\n}) {\n return (\n <SelectPrimitive.Trigger\n data-slot=\"select-trigger\"\n data-size={size}\n className={cn(\n \"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 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 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n >\n {children}\n <SelectPrimitive.Icon asChild>\n <ChevronDownIcon className=\"size-4 opacity-50\" />\n </SelectPrimitive.Icon>\n </SelectPrimitive.Trigger>\n )\n}\n\nfunction SelectContent({\n className,\n children,\n position = \"popper\",\n align = \"center\",\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Content>) {\n return (\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n data-slot=\"select-content\"\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]: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 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 shadow-md\",\n position === \"popper\" &&\n \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n className\n )}\n position={position}\n align={align}\n {...props}\n >\n <SelectScrollUpButton />\n <SelectPrimitive.Viewport\n className={cn(\n \"p-1\",\n position === \"popper\" &&\n \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1\"\n )}\n >\n {children}\n </SelectPrimitive.Viewport>\n <SelectScrollDownButton />\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n )\n}\n\nfunction SelectLabel({\n className,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Label>) {\n return (\n <SelectPrimitive.Label\n data-slot=\"select-label\"\n className={cn(\"text-muted-foreground px-2 py-1.5 text-xs\", className)}\n {...props}\n />\n )\n}\n\nfunction SelectItem({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Item>) {\n return (\n <SelectPrimitive.Item\n data-slot=\"select-item\"\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground 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 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2\",\n className\n )}\n {...props}\n >\n <span className=\"absolute right-2 flex size-3.5 items-center justify-center\">\n <SelectPrimitive.ItemIndicator>\n <CheckIcon className=\"size-4\" />\n </SelectPrimitive.ItemIndicator>\n </span>\n <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n </SelectPrimitive.Item>\n )\n}\n\nfunction SelectSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.Separator>) {\n return (\n <SelectPrimitive.Separator\n data-slot=\"select-separator\"\n className={cn(\"bg-border pointer-events-none -mx-1 my-1 h-px\", className)}\n {...props}\n />\n )\n}\n\nfunction SelectScrollUpButton({\n className,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {\n return (\n <SelectPrimitive.ScrollUpButton\n data-slot=\"select-scroll-up-button\"\n className={cn(\n \"flex cursor-default items-center justify-center py-1\",\n className\n )}\n {...props}\n >\n <ChevronUpIcon className=\"size-4\" />\n </SelectPrimitive.ScrollUpButton>\n )\n}\n\nfunction SelectScrollDownButton({\n className,\n ...props\n}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {\n return (\n <SelectPrimitive.ScrollDownButton\n data-slot=\"select-scroll-down-button\"\n className={cn(\n \"flex cursor-default items-center justify-center py-1\",\n className\n )}\n {...props}\n >\n <ChevronDownIcon className=\"size-4\" />\n </SelectPrimitive.ScrollDownButton>\n )\n}\n\nexport {\n Select,\n SelectContent,\n SelectGroup,\n SelectItem,\n SelectLabel,\n SelectScrollDownButton,\n SelectScrollUpButton,\n SelectSeparator,\n SelectTrigger,\n SelectValue,\n}\n"
}
},
"slider.tsx": {
"file": {
"contents": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SliderPrimitive from \"@radix-ui/react-slider\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Slider({\n className,\n defaultValue,\n value,\n min = 0,\n max = 100,\n ...props\n}: React.ComponentProps<typeof SliderPrimitive.Root>) {\n const _values = React.useMemo(\n () =>\n Array.isArray(value)\n ? value\n : Array.isArray(defaultValue)\n ? defaultValue\n : [min, max],\n [value, defaultValue, min, max]\n )\n\n return (\n <SliderPrimitive.Root\n data-slot=\"slider\"\n defaultValue={defaultValue}\n value={value}\n min={min}\n max={max}\n className={cn(\n \"relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col\",\n className\n )}\n {...props}\n >\n <SliderPrimitive.Track\n data-slot=\"slider-track\"\n className={cn(\n \"bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5\"\n )}\n >\n <SliderPrimitive.Range\n data-slot=\"slider-range\"\n className={cn(\n \"bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full\"\n )}\n />\n </SliderPrimitive.Track>\n {Array.from({ length: _values.length }, (_, index) => (\n <SliderPrimitive.Thumb\n data-slot=\"slider-thumb\"\n key={index}\n className=\"border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50\"\n />\n ))}\n </SliderPrimitive.Root>\n )\n}\n\nexport { Slider }\n"
}
},
"sonner.tsx": {
"file": {
"contents": "import { useTheme } from \"next-themes\"\nimport { Toaster as Sonner, ToasterProps } from \"sonner\"\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n const { theme = \"system\" } = useTheme()\n\n return (\n <Sonner\n theme={theme as ToasterProps[\"theme\"]}\n className=\"toaster group\"\n style={\n {\n \"--normal-bg\": \"var(--popover)\",\n \"--normal-text\": \"var(--popover-foreground)\",\n \"--normal-border\": \"var(--border)\",\n } as React.CSSProperties\n }\n {...props}\n />\n )\n}\n\nexport { Toaster }\n"
}
},
"switch.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport * as SwitchPrimitive from \"@radix-ui/react-switch\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Switch({\n className,\n ...props\n}: React.ComponentProps<typeof SwitchPrimitive.Root>) {\n return (\n <SwitchPrimitive.Root\n data-slot=\"switch\"\n className={cn(\n \"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n {...props}\n >\n <SwitchPrimitive.Thumb\n data-slot=\"switch-thumb\"\n className={cn(\n \"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0\"\n )}\n />\n </SwitchPrimitive.Root>\n )\n}\n\nexport { Switch }\n"
}
},
"toggle.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport * as TogglePrimitive from \"@radix-ui/react-toggle\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst toggleVariants = cva(\n \"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap\",\n {\n variants: {\n variant: {\n default: \"bg-transparent\",\n outline:\n \"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground\",\n },\n size: {\n default: \"h-9 px-2 min-w-9\",\n sm: \"h-8 px-1.5 min-w-8\",\n lg: \"h-10 px-2.5 min-w-10\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\nfunction Toggle({\n className,\n variant,\n size,\n ...props\n}: React.ComponentProps<typeof TogglePrimitive.Root> &\n VariantProps<typeof toggleVariants>) {\n return (\n <TogglePrimitive.Root\n data-slot=\"toggle\"\n className={cn(toggleVariants({ variant, size, className }))}\n {...props}\n />\n )\n}\n\nexport { Toggle, toggleVariants }\n"
}
},
"command.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport { Command as CommandPrimitive } from \"cmdk\"\nimport { SearchIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n} from \"@/components/ui/dialog\"\n\nfunction Command({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive>) {\n return (\n <CommandPrimitive\n data-slot=\"command\"\n className={cn(\n \"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CommandDialog({\n title = \"Command Palette\",\n description = \"Search for a command to run...\",\n children,\n className,\n showCloseButton = true,\n ...props\n}: React.ComponentProps<typeof Dialog> & {\n title?: string\n description?: string\n className?: string\n showCloseButton?: boolean\n}) {\n return (\n <Dialog {...props}>\n <DialogHeader className=\"sr-only\">\n <DialogTitle>{title}</DialogTitle>\n <DialogDescription>{description}</DialogDescription>\n </DialogHeader>\n <DialogContent\n className={cn(\"overflow-hidden p-0\", className)}\n showCloseButton={showCloseButton}\n >\n <Command className=\"[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[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\">\n {children}\n </Command>\n </DialogContent>\n </Dialog>\n )\n}\n\nfunction CommandInput({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Input>) {\n return (\n <div\n data-slot=\"command-input-wrapper\"\n className=\"flex h-9 items-center gap-2 border-b px-3\"\n >\n <SearchIcon className=\"size-4 shrink-0 opacity-50\" />\n <CommandPrimitive.Input\n data-slot=\"command-input\"\n className={cn(\n \"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n {...props}\n />\n </div>\n )\n}\n\nfunction CommandList({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.List>) {\n return (\n <CommandPrimitive.List\n data-slot=\"command-list\"\n className={cn(\n \"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CommandEmpty({\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Empty>) {\n return (\n <CommandPrimitive.Empty\n data-slot=\"command-empty\"\n className=\"py-6 text-center text-sm\"\n {...props}\n />\n )\n}\n\nfunction CommandGroup({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Group>) {\n return (\n <CommandPrimitive.Group\n data-slot=\"command-group\"\n className={cn(\n \"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CommandSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Separator>) {\n return (\n <CommandPrimitive.Separator\n data-slot=\"command-separator\"\n className={cn(\"bg-border -mx-1 h-px\", className)}\n {...props}\n />\n )\n}\n\nfunction CommandItem({\n className,\n ...props\n}: React.ComponentProps<typeof CommandPrimitive.Item>) {\n return (\n <CommandPrimitive.Item\n data-slot=\"command-item\"\n className={cn(\n \"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground 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 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CommandShortcut({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"command-shortcut\"\n className={cn(\n \"text-muted-foreground ml-auto text-xs tracking-widest\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport {\n Command,\n CommandDialog,\n CommandInput,\n CommandList,\n CommandEmpty,\n CommandGroup,\n CommandItem,\n CommandShortcut,\n CommandSeparator,\n}\n"
}
},
"menubar.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport * as MenubarPrimitive from \"@radix-ui/react-menubar\"\nimport { CheckIcon, ChevronRightIcon, CircleIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Menubar({\n className,\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.Root>) {\n return (\n <MenubarPrimitive.Root\n data-slot=\"menubar\"\n className={cn(\n \"bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction MenubarMenu({\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.Menu>) {\n return <MenubarPrimitive.Menu data-slot=\"menubar-menu\" {...props} />\n}\n\nfunction MenubarGroup({\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.Group>) {\n return <MenubarPrimitive.Group data-slot=\"menubar-group\" {...props} />\n}\n\nfunction MenubarPortal({\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.Portal>) {\n return <MenubarPrimitive.Portal data-slot=\"menubar-portal\" {...props} />\n}\n\nfunction MenubarRadioGroup({\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {\n return (\n <MenubarPrimitive.RadioGroup data-slot=\"menubar-radio-group\" {...props} />\n )\n}\n\nfunction MenubarTrigger({\n className,\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {\n return (\n <MenubarPrimitive.Trigger\n data-slot=\"menubar-trigger\"\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction MenubarContent({\n className,\n align = \"start\",\n alignOffset = -4,\n sideOffset = 8,\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.Content>) {\n return (\n <MenubarPortal>\n <MenubarPrimitive.Content\n data-slot=\"menubar-content\"\n align={align}\n alignOffset={alignOffset}\n sideOffset={sideOffset}\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]: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 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md\",\n className\n )}\n {...props}\n />\n </MenubarPortal>\n )\n}\n\nfunction MenubarItem({\n className,\n inset,\n variant = \"default\",\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.Item> & {\n inset?: boolean\n variant?: \"default\" | \"destructive\"\n}) {\n return (\n <MenubarPrimitive.Item\n data-slot=\"menubar-item\"\n data-inset={inset}\n data-variant={variant}\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction MenubarCheckboxItem({\n className,\n children,\n checked,\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {\n return (\n <MenubarPrimitive.CheckboxItem\n data-slot=\"menubar-checkbox-item\"\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n checked={checked}\n {...props}\n >\n <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n <MenubarPrimitive.ItemIndicator>\n <CheckIcon className=\"size-4\" />\n </MenubarPrimitive.ItemIndicator>\n </span>\n {children}\n </MenubarPrimitive.CheckboxItem>\n )\n}\n\nfunction MenubarRadioItem({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {\n return (\n <MenubarPrimitive.RadioItem\n data-slot=\"menubar-radio-item\"\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n >\n <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n <MenubarPrimitive.ItemIndicator>\n <CircleIcon className=\"size-2 fill-current\" />\n </MenubarPrimitive.ItemIndicator>\n </span>\n {children}\n </MenubarPrimitive.RadioItem>\n )\n}\n\nfunction MenubarLabel({\n className,\n inset,\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.Label> & {\n inset?: boolean\n}) {\n return (\n <MenubarPrimitive.Label\n data-slot=\"menubar-label\"\n data-inset={inset}\n className={cn(\n \"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction MenubarSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.Separator>) {\n return (\n <MenubarPrimitive.Separator\n data-slot=\"menubar-separator\"\n className={cn(\"bg-border -mx-1 my-1 h-px\", className)}\n {...props}\n />\n )\n}\n\nfunction MenubarShortcut({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"menubar-shortcut\"\n className={cn(\n \"text-muted-foreground ml-auto text-xs tracking-widest\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction MenubarSub({\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.Sub>) {\n return <MenubarPrimitive.Sub data-slot=\"menubar-sub\" {...props} />\n}\n\nfunction MenubarSubTrigger({\n className,\n inset,\n children,\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {\n inset?: boolean\n}) {\n return (\n <MenubarPrimitive.SubTrigger\n data-slot=\"menubar-sub-trigger\"\n data-inset={inset}\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8\",\n className\n )}\n {...props}\n >\n {children}\n <ChevronRightIcon className=\"ml-auto h-4 w-4\" />\n </MenubarPrimitive.SubTrigger>\n )\n}\n\nfunction MenubarSubContent({\n className,\n ...props\n}: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {\n return (\n <MenubarPrimitive.SubContent\n data-slot=\"menubar-sub-content\"\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]: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 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport {\n Menubar,\n MenubarPortal,\n MenubarMenu,\n MenubarTrigger,\n MenubarContent,\n MenubarGroup,\n MenubarSeparator,\n MenubarLabel,\n MenubarItem,\n MenubarShortcut,\n MenubarCheckboxItem,\n MenubarRadioGroup,\n MenubarRadioItem,\n MenubarSub,\n MenubarSubTrigger,\n MenubarSubContent,\n}\n"
}
},
"popover.tsx": {
"file": {
"contents": "\"use client\"\n\nimport * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Popover({\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Root>) {\n return <PopoverPrimitive.Root data-slot=\"popover\" {...props} />\n}\n\nfunction PopoverTrigger({\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {\n return <PopoverPrimitive.Trigger data-slot=\"popover-trigger\" {...props} />\n}\n\nfunction PopoverContent({\n className,\n align = \"center\",\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Content>) {\n return (\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n data-slot=\"popover-content\"\n align={align}\n sideOffset={sideOffset}\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]: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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden\",\n className\n )}\n {...props}\n />\n </PopoverPrimitive.Portal>\n )\n}\n\nfunction PopoverAnchor({\n ...props\n}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {\n return <PopoverPrimitive.Anchor data-slot=\"popover-anchor\" {...props} />\n}\n\nexport { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }\n"
}
},
"sidebar.tsx": {
"file": {
"contents": "\"use client\"\n\nimport * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, VariantProps } from \"class-variance-authority\"\nimport { PanelLeftIcon } from \"lucide-react\"\n\nimport { useIsMobile } from \"@/hooks/use-mobile\"\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport { Separator } from \"@/components/ui/separator\"\nimport {\n Sheet,\n SheetContent,\n SheetDescription,\n SheetHeader,\n SheetTitle,\n} from \"@/components/ui/sheet\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\"\n\nconst SIDEBAR_COOKIE_NAME = \"sidebar_state\"\nconst SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7\nconst SIDEBAR_WIDTH = \"16rem\"\nconst SIDEBAR_WIDTH_MOBILE = \"18rem\"\nconst SIDEBAR_WIDTH_ICON = \"3rem\"\nconst SIDEBAR_KEYBOARD_SHORTCUT = \"b\"\n\ntype SidebarContextProps = {\n state: \"expanded\" | \"collapsed\"\n open: boolean\n setOpen: (open: boolean) => void\n openMobile: boolean\n setOpenMobile: (open: boolean) => void\n isMobile: boolean\n toggleSidebar: () => void\n}\n\nconst SidebarContext = React.createContext<SidebarContextProps | null>(null)\n\nfunction useSidebar() {\n const context = React.useContext(SidebarContext)\n if (!context) {\n throw new Error(\"useSidebar must be used within a SidebarProvider.\")\n }\n\n return context\n}\n\nfunction SidebarProvider({\n defaultOpen = true,\n open: openProp,\n onOpenChange: setOpenProp,\n className,\n style,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & {\n defaultOpen?: boolean\n open?: boolean\n onOpenChange?: (open: boolean) => void\n}) {\n const isMobile = useIsMobile()\n const [openMobile, setOpenMobile] = React.useState(false)\n\n // This is the internal state of the sidebar.\n // We use openProp and setOpenProp for control from outside the component.\n const [_open, _setOpen] = React.useState(defaultOpen)\n const open = openProp ?? _open\n const setOpen = React.useCallback(\n (value: boolean | ((value: boolean) => boolean)) => {\n const openState = typeof value === \"function\" ? value(open) : value\n if (setOpenProp) {\n setOpenProp(openState)\n } else {\n _setOpen(openState)\n }\n\n // This sets the cookie to keep the sidebar state.\n document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`\n },\n [setOpenProp, open]\n )\n\n // Helper to toggle the sidebar.\n const toggleSidebar = React.useCallback(() => {\n return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)\n }, [isMobile, setOpen, setOpenMobile])\n\n // Adds a keyboard shortcut to toggle the sidebar.\n React.useEffect(() => {\n const handleKeyDown = (event: KeyboardEvent) => {\n if (\n event.key === SIDEBAR_KEYBOARD_SHORTCUT &&\n (event.metaKey || event.ctrlKey)\n ) {\n event.preventDefault()\n toggleSidebar()\n }\n }\n\n window.addEventListener(\"keydown\", handleKeyDown)\n return () => window.removeEventListener(\"keydown\", handleKeyDown)\n }, [toggleSidebar])\n\n // We add a state so that we can do data-state=\"expanded\" or \"collapsed\".\n // This makes it easier to style the sidebar with Tailwind classes.\n const state = open ? \"expanded\" : \"collapsed\"\n\n const contextValue = React.useMemo<SidebarContextProps>(\n () => ({\n state,\n open,\n setOpen,\n isMobile,\n openMobile,\n setOpenMobile,\n toggleSidebar,\n }),\n [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]\n )\n\n return (\n <SidebarContext.Provider value={contextValue}>\n <TooltipProvider delayDuration={0}>\n <div\n data-slot=\"sidebar-wrapper\"\n style={\n {\n \"--sidebar-width\": SIDEBAR_WIDTH,\n \"--sidebar-width-icon\": SIDEBAR_WIDTH_ICON,\n ...style,\n } as React.CSSProperties\n }\n className={cn(\n \"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full\",\n className\n )}\n {...props}\n >\n {children}\n </div>\n </TooltipProvider>\n </SidebarContext.Provider>\n )\n}\n\nfunction Sidebar({\n side = \"left\",\n variant = \"sidebar\",\n collapsible = \"offcanvas\",\n className,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & {\n side?: \"left\" | \"right\"\n variant?: \"sidebar\" | \"floating\" | \"inset\"\n collapsible?: \"offcanvas\" | \"icon\" | \"none\"\n}) {\n const { isMobile, state, openMobile, setOpenMobile } = useSidebar()\n\n if (collapsible === \"none\") {\n return (\n <div\n data-slot=\"sidebar\"\n className={cn(\n \"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col\",\n className\n )}\n {...props}\n >\n {children}\n </div>\n )\n }\n\n if (isMobile) {\n return (\n <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>\n <SheetContent\n data-sidebar=\"sidebar\"\n data-slot=\"sidebar\"\n data-mobile=\"true\"\n className=\"bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden\"\n style={\n {\n \"--sidebar-width\": SIDEBAR_WIDTH_MOBILE,\n } as React.CSSProperties\n }\n side={side}\n >\n <SheetHeader className=\"sr-only\">\n <SheetTitle>Sidebar</SheetTitle>\n <SheetDescription>Displays the mobile sidebar.</SheetDescription>\n </SheetHeader>\n <div className=\"flex h-full w-full flex-col\">{children}</div>\n </SheetContent>\n </Sheet>\n )\n }\n\n return (\n <div\n className=\"group peer text-sidebar-foreground hidden md:block\"\n data-state={state}\n data-collapsible={state === \"collapsed\" ? collapsible : \"\"}\n data-variant={variant}\n data-side={side}\n data-slot=\"sidebar\"\n >\n {/* This is what handles the sidebar gap on desktop */}\n <div\n data-slot=\"sidebar-gap\"\n className={cn(\n \"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear\",\n \"group-data-[collapsible=offcanvas]:w-0\",\n \"group-data-[side=right]:rotate-180\",\n variant === \"floating\" || variant === \"inset\"\n ? \"group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]\"\n : \"group-data-[collapsible=icon]:w-(--sidebar-width-icon)\"\n )}\n />\n <div\n data-slot=\"sidebar-container\"\n className={cn(\n \"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex\",\n side === \"left\"\n ? \"left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]\"\n : \"right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]\",\n // Adjust the padding for floating and inset variants.\n variant === \"floating\" || variant === \"inset\"\n ? \"p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]\"\n : \"group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l\",\n className\n )}\n {...props}\n >\n <div\n data-sidebar=\"sidebar\"\n data-slot=\"sidebar-inner\"\n className=\"bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm\"\n >\n {children}\n </div>\n </div>\n </div>\n )\n}\n\nfunction SidebarTrigger({\n className,\n onClick,\n ...props\n}: React.ComponentProps<typeof Button>) {\n const { toggleSidebar } = useSidebar()\n\n return (\n <Button\n data-sidebar=\"trigger\"\n data-slot=\"sidebar-trigger\"\n variant=\"ghost\"\n size=\"icon\"\n className={cn(\"size-7\", className)}\n onClick={(event) => {\n onClick?.(event)\n toggleSidebar()\n }}\n {...props}\n >\n <PanelLeftIcon />\n <span className=\"sr-only\">Toggle Sidebar</span>\n </Button>\n )\n}\n\nfunction SidebarRail({ className, ...props }: React.ComponentProps<\"button\">) {\n const { toggleSidebar } = useSidebar()\n\n return (\n <button\n data-sidebar=\"rail\"\n data-slot=\"sidebar-rail\"\n aria-label=\"Toggle Sidebar\"\n tabIndex={-1}\n onClick={toggleSidebar}\n title=\"Toggle Sidebar\"\n className={cn(\n \"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex\",\n \"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize\",\n \"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize\",\n \"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full\",\n \"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2\",\n \"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction SidebarInset({ className, ...props }: React.ComponentProps<\"main\">) {\n return (\n <main\n data-slot=\"sidebar-inset\"\n className={cn(\n \"bg-background relative flex w-full flex-1 flex-col\",\n \"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction SidebarInput({\n className,\n ...props\n}: React.ComponentProps<typeof Input>) {\n return (\n <Input\n data-slot=\"sidebar-input\"\n data-sidebar=\"input\"\n className={cn(\"bg-background h-8 w-full shadow-none\", className)}\n {...props}\n />\n )\n}\n\nfunction SidebarHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sidebar-header\"\n data-sidebar=\"header\"\n className={cn(\"flex flex-col gap-2 p-2\", className)}\n {...props}\n />\n )\n}\n\nfunction SidebarFooter({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sidebar-footer\"\n data-sidebar=\"footer\"\n className={cn(\"flex flex-col gap-2 p-2\", className)}\n {...props}\n />\n )\n}\n\nfunction SidebarSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof Separator>) {\n return (\n <Separator\n data-slot=\"sidebar-separator\"\n data-sidebar=\"separator\"\n className={cn(\"bg-sidebar-border mx-2 w-auto\", className)}\n {...props}\n />\n )\n}\n\nfunction SidebarContent({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sidebar-content\"\n data-sidebar=\"content\"\n className={cn(\n \"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction SidebarGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sidebar-group\"\n data-sidebar=\"group\"\n className={cn(\"relative flex w-full min-w-0 flex-col p-2\", className)}\n {...props}\n />\n )\n}\n\nfunction SidebarGroupLabel({\n className,\n asChild = false,\n ...props\n}: React.ComponentProps<\"div\"> & { asChild?: boolean }) {\n const Comp = asChild ? Slot : \"div\"\n\n return (\n <Comp\n data-slot=\"sidebar-group-label\"\n data-sidebar=\"group-label\"\n className={cn(\n \"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0\",\n \"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction SidebarGroupAction({\n className,\n asChild = false,\n ...props\n}: React.ComponentProps<\"button\"> & { asChild?: boolean }) {\n const Comp = asChild ? Slot : \"button\"\n\n return (\n <Comp\n data-slot=\"sidebar-group-action\"\n data-sidebar=\"group-action\"\n className={cn(\n \"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0\",\n // Increases the hit area of the button on mobile.\n \"after:absolute after:-inset-2 md:after:hidden\",\n \"group-data-[collapsible=icon]:hidden\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction SidebarGroupContent({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sidebar-group-content\"\n data-sidebar=\"group-content\"\n className={cn(\"w-full text-sm\", className)}\n {...props}\n />\n )\n}\n\nfunction SidebarMenu({ className, ...props }: React.ComponentProps<\"ul\">) {\n return (\n <ul\n data-slot=\"sidebar-menu\"\n data-sidebar=\"menu\"\n className={cn(\"flex w-full min-w-0 flex-col gap-1\", className)}\n {...props}\n />\n )\n}\n\nfunction SidebarMenuItem({ className, ...props }: React.ComponentProps<\"li\">) {\n return (\n <li\n data-slot=\"sidebar-menu-item\"\n data-sidebar=\"menu-item\"\n className={cn(\"group/menu-item relative\", className)}\n {...props}\n />\n )\n}\n\nconst sidebarMenuButtonVariants = cva(\n \"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0\",\n {\n variants: {\n variant: {\n default: \"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground\",\n outline:\n \"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]\",\n },\n size: {\n default: \"h-8 text-sm\",\n sm: \"h-7 text-xs\",\n lg: \"h-12 text-sm group-data-[collapsible=icon]:p-0!\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n)\n\nfunction SidebarMenuButton({\n asChild = false,\n isActive = false,\n variant = \"default\",\n size = \"default\",\n tooltip,\n className,\n ...props\n}: React.ComponentProps<\"button\"> & {\n asChild?: boolean\n isActive?: boolean\n tooltip?: string | React.ComponentProps<typeof TooltipContent>\n} & VariantProps<typeof sidebarMenuButtonVariants>) {\n const Comp = asChild ? Slot : \"button\"\n const { isMobile, state } = useSidebar()\n\n const button = (\n <Comp\n data-slot=\"sidebar-menu-button\"\n data-sidebar=\"menu-button\"\n data-size={size}\n data-active={isActive}\n className={cn(sidebarMenuButtonVariants({ variant, size }), className)}\n {...props}\n />\n )\n\n if (!tooltip) {\n return button\n }\n\n if (typeof tooltip === \"string\") {\n tooltip = {\n children: tooltip,\n }\n }\n\n return (\n <Tooltip>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent\n side=\"right\"\n align=\"center\"\n hidden={state !== \"collapsed\" || isMobile}\n {...tooltip}\n />\n </Tooltip>\n )\n}\n\nfunction SidebarMenuAction({\n className,\n asChild = false,\n showOnHover = false,\n ...props\n}: React.ComponentProps<\"button\"> & {\n asChild?: boolean\n showOnHover?: boolean\n}) {\n const Comp = asChild ? Slot : \"button\"\n\n return (\n <Comp\n data-slot=\"sidebar-menu-action\"\n data-sidebar=\"menu-action\"\n className={cn(\n \"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0\",\n // Increases the hit area of the button on mobile.\n \"after:absolute after:-inset-2 md:after:hidden\",\n \"peer-data-[size=sm]/menu-button:top-1\",\n \"peer-data-[size=default]/menu-button:top-1.5\",\n \"peer-data-[size=lg]/menu-button:top-2.5\",\n \"group-data-[collapsible=icon]:hidden\",\n showOnHover &&\n \"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction SidebarMenuBadge({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"sidebar-menu-badge\"\n data-sidebar=\"menu-badge\"\n className={cn(\n \"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none\",\n \"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground\",\n \"peer-data-[size=sm]/menu-button:top-1\",\n \"peer-data-[size=default]/menu-button:top-1.5\",\n \"peer-data-[size=lg]/menu-button:top-2.5\",\n \"group-data-[collapsible=icon]:hidden\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction SidebarMenuSkeleton({\n className,\n showIcon = false,\n ...props\n}: React.ComponentProps<\"div\"> & {\n showIcon?: boolean\n}) {\n // Random width between 50 to 90%.\n const width = React.useMemo(() => {\n return `${Math.floor(Math.random() * 40) + 50}%`\n }, [])\n\n return (\n <div\n data-slot=\"sidebar-menu-skeleton\"\n data-sidebar=\"menu-skeleton\"\n className={cn(\"flex h-8 items-center gap-2 rounded-md px-2\", className)}\n {...props}\n >\n {showIcon && (\n <Skeleton\n className=\"size-4 rounded-md\"\n data-sidebar=\"menu-skeleton-icon\"\n />\n )}\n <Skeleton\n className=\"h-4 max-w-(--skeleton-width) flex-1\"\n data-sidebar=\"menu-skeleton-text\"\n style={\n {\n \"--skeleton-width\": width,\n } as React.CSSProperties\n }\n />\n </div>\n )\n}\n\nfunction SidebarMenuSub({ className, ...props }: React.ComponentProps<\"ul\">) {\n return (\n <ul\n data-slot=\"sidebar-menu-sub\"\n data-sidebar=\"menu-sub\"\n className={cn(\n \"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5\",\n \"group-data-[collapsible=icon]:hidden\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction SidebarMenuSubItem({\n className,\n ...props\n}: React.ComponentProps<\"li\">) {\n return (\n <li\n data-slot=\"sidebar-menu-sub-item\"\n data-sidebar=\"menu-sub-item\"\n className={cn(\"group/menu-sub-item relative\", className)}\n {...props}\n />\n )\n}\n\nfunction SidebarMenuSubButton({\n asChild = false,\n size = \"md\",\n isActive = false,\n className,\n ...props\n}: React.ComponentProps<\"a\"> & {\n asChild?: boolean\n size?: \"sm\" | \"md\"\n isActive?: boolean\n}) {\n const Comp = asChild ? Slot : \"a\"\n\n return (\n <Comp\n data-slot=\"sidebar-menu-sub-button\"\n data-sidebar=\"menu-sub-button\"\n data-size={size}\n data-active={isActive}\n className={cn(\n \"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0\",\n \"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground\",\n size === \"sm\" && \"text-xs\",\n size === \"md\" && \"text-sm\",\n \"group-data-[collapsible=icon]:hidden\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport {\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarGroup,\n SidebarGroupAction,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarInput,\n SidebarInset,\n SidebarMenu,\n SidebarMenuAction,\n SidebarMenuBadge,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarMenuSkeleton,\n SidebarMenuSub,\n SidebarMenuSubButton,\n SidebarMenuSubItem,\n SidebarProvider,\n SidebarRail,\n SidebarSeparator,\n SidebarTrigger,\n useSidebar,\n}\n"
}
},
"spinner.tsx": {
"file": {
"contents": "import { Loader2Icon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Spinner({ className, ...props }: React.ComponentProps<\"svg\">) {\n return (\n <Loader2Icon\n role=\"status\"\n aria-label=\"Loading\"\n className={cn(\"size-4 animate-spin\", className)}\n {...props}\n />\n )\n}\n\nexport { Spinner }\n"
}
},
"tooltip.tsx": {
"file": {
"contents": "\"use client\"\n\nimport * as React from \"react\"\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction TooltipProvider({\n delayDuration = 0,\n ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {\n return (\n <TooltipPrimitive.Provider\n data-slot=\"tooltip-provider\"\n delayDuration={delayDuration}\n {...props}\n />\n )\n}\n\nfunction Tooltip({\n ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Root>) {\n return (\n <TooltipProvider>\n <TooltipPrimitive.Root data-slot=\"tooltip\" {...props} />\n </TooltipProvider>\n )\n}\n\nfunction TooltipTrigger({\n ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {\n return <TooltipPrimitive.Trigger data-slot=\"tooltip-trigger\" {...props} />\n}\n\nfunction TooltipContent({\n className,\n sideOffset = 0,\n children,\n ...props\n}: React.ComponentProps<typeof TooltipPrimitive.Content>) {\n return (\n <TooltipPrimitive.Portal>\n <TooltipPrimitive.Content\n data-slot=\"tooltip-content\"\n sideOffset={sideOffset}\n className={cn(\n \"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance\",\n className\n )}\n {...props}\n >\n {children}\n <TooltipPrimitive.Arrow className=\"bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]\" />\n </TooltipPrimitive.Content>\n </TooltipPrimitive.Portal>\n )\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }\n"
}
},
"calendar.tsx": {
"file": {
"contents": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n ChevronDownIcon,\n ChevronLeftIcon,\n ChevronRightIcon,\n} from \"lucide-react\"\nimport { DayButton, DayPicker, getDefaultClassNames } from \"react-day-picker\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button, buttonVariants } from \"@/components/ui/button\"\n\nfunction Calendar({\n className,\n classNames,\n showOutsideDays = true,\n captionLayout = \"label\",\n buttonVariant = \"ghost\",\n formatters,\n components,\n ...props\n}: React.ComponentProps<typeof DayPicker> & {\n buttonVariant?: React.ComponentProps<typeof Button>[\"variant\"]\n}) {\n const defaultClassNames = getDefaultClassNames()\n\n return (\n <DayPicker\n showOutsideDays={showOutsideDays}\n className={cn(\n \"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent\",\n String.raw`rtl:**:[.rdp-button\\_next>svg]:rotate-180`,\n String.raw`rtl:**:[.rdp-button\\_previous>svg]:rotate-180`,\n className\n )}\n captionLayout={captionLayout}\n formatters={{\n formatMonthDropdown: (date) =>\n date.toLocaleString(\"default\", { month: \"short\" }),\n ...formatters,\n }}\n classNames={{\n root: cn(\"w-fit\", defaultClassNames.root),\n months: cn(\n \"flex gap-4 flex-col md:flex-row relative\",\n defaultClassNames.months\n ),\n month: cn(\"flex flex-col w-full gap-4\", defaultClassNames.month),\n nav: cn(\n \"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between\",\n defaultClassNames.nav\n ),\n button_previous: cn(\n buttonVariants({ variant: buttonVariant }),\n \"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none\",\n defaultClassNames.button_previous\n ),\n button_next: cn(\n buttonVariants({ variant: buttonVariant }),\n \"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none\",\n defaultClassNames.button_next\n ),\n month_caption: cn(\n \"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)\",\n defaultClassNames.month_caption\n ),\n dropdowns: cn(\n \"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5\",\n defaultClassNames.dropdowns\n ),\n dropdown_root: cn(\n \"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md\",\n defaultClassNames.dropdown_root\n ),\n dropdown: cn(\n \"absolute bg-popover inset-0 opacity-0\",\n defaultClassNames.dropdown\n ),\n caption_label: cn(\n \"select-none font-medium\",\n captionLayout === \"label\"\n ? \"text-sm\"\n : \"rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5\",\n defaultClassNames.caption_label\n ),\n table: \"w-full border-collapse\",\n weekdays: cn(\"flex\", defaultClassNames.weekdays),\n weekday: cn(\n \"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none\",\n defaultClassNames.weekday\n ),\n week: cn(\"flex w-full mt-2\", defaultClassNames.week),\n week_number_header: cn(\n \"select-none w-(--cell-size)\",\n defaultClassNames.week_number_header\n ),\n week_number: cn(\n \"text-[0.8rem] select-none text-muted-foreground\",\n defaultClassNames.week_number\n ),\n day: cn(\n \"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none\",\n defaultClassNames.day\n ),\n range_start: cn(\n \"rounded-l-md bg-accent\",\n defaultClassNames.range_start\n ),\n range_middle: cn(\"rounded-none\", defaultClassNames.range_middle),\n range_end: cn(\"rounded-r-md bg-accent\", defaultClassNames.range_end),\n today: cn(\n \"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none\",\n defaultClassNames.today\n ),\n outside: cn(\n \"text-muted-foreground aria-selected:text-muted-foreground\",\n defaultClassNames.outside\n ),\n disabled: cn(\n \"text-muted-foreground opacity-50\",\n defaultClassNames.disabled\n ),\n hidden: cn(\"invisible\", defaultClassNames.hidden),\n ...classNames,\n }}\n components={{\n Root: ({ className, rootRef, ...props }) => {\n return (\n <div\n data-slot=\"calendar\"\n ref={rootRef}\n className={cn(className)}\n {...props}\n />\n )\n },\n Chevron: ({ className, orientation, ...props }) => {\n if (orientation === \"left\") {\n return (\n <ChevronLeftIcon className={cn(\"size-4\", className)} {...props} />\n )\n }\n\n if (orientation === \"right\") {\n return (\n <ChevronRightIcon\n className={cn(\"size-4\", className)}\n {...props}\n />\n )\n }\n\n return (\n <ChevronDownIcon className={cn(\"size-4\", className)} {...props} />\n )\n },\n DayButton: CalendarDayButton,\n WeekNumber: ({ children, ...props }) => {\n return (\n <td {...props}>\n <div className=\"flex size-(--cell-size) items-center justify-center text-center\">\n {children}\n </div>\n </td>\n )\n },\n ...components,\n }}\n {...props}\n />\n )\n}\n\nfunction CalendarDayButton({\n className,\n day,\n modifiers,\n ...props\n}: React.ComponentProps<typeof DayButton>) {\n const defaultClassNames = getDefaultClassNames()\n\n const ref = React.useRef<HTMLButtonElement>(null)\n React.useEffect(() => {\n if (modifiers.focused) ref.current?.focus()\n }, [modifiers.focused])\n\n return (\n <Button\n ref={ref}\n variant=\"ghost\"\n size=\"icon\"\n data-day={day.date.toLocaleDateString()}\n data-selected-single={\n modifiers.selected &&\n !modifiers.range_start &&\n !modifiers.range_end &&\n !modifiers.range_middle\n }\n data-range-start={modifiers.range_start}\n data-range-end={modifiers.range_end}\n data-range-middle={modifiers.range_middle}\n className={cn(\n \"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70\",\n defaultClassNames.day,\n className\n )}\n {...props}\n />\n )\n}\n\nexport { Calendar, CalendarDayButton }\n"
}
},
"carousel.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport useEmblaCarousel, {\n type UseEmblaCarouselType,\n} from \"embla-carousel-react\"\nimport { ArrowLeft, ArrowRight } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\n\ntype CarouselApi = UseEmblaCarouselType[1]\ntype UseCarouselParameters = Parameters<typeof useEmblaCarousel>\ntype CarouselOptions = UseCarouselParameters[0]\ntype CarouselPlugin = UseCarouselParameters[1]\n\ntype CarouselProps = {\n opts?: CarouselOptions\n plugins?: CarouselPlugin\n orientation?: \"horizontal\" | \"vertical\"\n setApi?: (api: CarouselApi) => void\n}\n\ntype CarouselContextProps = {\n carouselRef: ReturnType<typeof useEmblaCarousel>[0]\n api: ReturnType<typeof useEmblaCarousel>[1]\n scrollPrev: () => void\n scrollNext: () => void\n canScrollPrev: boolean\n canScrollNext: boolean\n} & CarouselProps\n\nconst CarouselContext = React.createContext<CarouselContextProps | null>(null)\n\nfunction useCarousel() {\n const context = React.useContext(CarouselContext)\n\n if (!context) {\n throw new Error(\"useCarousel must be used within a <Carousel />\")\n }\n\n return context\n}\n\nfunction Carousel({\n orientation = \"horizontal\",\n opts,\n setApi,\n plugins,\n className,\n children,\n ...props\n}: React.ComponentProps<\"div\"> & CarouselProps) {\n const [carouselRef, api] = useEmblaCarousel(\n {\n ...opts,\n axis: orientation === \"horizontal\" ? \"x\" : \"y\",\n },\n plugins\n )\n const [canScrollPrev, setCanScrollPrev] = React.useState(false)\n const [canScrollNext, setCanScrollNext] = React.useState(false)\n\n const onSelect = React.useCallback((api: CarouselApi) => {\n if (!api) return\n setCanScrollPrev(api.canScrollPrev())\n setCanScrollNext(api.canScrollNext())\n }, [])\n\n const scrollPrev = React.useCallback(() => {\n api?.scrollPrev()\n }, [api])\n\n const scrollNext = React.useCallback(() => {\n api?.scrollNext()\n }, [api])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key === \"ArrowLeft\") {\n event.preventDefault()\n scrollPrev()\n } else if (event.key === \"ArrowRight\") {\n event.preventDefault()\n scrollNext()\n }\n },\n [scrollPrev, scrollNext]\n )\n\n React.useEffect(() => {\n if (!api || !setApi) return\n setApi(api)\n }, [api, setApi])\n\n React.useEffect(() => {\n if (!api) return\n onSelect(api)\n api.on(\"reInit\", onSelect)\n api.on(\"select\", onSelect)\n\n return () => {\n api?.off(\"select\", onSelect)\n }\n }, [api, onSelect])\n\n return (\n <CarouselContext.Provider\n value={{\n carouselRef,\n api: api,\n opts,\n orientation:\n orientation || (opts?.axis === \"y\" ? \"vertical\" : \"horizontal\"),\n scrollPrev,\n scrollNext,\n canScrollPrev,\n canScrollNext,\n }}\n >\n <div\n onKeyDownCapture={handleKeyDown}\n className={cn(\"relative\", className)}\n role=\"region\"\n aria-roledescription=\"carousel\"\n data-slot=\"carousel\"\n {...props}\n >\n {children}\n </div>\n </CarouselContext.Provider>\n )\n}\n\nfunction CarouselContent({ className, ...props }: React.ComponentProps<\"div\">) {\n const { carouselRef, orientation } = useCarousel()\n\n return (\n <div\n ref={carouselRef}\n className=\"overflow-hidden\"\n data-slot=\"carousel-content\"\n >\n <div\n className={cn(\n \"flex\",\n orientation === \"horizontal\" ? \"-ml-4\" : \"-mt-4 flex-col\",\n className\n )}\n {...props}\n />\n </div>\n )\n}\n\nfunction CarouselItem({ className, ...props }: React.ComponentProps<\"div\">) {\n const { orientation } = useCarousel()\n\n return (\n <div\n role=\"group\"\n aria-roledescription=\"slide\"\n data-slot=\"carousel-item\"\n className={cn(\n \"min-w-0 shrink-0 grow-0 basis-full\",\n orientation === \"horizontal\" ? \"pl-4\" : \"pt-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction CarouselPrevious({\n className,\n variant = \"outline\",\n size = \"icon\",\n ...props\n}: React.ComponentProps<typeof Button>) {\n const { orientation, scrollPrev, canScrollPrev } = useCarousel()\n\n return (\n <Button\n data-slot=\"carousel-previous\"\n variant={variant}\n size={size}\n className={cn(\n \"absolute size-8 rounded-full\",\n orientation === \"horizontal\"\n ? \"top-1/2 -left-12 -translate-y-1/2\"\n : \"-top-12 left-1/2 -translate-x-1/2 rotate-90\",\n className\n )}\n disabled={!canScrollPrev}\n onClick={scrollPrev}\n {...props}\n >\n <ArrowLeft />\n <span className=\"sr-only\">Previous slide</span>\n </Button>\n )\n}\n\nfunction CarouselNext({\n className,\n variant = \"outline\",\n size = \"icon\",\n ...props\n}: React.ComponentProps<typeof Button>) {\n const { orientation, scrollNext, canScrollNext } = useCarousel()\n\n return (\n <Button\n data-slot=\"carousel-next\"\n variant={variant}\n size={size}\n className={cn(\n \"absolute size-8 rounded-full\",\n orientation === \"horizontal\"\n ? \"top-1/2 -right-12 -translate-y-1/2\"\n : \"-bottom-12 left-1/2 -translate-x-1/2 rotate-90\",\n className\n )}\n disabled={!canScrollNext}\n onClick={scrollNext}\n {...props}\n >\n <ArrowRight />\n <span className=\"sr-only\">Next slide</span>\n </Button>\n )\n}\n\nexport {\n type CarouselApi,\n Carousel,\n CarouselContent,\n CarouselItem,\n CarouselPrevious,\n CarouselNext,\n}\n"
}
},
"checkbox.tsx": {
"file": {
"contents": "\"use client\"\n\nimport * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { CheckIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Checkbox({\n className,\n ...props\n}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {\n return (\n <CheckboxPrimitive.Root\n data-slot=\"checkbox\"\n className={cn(\n \"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n {...props}\n >\n <CheckboxPrimitive.Indicator\n data-slot=\"checkbox-indicator\"\n className=\"flex items-center justify-center text-current transition-none\"\n >\n <CheckIcon className=\"size-3.5\" />\n </CheckboxPrimitive.Indicator>\n </CheckboxPrimitive.Root>\n )\n}\n\nexport { Checkbox }\n"
}
},
"progress.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Progress({\n className,\n value,\n ...props\n}: React.ComponentProps<typeof ProgressPrimitive.Root>) {\n return (\n <ProgressPrimitive.Root\n data-slot=\"progress\"\n className={cn(\n \"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full\",\n className\n )}\n {...props}\n >\n <ProgressPrimitive.Indicator\n data-slot=\"progress-indicator\"\n className=\"bg-primary h-full w-full flex-1 transition-all\"\n style={{ transform: `translateX(-${100 - (value || 0)}%)` }}\n />\n </ProgressPrimitive.Root>\n )\n}\n\nexport { Progress }\n"
}
},
"skeleton.tsx": {
"file": {
"contents": "import { cn } from \"@/lib/utils\"\n\nfunction Skeleton({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"skeleton\"\n className={cn(\"bg-accent animate-pulse rounded-md\", className)}\n {...props}\n />\n )\n}\n\nexport { Skeleton }\n"
}
},
"textarea.tsx": {
"file": {
"contents": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Textarea({ className, ...props }: React.ComponentProps<\"textarea\">) {\n return (\n <textarea\n data-slot=\"textarea\"\n className={cn(\n \"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport { Textarea }\n"
}
},
"accordion.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\"\nimport { ChevronDownIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Accordion({\n ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Root>) {\n return <AccordionPrimitive.Root data-slot=\"accordion\" {...props} />\n}\n\nfunction AccordionItem({\n className,\n ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Item>) {\n return (\n <AccordionPrimitive.Item\n data-slot=\"accordion-item\"\n className={cn(\"border-b last:border-b-0\", className)}\n {...props}\n />\n )\n}\n\nfunction AccordionTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {\n return (\n <AccordionPrimitive.Header className=\"flex\">\n <AccordionPrimitive.Trigger\n data-slot=\"accordion-trigger\"\n className={cn(\n \"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180\",\n className\n )}\n {...props}\n >\n {children}\n <ChevronDownIcon className=\"text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200\" />\n </AccordionPrimitive.Trigger>\n </AccordionPrimitive.Header>\n )\n}\n\nfunction AccordionContent({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Content>) {\n return (\n <AccordionPrimitive.Content\n data-slot=\"accordion-content\"\n className=\"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm\"\n {...props}\n >\n <div className={cn(\"pt-0 pb-4\", className)}>{children}</div>\n </AccordionPrimitive.Content>\n )\n}\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent }\n"
}
},
"input-otp.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport { OTPInput, OTPInputContext } from \"input-otp\"\nimport { MinusIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction InputOTP({\n className,\n containerClassName,\n ...props\n}: React.ComponentProps<typeof OTPInput> & {\n containerClassName?: string\n}) {\n return (\n <OTPInput\n data-slot=\"input-otp\"\n containerClassName={cn(\n \"flex items-center gap-2 has-disabled:opacity-50\",\n containerClassName\n )}\n className={cn(\"disabled:cursor-not-allowed\", className)}\n {...props}\n />\n )\n}\n\nfunction InputOTPGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"input-otp-group\"\n className={cn(\"flex items-center\", className)}\n {...props}\n />\n )\n}\n\nfunction InputOTPSlot({\n index,\n className,\n ...props\n}: React.ComponentProps<\"div\"> & {\n index: number\n}) {\n const inputOTPContext = React.useContext(OTPInputContext)\n const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}\n\n return (\n <div\n data-slot=\"input-otp-slot\"\n data-active={isActive}\n className={cn(\n \"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]\",\n className\n )}\n {...props}\n >\n {char}\n {hasFakeCaret && (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n <div className=\"animate-caret-blink bg-foreground h-4 w-px duration-1000\" />\n </div>\n )}\n </div>\n )\n}\n\nfunction InputOTPSeparator({ ...props }: React.ComponentProps<\"div\">) {\n return (\n <div data-slot=\"input-otp-separator\" role=\"separator\" {...props}>\n <MinusIcon />\n </div>\n )\n}\n\nexport { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }\n"
}
},
"resizable.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport { GripVerticalIcon } from \"lucide-react\"\nimport * as ResizablePrimitive from \"react-resizable-panels\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction ResizablePanelGroup({\n className,\n ...props\n}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {\n return (\n <ResizablePrimitive.PanelGroup\n data-slot=\"resizable-panel-group\"\n className={cn(\n \"flex h-full w-full data-[panel-group-direction=vertical]:flex-col\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ResizablePanel({\n ...props\n}: React.ComponentProps<typeof ResizablePrimitive.Panel>) {\n return <ResizablePrimitive.Panel data-slot=\"resizable-panel\" {...props} />\n}\n\nfunction ResizableHandle({\n withHandle,\n className,\n ...props\n}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {\n withHandle?: boolean\n}) {\n return (\n <ResizablePrimitive.PanelResizeHandle\n data-slot=\"resizable-handle\"\n className={cn(\n \"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90\",\n className\n )}\n {...props}\n >\n {withHandle && (\n <div className=\"bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border\">\n <GripVerticalIcon className=\"size-2.5\" />\n </div>\n )}\n </ResizablePrimitive.PanelResizeHandle>\n )\n}\n\nexport { ResizablePanelGroup, ResizablePanel, ResizableHandle }\n"
}
},
"separator.tsx": {
"file": {
"contents": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Separator({\n className,\n orientation = \"horizontal\",\n decorative = true,\n ...props\n}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {\n return (\n <SeparatorPrimitive.Root\n data-slot=\"separator\"\n decorative={decorative}\n orientation={orientation}\n className={cn(\n \"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport { Separator }\n"
}
},
"breadcrumb.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { ChevronRight, MoreHorizontal } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Breadcrumb({ ...props }: React.ComponentProps<\"nav\">) {\n return <nav aria-label=\"breadcrumb\" data-slot=\"breadcrumb\" {...props} />\n}\n\nfunction BreadcrumbList({ className, ...props }: React.ComponentProps<\"ol\">) {\n return (\n <ol\n data-slot=\"breadcrumb-list\"\n className={cn(\n \"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction BreadcrumbItem({ className, ...props }: React.ComponentProps<\"li\">) {\n return (\n <li\n data-slot=\"breadcrumb-item\"\n className={cn(\"inline-flex items-center gap-1.5\", className)}\n {...props}\n />\n )\n}\n\nfunction BreadcrumbLink({\n asChild,\n className,\n ...props\n}: React.ComponentProps<\"a\"> & {\n asChild?: boolean\n}) {\n const Comp = asChild ? Slot : \"a\"\n\n return (\n <Comp\n data-slot=\"breadcrumb-link\"\n className={cn(\"hover:text-foreground transition-colors\", className)}\n {...props}\n />\n )\n}\n\nfunction BreadcrumbPage({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"breadcrumb-page\"\n role=\"link\"\n aria-disabled=\"true\"\n aria-current=\"page\"\n className={cn(\"text-foreground font-normal\", className)}\n {...props}\n />\n )\n}\n\nfunction BreadcrumbSeparator({\n children,\n className,\n ...props\n}: React.ComponentProps<\"li\">) {\n return (\n <li\n data-slot=\"breadcrumb-separator\"\n role=\"presentation\"\n aria-hidden=\"true\"\n className={cn(\"[&>svg]:size-3.5\", className)}\n {...props}\n >\n {children ?? <ChevronRight />}\n </li>\n )\n}\n\nfunction BreadcrumbEllipsis({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"breadcrumb-ellipsis\"\n role=\"presentation\"\n aria-hidden=\"true\"\n className={cn(\"flex size-9 items-center justify-center\", className)}\n {...props}\n >\n <MoreHorizontal className=\"size-4\" />\n <span className=\"sr-only\">More</span>\n </span>\n )\n}\n\nexport {\n Breadcrumb,\n BreadcrumbList,\n BreadcrumbItem,\n BreadcrumbLink,\n BreadcrumbPage,\n BreadcrumbSeparator,\n BreadcrumbEllipsis,\n}\n"
}
},
"hover-card.tsx": {
"file": {
"contents": "\"use client\"\n\nimport * as React from \"react\"\nimport * as HoverCardPrimitive from \"@radix-ui/react-hover-card\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction HoverCard({\n ...props\n}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {\n return <HoverCardPrimitive.Root data-slot=\"hover-card\" {...props} />\n}\n\nfunction HoverCardTrigger({\n ...props\n}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {\n return (\n <HoverCardPrimitive.Trigger data-slot=\"hover-card-trigger\" {...props} />\n )\n}\n\nfunction HoverCardContent({\n className,\n align = \"center\",\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {\n return (\n <HoverCardPrimitive.Portal data-slot=\"hover-card-portal\">\n <HoverCardPrimitive.Content\n data-slot=\"hover-card-content\"\n align={align}\n sideOffset={sideOffset}\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]: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 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden\",\n className\n )}\n {...props}\n />\n </HoverCardPrimitive.Portal>\n )\n}\n\nexport { HoverCard, HoverCardTrigger, HoverCardContent }\n"
}
},
"pagination.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport {\n ChevronLeftIcon,\n ChevronRightIcon,\n MoreHorizontalIcon,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button, buttonVariants } from \"@/components/ui/button\"\n\nfunction Pagination({ className, ...props }: React.ComponentProps<\"nav\">) {\n return (\n <nav\n role=\"navigation\"\n aria-label=\"pagination\"\n data-slot=\"pagination\"\n className={cn(\"mx-auto flex w-full justify-center\", className)}\n {...props}\n />\n )\n}\n\nfunction PaginationContent({\n className,\n ...props\n}: React.ComponentProps<\"ul\">) {\n return (\n <ul\n data-slot=\"pagination-content\"\n className={cn(\"flex flex-row items-center gap-1\", className)}\n {...props}\n />\n )\n}\n\nfunction PaginationItem({ ...props }: React.ComponentProps<\"li\">) {\n return <li data-slot=\"pagination-item\" {...props} />\n}\n\ntype PaginationLinkProps = {\n isActive?: boolean\n} & Pick<React.ComponentProps<typeof Button>, \"size\"> &\n React.ComponentProps<\"a\">\n\nfunction PaginationLink({\n className,\n isActive,\n size = \"icon\",\n ...props\n}: PaginationLinkProps) {\n return (\n <a\n aria-current={isActive ? \"page\" : undefined}\n data-slot=\"pagination-link\"\n data-active={isActive}\n className={cn(\n buttonVariants({\n variant: isActive ? \"outline\" : \"ghost\",\n size,\n }),\n className\n )}\n {...props}\n />\n )\n}\n\nfunction PaginationPrevious({\n className,\n ...props\n}: React.ComponentProps<typeof PaginationLink>) {\n return (\n <PaginationLink\n aria-label=\"Go to previous page\"\n size=\"default\"\n className={cn(\"gap-1 px-2.5 sm:pl-2.5\", className)}\n {...props}\n >\n <ChevronLeftIcon />\n <span className=\"hidden sm:block\">Previous</span>\n </PaginationLink>\n )\n}\n\nfunction PaginationNext({\n className,\n ...props\n}: React.ComponentProps<typeof PaginationLink>) {\n return (\n <PaginationLink\n aria-label=\"Go to next page\"\n size=\"default\"\n className={cn(\"gap-1 px-2.5 sm:pr-2.5\", className)}\n {...props}\n >\n <span className=\"hidden sm:block\">Next</span>\n <ChevronRightIcon />\n </PaginationLink>\n )\n}\n\nfunction PaginationEllipsis({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n <span\n aria-hidden\n data-slot=\"pagination-ellipsis\"\n className={cn(\"flex size-9 items-center justify-center\", className)}\n {...props}\n >\n <MoreHorizontalIcon className=\"size-4\" />\n <span className=\"sr-only\">More pages</span>\n </span>\n )\n}\n\nexport {\n Pagination,\n PaginationContent,\n PaginationLink,\n PaginationItem,\n PaginationPrevious,\n PaginationNext,\n PaginationEllipsis,\n}\n"
}
},
"collapsible.tsx": {
"file": {
"contents": "import * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\"\n\nfunction Collapsible({\n ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {\n return <CollapsiblePrimitive.Root data-slot=\"collapsible\" {...props} />\n}\n\nfunction CollapsibleTrigger({\n ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {\n return (\n <CollapsiblePrimitive.CollapsibleTrigger\n data-slot=\"collapsible-trigger\"\n {...props}\n />\n )\n}\n\nfunction CollapsibleContent({\n ...props\n}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {\n return (\n <CollapsiblePrimitive.CollapsibleContent\n data-slot=\"collapsible-content\"\n {...props}\n />\n )\n}\n\nexport { Collapsible, CollapsibleTrigger, CollapsibleContent }\n"
}
},
"input-group.tsx": {
"file": {
"contents": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport { Textarea } from \"@/components/ui/textarea\"\n\nfunction InputGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"input-group\"\n role=\"group\"\n className={cn(\n \"group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none\",\n \"h-9 has-[>textarea]:h-auto\",\n\n // Variants based on alignment.\n \"has-[>[data-align=inline-start]]:[&>input]:pl-2\",\n \"has-[>[data-align=inline-end]]:[&>input]:pr-2\",\n \"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3\",\n \"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3\",\n\n // Focus state.\n \"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]\",\n\n // Error state.\n \"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40\",\n\n className\n )}\n {...props}\n />\n )\n}\n\nconst inputGroupAddonVariants = cva(\n \"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50\",\n {\n variants: {\n align: {\n \"inline-start\":\n \"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]\",\n \"inline-end\":\n \"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]\",\n \"block-start\":\n \"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5\",\n \"block-end\":\n \"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5\",\n },\n },\n defaultVariants: {\n align: \"inline-start\",\n },\n }\n)\n\nfunction InputGroupAddon({\n className,\n align = \"inline-start\",\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof inputGroupAddonVariants>) {\n return (\n <div\n role=\"group\"\n data-slot=\"input-group-addon\"\n data-align={align}\n className={cn(inputGroupAddonVariants({ align }), className)}\n onClick={(e) => {\n if ((e.target as HTMLElement).closest(\"button\")) {\n return\n }\n e.currentTarget.parentElement?.querySelector(\"input\")?.focus()\n }}\n {...props}\n />\n )\n}\n\nconst inputGroupButtonVariants = cva(\n \"text-sm shadow-none flex gap-2 items-center\",\n {\n variants: {\n size: {\n xs: \"h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2\",\n sm: \"h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5\",\n \"icon-xs\":\n \"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0\",\n \"icon-sm\": \"size-8 p-0 has-[>svg]:p-0\",\n },\n },\n defaultVariants: {\n size: \"xs\",\n },\n }\n)\n\nfunction InputGroupButton({\n className,\n type = \"button\",\n variant = \"ghost\",\n size = \"xs\",\n ...props\n}: Omit<React.ComponentProps<typeof Button>, \"size\"> &\n VariantProps<typeof inputGroupButtonVariants>) {\n return (\n <Button\n type={type}\n data-size={size}\n variant={variant}\n className={cn(inputGroupButtonVariants({ size }), className)}\n {...props}\n />\n )\n}\n\nfunction InputGroupText({ className, ...props }: React.ComponentProps<\"span\">) {\n return (\n <span\n className={cn(\n \"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction InputGroupInput({\n className,\n ...props\n}: React.ComponentProps<\"input\">) {\n return (\n <Input\n data-slot=\"input-group-control\"\n className={cn(\n \"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction InputGroupTextarea({\n className,\n ...props\n}: React.ComponentProps<\"textarea\">) {\n return (\n <Textarea\n data-slot=\"input-group-control\"\n className={cn(\n \"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport {\n InputGroup,\n InputGroupAddon,\n InputGroupButton,\n InputGroupText,\n InputGroupInput,\n InputGroupTextarea,\n}\n"
}
},
"radio-group.tsx": {
"file": {
"contents": "\"use client\"\n\nimport * as React from \"react\"\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\"\nimport { CircleIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction RadioGroup({\n className,\n ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {\n return (\n <RadioGroupPrimitive.Root\n data-slot=\"radio-group\"\n className={cn(\"grid gap-3\", className)}\n {...props}\n />\n )\n}\n\nfunction RadioGroupItem({\n className,\n ...props\n}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {\n return (\n <RadioGroupPrimitive.Item\n data-slot=\"radio-group-item\"\n className={cn(\n \"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\",\n className\n )}\n {...props}\n >\n <RadioGroupPrimitive.Indicator\n data-slot=\"radio-group-indicator\"\n className=\"relative flex items-center justify-center\"\n >\n <CircleIcon className=\"fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2\" />\n </RadioGroupPrimitive.Indicator>\n </RadioGroupPrimitive.Item>\n )\n}\n\nexport { RadioGroup, RadioGroupItem }\n"
}
},
"scroll-area.tsx": {
"file": {
"contents": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction ScrollArea({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {\n return (\n <ScrollAreaPrimitive.Root\n data-slot=\"scroll-area\"\n className={cn(\"relative\", className)}\n {...props}\n >\n <ScrollAreaPrimitive.Viewport\n data-slot=\"scroll-area-viewport\"\n className=\"focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1\"\n >\n {children}\n </ScrollAreaPrimitive.Viewport>\n <ScrollBar />\n <ScrollAreaPrimitive.Corner />\n </ScrollAreaPrimitive.Root>\n )\n}\n\nfunction ScrollBar({\n className,\n orientation = \"vertical\",\n ...props\n}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {\n return (\n <ScrollAreaPrimitive.ScrollAreaScrollbar\n data-slot=\"scroll-area-scrollbar\"\n orientation={orientation}\n className={cn(\n \"flex touch-none p-px transition-colors select-none\",\n orientation === \"vertical\" &&\n \"h-full w-2.5 border-l border-l-transparent\",\n orientation === \"horizontal\" &&\n \"h-2.5 flex-col border-t border-t-transparent\",\n className\n )}\n {...props}\n >\n <ScrollAreaPrimitive.ScrollAreaThumb\n data-slot=\"scroll-area-thumb\"\n className=\"bg-border relative flex-1 rounded-full\"\n />\n </ScrollAreaPrimitive.ScrollAreaScrollbar>\n )\n}\n\nexport { ScrollArea, ScrollBar }\n"
}
},
"alert-dialog.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\"\n\nimport { cn } from \"@/lib/utils\"\nimport { buttonVariants } from \"@/components/ui/button\"\n\nfunction AlertDialog({\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {\n return <AlertDialogPrimitive.Root data-slot=\"alert-dialog\" {...props} />\n}\n\nfunction AlertDialogTrigger({\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {\n return (\n <AlertDialogPrimitive.Trigger data-slot=\"alert-dialog-trigger\" {...props} />\n )\n}\n\nfunction AlertDialogPortal({\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {\n return (\n <AlertDialogPrimitive.Portal data-slot=\"alert-dialog-portal\" {...props} />\n )\n}\n\nfunction AlertDialogOverlay({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {\n return (\n <AlertDialogPrimitive.Overlay\n data-slot=\"alert-dialog-overlay\"\n className={cn(\n \"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction AlertDialogContent({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {\n return (\n <AlertDialogPortal>\n <AlertDialogOverlay />\n <AlertDialogPrimitive.Content\n data-slot=\"alert-dialog-content\"\n className={cn(\n \"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 p-6 shadow-lg duration-200 sm:max-w-lg\",\n className\n )}\n {...props}\n />\n </AlertDialogPortal>\n )\n}\n\nfunction AlertDialogHeader({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-dialog-header\"\n className={cn(\"flex flex-col gap-2 text-center sm:text-left\", className)}\n {...props}\n />\n )\n}\n\nfunction AlertDialogFooter({\n className,\n ...props\n}: React.ComponentProps<\"div\">) {\n return (\n <div\n data-slot=\"alert-dialog-footer\"\n className={cn(\n \"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction AlertDialogTitle({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {\n return (\n <AlertDialogPrimitive.Title\n data-slot=\"alert-dialog-title\"\n className={cn(\"text-lg font-semibold\", className)}\n {...props}\n />\n )\n}\n\nfunction AlertDialogDescription({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {\n return (\n <AlertDialogPrimitive.Description\n data-slot=\"alert-dialog-description\"\n className={cn(\"text-muted-foreground text-sm\", className)}\n {...props}\n />\n )\n}\n\nfunction AlertDialogAction({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {\n return (\n <AlertDialogPrimitive.Action\n className={cn(buttonVariants(), className)}\n {...props}\n />\n )\n}\n\nfunction AlertDialogCancel({\n className,\n ...props\n}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {\n return (\n <AlertDialogPrimitive.Cancel\n className={cn(buttonVariants({ variant: \"outline\" }), className)}\n {...props}\n />\n )\n}\n\nexport {\n AlertDialog,\n AlertDialogPortal,\n AlertDialogOverlay,\n AlertDialogTrigger,\n AlertDialogContent,\n AlertDialogHeader,\n AlertDialogFooter,\n AlertDialogTitle,\n AlertDialogDescription,\n AlertDialogAction,\n AlertDialogCancel,\n}\n"
}
},
"aspect-ratio.tsx": {
"file": {
"contents": "\"use client\"\n\nimport * as AspectRatioPrimitive from \"@radix-ui/react-aspect-ratio\"\n\nfunction AspectRatio({\n ...props\n}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {\n return <AspectRatioPrimitive.Root data-slot=\"aspect-ratio\" {...props} />\n}\n\nexport { AspectRatio }\n"
}
},
"button-group.tsx": {
"file": {
"contents": "import { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Separator } from \"@/components/ui/separator\"\n\nconst buttonGroupVariants = cva(\n \"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2\",\n {\n variants: {\n orientation: {\n horizontal:\n \"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none\",\n vertical:\n \"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none\",\n },\n },\n defaultVariants: {\n orientation: \"horizontal\",\n },\n }\n)\n\nfunction ButtonGroup({\n className,\n orientation,\n ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof buttonGroupVariants>) {\n return (\n <div\n role=\"group\"\n data-slot=\"button-group\"\n data-orientation={orientation}\n className={cn(buttonGroupVariants({ orientation }), className)}\n {...props}\n />\n )\n}\n\nfunction ButtonGroupText({\n className,\n asChild = false,\n ...props\n}: React.ComponentProps<\"div\"> & {\n asChild?: boolean\n}) {\n const Comp = asChild ? Slot : \"div\"\n\n return (\n <Comp\n className={cn(\n \"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ButtonGroupSeparator({\n className,\n orientation = \"vertical\",\n ...props\n}: React.ComponentProps<typeof Separator>) {\n return (\n <Separator\n data-slot=\"button-group-separator\"\n orientation={orientation}\n className={cn(\n \"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport {\n ButtonGroup,\n ButtonGroupSeparator,\n ButtonGroupText,\n buttonGroupVariants,\n}\n"
}
},
"context-menu.tsx": {
"file": {
"contents": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ContextMenuPrimitive from \"@radix-ui/react-context-menu\"\nimport { CheckIcon, ChevronRightIcon, CircleIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction ContextMenu({\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {\n return <ContextMenuPrimitive.Root data-slot=\"context-menu\" {...props} />\n}\n\nfunction ContextMenuTrigger({\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {\n return (\n <ContextMenuPrimitive.Trigger data-slot=\"context-menu-trigger\" {...props} />\n )\n}\n\nfunction ContextMenuGroup({\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {\n return (\n <ContextMenuPrimitive.Group data-slot=\"context-menu-group\" {...props} />\n )\n}\n\nfunction ContextMenuPortal({\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {\n return (\n <ContextMenuPrimitive.Portal data-slot=\"context-menu-portal\" {...props} />\n )\n}\n\nfunction ContextMenuSub({\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {\n return <ContextMenuPrimitive.Sub data-slot=\"context-menu-sub\" {...props} />\n}\n\nfunction ContextMenuRadioGroup({\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {\n return (\n <ContextMenuPrimitive.RadioGroup\n data-slot=\"context-menu-radio-group\"\n {...props}\n />\n )\n}\n\nfunction ContextMenuSubTrigger({\n className,\n inset,\n children,\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {\n inset?: boolean\n}) {\n return (\n <ContextMenuPrimitive.SubTrigger\n data-slot=\"context-menu-sub-trigger\"\n data-inset={inset}\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n >\n {children}\n <ChevronRightIcon className=\"ml-auto\" />\n </ContextMenuPrimitive.SubTrigger>\n )\n}\n\nfunction ContextMenuSubContent({\n className,\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {\n return (\n <ContextMenuPrimitive.SubContent\n data-slot=\"context-menu-sub-content\"\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]: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 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ContextMenuContent({\n className,\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {\n return (\n <ContextMenuPrimitive.Portal>\n <ContextMenuPrimitive.Content\n data-slot=\"context-menu-content\"\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]: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 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md\",\n className\n )}\n {...props}\n />\n </ContextMenuPrimitive.Portal>\n )\n}\n\nfunction ContextMenuItem({\n className,\n inset,\n variant = \"default\",\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {\n inset?: boolean\n variant?: \"default\" | \"destructive\"\n}) {\n return (\n <ContextMenuPrimitive.Item\n data-slot=\"context-menu-item\"\n data-inset={inset}\n data-variant={variant}\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ContextMenuCheckboxItem({\n className,\n children,\n checked,\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {\n return (\n <ContextMenuPrimitive.CheckboxItem\n data-slot=\"context-menu-checkbox-item\"\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n checked={checked}\n {...props}\n >\n <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n <ContextMenuPrimitive.ItemIndicator>\n <CheckIcon className=\"size-4\" />\n </ContextMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </ContextMenuPrimitive.CheckboxItem>\n )\n}\n\nfunction ContextMenuRadioItem({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {\n return (\n <ContextMenuPrimitive.RadioItem\n data-slot=\"context-menu-radio-item\"\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n >\n <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n <ContextMenuPrimitive.ItemIndicator>\n <CircleIcon className=\"size-2 fill-current\" />\n </ContextMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </ContextMenuPrimitive.RadioItem>\n )\n}\n\nfunction ContextMenuLabel({\n className,\n inset,\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {\n inset?: boolean\n}) {\n return (\n <ContextMenuPrimitive.Label\n data-slot=\"context-menu-label\"\n data-inset={inset}\n className={cn(\n \"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction ContextMenuSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {\n return (\n <ContextMenuPrimitive.Separator\n data-slot=\"context-menu-separator\"\n className={cn(\"bg-border -mx-1 my-1 h-px\", className)}\n {...props}\n />\n )\n}\n\nfunction ContextMenuShortcut({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"context-menu-shortcut\"\n className={cn(\n \"text-muted-foreground ml-auto text-xs tracking-widest\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport {\n ContextMenu,\n ContextMenuTrigger,\n ContextMenuContent,\n ContextMenuItem,\n ContextMenuCheckboxItem,\n ContextMenuRadioItem,\n ContextMenuLabel,\n ContextMenuSeparator,\n ContextMenuShortcut,\n ContextMenuGroup,\n ContextMenuPortal,\n ContextMenuSub,\n ContextMenuSubContent,\n ContextMenuSubTrigger,\n ContextMenuRadioGroup,\n}\n"
}
},
"toggle-group.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport * as ToggleGroupPrimitive from \"@radix-ui/react-toggle-group\"\nimport { type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { toggleVariants } from \"@/components/ui/toggle\"\n\nconst ToggleGroupContext = React.createContext<\n VariantProps<typeof toggleVariants>\n>({\n size: \"default\",\n variant: \"default\",\n})\n\nfunction ToggleGroup({\n className,\n variant,\n size,\n children,\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &\n VariantProps<typeof toggleVariants>) {\n return (\n <ToggleGroupPrimitive.Root\n data-slot=\"toggle-group\"\n data-variant={variant}\n data-size={size}\n className={cn(\n \"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs\",\n className\n )}\n {...props}\n >\n <ToggleGroupContext.Provider value={{ variant, size }}>\n {children}\n </ToggleGroupContext.Provider>\n </ToggleGroupPrimitive.Root>\n )\n}\n\nfunction ToggleGroupItem({\n className,\n children,\n variant,\n size,\n ...props\n}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &\n VariantProps<typeof toggleVariants>) {\n const context = React.useContext(ToggleGroupContext)\n\n return (\n <ToggleGroupPrimitive.Item\n data-slot=\"toggle-group-item\"\n data-variant={context.variant || variant}\n data-size={context.size || size}\n className={cn(\n toggleVariants({\n variant: context.variant || variant,\n size: context.size || size,\n }),\n \"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l\",\n className\n )}\n {...props}\n >\n {children}\n </ToggleGroupPrimitive.Item>\n )\n}\n\nexport { ToggleGroup, ToggleGroupItem }\n"
}
},
"dropdown-menu.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimport { CheckIcon, ChevronRightIcon, CircleIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction DropdownMenu({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {\n return <DropdownMenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />\n}\n\nfunction DropdownMenuPortal({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {\n return (\n <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />\n )\n}\n\nfunction DropdownMenuTrigger({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {\n return (\n <DropdownMenuPrimitive.Trigger\n data-slot=\"dropdown-menu-trigger\"\n {...props}\n />\n )\n}\n\nfunction DropdownMenuContent({\n className,\n sideOffset = 4,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n return (\n <DropdownMenuPrimitive.Portal>\n <DropdownMenuPrimitive.Content\n data-slot=\"dropdown-menu-content\"\n sideOffset={sideOffset}\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]: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 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 p-1 shadow-md\",\n className\n )}\n {...props}\n />\n </DropdownMenuPrimitive.Portal>\n )\n}\n\nfunction DropdownMenuGroup({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {\n return (\n <DropdownMenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />\n )\n}\n\nfunction DropdownMenuItem({\n className,\n inset,\n variant = \"default\",\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n inset?: boolean\n variant?: \"default\" | \"destructive\"\n}) {\n return (\n <DropdownMenuPrimitive.Item\n data-slot=\"dropdown-menu-item\"\n data-inset={inset}\n data-variant={variant}\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction DropdownMenuCheckboxItem({\n className,\n children,\n checked,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n return (\n <DropdownMenuPrimitive.CheckboxItem\n data-slot=\"dropdown-menu-checkbox-item\"\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n checked={checked}\n {...props}\n >\n <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <CheckIcon className=\"size-4\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.CheckboxItem>\n )\n}\n\nfunction DropdownMenuRadioGroup({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {\n return (\n <DropdownMenuPrimitive.RadioGroup\n data-slot=\"dropdown-menu-radio-group\"\n {...props}\n />\n )\n}\n\nfunction DropdownMenuRadioItem({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n return (\n <DropdownMenuPrimitive.RadioItem\n data-slot=\"dropdown-menu-radio-item\"\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n >\n <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n <DropdownMenuPrimitive.ItemIndicator>\n <CircleIcon className=\"size-2 fill-current\" />\n </DropdownMenuPrimitive.ItemIndicator>\n </span>\n {children}\n </DropdownMenuPrimitive.RadioItem>\n )\n}\n\nfunction DropdownMenuLabel({\n className,\n inset,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n inset?: boolean\n}) {\n return (\n <DropdownMenuPrimitive.Label\n data-slot=\"dropdown-menu-label\"\n data-inset={inset}\n className={cn(\n \"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction DropdownMenuSeparator({\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n return (\n <DropdownMenuPrimitive.Separator\n data-slot=\"dropdown-menu-separator\"\n className={cn(\"bg-border -mx-1 my-1 h-px\", className)}\n {...props}\n />\n )\n}\n\nfunction DropdownMenuShortcut({\n className,\n ...props\n}: React.ComponentProps<\"span\">) {\n return (\n <span\n data-slot=\"dropdown-menu-shortcut\"\n className={cn(\n \"text-muted-foreground ml-auto text-xs tracking-widest\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction DropdownMenuSub({\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {\n return <DropdownMenuPrimitive.Sub data-slot=\"dropdown-menu-sub\" {...props} />\n}\n\nfunction DropdownMenuSubTrigger({\n className,\n inset,\n children,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n inset?: boolean\n}) {\n return (\n <DropdownMenuPrimitive.SubTrigger\n data-slot=\"dropdown-menu-sub-trigger\"\n data-inset={inset}\n className={cn(\n \"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n >\n {children}\n <ChevronRightIcon className=\"ml-auto size-4\" />\n </DropdownMenuPrimitive.SubTrigger>\n )\n}\n\nfunction DropdownMenuSubContent({\n className,\n ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n return (\n <DropdownMenuPrimitive.SubContent\n data-slot=\"dropdown-menu-sub-content\"\n className={cn(\n \"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]: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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg\",\n className\n )}\n {...props}\n />\n )\n}\n\nexport {\n DropdownMenu,\n DropdownMenuPortal,\n DropdownMenuTrigger,\n DropdownMenuContent,\n DropdownMenuGroup,\n DropdownMenuLabel,\n DropdownMenuItem,\n DropdownMenuCheckboxItem,\n DropdownMenuRadioGroup,\n DropdownMenuRadioItem,\n DropdownMenuSeparator,\n DropdownMenuShortcut,\n DropdownMenuSub,\n DropdownMenuSubTrigger,\n DropdownMenuSubContent,\n}\n"
}
},
"navigation-menu.tsx": {
"file": {
"contents": "import * as React from \"react\"\nimport * as NavigationMenuPrimitive from \"@radix-ui/react-navigation-menu\"\nimport { cva } from \"class-variance-authority\"\nimport { ChevronDownIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction NavigationMenu({\n className,\n children,\n viewport = true,\n ...props\n}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {\n viewport?: boolean\n}) {\n return (\n <NavigationMenuPrimitive.Root\n data-slot=\"navigation-menu\"\n data-viewport={viewport}\n className={cn(\n \"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center\",\n className\n )}\n {...props}\n >\n {children}\n {viewport && <NavigationMenuViewport />}\n </NavigationMenuPrimitive.Root>\n )\n}\n\nfunction NavigationMenuList({\n className,\n ...props\n}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {\n return (\n <NavigationMenuPrimitive.List\n data-slot=\"navigation-menu-list\"\n className={cn(\n \"group flex flex-1 list-none items-center justify-center gap-1\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction NavigationMenuItem({\n className,\n ...props\n}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {\n return (\n <NavigationMenuPrimitive.Item\n data-slot=\"navigation-menu-item\"\n className={cn(\"relative\", className)}\n {...props}\n />\n )\n}\n\nconst navigationMenuTriggerStyle = cva(\n \"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1\"\n)\n\nfunction NavigationMenuTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {\n return (\n <NavigationMenuPrimitive.Trigger\n data-slot=\"navigation-menu-trigger\"\n className={cn(navigationMenuTriggerStyle(), \"group\", className)}\n {...props}\n >\n {children}{\" \"}\n <ChevronDownIcon\n className=\"relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180\"\n aria-hidden=\"true\"\n />\n </NavigationMenuPrimitive.Trigger>\n )\n}\n\nfunction NavigationMenuContent({\n className,\n ...props\n}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {\n return (\n <NavigationMenuPrimitive.Content\n data-slot=\"navigation-menu-content\"\n className={cn(\n \"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto\",\n \"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction NavigationMenuViewport({\n className,\n ...props\n}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {\n return (\n <div\n className={cn(\n \"absolute top-full left-0 isolate z-50 flex justify-center\"\n )}\n >\n <NavigationMenuPrimitive.Viewport\n data-slot=\"navigation-menu-viewport\"\n className={cn(\n \"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]\",\n className\n )}\n {...props}\n />\n </div>\n )\n}\n\nfunction NavigationMenuLink({\n className,\n ...props\n}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {\n return (\n <NavigationMenuPrimitive.Link\n data-slot=\"navigation-menu-link\"\n className={cn(\n \"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4\",\n className\n )}\n {...props}\n />\n )\n}\n\nfunction NavigationMenuIndicator({\n className,\n ...props\n}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {\n return (\n <NavigationMenuPrimitive.Indicator\n data-slot=\"navigation-menu-indicator\"\n className={cn(\n \"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden\",\n className\n )}\n {...props}\n >\n <div className=\"bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md\" />\n </NavigationMenuPrimitive.Indicator>\n )\n}\n\nexport {\n NavigationMenu,\n NavigationMenuList,\n NavigationMenuItem,\n NavigationMenuContent,\n NavigationMenuTrigger,\n NavigationMenuLink,\n NavigationMenuIndicator,\n NavigationMenuViewport,\n navigationMenuTriggerStyle,\n}\n"
}
}
}
},
"Layout.tsx": {
"file": {
"contents": "import * as React from 'react';\nimport { NavLink, useLocation } from 'react-router-dom';\nimport { cn } from '@/lib/utils';\nimport {\n LayoutDashboard,\n Scale,\n BookOpen,\n Library,\n Menu,\n X,\n Sun,\n Moon,\n} from 'lucide-react';\nimport { useTheme } from 'next-themes';\n\nconst NAV = [\n { to: '/', label: 'Dashboard', icon: LayoutDashboard, exact: true },\n { to: '/council', label: 'Decision Council', icon: Scale, exact: false },\n { to: '/journal', label: 'Decision Journal', icon: BookOpen, exact: false },\n { to: '/library', label: 'Library', icon: Library, exact: false },\n];\n\nfunction ThemeToggle({ compact = false }: { compact?: boolean }) {\n const { theme, setTheme } = useTheme();\n const isDark = theme === 'dark';\n\n return (\n <button\n onClick={() => setTheme(isDark ? 'light' : 'dark')}\n className={cn(\n 'flex items-center gap-2 rounded-lg transition-all duration-200',\n compact\n ? 'p-2 text-muted-foreground hover:text-foreground hover:bg-accent'\n : 'px-3 py-2 w-full text-xs font-medium text-white/50 hover:text-white hover:bg-white/5',\n )}\n aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}\n >\n <div className=\"relative w-3.5 h-3.5\">\n <Sun\n className={cn(\n 'w-3.5 h-3.5 absolute inset-0 transition-all duration-300',\n isDark ? 'opacity-0 rotate-90 scale-0' : 'opacity-100 rotate-0 scale-100',\n )}\n />\n <Moon\n className={cn(\n 'w-3.5 h-3.5 absolute inset-0 transition-all duration-300',\n isDark ? 'opacity-100 rotate-0 scale-100' : 'opacity-0 -rotate-90 scale-0',\n )}\n />\n </div>\n {!compact && <span>{isDark ? 'Light Mode' : 'Dark Mode'}</span>}\n </button>\n );\n}\n\nexport function Layout({ children }: { children: React.ReactNode }) {\n const [mobileOpen, setMobileOpen] = React.useState(false);\n const location = useLocation();\n\n React.useEffect(() => {\n setMobileOpen(false);\n }, [location.pathname]);\n\n return (\n <div className=\"flex h-screen bg-background overflow-hidden\">\n {/* Mobile overlay */}\n {mobileOpen && (\n <div\n className=\"fixed inset-0 bg-black/60 z-40 lg:hidden\"\n onClick={() => setMobileOpen(false)}\n />\n )}\n\n {/* Sidebar */}\n <aside\n className={cn(\n 'fixed lg:relative inset-y-0 left-0 z-50 flex flex-col w-64 border-r border-white/10 bg-[#1a1a2e] text-white/80 transition-transform duration-300',\n mobileOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0',\n )}\n >\n {/* Logo */}\n <div className=\"flex items-center justify-between px-5 h-16 border-b border-white/10\">\n <div className=\"flex items-center gap-2.5\">\n <img\n src=\"https://files.taskade.com/staging/space-files/1a0faa0f-1dbc-4a57-9917-99ef4adff008/original/cortex_icon_dark.png\"\n alt=\"Cortex\"\n className=\"w-7 h-7 rounded-lg object-cover hidden dark:block\"\n />\n <img\n src=\"https://files.taskade.com/staging/space-files/475e7a8f-e27e-42ee-ab55-82e85e4088e0/original/cortex_icon_light.png\"\n alt=\"Cortex\"\n className=\"w-7 h-7 rounded-lg object-cover block dark:hidden\"\n />\n <span className=\"font-bold text-sm tracking-wide text-white\">CORTEX</span>\n </div>\n <button\n onClick={() => setMobileOpen(false)}\n className=\"lg:hidden p-1 rounded text-white/50 hover:text-white\"\n >\n <X className=\"w-4 h-4\" />\n </button>\n </div>\n\n {/* Nav */}\n <nav className=\"flex-1 px-3 py-4 space-y-1\">\n {NAV.map(({ to, label, icon: Icon, exact }) => (\n <NavLink\n key={to}\n to={to}\n end={exact}\n className={({ isActive }) =>\n cn(\n 'flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all duration-150',\n isActive\n ? 'bg-white/10 text-white border border-white/15'\n : 'text-white/60 hover:text-white hover:bg-white/5',\n )\n }\n >\n <Icon className=\"w-4 h-4 flex-shrink-0\" />\n {label}\n </NavLink>\n ))}\n </nav>\n\n {/* Footer */}\n <div className=\"p-4 border-t border-white/10\">\n <ThemeToggle />\n </div>\n </aside>\n\n {/* Main content */}\n <div className=\"flex-1 flex flex-col overflow-hidden\">\n {/* Mobile top bar */}\n <header className=\"lg:hidden flex items-center justify-between px-4 h-14 border-b border-border bg-card/50 backdrop-blur-xl\">\n <div className=\"flex items-center gap-3\">\n <button\n onClick={() => setMobileOpen(true)}\n className=\"p-2 rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent\"\n >\n <Menu className=\"w-5 h-5\" />\n </button>\n <div className=\"flex items-center gap-2\">\n <img\n src=\"https://files.taskade.com/staging/space-files/1a0faa0f-1dbc-4a57-9917-99ef4adff008/original/cortex_icon_dark.png\"\n alt=\"Cortex\"\n className=\"w-5 h-5 rounded object-cover hidden dark:block\"\n />\n <img\n src=\"https://files.taskade.com/staging/space-files/475e7a8f-e27e-42ee-ab55-82e85e4088e0/original/cortex_icon_light.png\"\n alt=\"Cortex\"\n className=\"w-5 h-5 rounded object-cover block dark:hidden\"\n />\n <span className=\"font-bold text-sm text-foreground\">CORTEX</span>\n </div>\n </div>\n <ThemeToggle compact />\n </header>\n\n <main className=\"flex-1 overflow-y-auto\">\n {children}\n </main>\n </div>\n </div>\n );\n}\n"
}
},
"ai-elements": {
"directory": {
"tool.tsx": {
"file": {
"contents": "\"use client\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { cn } from \"@/lib/utils\";\nimport type { DynamicToolUIPart, ToolUIPart } from \"ai\";\nimport {\n CheckCircleIcon,\n ChevronDownIcon,\n CircleIcon,\n ClockIcon,\n WrenchIcon,\n XCircleIcon,\n} from \"lucide-react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { isValidElement } from \"react\";\n\nimport { CodeBlock } from \"./code-block\";\n\nexport type ToolProps = ComponentProps<typeof Collapsible>;\n\nexport const Tool = ({ className, ...props }: ToolProps) => (\n <Collapsible\n className={cn(\"group not-prose mb-4 w-full rounded-md border\", className)}\n {...props}\n />\n);\n\nexport type ToolPart = ToolUIPart | DynamicToolUIPart;\n\nexport type ToolHeaderProps = {\n title?: string;\n className?: string;\n} & (\n | { type: ToolUIPart[\"type\"]; state: ToolUIPart[\"state\"]; toolName?: never }\n | {\n type: DynamicToolUIPart[\"type\"];\n state: DynamicToolUIPart[\"state\"];\n toolName: string;\n }\n);\n\nconst statusLabels: Record<ToolPart[\"state\"], string> = {\n \"approval-requested\": \"Awaiting Approval\",\n \"approval-responded\": \"Responded\",\n \"input-available\": \"Running\",\n \"input-streaming\": \"Pending\",\n \"output-available\": \"Completed\",\n \"output-denied\": \"Denied\",\n \"output-error\": \"Error\",\n};\n\nconst statusIcons: Record<ToolPart[\"state\"], ReactNode> = {\n \"approval-requested\": <ClockIcon className=\"size-4 text-yellow-600\" />,\n \"approval-responded\": <CheckCircleIcon className=\"size-4 text-blue-600\" />,\n \"input-available\": <ClockIcon className=\"size-4 animate-pulse\" />,\n \"input-streaming\": <CircleIcon className=\"size-4\" />,\n \"output-available\": <CheckCircleIcon className=\"size-4 text-green-600\" />,\n \"output-denied\": <XCircleIcon className=\"size-4 text-orange-600\" />,\n \"output-error\": <XCircleIcon className=\"size-4 text-red-600\" />,\n};\n\nexport const getStatusBadge = (status: ToolPart[\"state\"]) => (\n <Badge className=\"gap-1.5 rounded-full text-xs\" variant=\"secondary\">\n {statusIcons[status]}\n {statusLabels[status]}\n </Badge>\n);\n\nexport const ToolHeader = ({\n className,\n title,\n type,\n state,\n toolName,\n ...props\n}: ToolHeaderProps) => {\n const derivedName =\n type === \"dynamic-tool\" ? toolName : type.split(\"-\").slice(1).join(\"-\");\n\n return (\n <CollapsibleTrigger\n className={cn(\n \"flex w-full items-center justify-between gap-4 p-3\",\n className\n )}\n {...props}\n >\n <div className=\"flex items-center gap-2\">\n <WrenchIcon className=\"size-4 text-muted-foreground\" />\n <span className=\"font-medium text-sm\">{title ?? derivedName}</span>\n {getStatusBadge(state)}\n </div>\n <ChevronDownIcon className=\"size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180\" />\n </CollapsibleTrigger>\n );\n};\n\nexport type ToolContentProps = ComponentProps<typeof CollapsibleContent>;\n\nexport const ToolContent = ({ className, ...props }: ToolContentProps) => (\n <CollapsibleContent\n className={cn(\n \"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\",\n className\n )}\n {...props}\n />\n);\n\nexport type ToolInputProps = ComponentProps<\"div\"> & {\n input: ToolPart[\"input\"];\n};\n\nexport const ToolInput = ({ className, input, ...props }: ToolInputProps) => (\n <div className={cn(\"space-y-2 overflow-hidden\", className)} {...props}>\n <h4 className=\"font-medium text-muted-foreground text-xs uppercase tracking-wide\">\n Parameters\n </h4>\n <div className=\"rounded-md bg-muted/50\">\n <CodeBlock code={JSON.stringify(input, null, 2)} language=\"json\" />\n </div>\n </div>\n);\n\nexport type ToolOutputProps = ComponentProps<\"div\"> & {\n output: ToolPart[\"output\"];\n errorText: ToolPart[\"errorText\"];\n};\n\nexport const ToolOutput = ({\n className,\n output,\n errorText,\n ...props\n}: ToolOutputProps) => {\n if (!(output || errorText)) {\n return null;\n }\n\n let Output = <div>{output as ReactNode}</div>;\n\n if (typeof output === \"object\" && !isValidElement(output)) {\n Output = (\n <CodeBlock code={JSON.stringify(output, null, 2)} language=\"json\" />\n );\n } else if (typeof output === \"string\") {\n Output = <CodeBlock code={output} language=\"json\" />;\n }\n\n return (\n <div className={cn(\"space-y-2\", className)} {...props}>\n <h4 className=\"font-medium text-muted-foreground text-xs uppercase tracking-wide\">\n {errorText ? \"Error\" : \"Result\"}\n </h4>\n <div\n className={cn(\n \"overflow-x-auto rounded-md text-xs [&_table]:w-full\",\n errorText\n ? \"bg-destructive/10 text-destructive\"\n : \"bg-muted/50 text-foreground\"\n )}\n >\n {errorText && <div>{errorText}</div>}\n {Output}\n </div>\n </div>\n );\n};\n"
}
},
"image.tsx": {
"file": {
"contents": "import { cn } from \"@/lib/utils\";\nimport type { Experimental_GeneratedImage } from \"ai\";\n\nexport type ImageProps = Experimental_GeneratedImage & {\n className?: string;\n alt?: string;\n};\n\nexport const Image = ({\n base64,\n uint8Array: _uint8Array,\n mediaType,\n ...props\n}: ImageProps) => (\n <img\n {...props}\n alt={props.alt}\n className={cn(\n \"h-auto max-w-full overflow-hidden rounded-md\",\n props.className\n )}\n src={`data:${mediaType};base64,${base64}`}\n />\n);\n"
}
},
"message.tsx": {
"file": {
"contents": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n ButtonGroup,\n ButtonGroupText,\n} from \"@/components/ui/button-group\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport { cjk } from \"@streamdown/cjk\";\nimport { code } from \"@/lib/streamdown-code\";\nimport { math } from \"@streamdown/math\";\nimport { mermaid } from \"@/lib/streamdown-mermaid\";\nimport type { UIMessage } from \"ai\";\nimport { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\";\nimport type { ComponentProps, HTMLAttributes, ReactElement } from \"react\";\nimport {\n createContext,\n memo,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n} from \"react\";\nimport { Streamdown } from \"streamdown\";\n\nexport type MessageProps = HTMLAttributes<HTMLDivElement> & {\n from: UIMessage[\"role\"];\n};\n\nexport const Message = ({ className, from, ...props }: MessageProps) => (\n <div\n className={cn(\n \"group flex w-full max-w-[95%] flex-col gap-2\",\n from === \"user\" ? \"is-user ml-auto justify-end\" : \"is-assistant\",\n className\n )}\n {...props}\n />\n);\n\nexport type MessageContentProps = HTMLAttributes<HTMLDivElement>;\n\nexport const MessageContent = ({\n children,\n className,\n ...props\n}: MessageContentProps) => (\n <div\n className={cn(\n \"is-user:dark flex w-fit min-w-0 max-w-full flex-col gap-2 overflow-hidden text-sm\",\n \"group-[.is-user]:ml-auto group-[.is-user]:rounded-lg group-[.is-user]:bg-secondary group-[.is-user]:px-4 group-[.is-user]:py-3 group-[.is-user]:text-foreground\",\n \"group-[.is-assistant]:text-foreground\",\n className\n )}\n {...props}\n >\n {children}\n </div>\n);\n\nexport type MessageActionsProps = ComponentProps<\"div\">;\n\nexport const MessageActions = ({\n className,\n children,\n ...props\n}: MessageActionsProps) => (\n <div className={cn(\"flex items-center gap-1\", className)} {...props}>\n {children}\n </div>\n);\n\nexport type MessageActionProps = ComponentProps<typeof Button> & {\n tooltip?: string;\n label?: string;\n};\n\nexport const MessageAction = ({\n tooltip,\n children,\n label,\n variant = \"ghost\",\n size = \"icon-sm\",\n ...props\n}: MessageActionProps) => {\n const button = (\n <Button size={size} type=\"button\" variant={variant} {...props}>\n {children}\n <span className=\"sr-only\">{label || tooltip}</span>\n </Button>\n );\n\n if (tooltip) {\n return (\n <TooltipProvider>\n <Tooltip>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent>\n <p>{tooltip}</p>\n </TooltipContent>\n </Tooltip>\n </TooltipProvider>\n );\n }\n\n return button;\n};\n\ninterface MessageBranchContextType {\n currentBranch: number;\n totalBranches: number;\n goToPrevious: () => void;\n goToNext: () => void;\n branches: ReactElement[];\n setBranches: (branches: ReactElement[]) => void;\n}\n\nconst MessageBranchContext = createContext<MessageBranchContextType | null>(\n null\n);\n\nconst useMessageBranch = () => {\n const context = useContext(MessageBranchContext);\n\n if (!context) {\n throw new Error(\n \"MessageBranch components must be used within MessageBranch\"\n );\n }\n\n return context;\n};\n\nexport type MessageBranchProps = HTMLAttributes<HTMLDivElement> & {\n defaultBranch?: number;\n onBranchChange?: (branchIndex: number) => void;\n};\n\nexport const MessageBranch = ({\n defaultBranch = 0,\n onBranchChange,\n className,\n ...props\n}: MessageBranchProps) => {\n const [currentBranch, setCurrentBranch] = useState(defaultBranch);\n const [branches, setBranches] = useState<ReactElement[]>([]);\n\n const handleBranchChange = useCallback(\n (newBranch: number) => {\n setCurrentBranch(newBranch);\n onBranchChange?.(newBranch);\n },\n [onBranchChange]\n );\n\n const goToPrevious = useCallback(() => {\n const newBranch =\n currentBranch > 0 ? currentBranch - 1 : branches.length - 1;\n handleBranchChange(newBranch);\n }, [currentBranch, branches.length, handleBranchChange]);\n\n const goToNext = useCallback(() => {\n const newBranch =\n currentBranch < branches.length - 1 ? currentBranch + 1 : 0;\n handleBranchChange(newBranch);\n }, [currentBranch, branches.length, handleBranchChange]);\n\n const contextValue = useMemo<MessageBranchContextType>(\n () => ({\n branches,\n currentBranch,\n goToNext,\n goToPrevious,\n setBranches,\n totalBranches: branches.length,\n }),\n [branches, currentBranch, goToNext, goToPrevious]\n );\n\n return (\n <MessageBranchContext.Provider value={contextValue}>\n <div\n className={cn(\"grid w-full gap-2 [&>div]:pb-0\", className)}\n {...props}\n />\n </MessageBranchContext.Provider>\n );\n};\n\nexport type MessageBranchContentProps = HTMLAttributes<HTMLDivElement>;\n\nexport const MessageBranchContent = ({\n children,\n ...props\n}: MessageBranchContentProps) => {\n const { currentBranch, setBranches, branches } = useMessageBranch();\n const childrenArray = useMemo(\n () => (Array.isArray(children) ? children : [children]),\n [children]\n );\n\n // Use useEffect to update branches when they change\n useEffect(() => {\n if (branches.length !== childrenArray.length) {\n setBranches(childrenArray);\n }\n }, [childrenArray, branches, setBranches]);\n\n return childrenArray.map((branch, index) => (\n <div\n className={cn(\n \"grid gap-2 overflow-hidden [&>div]:pb-0\",\n index === currentBranch ? \"block\" : \"hidden\"\n )}\n key={branch.key}\n {...props}\n >\n {branch}\n </div>\n ));\n};\n\nexport type MessageBranchSelectorProps = ComponentProps<typeof ButtonGroup>;\n\nexport const MessageBranchSelector = ({\n className,\n ...props\n}: MessageBranchSelectorProps) => {\n const { totalBranches } = useMessageBranch();\n\n // Don't render if there's only one branch\n if (totalBranches <= 1) {\n return null;\n }\n\n return (\n <ButtonGroup\n className={cn(\n \"[&>*:not(:first-child)]:rounded-l-md [&>*:not(:last-child)]:rounded-r-md\",\n className\n )}\n orientation=\"horizontal\"\n {...props}\n />\n );\n};\n\nexport type MessageBranchPreviousProps = ComponentProps<typeof Button>;\n\nexport const MessageBranchPrevious = ({\n children,\n ...props\n}: MessageBranchPreviousProps) => {\n const { goToPrevious, totalBranches } = useMessageBranch();\n\n return (\n <Button\n aria-label=\"Previous branch\"\n disabled={totalBranches <= 1}\n onClick={goToPrevious}\n size=\"icon-sm\"\n type=\"button\"\n variant=\"ghost\"\n {...props}\n >\n {children ?? <ChevronLeftIcon size={14} />}\n </Button>\n );\n};\n\nexport type MessageBranchNextProps = ComponentProps<typeof Button>;\n\nexport const MessageBranchNext = ({\n children,\n ...props\n}: MessageBranchNextProps) => {\n const { goToNext, totalBranches } = useMessageBranch();\n\n return (\n <Button\n aria-label=\"Next branch\"\n disabled={totalBranches <= 1}\n onClick={goToNext}\n size=\"icon-sm\"\n type=\"button\"\n variant=\"ghost\"\n {...props}\n >\n {children ?? <ChevronRightIcon size={14} />}\n </Button>\n );\n};\n\nexport type MessageBranchPageProps = HTMLAttributes<HTMLSpanElement>;\n\nexport const MessageBranchPage = ({\n className,\n ...props\n}: MessageBranchPageProps) => {\n const { currentBranch, totalBranches } = useMessageBranch();\n\n return (\n <ButtonGroupText\n className={cn(\n \"border-none bg-transparent text-muted-foreground shadow-none\",\n className\n )}\n {...props}\n >\n {currentBranch + 1} of {totalBranches}\n </ButtonGroupText>\n );\n};\n\nexport type MessageResponseProps = ComponentProps<typeof Streamdown>;\n\nconst streamdownPlugins = { cjk, code, math, mermaid };\n\nexport const MessageResponse = memo(\n ({ className, ...props }: MessageResponseProps) => (\n <Streamdown\n className={cn(\n \"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0\",\n className\n )}\n plugins={streamdownPlugins}\n {...props}\n />\n ),\n (prevProps, nextProps) =>\n prevProps.children === nextProps.children &&\n nextProps.isAnimating === prevProps.isAnimating\n);\n\nMessageResponse.displayName = \"MessageResponse\";\n\nexport type MessageToolbarProps = ComponentProps<\"div\">;\n\nexport const MessageToolbar = ({\n className,\n children,\n ...props\n}: MessageToolbarProps) => (\n <div\n className={cn(\n \"mt-4 flex w-full items-center justify-between gap-4\",\n className\n )}\n {...props}\n >\n {children}\n </div>\n);\n"
}
},
"shimmer.tsx": {
"file": {
"contents": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport type { MotionProps } from \"motion/react\";\nimport { motion } from \"motion/react\";\nimport type { CSSProperties, ElementType, JSX } from \"react\";\nimport { memo, useMemo } from \"react\";\n\ntype MotionHTMLProps = MotionProps & Record<string, unknown>;\n\n// Cache motion components at module level to avoid creating during render\nconst motionComponentCache = new Map<\n keyof JSX.IntrinsicElements,\n React.ComponentType<MotionHTMLProps>\n>();\n\nconst getMotionComponent = (element: keyof JSX.IntrinsicElements) => {\n let component = motionComponentCache.get(element);\n if (!component) {\n component = motion.create(element);\n motionComponentCache.set(element, component);\n }\n return component;\n};\n\nexport interface TextShimmerProps {\n children: string;\n as?: ElementType;\n className?: string;\n duration?: number;\n spread?: number;\n}\n\nconst ShimmerComponent = ({\n children,\n as: Component = \"p\",\n className,\n duration = 2,\n spread = 2,\n}: TextShimmerProps) => {\n const MotionComponent = getMotionComponent(\n Component as keyof JSX.IntrinsicElements\n );\n\n const dynamicSpread = useMemo(\n () => (children?.length ?? 0) * spread,\n [children, spread]\n );\n\n return (\n <MotionComponent\n animate={{ backgroundPosition: \"0% center\" }}\n className={cn(\n \"relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent\",\n \"[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),hsl(var(--background)),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]\",\n className\n )}\n initial={{ backgroundPosition: \"100% center\" }}\n style={\n {\n \"--spread\": `${dynamicSpread}px`,\n backgroundImage:\n \"var(--bg), linear-gradient(hsl(var(--muted-foreground)), hsl(var(--muted-foreground)))\",\n } as CSSProperties\n }\n transition={{\n duration,\n ease: \"linear\",\n repeat: Number.POSITIVE_INFINITY,\n }}\n >\n {children}\n </MotionComponent>\n );\n};\n\nexport const Shimmer = memo(ShimmerComponent);\n"
}
},
"sources.tsx": {
"file": {
"contents": "\"use client\";\n\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { cn } from \"@/lib/utils\";\nimport { BookIcon, ChevronDownIcon } from \"lucide-react\";\nimport type { ComponentProps } from \"react\";\n\nexport type SourcesProps = ComponentProps<\"div\">;\n\nexport const Sources = ({ className, ...props }: SourcesProps) => (\n <Collapsible\n className={cn(\"not-prose mb-4 text-primary text-xs\", className)}\n {...props}\n />\n);\n\nexport type SourcesTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {\n count: number;\n};\n\nexport const SourcesTrigger = ({\n className,\n count,\n children,\n ...props\n}: SourcesTriggerProps) => (\n <CollapsibleTrigger\n className={cn(\"flex items-center gap-2\", className)}\n {...props}\n >\n {children ?? (\n <>\n <p className=\"font-medium\">Used {count} sources</p>\n <ChevronDownIcon className=\"h-4 w-4\" />\n </>\n )}\n </CollapsibleTrigger>\n);\n\nexport type SourcesContentProps = ComponentProps<typeof CollapsibleContent>;\n\nexport const SourcesContent = ({\n className,\n ...props\n}: SourcesContentProps) => (\n <CollapsibleContent\n className={cn(\n \"mt-3 flex w-fit flex-col gap-2\",\n \"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in\",\n className\n )}\n {...props}\n />\n);\n\nexport type SourceProps = ComponentProps<\"a\">;\n\nexport const Source = ({ href, title, children, ...props }: SourceProps) => (\n <a\n className=\"flex items-center gap-2\"\n href={href}\n rel=\"noreferrer\"\n target=\"_blank\"\n {...props}\n >\n {children ?? (\n <>\n <BookIcon className=\"h-4 w-4\" />\n <span className=\"block font-medium\">{title}</span>\n </>\n )}\n </a>\n);\n"
}
},
"reasoning.tsx": {
"file": {
"contents": "\"use client\";\n\nimport { useControllableState } from \"@radix-ui/react-use-controllable-state\";\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { cn } from \"@/lib/utils\";\nimport { cjk } from \"@streamdown/cjk\";\nimport { code } from \"@/lib/streamdown-code\";\nimport { math } from \"@streamdown/math\";\nimport { mermaid } from \"@/lib/streamdown-mermaid\";\nimport { BrainIcon, ChevronDownIcon } from \"lucide-react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport {\n createContext,\n memo,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { Streamdown } from \"streamdown\";\n\nimport { Shimmer } from \"./shimmer\";\n\ninterface ReasoningContextValue {\n isStreaming: boolean;\n isOpen: boolean;\n setIsOpen: (open: boolean) => void;\n duration: number | undefined;\n}\n\nconst ReasoningContext = createContext<ReasoningContextValue | null>(null);\n\nexport const useReasoning = () => {\n const context = useContext(ReasoningContext);\n if (!context) {\n throw new Error(\"Reasoning components must be used within Reasoning\");\n }\n return context;\n};\n\nexport type ReasoningProps = ComponentProps<typeof Collapsible> & {\n isStreaming?: boolean;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n duration?: number;\n};\n\nconst AUTO_CLOSE_DELAY = 1000;\nconst MS_IN_S = 1000;\n\nexport const Reasoning = memo(\n ({\n className,\n isStreaming = false,\n open,\n defaultOpen,\n onOpenChange,\n duration: durationProp,\n children,\n ...props\n }: ReasoningProps) => {\n const resolvedDefaultOpen = defaultOpen ?? isStreaming;\n // Track if defaultOpen was explicitly set to false (to prevent auto-open)\n const isExplicitlyClosed = defaultOpen === false;\n\n const [isOpen, setIsOpen] = useControllableState<boolean>({\n defaultProp: resolvedDefaultOpen,\n onChange: onOpenChange,\n prop: open,\n });\n const [duration, setDuration] = useControllableState<number | undefined>({\n defaultProp: undefined,\n prop: durationProp,\n });\n\n const hasEverStreamedRef = useRef(isStreaming);\n const [hasAutoClosed, setHasAutoClosed] = useState(false);\n const startTimeRef = useRef<number | null>(null);\n\n // Track when streaming starts and compute duration\n useEffect(() => {\n if (isStreaming) {\n hasEverStreamedRef.current = true;\n if (startTimeRef.current === null) {\n startTimeRef.current = Date.now();\n }\n } else if (startTimeRef.current !== null) {\n setDuration(Math.ceil((Date.now() - startTimeRef.current) / MS_IN_S));\n startTimeRef.current = null;\n }\n }, [isStreaming, setDuration]);\n\n // Auto-open when streaming starts (unless explicitly closed)\n useEffect(() => {\n if (isStreaming && !isOpen && !isExplicitlyClosed) {\n setIsOpen(true);\n }\n }, [isStreaming, isOpen, setIsOpen, isExplicitlyClosed]);\n\n // Auto-close when streaming ends (once only, and only if it ever streamed)\n useEffect(() => {\n if (\n hasEverStreamedRef.current &&\n !isStreaming &&\n isOpen &&\n !hasAutoClosed\n ) {\n const timer = setTimeout(() => {\n setIsOpen(false);\n setHasAutoClosed(true);\n }, AUTO_CLOSE_DELAY);\n\n return () => clearTimeout(timer);\n }\n }, [isStreaming, isOpen, setIsOpen, hasAutoClosed]);\n\n const handleOpenChange = useCallback(\n (newOpen: boolean) => {\n setIsOpen(newOpen);\n },\n [setIsOpen]\n );\n\n const contextValue = useMemo(\n () => ({ duration, isOpen, isStreaming, setIsOpen }),\n [duration, isOpen, isStreaming, setIsOpen]\n );\n\n return (\n <ReasoningContext.Provider value={contextValue}>\n <Collapsible\n className={cn(\"not-prose mb-4\", className)}\n onOpenChange={handleOpenChange}\n open={isOpen}\n {...props}\n >\n {children}\n </Collapsible>\n </ReasoningContext.Provider>\n );\n }\n);\n\nexport type ReasoningTriggerProps = ComponentProps<\n typeof CollapsibleTrigger\n> & {\n getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;\n};\n\nconst defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {\n if (isStreaming || duration === 0) {\n return <Shimmer duration={1}>Thinking...</Shimmer>;\n }\n if (duration === undefined) {\n return <p>Thought for a few seconds</p>;\n }\n return <p>Thought for {duration} seconds</p>;\n};\n\nexport const ReasoningTrigger = memo(\n ({\n className,\n children,\n getThinkingMessage = defaultGetThinkingMessage,\n ...props\n }: ReasoningTriggerProps) => {\n const { isStreaming, isOpen, duration } = useReasoning();\n\n return (\n <CollapsibleTrigger\n className={cn(\n \"flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground\",\n className\n )}\n {...props}\n >\n {children ?? (\n <>\n <BrainIcon className=\"size-4\" />\n {getThinkingMessage(isStreaming, duration)}\n <ChevronDownIcon\n className={cn(\n \"size-4 transition-transform\",\n isOpen ? \"rotate-180\" : \"rotate-0\"\n )}\n />\n </>\n )}\n </CollapsibleTrigger>\n );\n }\n);\n\nexport type ReasoningContentProps = ComponentProps<\n typeof CollapsibleContent\n> & {\n children: string;\n};\n\nconst streamdownPlugins = { cjk, code, math, mermaid };\n\nexport const ReasoningContent = memo(\n ({ className, children, ...props }: ReasoningContentProps) => (\n <CollapsibleContent\n className={cn(\n \"mt-4 text-sm\",\n \"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\",\n className\n )}\n {...props}\n >\n <Streamdown plugins={streamdownPlugins}>{children}</Streamdown>\n </CollapsibleContent>\n )\n);\n\nReasoning.displayName = \"Reasoning\";\nReasoningTrigger.displayName = \"ReasoningTrigger\";\nReasoningContent.displayName = \"ReasoningContent\";\n"
}
},
"code-block.tsx": {
"file": {
"contents": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, CopyIcon } from \"lucide-react\";\nimport type { ComponentProps, CSSProperties, HTMLAttributes } from \"react\";\nimport {\n createContext,\n memo,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport type {\n BundledLanguage,\n ThemedToken,\n} from \"@/lib/shiki\";\nimport { getHighlighter } from \"@/lib/shiki\";\n\n// Shiki uses bitflags for font styles: 1=italic, 2=bold, 4=underline\n// oxlint-disable-next-line eslint(no-bitwise)\nconst isItalic = (fontStyle: number | undefined) => fontStyle && fontStyle & 1;\n// oxlint-disable-next-line eslint(no-bitwise)\nconst isBold = (fontStyle: number | undefined) => fontStyle && fontStyle & 2;\nconst isUnderline = (fontStyle: number | undefined) =>\n // oxlint-disable-next-line eslint(no-bitwise)\n fontStyle && fontStyle & 4;\n\n// Transform tokens to include pre-computed keys to avoid noArrayIndexKey lint\ninterface KeyedToken {\n token: ThemedToken;\n key: string;\n}\ninterface KeyedLine {\n tokens: KeyedToken[];\n key: string;\n}\n\nconst addKeysToTokens = (lines: ThemedToken[][]): KeyedLine[] =>\n lines.map((line, lineIdx) => ({\n key: `line-${lineIdx}`,\n tokens: line.map((token, tokenIdx) => ({\n key: `line-${lineIdx}-${tokenIdx}`,\n token,\n })),\n }));\n\n// Token rendering component\nconst TokenSpan = ({ token }: { token: ThemedToken }) => (\n <span\n className=\"dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)]\"\n style={\n {\n backgroundColor: token.bgColor,\n color: token.color,\n fontStyle: isItalic(token.fontStyle) ? \"italic\" : undefined,\n fontWeight: isBold(token.fontStyle) ? \"bold\" : undefined,\n textDecoration: isUnderline(token.fontStyle) ? \"underline\" : undefined,\n ...token.htmlStyle,\n } as CSSProperties\n }\n >\n {token.content}\n </span>\n);\n\n// Line number styles using CSS counters\nconst LINE_NUMBER_CLASSES = cn(\n \"block\",\n \"before:content-[counter(line)]\",\n \"before:inline-block\",\n \"before:[counter-increment:line]\",\n \"before:w-8\",\n \"before:mr-4\",\n \"before:text-right\",\n \"before:text-muted-foreground/50\",\n \"before:font-mono\",\n \"before:select-none\"\n);\n\n// Line rendering component\nconst LineSpan = ({\n keyedLine,\n showLineNumbers,\n}: {\n keyedLine: KeyedLine;\n showLineNumbers: boolean;\n}) => (\n <span className={showLineNumbers ? LINE_NUMBER_CLASSES : \"block\"}>\n {keyedLine.tokens.length === 0\n ? \"\\n\"\n : keyedLine.tokens.map(({ token, key }) => (\n <TokenSpan key={key} token={token} />\n ))}\n </span>\n);\n\n// Types\ntype CodeBlockProps = HTMLAttributes<HTMLDivElement> & {\n code: string;\n language: BundledLanguage;\n showLineNumbers?: boolean;\n};\n\ninterface TokenizedCode {\n tokens: ThemedToken[][];\n fg: string;\n bg: string;\n}\n\ninterface CodeBlockContextType {\n code: string;\n}\n\n// Context\nconst CodeBlockContext = createContext<CodeBlockContextType>({\n code: \"\",\n});\n\n// Token cache\nconst tokensCache = new Map<string, TokenizedCode>();\n\n// Subscribers for async token updates\nconst subscribers = new Map<string, Set<(result: TokenizedCode) => void>>();\n\nconst getTokensCacheKey = (code: string, language: BundledLanguage) => {\n const start = code.slice(0, 100);\n const end = code.length > 100 ? code.slice(-100) : \"\";\n return `${language}:${code.length}:${start}:${end}`;\n};\n\n// Create raw tokens for immediate display while highlighting loads\nconst createRawTokens = (code: string): TokenizedCode => ({\n bg: \"transparent\",\n fg: \"inherit\",\n tokens: code.split(\"\\n\").map((line) =>\n line === \"\"\n ? []\n : [\n {\n color: \"inherit\",\n content: line,\n } as ThemedToken,\n ]\n ),\n});\n\n// Synchronous highlight with callback for async results\nexport const highlightCode = (\n code: string,\n language: BundledLanguage,\n // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks)\n callback?: (result: TokenizedCode) => void\n): TokenizedCode | null => {\n const tokensCacheKey = getTokensCacheKey(code, language);\n\n // Return cached result if available\n const cached = tokensCache.get(tokensCacheKey);\n if (cached) {\n return cached;\n }\n\n // Subscribe callback if provided\n if (callback) {\n if (!subscribers.has(tokensCacheKey)) {\n subscribers.set(tokensCacheKey, new Set());\n }\n subscribers.get(tokensCacheKey)?.add(callback);\n }\n\n // Start highlighting in background - fire-and-forget async pattern\n getHighlighter(language)\n // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then)\n .then((highlighter) => {\n const availableLangs = highlighter.getLoadedLanguages();\n const langToUse = availableLangs.includes(language) ? language : \"text\";\n\n const result = highlighter.codeToTokens(code, {\n lang: langToUse,\n themes: {\n dark: \"github-dark\",\n light: \"github-light\",\n },\n });\n\n const tokenized: TokenizedCode = {\n bg: result.bg ?? \"transparent\",\n fg: result.fg ?? \"inherit\",\n tokens: result.tokens,\n };\n\n // Cache the result\n tokensCache.set(tokensCacheKey, tokenized);\n\n // Notify all subscribers\n const subs = subscribers.get(tokensCacheKey);\n if (subs) {\n for (const sub of subs) {\n sub(tokenized);\n }\n subscribers.delete(tokensCacheKey);\n }\n })\n // oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then), eslint-plugin-promise(prefer-await-to-callbacks)\n .catch((error) => {\n console.error(\"Failed to highlight code:\", error);\n subscribers.delete(tokensCacheKey);\n });\n\n return null;\n};\n\nconst CodeBlockBody = memo(\n ({\n tokenized,\n showLineNumbers,\n className,\n }: {\n tokenized: TokenizedCode;\n showLineNumbers: boolean;\n className?: string;\n }) => {\n const preStyle = useMemo(\n () => ({\n backgroundColor: tokenized.bg,\n color: tokenized.fg,\n }),\n [tokenized.bg, tokenized.fg]\n );\n\n const keyedLines = useMemo(\n () => addKeysToTokens(tokenized.tokens),\n [tokenized.tokens]\n );\n\n return (\n <pre\n className={cn(\n \"dark:!bg-[var(--shiki-dark-bg)] dark:!text-[var(--shiki-dark)] m-0 p-4 text-sm\",\n className\n )}\n style={preStyle}\n >\n <code\n className={cn(\n \"font-mono text-sm\",\n showLineNumbers && \"[counter-increment:line_0] [counter-reset:line]\"\n )}\n >\n {keyedLines.map((keyedLine) => (\n <LineSpan\n key={keyedLine.key}\n keyedLine={keyedLine}\n showLineNumbers={showLineNumbers}\n />\n ))}\n </code>\n </pre>\n );\n },\n (prevProps, nextProps) =>\n prevProps.tokenized === nextProps.tokenized &&\n prevProps.showLineNumbers === nextProps.showLineNumbers &&\n prevProps.className === nextProps.className\n);\n\nCodeBlockBody.displayName = \"CodeBlockBody\";\n\nexport const CodeBlockContainer = ({\n className,\n language,\n style,\n ...props\n}: HTMLAttributes<HTMLDivElement> & { language: string }) => (\n <div\n className={cn(\n \"group relative w-full overflow-hidden rounded-md border bg-background text-foreground\",\n className\n )}\n data-language={language}\n style={{\n containIntrinsicSize: \"auto 200px\",\n contentVisibility: \"auto\",\n ...style,\n }}\n {...props}\n />\n);\n\nexport const CodeBlockHeader = ({\n children,\n className,\n ...props\n}: HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\n \"flex items-center justify-between border-b bg-muted/80 px-3 py-2 text-muted-foreground text-xs\",\n className\n )}\n {...props}\n >\n {children}\n </div>\n);\n\nexport const CodeBlockTitle = ({\n children,\n className,\n ...props\n}: HTMLAttributes<HTMLDivElement>) => (\n <div className={cn(\"flex items-center gap-2\", className)} {...props}>\n {children}\n </div>\n);\n\nexport const CodeBlockFilename = ({\n children,\n className,\n ...props\n}: HTMLAttributes<HTMLSpanElement>) => (\n <span className={cn(\"font-mono\", className)} {...props}>\n {children}\n </span>\n);\n\nexport const CodeBlockActions = ({\n children,\n className,\n ...props\n}: HTMLAttributes<HTMLDivElement>) => (\n <div\n className={cn(\"-my-1 -mr-1 flex items-center gap-2\", className)}\n {...props}\n >\n {children}\n </div>\n);\n\nexport const CodeBlockContent = ({\n code,\n language,\n showLineNumbers = false,\n}: {\n code: string;\n language: BundledLanguage;\n showLineNumbers?: boolean;\n}) => {\n // Memoized raw tokens for immediate display\n const rawTokens = useMemo(() => createRawTokens(code), [code]);\n\n // Synchronous cache lookup — avoids setState in effect for cached results\n const syncTokens = useMemo(\n () => highlightCode(code, language) ?? rawTokens,\n [code, language, rawTokens]\n );\n\n // Async highlighting result (populated after shiki loads)\n const [asyncTokens, setAsyncTokens] = useState<TokenizedCode | null>(null);\n const asyncKeyRef = useRef({ code, language });\n\n // Invalidate stale async tokens synchronously during render\n if (\n asyncKeyRef.current.code !== code ||\n asyncKeyRef.current.language !== language\n ) {\n asyncKeyRef.current = { code, language };\n setAsyncTokens(null);\n }\n\n useEffect(() => {\n let cancelled = false;\n\n highlightCode(code, language, (result) => {\n if (!cancelled) {\n setAsyncTokens(result);\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [code, language]);\n\n const tokenized = asyncTokens ?? syncTokens;\n\n return (\n <div className=\"relative overflow-auto\">\n <CodeBlockBody showLineNumbers={showLineNumbers} tokenized={tokenized} />\n </div>\n );\n};\n\nexport const CodeBlock = ({\n code,\n language,\n showLineNumbers = false,\n className,\n children,\n ...props\n}: CodeBlockProps) => {\n const contextValue = useMemo(() => ({ code }), [code]);\n\n return (\n <CodeBlockContext.Provider value={contextValue}>\n <CodeBlockContainer className={className} language={language} {...props}>\n {children}\n <CodeBlockContent\n code={code}\n language={language}\n showLineNumbers={showLineNumbers}\n />\n </CodeBlockContainer>\n </CodeBlockContext.Provider>\n );\n};\n\nexport type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {\n onCopy?: () => void;\n onError?: (error: Error) => void;\n timeout?: number;\n};\n\nexport const CodeBlockCopyButton = ({\n onCopy,\n onError,\n timeout = 2000,\n children,\n className,\n ...props\n}: CodeBlockCopyButtonProps) => {\n const [isCopied, setIsCopied] = useState(false);\n const timeoutRef = useRef<number>(0);\n const { code } = useContext(CodeBlockContext);\n\n const copyToClipboard = useCallback(async () => {\n if (typeof window === \"undefined\" || !navigator?.clipboard?.writeText) {\n onError?.(new Error(\"Clipboard API not available\"));\n return;\n }\n\n try {\n if (!isCopied) {\n await navigator.clipboard.writeText(code);\n setIsCopied(true);\n onCopy?.();\n timeoutRef.current = window.setTimeout(\n () => setIsCopied(false),\n timeout\n );\n }\n } catch (error) {\n onError?.(error as Error);\n }\n }, [code, onCopy, onError, timeout, isCopied]);\n\n useEffect(\n () => () => {\n window.clearTimeout(timeoutRef.current);\n },\n []\n );\n\n const Icon = isCopied ? CheckIcon : CopyIcon;\n\n return (\n <Button\n className={cn(\"shrink-0\", className)}\n onClick={copyToClipboard}\n size=\"icon\"\n variant=\"ghost\"\n {...props}\n >\n {children ?? <Icon size={14} />}\n </Button>\n );\n};\n\nexport type CodeBlockLanguageSelectorProps = ComponentProps<typeof Select>;\n\nexport const CodeBlockLanguageSelector = (\n props: CodeBlockLanguageSelectorProps\n) => <Select {...props} />;\n\nexport type CodeBlockLanguageSelectorTriggerProps = ComponentProps<\n typeof SelectTrigger\n>;\n\nexport const CodeBlockLanguageSelectorTrigger = ({\n className,\n ...props\n}: CodeBlockLanguageSelectorTriggerProps) => (\n <SelectTrigger\n className={cn(\n \"h-7 border-none bg-transparent px-2 text-xs shadow-none\",\n className\n )}\n size=\"sm\"\n {...props}\n />\n);\n\nexport type CodeBlockLanguageSelectorValueProps = ComponentProps<\n typeof SelectValue\n>;\n\nexport const CodeBlockLanguageSelectorValue = (\n props: CodeBlockLanguageSelectorValueProps\n) => <SelectValue {...props} />;\n\nexport type CodeBlockLanguageSelectorContentProps = ComponentProps<\n typeof SelectContent\n>;\n\nexport const CodeBlockLanguageSelectorContent = ({\n align = \"end\",\n ...props\n}: CodeBlockLanguageSelectorContentProps) => (\n <SelectContent align={align} {...props} />\n);\n\nexport type CodeBlockLanguageSelectorItemProps = ComponentProps<\n typeof SelectItem\n>;\n\nexport const CodeBlockLanguageSelectorItem = (\n props: CodeBlockLanguageSelectorItemProps\n) => <SelectItem {...props} />;\n"
}
},
"suggestion.tsx": {
"file": {
"contents": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n ScrollArea,\n ScrollBar,\n} from \"@/components/ui/scroll-area\";\nimport { cn } from \"@/lib/utils\";\nimport type { ComponentProps } from \"react\";\nimport { useCallback } from \"react\";\n\nexport type SuggestionsProps = ComponentProps<typeof ScrollArea>;\n\nexport const Suggestions = ({\n className,\n children,\n ...props\n}: SuggestionsProps) => (\n <ScrollArea className=\"w-full overflow-x-auto whitespace-nowrap\" {...props}>\n <div className={cn(\"flex w-max flex-nowrap items-center gap-2\", className)}>\n {children}\n </div>\n <ScrollBar className=\"hidden\" orientation=\"horizontal\" />\n </ScrollArea>\n);\n\nexport type SuggestionProps = Omit<ComponentProps<typeof Button>, \"onClick\"> & {\n suggestion: string;\n onClick?: (suggestion: string) => void;\n};\n\nexport const Suggestion = ({\n suggestion,\n onClick,\n className,\n variant = \"outline\",\n size = \"sm\",\n children,\n ...props\n}: SuggestionProps) => {\n const handleClick = useCallback(() => {\n onClick?.(suggestion);\n }, [onClick, suggestion]);\n\n return (\n <Button\n className={cn(\"cursor-pointer rounded-full px-4\", className)}\n onClick={handleClick}\n size={size}\n type=\"button\"\n variant={variant}\n {...props}\n >\n {children || suggestion}\n </Button>\n );\n};\n"
}
},
"attachments.tsx": {
"file": {
"contents": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n HoverCard,\n HoverCardContent,\n HoverCardTrigger,\n} from \"@/components/ui/hover-card\";\nimport { cn } from \"@/lib/utils\";\nimport type { FileUIPart, SourceDocumentUIPart } from \"ai\";\nimport {\n FileTextIcon,\n GlobeIcon,\n ImageIcon,\n Music2Icon,\n PaperclipIcon,\n VideoIcon,\n XIcon,\n} from \"lucide-react\";\nimport type { ComponentProps, HTMLAttributes, ReactNode } from \"react\";\nimport { createContext, useCallback, useContext, useMemo } from \"react\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type AttachmentData =\n | (FileUIPart & { id: string })\n | (SourceDocumentUIPart & { id: string });\n\nexport type AttachmentMediaCategory =\n | \"image\"\n | \"video\"\n | \"audio\"\n | \"document\"\n | \"source\"\n | \"unknown\";\n\nexport type AttachmentVariant = \"grid\" | \"inline\" | \"list\";\n\nconst mediaCategoryIcons: Record<AttachmentMediaCategory, typeof ImageIcon> = {\n audio: Music2Icon,\n document: FileTextIcon,\n image: ImageIcon,\n source: GlobeIcon,\n unknown: PaperclipIcon,\n video: VideoIcon,\n};\n\n// ============================================================================\n// Utility Functions\n// ============================================================================\n\nexport const getMediaCategory = (\n data: AttachmentData\n): AttachmentMediaCategory => {\n if (data.type === \"source-document\") {\n return \"source\";\n }\n\n const mediaType = data.mediaType ?? \"\";\n\n if (mediaType.startsWith(\"image/\")) {\n return \"image\";\n }\n if (mediaType.startsWith(\"video/\")) {\n return \"video\";\n }\n if (mediaType.startsWith(\"audio/\")) {\n return \"audio\";\n }\n if (mediaType.startsWith(\"application/\") || mediaType.startsWith(\"text/\")) {\n return \"document\";\n }\n\n return \"unknown\";\n};\n\nexport const getAttachmentLabel = (data: AttachmentData): string => {\n if (data.type === \"source-document\") {\n return data.title || data.filename || \"Source\";\n }\n\n const category = getMediaCategory(data);\n return data.filename || (category === \"image\" ? \"Image\" : \"Attachment\");\n};\n\nconst renderAttachmentImage = (\n url: string,\n filename: string | undefined,\n isGrid: boolean\n) =>\n isGrid ? (\n <img\n alt={filename || \"Image\"}\n className=\"size-full object-cover\"\n height={96}\n src={url}\n width={96}\n />\n ) : (\n <img\n alt={filename || \"Image\"}\n className=\"size-full rounded object-cover\"\n height={20}\n src={url}\n width={20}\n />\n );\n\n// ============================================================================\n// Contexts\n// ============================================================================\n\ninterface AttachmentsContextValue {\n variant: AttachmentVariant;\n}\n\nconst AttachmentsContext = createContext<AttachmentsContextValue | null>(null);\n\ninterface AttachmentContextValue {\n data: AttachmentData;\n mediaCategory: AttachmentMediaCategory;\n onRemove?: () => void;\n variant: AttachmentVariant;\n}\n\nconst AttachmentContext = createContext<AttachmentContextValue | null>(null);\n\n// ============================================================================\n// Hooks\n// ============================================================================\n\nexport const useAttachmentsContext = () =>\n useContext(AttachmentsContext) ?? { variant: \"grid\" as const };\n\nexport const useAttachmentContext = () => {\n const ctx = useContext(AttachmentContext);\n if (!ctx) {\n throw new Error(\"Attachment components must be used within <Attachment>\");\n }\n return ctx;\n};\n\n// ============================================================================\n// Attachments - Container\n// ============================================================================\n\nexport type AttachmentsProps = HTMLAttributes<HTMLDivElement> & {\n variant?: AttachmentVariant;\n};\n\nexport const Attachments = ({\n variant = \"grid\",\n className,\n children,\n ...props\n}: AttachmentsProps) => {\n const contextValue = useMemo(() => ({ variant }), [variant]);\n\n return (\n <AttachmentsContext.Provider value={contextValue}>\n <div\n className={cn(\n \"flex items-start\",\n variant === \"list\" ? \"flex-col gap-2\" : \"flex-wrap gap-2\",\n variant === \"grid\" && \"ml-auto w-fit\",\n className\n )}\n {...props}\n >\n {children}\n </div>\n </AttachmentsContext.Provider>\n );\n};\n\n// ============================================================================\n// Attachment - Item\n// ============================================================================\n\nexport type AttachmentProps = HTMLAttributes<HTMLDivElement> & {\n data: AttachmentData;\n onRemove?: () => void;\n};\n\nexport const Attachment = ({\n data,\n onRemove,\n className,\n children,\n ...props\n}: AttachmentProps) => {\n const { variant } = useAttachmentsContext();\n const mediaCategory = getMediaCategory(data);\n\n const contextValue = useMemo<AttachmentContextValue>(\n () => ({ data, mediaCategory, onRemove, variant }),\n [data, mediaCategory, onRemove, variant]\n );\n\n return (\n <AttachmentContext.Provider value={contextValue}>\n <div\n className={cn(\n \"group relative\",\n variant === \"grid\" && \"size-24 overflow-hidden rounded-lg\",\n variant === \"inline\" && [\n \"flex h-8 cursor-pointer select-none items-center gap-1.5\",\n \"rounded-md border border-border px-1.5\",\n \"font-medium text-sm transition-all\",\n \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n ],\n variant === \"list\" && [\n \"flex w-full items-center gap-3 rounded-lg border p-3\",\n \"hover:bg-accent/50\",\n ],\n className\n )}\n {...props}\n >\n {children}\n </div>\n </AttachmentContext.Provider>\n );\n};\n\n// ============================================================================\n// AttachmentPreview - Media preview\n// ============================================================================\n\nexport type AttachmentPreviewProps = HTMLAttributes<HTMLDivElement> & {\n fallbackIcon?: ReactNode;\n};\n\nexport const AttachmentPreview = ({\n fallbackIcon,\n className,\n ...props\n}: AttachmentPreviewProps) => {\n const { data, mediaCategory, variant } = useAttachmentContext();\n\n const iconSize = variant === \"inline\" ? \"size-3\" : \"size-4\";\n\n const renderIcon = (Icon: typeof ImageIcon) => (\n <Icon className={cn(iconSize, \"text-muted-foreground\")} />\n );\n\n const renderContent = () => {\n if (mediaCategory === \"image\" && data.type === \"file\" && data.url) {\n return renderAttachmentImage(data.url, data.filename, variant === \"grid\");\n }\n\n if (mediaCategory === \"video\" && data.type === \"file\" && data.url) {\n return <video className=\"size-full object-cover\" muted src={data.url} />;\n }\n\n const Icon = mediaCategoryIcons[mediaCategory];\n return fallbackIcon ?? renderIcon(Icon);\n };\n\n return (\n <div\n className={cn(\n \"flex shrink-0 items-center justify-center overflow-hidden\",\n variant === \"grid\" && \"size-full bg-muted\",\n variant === \"inline\" && \"size-5 rounded bg-background\",\n variant === \"list\" && \"size-12 rounded bg-muted\",\n className\n )}\n {...props}\n >\n {renderContent()}\n </div>\n );\n};\n\n// ============================================================================\n// AttachmentInfo - Name and type display\n// ============================================================================\n\nexport type AttachmentInfoProps = HTMLAttributes<HTMLDivElement> & {\n showMediaType?: boolean;\n};\n\nexport const AttachmentInfo = ({\n showMediaType = false,\n className,\n ...props\n}: AttachmentInfoProps) => {\n const { data, variant } = useAttachmentContext();\n const label = getAttachmentLabel(data);\n\n if (variant === \"grid\") {\n return null;\n }\n\n return (\n <div className={cn(\"min-w-0 flex-1\", className)} {...props}>\n <span className=\"block truncate\">{label}</span>\n {showMediaType && data.mediaType && (\n <span className=\"block truncate text-muted-foreground text-xs\">\n {data.mediaType}\n </span>\n )}\n </div>\n );\n};\n\n// ============================================================================\n// AttachmentRemove - Remove button\n// ============================================================================\n\nexport type AttachmentRemoveProps = ComponentProps<typeof Button> & {\n label?: string;\n};\n\nexport const AttachmentRemove = ({\n label = \"Remove\",\n className,\n children,\n ...props\n}: AttachmentRemoveProps) => {\n const { onRemove, variant } = useAttachmentContext();\n\n const handleClick = useCallback(\n (e: React.MouseEvent) => {\n e.stopPropagation();\n onRemove?.();\n },\n [onRemove]\n );\n\n if (!onRemove) {\n return null;\n }\n\n return (\n <Button\n aria-label={label}\n className={cn(\n variant === \"grid\" && [\n \"absolute top-2 right-2 size-6 rounded-full p-0\",\n \"bg-background/80 backdrop-blur-sm\",\n \"opacity-0 transition-opacity group-hover:opacity-100\",\n \"hover:bg-background\",\n \"[&>svg]:size-3\",\n ],\n variant === \"inline\" && [\n \"size-5 rounded p-0\",\n \"opacity-0 transition-opacity group-hover:opacity-100\",\n \"[&>svg]:size-2.5\",\n ],\n variant === \"list\" && [\"size-8 shrink-0 rounded p-0\", \"[&>svg]:size-4\"],\n className\n )}\n onClick={handleClick}\n type=\"button\"\n variant=\"ghost\"\n {...props}\n >\n {children ?? <XIcon />}\n <span className=\"sr-only\">{label}</span>\n </Button>\n );\n};\n\n// ============================================================================\n// AttachmentHoverCard - Hover preview\n// ============================================================================\n\nexport type AttachmentHoverCardProps = ComponentProps<typeof HoverCard>;\n\nexport const AttachmentHoverCard = ({\n openDelay = 0,\n closeDelay = 0,\n ...props\n}: AttachmentHoverCardProps) => (\n <HoverCard closeDelay={closeDelay} openDelay={openDelay} {...props} />\n);\n\nexport type AttachmentHoverCardTriggerProps = ComponentProps<\n typeof HoverCardTrigger\n>;\n\nexport const AttachmentHoverCardTrigger = (\n props: AttachmentHoverCardTriggerProps\n) => <HoverCardTrigger {...props} />;\n\nexport type AttachmentHoverCardContentProps = ComponentProps<\n typeof HoverCardContent\n>;\n\nexport const AttachmentHoverCardContent = ({\n align = \"start\",\n className,\n ...props\n}: AttachmentHoverCardContentProps) => (\n <HoverCardContent\n align={align}\n className={cn(\"w-auto p-2\", className)}\n {...props}\n />\n);\n\n// ============================================================================\n// AttachmentEmpty - Empty state\n// ============================================================================\n\nexport type AttachmentEmptyProps = HTMLAttributes<HTMLDivElement>;\n\nexport const AttachmentEmpty = ({\n className,\n children,\n ...props\n}: AttachmentEmptyProps) => (\n <div\n className={cn(\n \"flex items-center justify-center p-4 text-muted-foreground text-sm\",\n className\n )}\n {...props}\n >\n {children ?? \"No attachments\"}\n </div>\n);\n"
}
},
"confirmation.tsx": {
"file": {
"contents": "\"use client\";\n\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport type { ToolUIPart } from \"ai\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { createContext, useContext, useMemo } from \"react\";\n\ntype ToolUIPartApproval =\n | {\n id: string;\n approved?: never;\n reason?: never;\n }\n | {\n id: string;\n approved: boolean;\n reason?: string;\n }\n | {\n id: string;\n approved: true;\n reason?: string;\n }\n | {\n id: string;\n approved: true;\n reason?: string;\n }\n | {\n id: string;\n approved: false;\n reason?: string;\n }\n | undefined;\n\ninterface ConfirmationContextValue {\n approval: ToolUIPartApproval;\n state: ToolUIPart[\"state\"];\n}\n\nconst ConfirmationContext = createContext<ConfirmationContextValue | null>(\n null\n);\n\nconst useConfirmation = () => {\n const context = useContext(ConfirmationContext);\n\n if (!context) {\n throw new Error(\"Confirmation components must be used within Confirmation\");\n }\n\n return context;\n};\n\nexport type ConfirmationProps = ComponentProps<typeof Alert> & {\n approval?: ToolUIPartApproval;\n state: ToolUIPart[\"state\"];\n};\n\nexport const Confirmation = ({\n className,\n approval,\n state,\n ...props\n}: ConfirmationProps) => {\n const contextValue = useMemo(() => ({ approval, state }), [approval, state]);\n\n if (!approval || state === \"input-streaming\" || state === \"input-available\") {\n return null;\n }\n\n return (\n <ConfirmationContext.Provider value={contextValue}>\n <Alert className={cn(\"flex flex-col gap-2\", className)} {...props} />\n </ConfirmationContext.Provider>\n );\n};\n\nexport type ConfirmationTitleProps = ComponentProps<typeof AlertDescription>;\n\nexport const ConfirmationTitle = ({\n className,\n ...props\n}: ConfirmationTitleProps) => (\n <AlertDescription className={cn(\"inline\", className)} {...props} />\n);\n\nexport interface ConfirmationRequestProps {\n children?: ReactNode;\n}\n\nexport const ConfirmationRequest = ({ children }: ConfirmationRequestProps) => {\n const { state } = useConfirmation();\n\n // Only show when approval is requested\n if (state !== \"approval-requested\") {\n return null;\n }\n\n return children;\n};\n\nexport interface ConfirmationAcceptedProps {\n children?: ReactNode;\n}\n\nexport const ConfirmationAccepted = ({\n children,\n}: ConfirmationAcceptedProps) => {\n const { approval, state } = useConfirmation();\n\n // Only show when approved and in response states\n if (\n !approval?.approved ||\n (state !== \"approval-responded\" &&\n state !== \"output-denied\" &&\n state !== \"output-available\")\n ) {\n return null;\n }\n\n return children;\n};\n\nexport interface ConfirmationRejectedProps {\n children?: ReactNode;\n}\n\nexport const ConfirmationRejected = ({\n children,\n}: ConfirmationRejectedProps) => {\n const { approval, state } = useConfirmation();\n\n // Only show when rejected and in response states\n if (\n approval?.approved !== false ||\n (state !== \"approval-responded\" &&\n state !== \"output-denied\" &&\n state !== \"output-available\")\n ) {\n return null;\n }\n\n return children;\n};\n\nexport type ConfirmationActionsProps = ComponentProps<\"div\">;\n\nexport const ConfirmationActions = ({\n className,\n ...props\n}: ConfirmationActionsProps) => {\n const { state } = useConfirmation();\n\n // Only show when approval is requested\n if (state !== \"approval-requested\") {\n return null;\n }\n\n return (\n <div\n className={cn(\"flex items-center justify-end gap-2 self-end\", className)}\n {...props}\n />\n );\n};\n\nexport type ConfirmationActionProps = ComponentProps<typeof Button>;\n\nexport const ConfirmationAction = (props: ConfirmationActionProps) => (\n <Button className=\"h-8 px-3 text-sm\" type=\"button\" {...props} />\n);\n"
}
},
"conversation.tsx": {
"file": {
"contents": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport type { UIMessage } from \"ai\";\nimport { ArrowDownIcon, DownloadIcon } from \"lucide-react\";\nimport type { ComponentProps } from \"react\";\nimport { useCallback } from \"react\";\nimport { StickToBottom, useStickToBottomContext } from \"use-stick-to-bottom\";\n\nexport type ConversationProps = ComponentProps<typeof StickToBottom>;\n\nexport const Conversation = ({ className, ...props }: ConversationProps) => (\n <StickToBottom\n className={cn(\"relative flex-1 overflow-y-hidden\", className)}\n initial=\"smooth\"\n resize=\"smooth\"\n role=\"log\"\n {...props}\n />\n);\n\nexport type ConversationContentProps = ComponentProps<\n typeof StickToBottom.Content\n>;\n\nexport const ConversationContent = ({\n className,\n ...props\n}: ConversationContentProps) => (\n <StickToBottom.Content\n className={cn(\"flex flex-col gap-8 p-4\", className)}\n {...props}\n />\n);\n\nexport type ConversationEmptyStateProps = ComponentProps<\"div\"> & {\n title?: string;\n description?: string;\n icon?: React.ReactNode;\n};\n\nexport const ConversationEmptyState = ({\n className,\n title = \"No messages yet\",\n description = \"Start a conversation to see messages here\",\n icon,\n children,\n ...props\n}: ConversationEmptyStateProps) => (\n <div\n className={cn(\n \"flex size-full flex-col items-center justify-center gap-3 p-8 text-center\",\n className\n )}\n {...props}\n >\n {children ?? (\n <>\n {icon && <div className=\"text-muted-foreground\">{icon}</div>}\n <div className=\"space-y-1\">\n <h3 className=\"font-medium text-sm\">{title}</h3>\n {description && (\n <p className=\"text-muted-foreground text-sm\">{description}</p>\n )}\n </div>\n </>\n )}\n </div>\n);\n\nexport type ConversationScrollButtonProps = ComponentProps<typeof Button>;\n\nexport const ConversationScrollButton = ({\n className,\n ...props\n}: ConversationScrollButtonProps) => {\n const { isAtBottom, scrollToBottom } = useStickToBottomContext();\n\n const handleScrollToBottom = useCallback(() => {\n scrollToBottom();\n }, [scrollToBottom]);\n\n return (\n !isAtBottom && (\n <Button\n className={cn(\n \"absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full dark:bg-background dark:hover:bg-muted\",\n className\n )}\n onClick={handleScrollToBottom}\n size=\"icon\"\n type=\"button\"\n variant=\"outline\"\n {...props}\n >\n <ArrowDownIcon className=\"size-4\" />\n </Button>\n )\n );\n};\n\nconst getMessageText = (message: UIMessage): string =>\n message.parts\n .filter((part) => part.type === \"text\")\n .map((part) => part.text)\n .join(\"\");\n\nexport type ConversationDownloadProps = Omit<\n ComponentProps<typeof Button>,\n \"onClick\"\n> & {\n messages: UIMessage[];\n filename?: string;\n formatMessage?: (message: UIMessage, index: number) => string;\n};\n\nconst defaultFormatMessage = (message: UIMessage): string => {\n const roleLabel =\n message.role.charAt(0).toUpperCase() + message.role.slice(1);\n return `**${roleLabel}:** ${getMessageText(message)}`;\n};\n\nexport const messagesToMarkdown = (\n messages: UIMessage[],\n formatMessage: (\n message: UIMessage,\n index: number\n ) => string = defaultFormatMessage\n): string => messages.map((msg, i) => formatMessage(msg, i)).join(\"\\n\\n\");\n\nexport const ConversationDownload = ({\n messages,\n filename = \"conversation.md\",\n formatMessage = defaultFormatMessage,\n className,\n children,\n ...props\n}: ConversationDownloadProps) => {\n const handleDownload = useCallback(() => {\n const markdown = messagesToMarkdown(messages, formatMessage);\n const blob = new Blob([markdown], { type: \"text/markdown\" });\n const url = URL.createObjectURL(blob);\n const link = document.createElement(\"a\");\n link.href = url;\n link.download = filename;\n document.body.append(link);\n link.click();\n link.remove();\n URL.revokeObjectURL(url);\n }, [messages, filename, formatMessage]);\n\n return (\n <Button\n className={cn(\n \"absolute top-4 right-4 rounded-full dark:bg-background dark:hover:bg-muted\",\n className\n )}\n onClick={handleDownload}\n size=\"icon\"\n type=\"button\"\n variant=\"outline\"\n {...props}\n >\n {children ?? <DownloadIcon className=\"size-4\" />}\n </Button>\n );\n};\n"
}
},
"prompt-input.tsx": {
"file": {
"contents": "\"use client\";\n\nimport {\n Command,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n CommandSeparator,\n} from \"@/components/ui/command\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport {\n HoverCard,\n HoverCardContent,\n HoverCardTrigger,\n} from \"@/components/ui/hover-card\";\nimport {\n InputGroup,\n InputGroupAddon,\n InputGroupButton,\n InputGroupTextarea,\n} from \"@/components/ui/input-group\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { Spinner } from \"@/components/ui/spinner\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport type { ChatStatus, FileUIPart, SourceDocumentUIPart } from \"ai\";\nimport {\n CornerDownLeftIcon,\n ImageIcon,\n Monitor,\n PlusIcon,\n SquareIcon,\n XIcon,\n} from \"lucide-react\";\nimport { nanoid } from \"nanoid\";\nimport type {\n ChangeEvent,\n ChangeEventHandler,\n ClipboardEventHandler,\n ComponentProps,\n FormEvent,\n FormEventHandler,\n HTMLAttributes,\n KeyboardEventHandler,\n PropsWithChildren,\n ReactNode,\n RefObject,\n} from \"react\";\nimport {\n Children,\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nconst convertBlobUrlToDataUrl = async (url: string): Promise<string | null> => {\n try {\n const response = await fetch(url);\n const blob = await response.blob();\n // FileReader uses callback-based API, wrapping in Promise is necessary\n // oxlint-disable-next-line eslint-plugin-promise(avoid-new)\n return new Promise((resolve) => {\n const reader = new FileReader();\n // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener)\n reader.onloadend = () => resolve(reader.result as string);\n // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener)\n reader.onerror = () => resolve(null);\n reader.readAsDataURL(blob);\n });\n } catch {\n return null;\n }\n};\n\nconst captureScreenshot = async (): Promise<File | null> => {\n if (\n typeof navigator === \"undefined\" ||\n !navigator.mediaDevices?.getDisplayMedia\n ) {\n return null;\n }\n\n let stream: MediaStream | null = null;\n const video = document.createElement(\"video\");\n video.muted = true;\n video.playsInline = true;\n\n try {\n stream = await navigator.mediaDevices.getDisplayMedia({\n audio: false,\n video: true,\n });\n\n video.srcObject = stream;\n\n // Video element uses callback-based API, wrapping in Promise is necessary\n // oxlint-disable-next-line eslint-plugin-promise(avoid-new)\n await new Promise<void>((resolve, reject) => {\n // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener)\n video.onloadedmetadata = () => resolve();\n // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener)\n video.onerror = () => reject(new Error(\"Failed to load screen stream\"));\n });\n\n await video.play();\n\n const width = video.videoWidth;\n const height = video.videoHeight;\n if (!width || !height) {\n return null;\n }\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = width;\n canvas.height = height;\n const context = canvas.getContext(\"2d\");\n if (!context) {\n return null;\n }\n\n context.drawImage(video, 0, 0, width, height);\n // canvas.toBlob uses callback-based API, wrapping in Promise is necessary\n // oxlint-disable-next-line eslint-plugin-promise(avoid-new)\n const blob = await new Promise<Blob | null>((resolve) => {\n canvas.toBlob(resolve, \"image/png\");\n });\n if (!blob) {\n return null;\n }\n\n const timestamp = new Date()\n .toISOString()\n .replaceAll(/[:.]/g, \"-\")\n .replace(\"T\", \"_\")\n .replace(\"Z\", \"\");\n\n return new File([blob], `screenshot-${timestamp}.png`, {\n lastModified: Date.now(),\n type: \"image/png\",\n });\n } finally {\n if (stream) {\n for (const track of stream.getTracks()) {\n track.stop();\n }\n }\n video.pause();\n video.srcObject = null;\n }\n};\n\n// ============================================================================\n// Provider Context & Types\n// ============================================================================\n\nexport interface AttachmentsContext {\n files: (FileUIPart & { id: string })[];\n add: (files: File[] | FileList) => void;\n remove: (id: string) => void;\n clear: () => void;\n openFileDialog: () => void;\n fileInputRef: RefObject<HTMLInputElement | null>;\n}\n\nexport interface TextInputContext {\n value: string;\n setInput: (v: string) => void;\n clear: () => void;\n}\n\nexport interface PromptInputControllerProps {\n textInput: TextInputContext;\n attachments: AttachmentsContext;\n /** INTERNAL: Allows PromptInput to register its file textInput + \"open\" callback */\n __registerFileInput: (\n ref: RefObject<HTMLInputElement | null>,\n open: () => void\n ) => void;\n}\n\nconst PromptInputController = createContext<PromptInputControllerProps | null>(\n null\n);\nconst ProviderAttachmentsContext = createContext<AttachmentsContext | null>(\n null\n);\n\nexport const usePromptInputController = () => {\n const ctx = useContext(PromptInputController);\n if (!ctx) {\n throw new Error(\n \"Wrap your component inside <PromptInputProvider> to use usePromptInputController().\"\n );\n }\n return ctx;\n};\n\n// Optional variants (do NOT throw). Useful for dual-mode components.\nconst useOptionalPromptInputController = () =>\n useContext(PromptInputController);\n\nexport const useProviderAttachments = () => {\n const ctx = useContext(ProviderAttachmentsContext);\n if (!ctx) {\n throw new Error(\n \"Wrap your component inside <PromptInputProvider> to use useProviderAttachments().\"\n );\n }\n return ctx;\n};\n\nconst useOptionalProviderAttachments = () =>\n useContext(ProviderAttachmentsContext);\n\nexport type PromptInputProviderProps = PropsWithChildren<{\n initialInput?: string;\n}>;\n\n/**\n * Optional global provider that lifts PromptInput state outside of PromptInput.\n * If you don't use it, PromptInput stays fully self-managed.\n */\nexport const PromptInputProvider = ({\n initialInput: initialTextInput = \"\",\n children,\n}: PromptInputProviderProps) => {\n // ----- textInput state\n const [textInput, setTextInput] = useState(initialTextInput);\n const clearInput = useCallback(() => setTextInput(\"\"), []);\n\n // ----- attachments state (global when wrapped)\n const [attachmentFiles, setAttachmentFiles] = useState<\n (FileUIPart & { id: string })[]\n >([]);\n const fileInputRef = useRef<HTMLInputElement | null>(null);\n // oxlint-disable-next-line eslint(no-empty-function)\n const openRef = useRef<() => void>(() => {});\n\n const add = useCallback((files: File[] | FileList) => {\n const incoming = [...files];\n if (incoming.length === 0) {\n return;\n }\n\n setAttachmentFiles((prev) => [\n ...prev,\n ...incoming.map((file) => ({\n filename: file.name,\n id: nanoid(),\n mediaType: file.type,\n type: \"file\" as const,\n url: URL.createObjectURL(file),\n })),\n ]);\n }, []);\n\n const remove = useCallback((id: string) => {\n setAttachmentFiles((prev) => {\n const found = prev.find((f) => f.id === id);\n if (found?.url) {\n URL.revokeObjectURL(found.url);\n }\n return prev.filter((f) => f.id !== id);\n });\n }, []);\n\n const clear = useCallback(() => {\n setAttachmentFiles((prev) => {\n for (const f of prev) {\n if (f.url) {\n URL.revokeObjectURL(f.url);\n }\n }\n return [];\n });\n }, []);\n\n // Keep a ref to attachments for cleanup on unmount (avoids stale closure)\n const attachmentsRef = useRef(attachmentFiles);\n\n useEffect(() => {\n attachmentsRef.current = attachmentFiles;\n }, [attachmentFiles]);\n\n // Cleanup blob URLs on unmount to prevent memory leaks\n useEffect(\n () => () => {\n for (const f of attachmentsRef.current) {\n if (f.url) {\n URL.revokeObjectURL(f.url);\n }\n }\n },\n []\n );\n\n const openFileDialog = useCallback(() => {\n openRef.current?.();\n }, []);\n\n const attachments = useMemo<AttachmentsContext>(\n () => ({\n add,\n clear,\n fileInputRef,\n files: attachmentFiles,\n openFileDialog,\n remove,\n }),\n [attachmentFiles, add, remove, clear, openFileDialog]\n );\n\n const __registerFileInput = useCallback(\n (ref: RefObject<HTMLInputElement | null>, open: () => void) => {\n fileInputRef.current = ref.current;\n openRef.current = open;\n },\n []\n );\n\n const controller = useMemo<PromptInputControllerProps>(\n () => ({\n __registerFileInput,\n attachments,\n textInput: {\n clear: clearInput,\n setInput: setTextInput,\n value: textInput,\n },\n }),\n [textInput, clearInput, attachments, __registerFileInput]\n );\n\n return (\n <PromptInputController.Provider value={controller}>\n <ProviderAttachmentsContext.Provider value={attachments}>\n {children}\n </ProviderAttachmentsContext.Provider>\n </PromptInputController.Provider>\n );\n};\n\n// ============================================================================\n// Component Context & Hooks\n// ============================================================================\n\nconst LocalAttachmentsContext = createContext<AttachmentsContext | null>(null);\n\nexport const usePromptInputAttachments = () => {\n // Prefer local context (inside PromptInput) as it has validation, fall back to provider\n const provider = useOptionalProviderAttachments();\n const local = useContext(LocalAttachmentsContext);\n const context = local ?? provider;\n if (!context) {\n throw new Error(\n \"usePromptInputAttachments must be used within a PromptInput or PromptInputProvider\"\n );\n }\n return context;\n};\n\n// ============================================================================\n// Referenced Sources (Local to PromptInput)\n// ============================================================================\n\nexport interface ReferencedSourcesContext {\n sources: (SourceDocumentUIPart & { id: string })[];\n add: (sources: SourceDocumentUIPart[] | SourceDocumentUIPart) => void;\n remove: (id: string) => void;\n clear: () => void;\n}\n\nexport const LocalReferencedSourcesContext =\n createContext<ReferencedSourcesContext | null>(null);\n\nexport const usePromptInputReferencedSources = () => {\n const ctx = useContext(LocalReferencedSourcesContext);\n if (!ctx) {\n throw new Error(\n \"usePromptInputReferencedSources must be used within a LocalReferencedSourcesContext.Provider\"\n );\n }\n return ctx;\n};\n\nexport type PromptInputActionAddAttachmentsProps = ComponentProps<\n typeof DropdownMenuItem\n> & {\n label?: string;\n};\n\nexport const PromptInputActionAddAttachments = ({\n label = \"Add photos or files\",\n ...props\n}: PromptInputActionAddAttachmentsProps) => {\n const attachments = usePromptInputAttachments();\n\n const handleSelect = useCallback(\n (e: Event) => {\n e.preventDefault();\n attachments.openFileDialog();\n },\n [attachments]\n );\n\n return (\n <DropdownMenuItem {...props} onSelect={handleSelect}>\n <ImageIcon className=\"mr-2 size-4\" /> {label}\n </DropdownMenuItem>\n );\n};\n\nexport type PromptInputActionAddScreenshotProps = ComponentProps<\n typeof DropdownMenuItem\n> & {\n label?: string;\n};\n\nexport const PromptInputActionAddScreenshot = ({\n label = \"Take screenshot\",\n onSelect,\n ...props\n}: PromptInputActionAddScreenshotProps) => {\n const attachments = usePromptInputAttachments();\n\n const handleSelect = useCallback(\n async (event: Event) => {\n onSelect?.(event);\n if (event.defaultPrevented) {\n return;\n }\n\n try {\n const screenshot = await captureScreenshot();\n if (screenshot) {\n attachments.add([screenshot]);\n }\n } catch (error) {\n if (\n error instanceof DOMException &&\n (error.name === \"NotAllowedError\" || error.name === \"AbortError\")\n ) {\n return;\n }\n throw error;\n }\n },\n [onSelect, attachments]\n );\n\n return (\n <DropdownMenuItem {...props} onSelect={handleSelect}>\n <Monitor className=\"mr-2 size-4\" />\n {label}\n </DropdownMenuItem>\n );\n};\n\nexport interface PromptInputMessage {\n text: string;\n files: FileUIPart[];\n}\n\nexport type PromptInputProps = Omit<\n HTMLAttributes<HTMLFormElement>,\n \"onSubmit\" | \"onError\"\n> & {\n // e.g., \"image/*\" or leave undefined for any\n accept?: string;\n multiple?: boolean;\n // When true, accepts drops anywhere on document. Default false (opt-in).\n globalDrop?: boolean;\n // Render a hidden input with given name and keep it in sync for native form posts. Default false.\n syncHiddenInput?: boolean;\n // Minimal constraints\n maxFiles?: number;\n // bytes\n maxFileSize?: number;\n onError?: (err: {\n code: \"max_files\" | \"max_file_size\" | \"accept\";\n message: string;\n }) => void;\n onSubmit: (\n message: PromptInputMessage,\n event: FormEvent<HTMLFormElement>\n ) => void | Promise<void>;\n};\n\nexport const PromptInput = ({\n className,\n accept,\n multiple,\n globalDrop,\n syncHiddenInput,\n maxFiles,\n maxFileSize,\n onError,\n onSubmit,\n children,\n ...props\n}: PromptInputProps) => {\n // Try to use a provider controller if present\n const controller = useOptionalPromptInputController();\n const usingProvider = !!controller;\n\n // Refs\n const inputRef = useRef<HTMLInputElement | null>(null);\n const formRef = useRef<HTMLFormElement | null>(null);\n\n // ----- Local attachments (only used when no provider)\n const [items, setItems] = useState<(FileUIPart & { id: string })[]>([]);\n const files = usingProvider ? controller.attachments.files : items;\n\n // ----- Local referenced sources (always local to PromptInput)\n const [referencedSources, setReferencedSources] = useState<\n (SourceDocumentUIPart & { id: string })[]\n >([]);\n\n // Keep a ref to files for cleanup on unmount (avoids stale closure)\n const filesRef = useRef(files);\n\n useEffect(() => {\n filesRef.current = files;\n }, [files]);\n\n const openFileDialogLocal = useCallback(() => {\n inputRef.current?.click();\n }, []);\n\n const matchesAccept = useCallback(\n (f: File) => {\n if (!accept || accept.trim() === \"\") {\n return true;\n }\n\n const patterns = accept\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean);\n\n return patterns.some((pattern) => {\n if (pattern.endsWith(\"/*\")) {\n // e.g: image/* -> image/\n const prefix = pattern.slice(0, -1);\n return f.type.startsWith(prefix);\n }\n return f.type === pattern;\n });\n },\n [accept]\n );\n\n const addLocal = useCallback(\n (fileList: File[] | FileList) => {\n const incoming = [...fileList];\n const accepted = incoming.filter((f) => matchesAccept(f));\n if (incoming.length && accepted.length === 0) {\n onError?.({\n code: \"accept\",\n message: \"No files match the accepted types.\",\n });\n return;\n }\n const withinSize = (f: File) =>\n maxFileSize ? f.size <= maxFileSize : true;\n const sized = accepted.filter(withinSize);\n if (accepted.length > 0 && sized.length === 0) {\n onError?.({\n code: \"max_file_size\",\n message: \"All files exceed the maximum size.\",\n });\n return;\n }\n\n setItems((prev) => {\n const capacity =\n typeof maxFiles === \"number\"\n ? Math.max(0, maxFiles - prev.length)\n : undefined;\n const capped =\n typeof capacity === \"number\" ? sized.slice(0, capacity) : sized;\n if (typeof capacity === \"number\" && sized.length > capacity) {\n onError?.({\n code: \"max_files\",\n message: \"Too many files. Some were not added.\",\n });\n }\n const next: (FileUIPart & { id: string })[] = [];\n for (const file of capped) {\n next.push({\n filename: file.name,\n id: nanoid(),\n mediaType: file.type,\n type: \"file\",\n url: URL.createObjectURL(file),\n });\n }\n return [...prev, ...next];\n });\n },\n [matchesAccept, maxFiles, maxFileSize, onError]\n );\n\n const removeLocal = useCallback(\n (id: string) =>\n setItems((prev) => {\n const found = prev.find((file) => file.id === id);\n if (found?.url) {\n URL.revokeObjectURL(found.url);\n }\n return prev.filter((file) => file.id !== id);\n }),\n []\n );\n\n // Wrapper that validates files before calling provider's add\n const addWithProviderValidation = useCallback(\n (fileList: File[] | FileList) => {\n const incoming = [...fileList];\n const accepted = incoming.filter((f) => matchesAccept(f));\n if (incoming.length && accepted.length === 0) {\n onError?.({\n code: \"accept\",\n message: \"No files match the accepted types.\",\n });\n return;\n }\n const withinSize = (f: File) =>\n maxFileSize ? f.size <= maxFileSize : true;\n const sized = accepted.filter(withinSize);\n if (accepted.length > 0 && sized.length === 0) {\n onError?.({\n code: \"max_file_size\",\n message: \"All files exceed the maximum size.\",\n });\n return;\n }\n\n const currentCount = files.length;\n const capacity =\n typeof maxFiles === \"number\"\n ? Math.max(0, maxFiles - currentCount)\n : undefined;\n const capped =\n typeof capacity === \"number\" ? sized.slice(0, capacity) : sized;\n if (typeof capacity === \"number\" && sized.length > capacity) {\n onError?.({\n code: \"max_files\",\n message: \"Too many files. Some were not added.\",\n });\n }\n\n if (capped.length > 0) {\n controller?.attachments.add(capped);\n }\n },\n [matchesAccept, maxFileSize, maxFiles, onError, files.length, controller]\n );\n\n const clearAttachments = useCallback(\n () =>\n usingProvider\n ? controller?.attachments.clear()\n : setItems((prev) => {\n for (const file of prev) {\n if (file.url) {\n URL.revokeObjectURL(file.url);\n }\n }\n return [];\n }),\n [usingProvider, controller]\n );\n\n const clearReferencedSources = useCallback(\n () => setReferencedSources([]),\n []\n );\n\n const add = usingProvider ? addWithProviderValidation : addLocal;\n const remove = usingProvider ? controller.attachments.remove : removeLocal;\n const openFileDialog = usingProvider\n ? controller.attachments.openFileDialog\n : openFileDialogLocal;\n\n const clear = useCallback(() => {\n clearAttachments();\n clearReferencedSources();\n }, [clearAttachments, clearReferencedSources]);\n\n // Let provider know about our hidden file input so external menus can call openFileDialog()\n useEffect(() => {\n if (!usingProvider) {\n return;\n }\n controller.__registerFileInput(inputRef, () => inputRef.current?.click());\n }, [usingProvider, controller]);\n\n // Note: File input cannot be programmatically set for security reasons\n // The syncHiddenInput prop is no longer functional\n useEffect(() => {\n if (syncHiddenInput && inputRef.current && files.length === 0) {\n inputRef.current.value = \"\";\n }\n }, [files, syncHiddenInput]);\n\n // Attach drop handlers on nearest form and document (opt-in)\n useEffect(() => {\n const form = formRef.current;\n if (!form) {\n return;\n }\n if (globalDrop) {\n // when global drop is on, let the document-level handler own drops\n return;\n }\n\n const onDragOver = (e: DragEvent) => {\n if (e.dataTransfer?.types?.includes(\"Files\")) {\n e.preventDefault();\n }\n };\n const onDrop = (e: DragEvent) => {\n if (e.dataTransfer?.types?.includes(\"Files\")) {\n e.preventDefault();\n }\n if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {\n add(e.dataTransfer.files);\n }\n };\n form.addEventListener(\"dragover\", onDragOver);\n form.addEventListener(\"drop\", onDrop);\n return () => {\n form.removeEventListener(\"dragover\", onDragOver);\n form.removeEventListener(\"drop\", onDrop);\n };\n }, [add, globalDrop]);\n\n useEffect(() => {\n if (!globalDrop) {\n return;\n }\n\n const onDragOver = (e: DragEvent) => {\n if (e.dataTransfer?.types?.includes(\"Files\")) {\n e.preventDefault();\n }\n };\n const onDrop = (e: DragEvent) => {\n if (e.dataTransfer?.types?.includes(\"Files\")) {\n e.preventDefault();\n }\n if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {\n add(e.dataTransfer.files);\n }\n };\n document.addEventListener(\"dragover\", onDragOver);\n document.addEventListener(\"drop\", onDrop);\n return () => {\n document.removeEventListener(\"dragover\", onDragOver);\n document.removeEventListener(\"drop\", onDrop);\n };\n }, [add, globalDrop]);\n\n useEffect(\n () => () => {\n if (!usingProvider) {\n for (const f of filesRef.current) {\n if (f.url) {\n URL.revokeObjectURL(f.url);\n }\n }\n }\n },\n [usingProvider]\n );\n\n const handleChange: ChangeEventHandler<HTMLInputElement> = useCallback(\n (event) => {\n if (event.currentTarget.files) {\n add(event.currentTarget.files);\n }\n // Reset input value to allow selecting files that were previously removed\n event.currentTarget.value = \"\";\n },\n [add]\n );\n\n const attachmentsCtx = useMemo<AttachmentsContext>(\n () => ({\n add,\n clear: clearAttachments,\n fileInputRef: inputRef,\n files: files.map((item) => ({ ...item, id: item.id })),\n openFileDialog,\n remove,\n }),\n [files, add, remove, clearAttachments, openFileDialog]\n );\n\n const refsCtx = useMemo<ReferencedSourcesContext>(\n () => ({\n add: (incoming: SourceDocumentUIPart[] | SourceDocumentUIPart) => {\n const array = Array.isArray(incoming) ? incoming : [incoming];\n setReferencedSources((prev) => [\n ...prev,\n ...array.map((s) => ({ ...s, id: nanoid() })),\n ]);\n },\n clear: clearReferencedSources,\n remove: (id: string) => {\n setReferencedSources((prev) => prev.filter((s) => s.id !== id));\n },\n sources: referencedSources,\n }),\n [referencedSources, clearReferencedSources]\n );\n\n const handleSubmit: FormEventHandler<HTMLFormElement> = useCallback(\n async (event) => {\n event.preventDefault();\n\n const form = event.currentTarget;\n const text = usingProvider\n ? controller.textInput.value\n : (() => {\n const formData = new FormData(form);\n return (formData.get(\"message\") as string) || \"\";\n })();\n\n // Reset form immediately after capturing text to avoid race condition\n // where user input during async blob conversion would be lost\n if (!usingProvider) {\n form.reset();\n }\n\n try {\n // Convert blob URLs to data URLs asynchronously\n const convertedFiles: FileUIPart[] = await Promise.all(\n files.map(async ({ id: _id, ...item }) => {\n if (item.url?.startsWith(\"blob:\")) {\n const dataUrl = await convertBlobUrlToDataUrl(item.url);\n // If conversion failed, keep the original blob URL\n return {\n ...item,\n url: dataUrl ?? item.url,\n };\n }\n return item;\n })\n );\n\n const result = onSubmit({ files: convertedFiles, text }, event);\n\n // Handle both sync and async onSubmit\n if (result instanceof Promise) {\n try {\n await result;\n clear();\n if (usingProvider) {\n controller.textInput.clear();\n }\n } catch {\n // Don't clear on error - user may want to retry\n }\n } else {\n // Sync function completed without throwing, clear inputs\n clear();\n if (usingProvider) {\n controller.textInput.clear();\n }\n }\n } catch {\n // Don't clear on error - user may want to retry\n }\n },\n [usingProvider, controller, files, onSubmit, clear]\n );\n\n // Render with or without local provider\n const inner = (\n <>\n <input\n accept={accept}\n aria-label=\"Upload files\"\n className=\"hidden\"\n multiple={multiple}\n onChange={handleChange}\n ref={inputRef}\n title=\"Upload files\"\n type=\"file\"\n />\n <form\n className={cn(\"w-full\", className)}\n onSubmit={handleSubmit}\n ref={formRef}\n {...props}\n >\n <InputGroup className=\"overflow-hidden\">{children}</InputGroup>\n </form>\n </>\n );\n\n const withReferencedSources = (\n <LocalReferencedSourcesContext.Provider value={refsCtx}>\n {inner}\n </LocalReferencedSourcesContext.Provider>\n );\n\n // Always provide LocalAttachmentsContext so children get validated add function\n return (\n <LocalAttachmentsContext.Provider value={attachmentsCtx}>\n {withReferencedSources}\n </LocalAttachmentsContext.Provider>\n );\n};\n\nexport type PromptInputBodyProps = HTMLAttributes<HTMLDivElement>;\n\nexport const PromptInputBody = ({\n className,\n ...props\n}: PromptInputBodyProps) => (\n <div className={cn(\"contents\", className)} {...props} />\n);\n\nexport type PromptInputTextareaProps = ComponentProps<\n typeof InputGroupTextarea\n>;\n\nexport const PromptInputTextarea = ({\n onChange,\n onKeyDown,\n className,\n placeholder = \"What would you like to know?\",\n ...props\n}: PromptInputTextareaProps) => {\n const controller = useOptionalPromptInputController();\n const attachments = usePromptInputAttachments();\n const [isComposing, setIsComposing] = useState(false);\n\n const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = useCallback(\n (e) => {\n // Call the external onKeyDown handler first\n onKeyDown?.(e);\n\n // If the external handler prevented default, don't run internal logic\n if (e.defaultPrevented) {\n return;\n }\n\n if (e.key === \"Enter\") {\n if (isComposing || e.nativeEvent.isComposing) {\n return;\n }\n if (e.shiftKey) {\n return;\n }\n e.preventDefault();\n\n // Check if the submit button is disabled before submitting\n const { form } = e.currentTarget;\n const submitButton = form?.querySelector(\n 'button[type=\"submit\"]'\n ) as HTMLButtonElement | null;\n if (submitButton?.disabled) {\n return;\n }\n\n form?.requestSubmit();\n }\n\n // Remove last attachment when Backspace is pressed and textarea is empty\n if (\n e.key === \"Backspace\" &&\n e.currentTarget.value === \"\" &&\n attachments.files.length > 0\n ) {\n e.preventDefault();\n const lastAttachment = attachments.files.at(-1);\n if (lastAttachment) {\n attachments.remove(lastAttachment.id);\n }\n }\n },\n [onKeyDown, isComposing, attachments]\n );\n\n const handlePaste: ClipboardEventHandler<HTMLTextAreaElement> = useCallback(\n (event) => {\n const items = event.clipboardData?.items;\n\n if (!items) {\n return;\n }\n\n const files: File[] = [];\n\n for (const item of items) {\n if (item.kind === \"file\") {\n const file = item.getAsFile();\n if (file) {\n files.push(file);\n }\n }\n }\n\n if (files.length > 0) {\n event.preventDefault();\n attachments.add(files);\n }\n },\n [attachments]\n );\n\n const handleCompositionEnd = useCallback(() => setIsComposing(false), []);\n const handleCompositionStart = useCallback(() => setIsComposing(true), []);\n\n const controlledProps = controller\n ? {\n onChange: (e: ChangeEvent<HTMLTextAreaElement>) => {\n controller.textInput.setInput(e.currentTarget.value);\n onChange?.(e);\n },\n value: controller.textInput.value,\n }\n : {\n onChange,\n };\n\n return (\n <InputGroupTextarea\n className={cn(\"field-sizing-content max-h-48 min-h-16\", className)}\n name=\"message\"\n onCompositionEnd={handleCompositionEnd}\n onCompositionStart={handleCompositionStart}\n onKeyDown={handleKeyDown}\n onPaste={handlePaste}\n placeholder={placeholder}\n {...props}\n {...controlledProps}\n />\n );\n};\n\nexport type PromptInputHeaderProps = Omit<\n ComponentProps<typeof InputGroupAddon>,\n \"align\"\n>;\n\nexport const PromptInputHeader = ({\n className,\n ...props\n}: PromptInputHeaderProps) => (\n <InputGroupAddon\n align=\"block-end\"\n className={cn(\"order-first flex-wrap gap-1\", className)}\n {...props}\n />\n);\n\nexport type PromptInputFooterProps = Omit<\n ComponentProps<typeof InputGroupAddon>,\n \"align\"\n>;\n\nexport const PromptInputFooter = ({\n className,\n ...props\n}: PromptInputFooterProps) => (\n <InputGroupAddon\n align=\"block-end\"\n className={cn(\"justify-between gap-1\", className)}\n {...props}\n />\n);\n\nexport type PromptInputToolsProps = HTMLAttributes<HTMLDivElement>;\n\nexport const PromptInputTools = ({\n className,\n ...props\n}: PromptInputToolsProps) => (\n <div\n className={cn(\"flex min-w-0 items-center gap-1\", className)}\n {...props}\n />\n);\n\nexport type PromptInputButtonTooltip =\n | string\n | {\n content: ReactNode;\n shortcut?: string;\n side?: ComponentProps<typeof TooltipContent>[\"side\"];\n };\n\nexport type PromptInputButtonProps = ComponentProps<typeof InputGroupButton> & {\n tooltip?: PromptInputButtonTooltip;\n};\n\nexport const PromptInputButton = ({\n variant = \"ghost\",\n className,\n size,\n tooltip,\n ...props\n}: PromptInputButtonProps) => {\n const newSize =\n size ?? (Children.count(props.children) > 1 ? \"sm\" : \"icon-sm\");\n\n const button = (\n <InputGroupButton\n className={cn(className)}\n size={newSize}\n type=\"button\"\n variant={variant}\n {...props}\n />\n );\n\n if (!tooltip) {\n return button;\n }\n\n const tooltipContent =\n typeof tooltip === \"string\" ? tooltip : tooltip.content;\n const shortcut = typeof tooltip === \"string\" ? undefined : tooltip.shortcut;\n const side = typeof tooltip === \"string\" ? \"top\" : (tooltip.side ?? \"top\");\n\n return (\n <Tooltip>\n <TooltipTrigger asChild>{button}</TooltipTrigger>\n <TooltipContent side={side}>\n {tooltipContent}\n {shortcut && (\n <span className=\"ml-2 text-muted-foreground\">{shortcut}</span>\n )}\n </TooltipContent>\n </Tooltip>\n );\n};\n\nexport type PromptInputActionMenuProps = ComponentProps<typeof DropdownMenu>;\nexport const PromptInputActionMenu = (props: PromptInputActionMenuProps) => (\n <DropdownMenu {...props} />\n);\n\nexport type PromptInputActionMenuTriggerProps = PromptInputButtonProps;\n\nexport const PromptInputActionMenuTrigger = ({\n className,\n children,\n ...props\n}: PromptInputActionMenuTriggerProps) => (\n <DropdownMenuTrigger asChild>\n <PromptInputButton className={className} {...props}>\n {children ?? <PlusIcon className=\"size-4\" />}\n </PromptInputButton>\n </DropdownMenuTrigger>\n);\n\nexport type PromptInputActionMenuContentProps = ComponentProps<\n typeof DropdownMenuContent\n>;\nexport const PromptInputActionMenuContent = ({\n className,\n ...props\n}: PromptInputActionMenuContentProps) => (\n <DropdownMenuContent align=\"start\" className={cn(className)} {...props} />\n);\n\nexport type PromptInputActionMenuItemProps = ComponentProps<\n typeof DropdownMenuItem\n>;\nexport const PromptInputActionMenuItem = ({\n className,\n ...props\n}: PromptInputActionMenuItemProps) => (\n <DropdownMenuItem className={cn(className)} {...props} />\n);\n\n// Note: Actions that perform side-effects (like opening a file dialog)\n// are provided in opt-in modules (e.g., prompt-input-attachments).\n\nexport type PromptInputSubmitProps = ComponentProps<typeof InputGroupButton> & {\n status?: ChatStatus;\n onStop?: () => void;\n};\n\nexport const PromptInputSubmit = ({\n className,\n variant = \"default\",\n size = \"icon-sm\",\n status,\n onStop,\n onClick,\n children,\n ...props\n}: PromptInputSubmitProps) => {\n const isGenerating = status === \"submitted\" || status === \"streaming\";\n\n let Icon = <CornerDownLeftIcon className=\"size-4\" />;\n\n if (status === \"submitted\") {\n Icon = <Spinner />;\n } else if (status === \"streaming\") {\n Icon = <SquareIcon className=\"size-4\" />;\n } else if (status === \"error\") {\n Icon = <XIcon className=\"size-4\" />;\n }\n\n const handleClick = useCallback(\n (e: React.MouseEvent<HTMLButtonElement>) => {\n if (isGenerating && onStop) {\n e.preventDefault();\n onStop();\n return;\n }\n onClick?.(e);\n },\n [isGenerating, onStop, onClick]\n );\n\n return (\n <InputGroupButton\n aria-label={isGenerating ? \"Stop\" : \"Submit\"}\n className={cn(className)}\n onClick={handleClick}\n size={size}\n type={isGenerating && onStop ? \"button\" : \"submit\"}\n variant={variant}\n {...props}\n >\n {children ?? Icon}\n </InputGroupButton>\n );\n};\n\nexport type PromptInputSelectProps = ComponentProps<typeof Select>;\n\nexport const PromptInputSelect = (props: PromptInputSelectProps) => (\n <Select {...props} />\n);\n\nexport type PromptInputSelectTriggerProps = ComponentProps<\n typeof SelectTrigger\n>;\n\nexport const PromptInputSelectTrigger = ({\n className,\n ...props\n}: PromptInputSelectTriggerProps) => (\n <SelectTrigger\n className={cn(\n \"border-none bg-transparent font-medium text-muted-foreground shadow-none transition-colors\",\n \"hover:bg-accent hover:text-foreground aria-expanded:bg-accent aria-expanded:text-foreground\",\n className\n )}\n {...props}\n />\n);\n\nexport type PromptInputSelectContentProps = ComponentProps<\n typeof SelectContent\n>;\n\nexport const PromptInputSelectContent = ({\n className,\n ...props\n}: PromptInputSelectContentProps) => (\n <SelectContent className={cn(className)} {...props} />\n);\n\nexport type PromptInputSelectItemProps = ComponentProps<typeof SelectItem>;\n\nexport const PromptInputSelectItem = ({\n className,\n ...props\n}: PromptInputSelectItemProps) => (\n <SelectItem className={cn(className)} {...props} />\n);\n\nexport type PromptInputSelectValueProps = ComponentProps<typeof SelectValue>;\n\nexport const PromptInputSelectValue = ({\n className,\n ...props\n}: PromptInputSelectValueProps) => (\n <SelectValue className={cn(className)} {...props} />\n);\n\nexport type PromptInputHoverCardProps = ComponentProps<typeof HoverCard>;\n\nexport const PromptInputHoverCard = ({\n openDelay = 0,\n closeDelay = 0,\n ...props\n}: PromptInputHoverCardProps) => (\n <HoverCard closeDelay={closeDelay} openDelay={openDelay} {...props} />\n);\n\nexport type PromptInputHoverCardTriggerProps = ComponentProps<\n typeof HoverCardTrigger\n>;\n\nexport const PromptInputHoverCardTrigger = (\n props: PromptInputHoverCardTriggerProps\n) => <HoverCardTrigger {...props} />;\n\nexport type PromptInputHoverCardContentProps = ComponentProps<\n typeof HoverCardContent\n>;\n\nexport const PromptInputHoverCardContent = ({\n align = \"start\",\n ...props\n}: PromptInputHoverCardContentProps) => (\n <HoverCardContent align={align} {...props} />\n);\n\nexport type PromptInputTabsListProps = HTMLAttributes<HTMLDivElement>;\n\nexport const PromptInputTabsList = ({\n className,\n ...props\n}: PromptInputTabsListProps) => <div className={cn(className)} {...props} />;\n\nexport type PromptInputTabProps = HTMLAttributes<HTMLDivElement>;\n\nexport const PromptInputTab = ({\n className,\n ...props\n}: PromptInputTabProps) => <div className={cn(className)} {...props} />;\n\nexport type PromptInputTabLabelProps = HTMLAttributes<HTMLHeadingElement>;\n\nexport const PromptInputTabLabel = ({\n className,\n ...props\n}: PromptInputTabLabelProps) => (\n // Content provided via children in props\n // oxlint-disable-next-line eslint-plugin-jsx-a11y(heading-has-content)\n <h3\n className={cn(\n \"mb-2 px-3 font-medium text-muted-foreground text-xs\",\n className\n )}\n {...props}\n />\n);\n\nexport type PromptInputTabBodyProps = HTMLAttributes<HTMLDivElement>;\n\nexport const PromptInputTabBody = ({\n className,\n ...props\n}: PromptInputTabBodyProps) => (\n <div className={cn(\"space-y-1\", className)} {...props} />\n);\n\nexport type PromptInputTabItemProps = HTMLAttributes<HTMLDivElement>;\n\nexport const PromptInputTabItem = ({\n className,\n ...props\n}: PromptInputTabItemProps) => (\n <div\n className={cn(\n \"flex items-center gap-2 px-3 py-2 text-xs hover:bg-accent\",\n className\n )}\n {...props}\n />\n);\n\nexport type PromptInputCommandProps = ComponentProps<typeof Command>;\n\nexport const PromptInputCommand = ({\n className,\n ...props\n}: PromptInputCommandProps) => <Command className={cn(className)} {...props} />;\n\nexport type PromptInputCommandInputProps = ComponentProps<typeof CommandInput>;\n\nexport const PromptInputCommandInput = ({\n className,\n ...props\n}: PromptInputCommandInputProps) => (\n <CommandInput className={cn(className)} {...props} />\n);\n\nexport type PromptInputCommandListProps = ComponentProps<typeof CommandList>;\n\nexport const PromptInputCommandList = ({\n className,\n ...props\n}: PromptInputCommandListProps) => (\n <CommandList className={cn(className)} {...props} />\n);\n\nexport type PromptInputCommandEmptyProps = ComponentProps<typeof CommandEmpty>;\n\nexport const PromptInputCommandEmpty = ({\n className,\n ...props\n}: PromptInputCommandEmptyProps) => (\n <CommandEmpty className={cn(className)} {...props} />\n);\n\nexport type PromptInputCommandGroupProps = ComponentProps<typeof CommandGroup>;\n\nexport const PromptInputCommandGroup = ({\n className,\n ...props\n}: PromptInputCommandGroupProps) => (\n <CommandGroup className={cn(className)} {...props} />\n);\n\nexport type PromptInputCommandItemProps = ComponentProps<typeof CommandItem>;\n\nexport const PromptInputCommandItem = ({\n className,\n ...props\n}: PromptInputCommandItemProps) => (\n <CommandItem className={cn(className)} {...props} />\n);\n\nexport type PromptInputCommandSeparatorProps = ComponentProps<\n typeof CommandSeparator\n>;\n\nexport const PromptInputCommandSeparator = ({\n className,\n ...props\n}: PromptInputCommandSeparatorProps) => (\n <CommandSeparator className={cn(className)} {...props} />\n);\n"
}
},
"test-results.tsx": {
"file": {
"contents": "\"use client\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { cn } from \"@/lib/utils\";\nimport {\n CheckCircle2Icon,\n ChevronRightIcon,\n CircleDotIcon,\n CircleIcon,\n XCircleIcon,\n} from \"lucide-react\";\nimport type { ComponentProps, HTMLAttributes } from \"react\";\nimport { createContext, useContext, useMemo } from \"react\";\n\ntype TestStatus = \"passed\" | \"failed\" | \"skipped\" | \"running\";\n\ninterface TestResultsSummary {\n passed: number;\n failed: number;\n skipped: number;\n total: number;\n duration?: number;\n}\n\ninterface TestResultsContextType {\n summary?: TestResultsSummary;\n}\n\nconst TestResultsContext = createContext<TestResultsContextType>({});\n\nconst formatDuration = (ms: number) => {\n if (ms < 1000) {\n return `${ms}ms`;\n }\n return `${(ms / 1000).toFixed(2)}s`;\n};\n\nexport type TestResultsHeaderProps = HTMLAttributes<HTMLDivElement>;\n\nexport const TestResultsHeader = ({\n className,\n children,\n ...props\n}: TestResultsHeaderProps) => (\n <div\n className={cn(\n \"flex items-center justify-between border-b px-4 py-3\",\n className\n )}\n {...props}\n >\n {children}\n </div>\n);\n\nexport type TestResultsDurationProps = HTMLAttributes<HTMLSpanElement>;\n\nexport const TestResultsDuration = ({\n className,\n children,\n ...props\n}: TestResultsDurationProps) => {\n const { summary } = useContext(TestResultsContext);\n\n if (!summary?.duration) {\n return null;\n }\n\n return (\n <span className={cn(\"text-muted-foreground text-sm\", className)} {...props}>\n {children ?? formatDuration(summary.duration)}\n </span>\n );\n};\n\nexport type TestResultsSummaryProps = HTMLAttributes<HTMLDivElement>;\n\nexport const TestResultsSummary = ({\n className,\n children,\n ...props\n}: TestResultsSummaryProps) => {\n const { summary } = useContext(TestResultsContext);\n\n if (!summary) {\n return null;\n }\n\n return (\n <div className={cn(\"flex items-center gap-3\", className)} {...props}>\n {children ?? (\n <>\n <Badge\n className=\"gap-1 bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400\"\n variant=\"secondary\"\n >\n <CheckCircle2Icon className=\"size-3\" />\n {summary.passed} passed\n </Badge>\n {summary.failed > 0 && (\n <Badge\n className=\"gap-1 bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400\"\n variant=\"secondary\"\n >\n <XCircleIcon className=\"size-3\" />\n {summary.failed} failed\n </Badge>\n )}\n {summary.skipped > 0 && (\n <Badge\n className=\"gap-1 bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400\"\n variant=\"secondary\"\n >\n <CircleIcon className=\"size-3\" />\n {summary.skipped} skipped\n </Badge>\n )}\n </>\n )}\n </div>\n );\n};\n\nexport type TestResultsProps = HTMLAttributes<HTMLDivElement> & {\n summary?: TestResultsSummary;\n};\n\nexport const TestResults = ({\n summary,\n className,\n children,\n ...props\n}: TestResultsProps) => {\n const contextValue = useMemo(() => ({ summary }), [summary]);\n\n return (\n <TestResultsContext.Provider value={contextValue}>\n <div\n className={cn(\"rounded-lg border bg-background\", className)}\n {...props}\n >\n {children ??\n (summary && (\n <TestResultsHeader>\n <TestResultsSummary />\n <TestResultsDuration />\n </TestResultsHeader>\n ))}\n </div>\n </TestResultsContext.Provider>\n );\n};\n\nexport type TestResultsProgressProps = HTMLAttributes<HTMLDivElement>;\n\nexport const TestResultsProgress = ({\n className,\n children,\n ...props\n}: TestResultsProgressProps) => {\n const { summary } = useContext(TestResultsContext);\n\n if (!summary) {\n return null;\n }\n\n const passedPercent = (summary.passed / summary.total) * 100;\n const failedPercent = (summary.failed / summary.total) * 100;\n\n return (\n <div className={cn(\"space-y-2\", className)} {...props}>\n {children ?? (\n <>\n <div className=\"flex h-2 overflow-hidden rounded-full bg-muted\">\n <div\n className=\"bg-green-500 transition-all\"\n style={{ width: `${passedPercent}%` }}\n />\n <div\n className=\"bg-red-500 transition-all\"\n style={{ width: `${failedPercent}%` }}\n />\n </div>\n <div className=\"flex justify-between text-muted-foreground text-xs\">\n <span>\n {summary.passed}/{summary.total} tests passed\n </span>\n <span>{passedPercent.toFixed(0)}%</span>\n </div>\n </>\n )}\n </div>\n );\n};\n\nexport type TestResultsContentProps = HTMLAttributes<HTMLDivElement>;\n\nexport const TestResultsContent = ({\n className,\n children,\n ...props\n}: TestResultsContentProps) => (\n <div className={cn(\"space-y-2 p-4\", className)} {...props}>\n {children}\n </div>\n);\n\ninterface TestSuiteContextType {\n name: string;\n status: TestStatus;\n}\n\nconst TestSuiteContext = createContext<TestSuiteContextType>({\n name: \"\",\n status: \"passed\",\n});\n\nconst statusStyles: Record<TestStatus, string> = {\n failed: \"text-red-600 dark:text-red-400\",\n passed: \"text-green-600 dark:text-green-400\",\n running: \"text-blue-600 dark:text-blue-400\",\n skipped: \"text-yellow-600 dark:text-yellow-400\",\n};\n\nconst statusIcons: Record<TestStatus, React.ReactNode> = {\n failed: <XCircleIcon className=\"size-4\" />,\n passed: <CheckCircle2Icon className=\"size-4\" />,\n running: <CircleDotIcon className=\"size-4 animate-pulse\" />,\n skipped: <CircleIcon className=\"size-4\" />,\n};\n\nconst TestStatusIcon = ({ status }: { status: TestStatus }) => (\n <span className={cn(\"shrink-0\", statusStyles[status])}>\n {statusIcons[status]}\n </span>\n);\n\nexport type TestSuiteProps = ComponentProps<typeof Collapsible> & {\n name: string;\n status: TestStatus;\n};\n\nexport const TestSuite = ({\n name,\n status,\n className,\n children,\n ...props\n}: TestSuiteProps) => {\n const contextValue = useMemo(() => ({ name, status }), [name, status]);\n\n return (\n <TestSuiteContext.Provider value={contextValue}>\n <Collapsible className={cn(\"rounded-lg border\", className)} {...props}>\n {children}\n </Collapsible>\n </TestSuiteContext.Provider>\n );\n};\n\nexport type TestSuiteNameProps = ComponentProps<typeof CollapsibleTrigger>;\n\nexport const TestSuiteName = ({\n className,\n children,\n ...props\n}: TestSuiteNameProps) => {\n const { name, status } = useContext(TestSuiteContext);\n\n return (\n <CollapsibleTrigger\n className={cn(\n \"group flex w-full items-center gap-2 px-4 py-3 text-left transition-colors hover:bg-muted/50\",\n className\n )}\n {...props}\n >\n <ChevronRightIcon className=\"size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90\" />\n <TestStatusIcon status={status} />\n <span className=\"font-medium text-sm\">{children ?? name}</span>\n </CollapsibleTrigger>\n );\n};\n\nexport type TestSuiteStatsProps = HTMLAttributes<HTMLDivElement> & {\n passed?: number;\n failed?: number;\n skipped?: number;\n};\n\nexport const TestSuiteStats = ({\n passed = 0,\n failed = 0,\n skipped = 0,\n className,\n children,\n ...props\n}: TestSuiteStatsProps) => (\n <div\n className={cn(\"ml-auto flex items-center gap-2 text-xs\", className)}\n {...props}\n >\n {children ?? (\n <>\n {passed > 0 && (\n <span className=\"text-green-600 dark:text-green-400\">\n {passed} passed\n </span>\n )}\n {failed > 0 && (\n <span className=\"text-red-600 dark:text-red-400\">\n {failed} failed\n </span>\n )}\n {skipped > 0 && (\n <span className=\"text-yellow-600 dark:text-yellow-400\">\n {skipped} skipped\n </span>\n )}\n </>\n )}\n </div>\n);\n\nexport type TestSuiteContentProps = ComponentProps<typeof CollapsibleContent>;\n\nexport const TestSuiteContent = ({\n className,\n children,\n ...props\n}: TestSuiteContentProps) => (\n <CollapsibleContent className={cn(\"border-t\", className)} {...props}>\n <div className=\"divide-y\">{children}</div>\n </CollapsibleContent>\n);\n\ninterface TestContextType {\n name: string;\n status: TestStatus;\n duration?: number;\n}\n\nconst TestContext = createContext<TestContextType>({\n name: \"\",\n status: \"passed\",\n});\n\nexport type TestNameProps = HTMLAttributes<HTMLSpanElement>;\n\nexport const TestName = ({ className, children, ...props }: TestNameProps) => {\n const { name } = useContext(TestContext);\n\n return (\n <span className={cn(\"flex-1\", className)} {...props}>\n {children ?? name}\n </span>\n );\n};\n\nexport type TestDurationProps = HTMLAttributes<HTMLSpanElement>;\n\nexport const TestDuration = ({\n className,\n children,\n ...props\n}: TestDurationProps) => {\n const { duration } = useContext(TestContext);\n\n if (duration === undefined) {\n return null;\n }\n\n return (\n <span\n className={cn(\"ml-auto text-muted-foreground text-xs\", className)}\n {...props}\n >\n {children ?? `${duration}ms`}\n </span>\n );\n};\n\nexport type TestStatusProps = HTMLAttributes<HTMLSpanElement>;\n\nexport const TestStatus = ({\n className,\n children,\n ...props\n}: TestStatusProps) => {\n const { status } = useContext(TestContext);\n\n return (\n <span\n className={cn(\"shrink-0\", statusStyles[status], className)}\n {...props}\n >\n {children ?? statusIcons[status]}\n </span>\n );\n};\n\nexport type TestProps = HTMLAttributes<HTMLDivElement> & {\n name: string;\n status: TestStatus;\n duration?: number;\n};\n\nexport const Test = ({\n name,\n status,\n duration,\n className,\n children,\n ...props\n}: TestProps) => {\n const contextValue = useMemo(\n () => ({ duration, name, status }),\n [duration, name, status]\n );\n\n return (\n <TestContext.Provider value={contextValue}>\n <div\n className={cn(\"flex items-center gap-2 px-4 py-2 text-sm\", className)}\n {...props}\n >\n {children ?? (\n <>\n <TestStatus />\n <TestName />\n {duration !== undefined && <TestDuration />}\n </>\n )}\n </div>\n </TestContext.Provider>\n );\n};\n\nexport type TestErrorProps = HTMLAttributes<HTMLDivElement>;\n\nexport const TestError = ({\n className,\n children,\n ...props\n}: TestErrorProps) => (\n <div\n className={cn(\n \"mt-2 rounded-md bg-red-50 p-3 dark:bg-red-900/20\",\n className\n )}\n {...props}\n >\n {children}\n </div>\n);\n\nexport type TestErrorMessageProps = HTMLAttributes<HTMLParagraphElement>;\n\nexport const TestErrorMessage = ({\n className,\n children,\n ...props\n}: TestErrorMessageProps) => (\n <p\n className={cn(\n \"font-medium text-red-700 text-sm dark:text-red-400\",\n className\n )}\n {...props}\n >\n {children}\n </p>\n);\n\nexport type TestErrorStackProps = HTMLAttributes<HTMLPreElement>;\n\nexport const TestErrorStack = ({\n className,\n children,\n ...props\n}: TestErrorStackProps) => (\n <pre\n className={cn(\n \"mt-2 overflow-auto font-mono text-red-600 text-xs dark:text-red-400\",\n className\n )}\n {...props}\n >\n {children}\n </pre>\n);\n"
}
},
"inline-citation.tsx": {
"file": {
"contents": "\"use client\";\n\nimport { Badge } from \"@/components/ui/badge\";\nimport type { CarouselApi } from \"@/components/ui/carousel\";\nimport {\n Carousel,\n CarouselContent,\n CarouselItem,\n} from \"@/components/ui/carousel\";\nimport {\n HoverCard,\n HoverCardContent,\n HoverCardTrigger,\n} from \"@/components/ui/hover-card\";\nimport { cn } from \"@/lib/utils\";\nimport { ArrowLeftIcon, ArrowRightIcon } from \"lucide-react\";\nimport type { ComponentProps } from \"react\";\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useState,\n} from \"react\";\n\nexport type InlineCitationProps = ComponentProps<\"span\">;\n\nexport const InlineCitation = ({\n className,\n ...props\n}: InlineCitationProps) => (\n <span\n className={cn(\"group inline items-center gap-1\", className)}\n {...props}\n />\n);\n\nexport type InlineCitationTextProps = ComponentProps<\"span\">;\n\nexport const InlineCitationText = ({\n className,\n ...props\n}: InlineCitationTextProps) => (\n <span\n className={cn(\"transition-colors group-hover:bg-accent\", className)}\n {...props}\n />\n);\n\nexport type InlineCitationCardProps = ComponentProps<typeof HoverCard>;\n\nexport const InlineCitationCard = (props: InlineCitationCardProps) => (\n <HoverCard closeDelay={0} openDelay={0} {...props} />\n);\n\nexport type InlineCitationCardTriggerProps = ComponentProps<typeof Badge> & {\n sources: string[];\n};\n\nexport const InlineCitationCardTrigger = ({\n sources,\n className,\n ...props\n}: InlineCitationCardTriggerProps) => (\n <HoverCardTrigger asChild>\n <Badge\n className={cn(\"ml-1 rounded-full\", className)}\n variant=\"secondary\"\n {...props}\n >\n {sources[0] ? (\n <>\n {new URL(sources[0]).hostname}{\" \"}\n {sources.length > 1 && `+${sources.length - 1}`}\n </>\n ) : (\n \"unknown\"\n )}\n </Badge>\n </HoverCardTrigger>\n);\n\nexport type InlineCitationCardBodyProps = ComponentProps<\"div\">;\n\nexport const InlineCitationCardBody = ({\n className,\n ...props\n}: InlineCitationCardBodyProps) => (\n <HoverCardContent className={cn(\"relative w-80 p-0\", className)} {...props} />\n);\n\nconst CarouselApiContext = createContext<CarouselApi | undefined>(undefined);\n\nconst useCarouselApi = () => {\n const context = useContext(CarouselApiContext);\n return context;\n};\n\nexport type InlineCitationCarouselProps = ComponentProps<typeof Carousel>;\n\nexport const InlineCitationCarousel = ({\n className,\n children,\n ...props\n}: InlineCitationCarouselProps) => {\n const [api, setApi] = useState<CarouselApi>();\n\n return (\n <CarouselApiContext.Provider value={api}>\n <Carousel className={cn(\"w-full\", className)} setApi={setApi} {...props}>\n {children}\n </Carousel>\n </CarouselApiContext.Provider>\n );\n};\n\nexport type InlineCitationCarouselContentProps = ComponentProps<\"div\">;\n\nexport const InlineCitationCarouselContent = (\n props: InlineCitationCarouselContentProps\n) => <CarouselContent {...props} />;\n\nexport type InlineCitationCarouselItemProps = ComponentProps<\"div\">;\n\nexport const InlineCitationCarouselItem = ({\n className,\n ...props\n}: InlineCitationCarouselItemProps) => (\n <CarouselItem\n className={cn(\"w-full space-y-2 p-4 pl-8\", className)}\n {...props}\n />\n);\n\nexport type InlineCitationCarouselHeaderProps = ComponentProps<\"div\">;\n\nexport const InlineCitationCarouselHeader = ({\n className,\n ...props\n}: InlineCitationCarouselHeaderProps) => (\n <div\n className={cn(\n \"flex items-center justify-between gap-2 rounded-t-md bg-secondary p-2\",\n className\n )}\n {...props}\n />\n);\n\nexport type InlineCitationCarouselIndexProps = ComponentProps<\"div\">;\n\nexport const InlineCitationCarouselIndex = ({\n children,\n className,\n ...props\n}: InlineCitationCarouselIndexProps) => {\n const api = useCarouselApi();\n const [current, setCurrent] = useState(0);\n const [count, setCount] = useState(0);\n\n const syncState = useCallback(() => {\n if (!api) {\n return;\n }\n setCount(api.scrollSnapList().length);\n setCurrent(api.selectedScrollSnap() + 1);\n }, [api]);\n\n useEffect(() => {\n if (!api) {\n return;\n }\n\n syncState();\n\n api.on(\"select\", syncState);\n\n return () => {\n api.off(\"select\", syncState);\n };\n }, [api, syncState]);\n\n return (\n <div\n className={cn(\n \"flex flex-1 items-center justify-end px-3 py-1 text-muted-foreground text-xs\",\n className\n )}\n {...props}\n >\n {children ?? `${current}/${count}`}\n </div>\n );\n};\n\nexport type InlineCitationCarouselPrevProps = ComponentProps<\"button\">;\n\nexport const InlineCitationCarouselPrev = ({\n className,\n ...props\n}: InlineCitationCarouselPrevProps) => {\n const api = useCarouselApi();\n\n const handleClick = useCallback(() => {\n if (api) {\n api.scrollPrev();\n }\n }, [api]);\n\n return (\n <button\n aria-label=\"Previous\"\n className={cn(\"shrink-0\", className)}\n onClick={handleClick}\n type=\"button\"\n {...props}\n >\n <ArrowLeftIcon className=\"size-4 text-muted-foreground\" />\n </button>\n );\n};\n\nexport type InlineCitationCarouselNextProps = ComponentProps<\"button\">;\n\nexport const InlineCitationCarouselNext = ({\n className,\n ...props\n}: InlineCitationCarouselNextProps) => {\n const api = useCarouselApi();\n\n const handleClick = useCallback(() => {\n if (api) {\n api.scrollNext();\n }\n }, [api]);\n\n return (\n <button\n aria-label=\"Next\"\n className={cn(\"shrink-0\", className)}\n onClick={handleClick}\n type=\"button\"\n {...props}\n >\n <ArrowRightIcon className=\"size-4 text-muted-foreground\" />\n </button>\n );\n};\n\nexport type InlineCitationSourceProps = ComponentProps<\"div\"> & {\n title?: string;\n url?: string;\n description?: string;\n};\n\nexport const InlineCitationSource = ({\n title,\n url,\n description,\n className,\n children,\n ...props\n}: InlineCitationSourceProps) => (\n <div className={cn(\"space-y-1\", className)} {...props}>\n {title && (\n <h4 className=\"truncate font-medium text-sm leading-tight\">{title}</h4>\n )}\n {url && (\n <p className=\"truncate break-all text-muted-foreground text-xs\">{url}</p>\n )}\n {description && (\n <p className=\"line-clamp-3 text-muted-foreground text-sm leading-relaxed\">\n {description}\n </p>\n )}\n {children}\n </div>\n);\n\nexport type InlineCitationQuoteProps = ComponentProps<\"blockquote\">;\n\nexport const InlineCitationQuote = ({\n children,\n className,\n ...props\n}: InlineCitationQuoteProps) => (\n <blockquote\n className={cn(\n \"border-muted border-l-2 pl-3 text-muted-foreground text-sm italic\",\n className\n )}\n {...props}\n >\n {children}\n </blockquote>\n);\n"
}
},
"chain-of-thought.tsx": {
"file": {
"contents": "\"use client\";\n\nimport { useControllableState } from \"@radix-ui/react-use-controllable-state\";\nimport { Badge } from \"@/components/ui/badge\";\nimport {\n Collapsible,\n CollapsibleContent,\n CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { cn } from \"@/lib/utils\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { BrainIcon, ChevronDownIcon, DotIcon } from \"lucide-react\";\nimport type { ComponentProps, ReactNode } from \"react\";\nimport { createContext, memo, useContext, useMemo } from \"react\";\n\ninterface ChainOfThoughtContextValue {\n isOpen: boolean;\n setIsOpen: (open: boolean) => void;\n}\n\nconst ChainOfThoughtContext = createContext<ChainOfThoughtContextValue | null>(\n null\n);\n\nconst useChainOfThought = () => {\n const context = useContext(ChainOfThoughtContext);\n if (!context) {\n throw new Error(\n \"ChainOfThought components must be used within ChainOfThought\"\n );\n }\n return context;\n};\n\nexport type ChainOfThoughtProps = ComponentProps<\"div\"> & {\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n};\n\nexport const ChainOfThought = memo(\n ({\n className,\n open,\n defaultOpen = false,\n onOpenChange,\n children,\n ...props\n }: ChainOfThoughtProps) => {\n const [isOpen, setIsOpen] = useControllableState({\n defaultProp: defaultOpen,\n onChange: onOpenChange,\n prop: open,\n });\n\n const chainOfThoughtContext = useMemo(\n () => ({ isOpen, setIsOpen }),\n [isOpen, setIsOpen]\n );\n\n return (\n <ChainOfThoughtContext.Provider value={chainOfThoughtContext}>\n <div className={cn(\"not-prose w-full space-y-4\", className)} {...props}>\n {children}\n </div>\n </ChainOfThoughtContext.Provider>\n );\n }\n);\n\nexport type ChainOfThoughtHeaderProps = ComponentProps<\n typeof CollapsibleTrigger\n>;\n\nexport const ChainOfThoughtHeader = memo(\n ({ className, children, ...props }: ChainOfThoughtHeaderProps) => {\n const { isOpen, setIsOpen } = useChainOfThought();\n\n return (\n <Collapsible onOpenChange={setIsOpen} open={isOpen}>\n <CollapsibleTrigger\n className={cn(\n \"flex w-full items-center gap-2 text-muted-foreground text-sm transition-colors hover:text-foreground\",\n className\n )}\n {...props}\n >\n <BrainIcon className=\"size-4\" />\n <span className=\"flex-1 text-left\">\n {children ?? \"Chain of Thought\"}\n </span>\n <ChevronDownIcon\n className={cn(\n \"size-4 transition-transform\",\n isOpen ? \"rotate-180\" : \"rotate-0\"\n )}\n />\n </CollapsibleTrigger>\n </Collapsible>\n );\n }\n);\n\nexport type ChainOfThoughtStepProps = ComponentProps<\"div\"> & {\n icon?: LucideIcon;\n label: ReactNode;\n description?: ReactNode;\n status?: \"complete\" | \"active\" | \"pending\";\n};\n\nconst stepStatusStyles = {\n active: \"text-foreground\",\n complete: \"text-muted-foreground\",\n pending: \"text-muted-foreground/50\",\n};\n\nexport const ChainOfThoughtStep = memo(\n ({\n className,\n icon: Icon = DotIcon,\n label,\n description,\n status = \"complete\",\n children,\n ...props\n }: ChainOfThoughtStepProps) => (\n <div\n className={cn(\n \"flex gap-2 text-sm\",\n stepStatusStyles[status],\n \"fade-in-0 slide-in-from-top-2 animate-in\",\n className\n )}\n {...props}\n >\n <div className=\"relative mt-0.5\">\n <Icon className=\"size-4\" />\n <div className=\"absolute top-7 bottom-0 left-1/2 -mx-px w-px bg-border\" />\n </div>\n <div className=\"flex-1 space-y-2 overflow-hidden\">\n <div>{label}</div>\n {description && (\n <div className=\"text-muted-foreground text-xs\">{description}</div>\n )}\n {children}\n </div>\n </div>\n )\n);\n\nexport type ChainOfThoughtSearchResultsProps = ComponentProps<\"div\">;\n\nexport const ChainOfThoughtSearchResults = memo(\n ({ className, ...props }: ChainOfThoughtSearchResultsProps) => (\n <div\n className={cn(\"flex flex-wrap items-center gap-2\", className)}\n {...props}\n />\n )\n);\n\nexport type ChainOfThoughtSearchResultProps = ComponentProps<typeof Badge>;\n\nexport const ChainOfThoughtSearchResult = memo(\n ({ className, children, ...props }: ChainOfThoughtSearchResultProps) => (\n <Badge\n className={cn(\"gap-1 px-2 py-0.5 font-normal text-xs\", className)}\n variant=\"secondary\"\n {...props}\n >\n {children}\n </Badge>\n )\n);\n\nexport type ChainOfThoughtContentProps = ComponentProps<\n typeof CollapsibleContent\n>;\n\nexport const ChainOfThoughtContent = memo(\n ({ className, children, ...props }: ChainOfThoughtContentProps) => {\n const { isOpen } = useChainOfThought();\n\n return (\n <Collapsible open={isOpen}>\n <CollapsibleContent\n className={cn(\n \"mt-2 space-y-3\",\n \"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\",\n className\n )}\n {...props}\n >\n {children}\n </CollapsibleContent>\n </Collapsible>\n );\n }\n);\n\nexport type ChainOfThoughtImageProps = ComponentProps<\"div\"> & {\n caption?: string;\n};\n\nexport const ChainOfThoughtImage = memo(\n ({ className, children, caption, ...props }: ChainOfThoughtImageProps) => (\n <div className={cn(\"mt-2 space-y-2\", className)} {...props}>\n <div className=\"relative flex max-h-[22rem] items-center justify-center overflow-hidden rounded-lg bg-muted p-3\">\n {children}\n </div>\n {caption && <p className=\"text-muted-foreground text-xs\">{caption}</p>}\n </div>\n )\n);\n\nChainOfThought.displayName = \"ChainOfThought\";\nChainOfThoughtHeader.displayName = \"ChainOfThoughtHeader\";\nChainOfThoughtStep.displayName = \"ChainOfThoughtStep\";\nChainOfThoughtSearchResults.displayName = \"ChainOfThoughtSearchResults\";\nChainOfThoughtSearchResult.displayName = \"ChainOfThoughtSearchResult\";\nChainOfThoughtContent.displayName = \"ChainOfThoughtContent\";\nChainOfThoughtImage.displayName = \"ChainOfThoughtImage\";\n"
}
}
}
}
}
}
}
},
"docs": {
"directory": {
"HOW_TO_USE.md": {
"file": {
"contents": "# Genesis Base Template v2 - AI Development Guide\n\n## Core Rules\n\n1. **Work ONLY in `src/`** - Create/modify files only under `src/`, never touch files outside\n2. **Don't modify `src/main.tsx`** - Entry point is fixed\n3. **Start with `src/App.tsx`** - Root component of the application\n4. **Use `@/` imports** - `import { cn } from '@/lib/utils'` not relative paths\n\n## File Structure\n\n```\nsrc/\n├── App.tsx # Root component (start here)\n├── main.tsx # Entry point (don't modify)\n├── index.css # Tailwind + CSS variables\n├── components/ui/ # Base UI components (Button, Dialog, etc.)\n├── components/ai-elements/ # Pre-built AI chat UI components\n└── lib/\n ├── utils.ts # cn() utility\n └── agent-chat/v2/ # Agent Chat SDK (useChat + createAgentChat)\n```\n\n## Available Dependencies\n\n### Core\n\n- react 18.3, react-dom 18.3, typescript 5.4, tailwindcss 3.4\n\n### UI & Styling\n\n- **@radix-ui/react-\\*** - Dialog, Dropdown Menu, Select, Switch, Tabs, Tooltip, Progress, Separator\n- **lucide-react** 0.542 - Icons (1000+ available)\n- **framer-motion** 12.9 - Animations\n- **next-themes** 0.4 - Dark mode\n- **tailwind-merge** + **clsx** - `cn()` utility\n- **class-variance-authority** 0.7 - Variant styling\n- **@radix-ui/colors** 3.0 - Color system\n\n### Forms & Validation\n\n- **react-hook-form** 7.54 + **@hookform/resolvers** 3.4\n- **zod** 3.25 - Schema validation\n\n### State & Routing\n\n- **zustand** 4.5 - State management\n- **react-router-dom** 6.30 - Routing\n- **react-oidc-context** 3.3 - OIDC auth client\n- **axios** 1.12 - HTTP client\n\n### UI Components\n\n- **cmdk** 1.1 - Command palette\n- **sonner** 2.0 - Toasts\n- **@hello-pangea/dnd** 18.0 - Drag & drop\n- **@formkit/auto-animate** 0.8 - Auto animations\n- **react-textarea-autosize** 8.5 - Auto-growing textarea\n- **react-intersection-observer** 9.16 - Visibility detection\n- **react-error-boundary** 5.0 - Error boundaries\n\n### Content & Data\n\n- **react-markdown** 10.1 + **remark-gfm** 4.0 - Markdown\n- **recharts** 2.15 - Charts\n- **date-fns** 4.1 - Date utilities\n\n### AI Chat\n\n- **@ai-sdk/react** - `useChat` hook for AI chat interfaces\n- **ai** - AI SDK core (types, transports)\n- **ai-elements** - Pre-built chat UI components at `@/components/ai-elements/`\n- **ulidx** - ULID generation for message IDs\n\n## Authentication (OIDC)\n\nA pre-built auth wrapper is available at `@/lib/genesis-auth`. Use it instead of configuring `AuthProvider` manually.\n\n### When to add auth\n\nAdd auth when the app needs to identify individual users (login, signup, per-user data, multi-user features).\nDo NOT add auth for single-purpose tools with no user concept.\n\n### Usage\n\n```tsx\nimport { GenesisAuth } from '@/lib/genesis-auth';\n\nfunction App() {\n return (\n <GenesisAuth>\n <ProtectedApp />\n </GenesisAuth>\n );\n}\n```\n\nAccess user profile after login:\n\n```tsx\nimport { useAuth } from 'react-oidc-context';\n\nfunction Profile() {\n const auth = useAuth();\n if (!auth.isAuthenticated) {\n return <button onClick={() => auth.signinRedirect()}>Sign in</button>;\n }\n const { email, name, preferred_username, sub } = auth.user?.profile ?? {};\n return <div>Hello {name}</div>;\n}\n```\n\n- `auth.signinRedirect()` — trigger login (Genesis provides the login/signup UI)\n- `auth.signoutRedirect()` — logout\n- `auth.isAuthenticated` — check login state\n- `auth.user?.profile` — `{ email, name, preferred_username, sub }`\n\nDo not build custom login forms or custom auth flows unless explicitly asked.\n\n## Pre-built Components\n\n**Base UI** (`@/components/ui/`) — Button, Dialog, Select, Switch, Tabs, etc.\n\n**AI Elements** (`@/components/ai-elements/`) — Pre-built chat UI components:\n\n- `Conversation`, `ConversationContent`, `ConversationScrollButton` — Scrollable chat container\n- `Message`, `MessageContent`, `MessageResponse` — Message bubbles with streaming markdown\n- `PromptInput`, `PromptInputTextarea`, `PromptInputFooter`, `PromptInputSubmit` — Chat input form\n- `Suggestions`, `Suggestion` — Quick-reply suggestion pills\n- `Reasoning`, `ReasoningTrigger`, `ReasoningContent` — Collapsible thinking display\n- `CodeBlock` — Syntax-highlighted code with copy button\n\nSee `src/lib/agent-chat/v2/README.md` for full Agent Chat SDK docs + AI Elements usage examples.\n\nCreate custom components freely in `src/components/` using Radix UI primitives.\n\n## CSS Design System\n\nUse semantic color classes (automatically supports light/dark mode):\n\n- `bg-background text-foreground` - Page default\n- `bg-primary text-primary-foreground` - Primary buttons\n- `bg-secondary text-secondary-foreground` - Secondary elements\n- `bg-muted text-muted-foreground` - Disabled/subtle\n- `bg-accent text-accent-foreground` - Highlights\n- `bg-destructive text-destructive-foreground` - Delete/error\n- `bg-card text-card-foreground` - Cards\n- `border-border` - Borders\n- `ring-ring` - Focus rings\n\n## Component Patterns\n\n### TypeScript\n\n- Define explicit Props interfaces for all components\n- Use `React.FC<Props>` and function declarations\n- Use Zod schemas with `z.infer<typeof schema>` for form types\n\n### Styling\n\n- Use Tailwind utility classes\n- Use `cn()` from `@/lib/utils` for conditional classes\n- Use semantic color classes for theme consistency\n\n### File Organization\n\n- `src/components/` - Reusable components\n- `src/pages/` - Route pages\n- `src/hooks/` - Custom hooks\n- `src/stores/` - Zustand stores\n- `src/lib/` - Utilities\n\n## Key Libraries Usage\n\n### Routing\n\n- Use `react-router-dom` with `BrowserRouter`, `Routes`, `Route`\n\n### State Management\n\n- Use `zustand` with `create<State>()` for global state\n\n### Forms\n\n- Use `react-hook-form` with `useForm()` and `zodResolver()` for validation\n\n### Icons\n\n- Import from `lucide-react`: `import { Home, User, Settings } from 'lucide-react'`\n\n### Dark Mode\n\n- Use `next-themes` with `ThemeProvider` and `useTheme()` hook\n\n### HTTP\n\n- Use `axios` for API calls\n\n## Summary\n\nReact 18 + TypeScript + Tailwind CSS template. Work only in `src/`. Use Radix UI primitives for accessible components, Zustand for state, React Router for routing, react-hook-form + Zod for forms, Lucide for icons. Build custom components as needed.\n"
}
}
}
},
"package.json": {
"file": {
"contents": "{\n \"name\": \"@taskade/parade-base-template-v2\",\n \"version\": \"2.2.1\",\n \"private\": true,\n \"files\": [\n \"dist\",\n \"README.md\",\n \"package.json\"\n ],\n \"type\": \"module\",\n \"exports\": {\n \"./fileSystemTree.json\": \"./dist/fileSystemTree.json\",\n \"./package.json\": \"./package.json\"\n },\n \"scripts\": {\n \"build\": \"node scripts/build.mjs\",\n \"dev\": \"node scripts/build.mjs\"\n },\n \"dependencies\": {\n \"@ai-sdk/react\": \"^3.0.148\",\n \"@formkit/auto-animate\": \"^0.8.2\",\n \"@hello-pangea/dnd\": \"^18.0.1\",\n \"@hookform/resolvers\": \"^5.2.2\",\n \"@radix-ui/colors\": \"^3.0.0\",\n \"@radix-ui/react-accordion\": \"^1.2.12\",\n \"@radix-ui/react-alert-dialog\": \"^1.1.15\",\n \"@radix-ui/react-aspect-ratio\": \"^1.1.7\",\n \"@radix-ui/react-avatar\": \"^1.1.10\",\n \"@radix-ui/react-checkbox\": \"^1.3.3\",\n \"@radix-ui/react-collapsible\": \"^1.1.12\",\n \"@radix-ui/react-context-menu\": \"^2.2.16\",\n \"@radix-ui/react-dialog\": \"^1.1.15\",\n \"@radix-ui/react-dropdown-menu\": \"^2.1.16\",\n \"@radix-ui/react-hover-card\": \"^1.1.15\",\n \"@radix-ui/react-label\": \"^2.1.7\",\n \"@radix-ui/react-menubar\": \"^1.1.16\",\n \"@radix-ui/react-navigation-menu\": \"^1.2.14\",\n \"@radix-ui/react-popover\": \"^1.1.15\",\n \"@radix-ui/react-progress\": \"^1.1.7\",\n \"@radix-ui/react-radio-group\": \"^1.3.8\",\n \"@radix-ui/react-scroll-area\": \"^1.2.10\",\n \"@radix-ui/react-select\": \"^2.2.6\",\n \"@radix-ui/react-separator\": \"^1.1.7\",\n \"@radix-ui/react-slider\": \"^1.3.6\",\n \"@radix-ui/react-slot\": \"^1.2.3\",\n \"@radix-ui/react-switch\": \"^1.2.6\",\n \"@radix-ui/react-tabs\": \"^1.1.13\",\n \"@radix-ui/react-toggle\": \"^1.1.10\",\n \"@radix-ui/react-toggle-group\": \"^1.1.11\",\n \"@radix-ui/react-tooltip\": \"^1.2.8\",\n \"@radix-ui/react-use-controllable-state\": \"^1.2.2\",\n \"@streamdown/cjk\": \"^1.0.3\",\n \"@streamdown/code\": \"^1.1.1\",\n \"@streamdown/math\": \"^1.0.2\",\n \"@streamdown/mermaid\": \"^1.0.2\",\n \"ai\": \"^6.0.146\",\n \"autoprefixer\": \"^10.4.14\",\n \"axios\": \"^1.13.5\",\n \"class-variance-authority\": \"^0.7.1\",\n \"clsx\": \"^2.1.1\",\n \"cmdk\": \"^1.1.1\",\n \"date-fns\": \"^4.1.0\",\n \"embla-carousel-react\": \"^8.6.0\",\n \"framer-motion\": \"^12.9.1\",\n \"input-otp\": \"^1.4.2\",\n \"leaflet\": \"^1.9.4\",\n \"lucide-react\": \"^0.544.0\",\n \"motion\": \"^12.38.0\",\n \"nanoid\": \"^5.1.7\",\n \"next-themes\": \"^0.4.6\",\n \"oidc-client-ts\": \"^3.4.1\",\n \"postcss\": \"^8.5.3\",\n \"react\": \"^18.3.1\",\n \"react-day-picker\": \"^9.11.0\",\n \"react-dom\": \"^18.3.1\",\n \"react-error-boundary\": \"^5.0.0\",\n \"react-hook-form\": \"^7.64.0\",\n \"react-intersection-observer\": \"^9.16.0\",\n \"react-leaflet\": \"^4.2.1\",\n \"react-markdown\": \"^10.1.0\",\n \"react-oidc-context\": \"^3.3.0\",\n \"react-resizable-panels\": \"^3.0.6\",\n \"react-router-dom\": \"^6.30.3\",\n \"react-textarea-autosize\": \"^8.5.3\",\n \"recharts\": \"2.15.4\",\n \"remark-gfm\": \"^4.0.1\",\n \"shiki\": \"^4.0.2\",\n \"sonner\": \"^2.0.7\",\n \"streamdown\": \"^2.5.0\",\n \"tailwind-merge\": \"^2.6.0\",\n \"tailwindcss\": \"^3.4.17\",\n \"tailwindcss-animate\": \"^1.0.7\",\n \"ulidx\": \"^2.4.1\",\n \"use-stick-to-bottom\": \"^1.1.3\",\n \"uuid\": \"^9.0.1\",\n \"vaul\": \"^1.1.2\",\n \"zod\": \"^4.1.11\",\n \"zustand\": \"^4.5.5\"\n },\n \"devDependencies\": {\n \"@taskade/parade-shared\": \"*\",\n \"@taskade/parade-template-utils\": \"*\",\n \"@types/leaflet\": \"^1.9.12\",\n \"@types/node\": \"^22.15.18\",\n \"@types/react\": \"^18.3.18\",\n \"@types/react-dom\": \"^18.3.5\",\n \"esbuild\": \"^0.27.4\",\n \"tsx\": \"^4.19.4\",\n \"typescript\": \"^5.4.5\",\n \"vitest\": \"^4.0.17\"\n }\n}\n"
}
}
}