diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 8c38ec9..433a1aa 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,58 +1,30 @@ +import { gateway } from "ai" +import { streamText } from "ai" import { NextResponse } from "next/server" -export async function POST(request: Request) { +export async function POST(req: Request) { try { - const { message, history } = await request.json() - - const ollamaUrl = process.env.OLLAMA_URL || "http://localhost:11434" - const ollamaModel = process.env.OLLAMA_MODEL || "deepseek-r1:7b" - - const messages = history - .slice(-10) - .map((msg: { role: string; content: string }) => ({ - role: msg.role === "user" ? "user" : "assistant", - content: msg.content, - })) - - messages.push({ - role: "user", - content: message, - }) - - const ollamaResponse = await fetch(`${ollamaUrl}/api/chat`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model: ollamaModel, - messages, - stream: false, - options: { - temperature: 0.7, - top_p: 0.95, - }, - }), - }) - - if (!ollamaResponse.ok) { - throw new Error(`Ollama API error: ${ollamaResponse.status}`) + const { messages, model } = await req.json() + + // Ensure API key is configured + if (!process.env.AI_GATEWAY_API_KEY) { + return NextResponse.json( + { error: "AI Gateway API Key not found. Please set AI_GATEWAY_API_KEY in your env." }, + { status: 500 } + ) } - const data = await ollamaResponse.json() - const aiResponse = data.message?.content || "Sorry, I couldn't generate a response." - - console.log(`✓ Using Ollama local model: ${ollamaModel}`) + const result = streamText({ + model: gateway(model || "google:gemini-1.5-pro"), + messages, + }) - return NextResponse.json({ response: aiResponse, provider: "Ollama (Local)" }) - } catch (error) { - console.error("Error in chat API:", error) + return result.toTextStreamResponse() + } catch (error: any) { + console.error("Error in AI chat:", error) return NextResponse.json( - { - error: "Failed to connect to Ollama. Please ensure Ollama is running.", - provider: "Error" - }, - { status: 500 }, + { error: error.message || "Failed to process chat request" }, + { status: 500 } ) } } diff --git a/app/dashboard/ai-tools/page.tsx b/app/dashboard/ai-tools/page.tsx index 4df04e6..b9b0d97 100644 --- a/app/dashboard/ai-tools/page.tsx +++ b/app/dashboard/ai-tools/page.tsx @@ -2,95 +2,69 @@ import { useState, useEffect, useRef } from "react" import { Button } from "@/components/ui/button" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" import { Send, Sparkles, Bot, User, Trash2, Copy, Check } from "lucide-react" import { getTranslations, getUserLanguage } from "@/lib/config" - -type Message = { - role: "user" | "assistant" - content: string - timestamp: string -} +import { useChat } from "@ai-sdk/react" export default function AIToolsPage() { const [t, setT] = useState(getTranslations("en")) - const [messages, setMessages] = useState([]) - const [input, setInput] = useState("") - const [isLoading, setIsLoading] = useState(false) - const [provider, setProvider] = useState("AI") const [copiedIndex, setCopiedIndex] = useState(null) const messagesEndRef = useRef(null) + + // Manual input state management for @ai-sdk/react v3+ + const [input, setInput] = useState("") + const [selectedModel, setSelectedModel] = useState("google:gemini-1.5-pro") - useEffect(() => { - setT(getTranslations(getUserLanguage())) - setMessages([ + const { messages, sendMessage, status, setMessages } = useChat({ + // Cast to any to bypass strict UIMessage type vs helper type conflicts if necessary + messages: [ { + id: "welcome", role: "assistant", - content: - "Hello! I'm your RAG-enhanced AI development assistant powered by Ollama. I can help you with code generation, debugging, architecture decisions, and more. My responses are based on your platform's documentation and codebase for accurate, context-aware answers. What would you like to work on today?", - timestamp: new Date().toLocaleTimeString(), - }, - ]) + content: "Hello! I'm your AI development assistant. I can help you with code generation, debugging, architecture decisions, and more.", + } as any, + ], + onError: (error) => { + console.error("Chat error:", error) + } + }) + + // Derived loading state + const isLoading = status === "submitted" || status === "streaming" + + useEffect(() => { + setT(getTranslations(getUserLanguage())) }, []) useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) }, [messages]) - const handleSend = async () => { - if (!input.trim() || isLoading) return - - const userMessage: Message = { - role: "user", - content: input, - timestamp: new Date().toLocaleTimeString(), - } - - setMessages((prev) => [...prev, userMessage]) - setInput("") - setIsLoading(true) + const handleInputChange = (e: React.ChangeEvent) => { + setInput(e.target.value) + } + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!input.trim() || isLoading) return + + // Send user message try { - const response = await fetch("/api/chat", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - message: input, - history: messages, - }), - }) - - if (!response.ok) { - throw new Error("Failed to get response") - } - - const data = await response.json() - - // Update provider info - if (data.provider) { - setProvider(data.provider) - } - - const aiMessage: Message = { - role: "assistant", - content: data.response, - timestamp: new Date().toLocaleTimeString(), - } - - setMessages((prev) => [...prev, aiMessage]) - } catch (error) { - console.error("Error calling AI API:", error) - const errorMessage: Message = { - role: "assistant", - content: - "Sorry, I encountered an error connecting to the RAG-enhanced AI. Please ensure Ollama is running and you have downloaded a model. Visit https://ollama.com for setup instructions or see docs/OLLAMA_SETUP.md", - timestamp: new Date().toLocaleTimeString(), - } - setMessages((prev) => [...prev, errorMessage]) - } finally { - setIsLoading(false) + await sendMessage( + { role: 'user', content: input } as any, + { body: { model: selectedModel } } + ) + } catch (err) { + console.error("Failed to send:", err) } + setInput("") } const copyToClipboard = async (text: string, index: number) => { @@ -102,11 +76,10 @@ export default function AIToolsPage() { const clearChat = () => { setMessages([ { + id: "welcome", role: "assistant", - content: - "Hello! I'm your RAG-enhanced AI development assistant powered by Ollama. I can help you with code generation, debugging, architecture decisions, and more. My responses are based on your platform's documentation and codebase for accurate, context-aware answers. What would you like to work on today?", - timestamp: new Date().toLocaleTimeString(), - }, + content: "Hello! I'm your AI development assistant. I can help you with code generation, debugging, architecture decisions, and more.", + } as any, ]) } @@ -124,23 +97,38 @@ export default function AIToolsPage() {

{t.nav.aiTools}

-
+

- {provider.includes("RAG") ? "🧠 RAG-Enhanced (Local)" : provider.includes("Ollama") ? "Running Locally" : provider} + Powered by Vercel AI Gateway

- + +
+
+ +
+ +
@@ -149,9 +137,9 @@ export default function AIToolsPage() {
- {messages.map((message, index) => ( + {messages.map((message: any, index: number) => (
@@ -172,9 +160,8 @@ export default function AIToolsPage() {
- {message.role === "user" ? "You" : "AI Assistant"} + {message.role === "user" ? "You" : selectedModel.split(':')[1] || selectedModel} - {message.timestamp}

- {message.content} + {message.content ? message.content : (Array.isArray(message.parts) ? message.parts.map((p: any) => p.text).join('') : JSON.stringify(message))}

- {message.role === "assistant" && ( + {message.role === "assistant" && message.content && (
))} + {/* Loading Indicator */} {isLoading && (
@@ -211,7 +199,7 @@ export default function AIToolsPage() {
- AI Assistant + {selectedModel.split(':')[1] || selectedModel}
@@ -234,14 +222,13 @@ export default function AIToolsPage() {
-
+
setInput(e.target.value)} - onKeyPress={(e) => e.key === "Enter" && !e.shiftKey && handleSend()} - placeholder="Ask anything... (Press Enter to send, Shift+Enter for new line)" + onChange={handleInputChange} + placeholder={`Ask ${selectedModel.split(':')[1] || "AI"} anything...`} className="w-full bg-background border border-border rounded-xl px-4 py-3 pr-12 text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent transition-all placeholder:text-muted-foreground/60" disabled={isLoading} /> @@ -250,7 +237,7 @@ export default function AIToolsPage() {
-
+

- {provider.includes("RAG") ? ( - <> - - - RAG-Enhanced AI - - - Context from your docs & code - - ) : provider.includes("Ollama") ? ( - <> - - - Running locally - - - Privacy-first, no data sent to cloud - - ) : ( - <> - Powered by {provider} - - Real-time responses - - )} + + + {selectedModel} + + + Production Ready

{messages.length - 1} {messages.length === 2 ? 'message' : 'messages'} diff --git a/components/ui/select.tsx b/components/ui/select.tsx new file mode 100644 index 0000000..cbe5a36 --- /dev/null +++ b/components/ui/select.tsx @@ -0,0 +1,160 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { Check, ChevronDown, ChevronUp } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +} diff --git a/package.json b/package.json index dc266a5..6c92093 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "start:next": "next start" }, "dependencies": { + "@ai-sdk/google": "^3.0.13", + "@ai-sdk/react": "^3.0.51", "@heroicons/react": "^2.2.0", "@hookform/resolvers": "^3.10.0", "@html2canvas/html2canvas": "^1.6.3", @@ -48,6 +50,7 @@ "@supabase/supabase-js": "^2.80.0", "@types/html2canvas": "^1.0.0", "@vercel/analytics": "latest", + "ai": "^6.0.49", "autoprefixer": "^10.4.20", "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee994b3..af2b07a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,12 @@ importers: .: dependencies: + '@ai-sdk/google': + specifier: ^3.0.13 + version: 3.0.13(zod@3.25.76) + '@ai-sdk/react': + specifier: ^3.0.51 + version: 3.0.51(react@19.2.0)(zod@3.25.76) '@heroicons/react': specifier: ^2.2.0 version: 2.2.0(react@19.2.0) @@ -119,6 +125,9 @@ importers: '@vercel/analytics': specifier: latest version: 1.6.1(next@16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0) + ai: + specifier: ^6.0.49 + version: 6.0.49(zod@3.25.76) autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.5.0) @@ -279,6 +288,34 @@ importers: packages: + '@ai-sdk/gateway@3.0.22': + resolution: {integrity: sha512-NgnlY73JNuooACHqUIz5uMOEWvqR1MMVbb2soGLMozLY1fgwEIF5iJFDAGa5/YArlzw2ATVU7zQu7HkR/FUjgA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/google@3.0.13': + resolution: {integrity: sha512-HYCh8miS4FLxOIpjo/BmoFVMO5BuxNpHVVDQkoJotoH8ZSFftkJJGGayIxQT/Lwx9GGvVVCOQ+lCdBBAnkl1sA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.9': + resolution: {integrity: sha512-bB4r6nfhBOpmoS9mePxjRoCy+LnzP3AfhyMGCkGL4Mn9clVNlqEeKj26zEKEtB6yoSVcT1IQ0Zh9fytwMCDnow==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@3.0.5': + resolution: {integrity: sha512-2Xmoq6DBJqmSl80U6V9z5jJSJP7ehaJJQMy2iFUqTay06wdCqTnPVBBQbtEL8RCChenL+q5DC5H5WzU3vV3v8w==} + engines: {node: '>=18'} + + '@ai-sdk/react@3.0.51': + resolution: {integrity: sha512-7nmCwEJM52NQZB4/ED8qJ4wbDg7EEWh94qJ7K9GSJxD6sWF3GOKrRZ5ivm4qNmKhY+JfCxCAxfghGY5mTKOsxw==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ~19.0.1 || ~19.1.2 || ^19.2.1 + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -1576,6 +1613,9 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@supabase/auth-js@2.80.0': resolution: {integrity: sha512-q2LyCVJGN4p7d92cOI7scWOoNwxJhZuFRwiimSUGJGI5zX7ubf1WUPznwOmYEn8WVo3Io+MyMinA7era6j5KPw==} engines: {node: '>=20.0.0'} @@ -1910,6 +1950,10 @@ packages: vue-router: optional: true + '@vercel/oidc@3.1.0': + resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} + engines: {node: '>= 20'} + '@webgpu/types@0.1.69': resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==} @@ -1926,6 +1970,12 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ai@6.0.49: + resolution: {integrity: sha512-LABniBX/0R6Tv+iUK5keUZhZLaZUe4YjP5M2rZ4wAdZ8iKV3EfTAoJxuL1aaWTSJKIilKa9QUEkCgnp89/32bw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -2330,6 +2380,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-gpu@5.0.70: resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==} @@ -2631,6 +2685,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + jsonwebtoken@9.0.2: resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} engines: {node: '>=12', npm: '>=6'} @@ -3308,6 +3365,11 @@ packages: resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==} engines: {node: '>=12.0.0'} + swr@2.3.8: + resolution: {integrity: sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + tailwind-merge@2.5.5: resolution: {integrity: sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==} @@ -3343,6 +3405,10 @@ packages: three@0.182.0: resolution: {integrity: sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ==} + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -3572,6 +3638,40 @@ packages: snapshots: + '@ai-sdk/gateway@3.0.22(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.5 + '@ai-sdk/provider-utils': 4.0.9(zod@3.25.76) + '@vercel/oidc': 3.1.0 + zod: 3.25.76 + + '@ai-sdk/google@3.0.13(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.5 + '@ai-sdk/provider-utils': 4.0.9(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/provider-utils@4.0.9(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.5 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 3.25.76 + + '@ai-sdk/provider@3.0.5': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/react@3.0.51(react@19.2.0)(zod@3.25.76)': + dependencies: + '@ai-sdk/provider-utils': 4.0.9(zod@3.25.76) + ai: 6.0.49(zod@3.25.76) + react: 19.2.0 + swr: 2.3.8(react@19.2.0) + throttleit: 2.1.0 + transitivePeerDependencies: + - zod + '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': @@ -4213,8 +4313,7 @@ snapshots: '@next/swc-win32-x64-msvc@16.0.10': optional: true - '@opentelemetry/api@1.9.0': - optional: true + '@opentelemetry/api@1.9.0': {} '@radix-ui/number@1.1.0': {} @@ -5237,6 +5336,8 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} + '@standard-schema/spec@1.1.0': {} + '@supabase/auth-js@2.80.0': dependencies: tslib: 2.8.1 @@ -5592,6 +5693,8 @@ snapshots: next: 16.0.10(@opentelemetry/api@1.9.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react: 19.2.0 + '@vercel/oidc@3.1.0': {} + '@webgpu/types@0.1.69': {} accepts@1.3.8: @@ -5606,6 +5709,14 @@ snapshots: acorn@8.15.0: {} + ai@6.0.49(zod@3.25.76): + dependencies: + '@ai-sdk/gateway': 3.0.22(zod@3.25.76) + '@ai-sdk/provider': 3.0.5 + '@ai-sdk/provider-utils': 4.0.9(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + zod: 3.25.76 + ajv-formats@3.0.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -6024,6 +6135,8 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + detect-gpu@5.0.70: dependencies: webgl-constants: 1.1.1 @@ -6329,6 +6442,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema@0.4.0: {} + jsonwebtoken@9.0.2: dependencies: jws: 3.2.2 @@ -7050,6 +7165,12 @@ snapshots: svg-pathdata@6.0.3: optional: true + swr@2.3.8(react@19.2.0): + dependencies: + dequal: 2.0.3 + react: 19.2.0 + use-sync-external-store: 1.6.0(react@19.2.0) + tailwind-merge@2.5.5: {} tailwindcss-animate@1.0.7(tailwindcss@4.1.9): @@ -7088,6 +7209,8 @@ snapshots: three@0.182.0: {} + throttleit@2.1.0: {} + tiny-invariant@1.3.3: {} tinyexec@1.0.2: {}