From e508f3143fcf8d1fe407fe743a61d8ca477d6941 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Thu, 9 Apr 2026 11:19:42 -0600 Subject: [PATCH] feat: implement AI chat panel and logging features - Added a new ChatPanel component for AI interactions, allowing users to chat with an AI assistant for infrastructure management. - Integrated the ChatPanel into the dashboard layout for easy access. - Created a new API endpoint for AI chat functionality, enabling dynamic interactions based on user context. - Implemented log reading capabilities across various services (application, compose, libsql, mariadb, mongo, mysql, postgres, redis) to enhance troubleshooting. - Introduced utility functions for fetching container logs, improving the overall user experience in managing deployments. This feature enriches the user interface by providing real-time AI assistance and log analysis, streamlining operational workflows. --- .../dashboard/ai-chat/chat-panel.tsx | 410 ++++++++++++++++++ .../components/layouts/dashboard-layout.tsx | 2 + apps/dokploy/package.json | 1 + apps/dokploy/pages/api/ai/chat.ts | 104 +++++ .../dokploy/server/api/routers/application.ts | 36 ++ apps/dokploy/server/api/routers/compose.ts | 42 +- apps/dokploy/server/api/routers/libsql.ts | 38 +- apps/dokploy/server/api/routers/mariadb.ts | 36 ++ apps/dokploy/server/api/routers/mongo.ts | 36 ++ apps/dokploy/server/api/routers/mysql.ts | 36 ++ apps/dokploy/server/api/routers/postgres.ts | 38 +- apps/dokploy/server/api/routers/redis.ts | 36 ++ packages/server/src/services/docker.ts | 35 ++ packages/server/src/utils/ai/chat-tools.ts | 306 +++++++++++++ pnpm-lock.yaml | 76 ++++ 15 files changed, 1229 insertions(+), 3 deletions(-) create mode 100644 apps/dokploy/components/dashboard/ai-chat/chat-panel.tsx create mode 100644 apps/dokploy/pages/api/ai/chat.ts create mode 100644 packages/server/src/utils/ai/chat-tools.ts 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... +
+
+ )} +
+ +
+