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...
- )} +
+ )}