From cee2e9f002cb49fbb8c520927d85e83ee7034032 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Sat, 11 Apr 2026 22:15:58 -0600 Subject: [PATCH] feat: enhance AI chat functionality and API integration - Updated the AI chat panel to support multiple service types, including applications, databases, and more, improving context handling. - Implemented local storage for chat messages, allowing users to retain their chat history. - Enhanced API integration by adding new endpoints for reading deployment build logs and creating tools from OpenAPI specifications. - Improved error handling and user experience in the chat interface, ensuring smoother interactions. These changes significantly enhance the AI chat capabilities and overall user experience within the Dokploy platform. --- .../dashboard/ai-chat/chat-panel.tsx | 395 +- apps/dokploy/pages/api/ai/chat.ts | 206 +- apps/dokploy/server/api/routers/deployment.ts | 39 + openapi.json | 54248 +++++++++------- packages/server/src/utils/ai/api-tool.ts | 398 + packages/server/src/utils/ai/chat-tools.ts | 15 +- packages/server/src/utils/ai/openapi-tools.ts | 317 + pnpm-lock.yaml | 56 +- 8 files changed, 33003 insertions(+), 22671 deletions(-) create mode 100644 packages/server/src/utils/ai/api-tool.ts create mode 100644 packages/server/src/utils/ai/openapi-tools.ts diff --git a/apps/dokploy/components/dashboard/ai-chat/chat-panel.tsx b/apps/dokploy/components/dashboard/ai-chat/chat-panel.tsx index 41054de7e..6fa89b537 100644 --- a/apps/dokploy/components/dashboard/ai-chat/chat-panel.tsx +++ b/apps/dokploy/components/dashboard/ai-chat/chat-panel.tsx @@ -45,17 +45,68 @@ function useChatContext(): ChatContext { 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 }; + const projectId = + typeof query.projectId === "string" ? query.projectId : undefined; + const environmentId = + typeof query.environmentId === "string" + ? query.environmentId + : undefined; + const serverId = + typeof query.serverId === "string" ? query.serverId : undefined; + + const serviceParams = [ + { key: "applicationId", type: "application" }, + { key: "composeId", type: "compose" }, + { key: "postgresId", type: "postgres" }, + { key: "mysqlId", type: "mysql" }, + { key: "redisId", type: "redis" }, + { key: "mongoId", type: "mongo" }, + { key: "mariadbId", type: "mariadb" }, + { key: "libsqlId", type: "libsql" }, + ] as const; + + for (const { key, type } of serviceParams) { + if (query[key] && typeof query[key] === "string") { + return { + type, + id: query[key] as string, + projectId, + environmentId, + serverId, + }; + } } + if (query.projectId && typeof query.projectId === "string") { - return { type: "project" as const, id: query.projectId }; + return { + type: "project" as const, + id: query.projectId, + projectId, + environmentId, + serverId, + }; } - return { type: "general" as const, id: "" }; - }, [query.applicationId, query.composeId, query.projectId, pathname]); + return { + type: "general" as const, + id: "", + projectId, + environmentId, + serverId, + }; + }, [ + query.applicationId, + query.composeId, + query.postgresId, + query.mysqlId, + query.redisId, + query.mongoId, + query.mariadbId, + query.libsqlId, + query.projectId, + query.environmentId, + query.serverId, + pathname, + ]); } export function ChatPanel() { @@ -63,33 +114,65 @@ export function ChatPanel() { const [aiId, setAiId] = useState(""); const [input, setInput] = useState(""); const scrollRef = useRef(null); + const inputRef = useRef(null); const context = useChatContext(); const aiIdRef = useRef(aiId); const contextRef = useRef(context); aiIdRef.current = aiId; contextRef.current = context; + const { data: isCloud } = api.settings.isCloud.useQuery(undefined, { + refetchOnWindowFocus: false, + }); + const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, { refetchOnWindowFocus: false, + enabled: !isCloud, }); 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 STORAGE_KEY = "dokploy-chat-messages"; + + const { messages, sendMessage, status, setMessages, addToolApprovalResponse } = useChat({ + id: "dokploy-chat", + transport: new DefaultChatTransport({ + api: "/api/ai/chat", + body: () => ({ + ...(isCloud ? {} : { aiId: aiIdRef.current }), + context: contextRef.current, + }), + }), + initialMessages: () => { + try { + const stored = localStorage.getItem(STORAGE_KEY); + return stored ? JSON.parse(stored) : []; + } catch { + return []; + } + }, + }); const isLoading = status === "streaming" || status === "submitted"; + // Persist messages to localStorage useEffect(() => { - if (!aiId && enabledProviders.length > 0 && enabledProviders[0]) { + if (messages.length > 0) { + try { + // Keep only last 50 messages to avoid localStorage bloat + const toStore = messages.slice(-50); + localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore)); + } catch { + // localStorage full or unavailable — ignore + } + } + }, [messages]); + + useEffect(() => { + if (!isCloud && !aiId && enabledProviders.length > 0 && enabledProviders[0]) { setAiId(enabledProviders[0].aiId); } - }, [enabledProviders, aiId]); + }, [enabledProviders, aiId, isCloud]); useEffect(() => { if (scrollRef.current) { @@ -97,33 +180,27 @@ export function ChatPanel() { } }, [messages, status]); - if (enabledProviders.length === 0) return null; + if (!isCloud && enabledProviders.length === 0) return null; const handleSend = () => { - if (!input.trim() || !aiId || isLoading) return; + if (!input.trim() || isLoading) return; + if (!isCloud && !aiId) return; sendMessage({ text: input }); setInput(""); + setTimeout(() => inputRef.current?.focus(), 0); }; const contextLabel = - context.type === "general" - ? "General" - : context.type; + 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 ( <> + + +
+ {(part as any).text || + (part as any).reasoning} +
+
+ + ); + } + + return null; })} ); })} - {/* Loading indicator when waiting for first response */} - {isLoading && - lastMessage?.role === "user" && ( -
-
- - Investigating... -
+ {isLoading && lastMessage?.role === "user" && ( +
+
+ + Investigating...
- )} +
+ )}