diff --git a/apps/dokploy/components/dashboard/ai-chat/chat-panel.tsx b/apps/dokploy/components/dashboard/ai-chat/chat-panel.tsx new file mode 100644 index 000000000..41054de7e --- /dev/null +++ b/apps/dokploy/components/dashboard/ai-chat/chat-panel.tsx @@ -0,0 +1,410 @@ +"use client"; + +import { useChat } from "@ai-sdk/react"; +import type { ChatContext } from "@dokploy/server/utils/ai/chat-tools"; +import { DefaultChatTransport } from "ai"; +import { + Bot, + Check, + ChevronDown, + Loader2, + Send, + Trash2, + Wrench, + X, +} from "lucide-react"; +import { useRouter } from "next/router"; +import { useEffect, useMemo, useRef, useState } from "react"; +import ReactMarkdown from "react-markdown"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; +import { Textarea } from "@/components/ui/textarea"; +import { api } from "@/utils/api"; + +function useChatContext(): ChatContext { + const router = useRouter(); + const { query, pathname } = router; + + return useMemo(() => { + if (query.applicationId && typeof query.applicationId === "string") { + return { type: "application" as const, id: query.applicationId }; + } + if (query.composeId && typeof query.composeId === "string") { + return { type: "compose" as const, id: query.composeId }; + } + if (query.projectId && typeof query.projectId === "string") { + return { type: "project" as const, id: query.projectId }; + } + return { type: "general" as const, id: "" }; + }, [query.applicationId, query.composeId, query.projectId, pathname]); +} + +export function ChatPanel() { + const [open, setOpen] = useState(false); + const [aiId, setAiId] = useState(""); + const [input, setInput] = useState(""); + const scrollRef = useRef(null); + const context = useChatContext(); + const aiIdRef = useRef(aiId); + const contextRef = useRef(context); + aiIdRef.current = aiId; + contextRef.current = context; + + const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, { + refetchOnWindowFocus: false, + }); + + const enabledProviders = providers ?? []; + + const { messages, sendMessage, status, setMessages } = useChat({ + id: "dokploy-chat", + transport: new DefaultChatTransport({ + api: "/api/ai/chat", + body: () => ({ aiId: aiIdRef.current, context: contextRef.current }), + }), + }); + + const isLoading = status === "streaming" || status === "submitted"; + + useEffect(() => { + if (!aiId && enabledProviders.length > 0 && enabledProviders[0]) { + setAiId(enabledProviders[0].aiId); + } + }, [enabledProviders, aiId]); + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [messages, status]); + + if (enabledProviders.length === 0) return null; + + const handleSend = () => { + if (!input.trim() || !aiId || isLoading) return; + sendMessage({ text: input }); + setInput(""); + }; + + const contextLabel = + context.type === "general" + ? "General" + : context.type; + + // Check if the AI is currently in a tool-calling phase (no text yet, just tools) + const lastMessage = messages[messages.length - 1]; + const isThinking = + isLoading && + lastMessage?.role === "assistant" && + lastMessage.parts.every( + (p) => p.type !== "text" || !(p as { text?: string }).text?.trim(), + ); + + return ( + <> + + + + + +
+
+ + AI Assistant + {isLoading && ( + + thinking... + + )} +
+
+ + Chat with AI to manage your infrastructure + +
+ + + {contextLabel} + + {messages.length > 0 && ( + + )} +
+
+ +
+ {messages.length === 0 && ( +
+ +

+ Ask me anything about your{" "} + {context.type === "general" + ? "infrastructure" + : context.type} +

+
+ {(context.type === "application" + ? [ + "What's the status of this app?", + "Why did the last build fail?", + "Show me recent deployments", + "Redeploy this app", + ] + : context.type === "project" + ? [ + "How many services do I have?", + "Show me all environments", + "Which services are failing?", + ] + : [ + "List all my projects", + "Show project overview", + ] + ).map((suggestion) => ( + + ))} +
+
+ )} + + {messages.map((message) => { + if (message.role === "user") { + return ( +
+
+

+ {message.parts + .filter( + (p): p is { type: "text"; text: string } => + p.type === "text", + ) + .map((p) => p.text) + .join("")} +

+
+
+ ); + } + + // Assistant message + const toolParts = message.parts.filter( + (p) => p.type === "dynamic-tool", + ); + const textParts = message.parts.filter( + (p) => p.type === "text" && (p as { text?: string }).text?.trim(), + ); + + return ( +
+
+ {/* Tool calls section */} + {toolParts.length > 0 && ( +
+ {toolParts.map((part) => { + if (part.type !== "dynamic-tool") return null; + return ( + + ); + })} +
+ )} + + {/* Text response */} + {textParts.map((part, i) => { + if (part.type !== "text") return null; + const text = (part as { text: string }).text; + if (!text.trim()) return null; + return ( +
+ {text} +
+ ); + })} +
+
+ ); + })} + + {/* Loading indicator when waiting for first response */} + {isLoading && + lastMessage?.role === "user" && ( +
+
+ + Investigating... +
+
+ )} +
+ +
+