Skip to content

Commit c1b1937

Browse files
authored
Merge pull request #4183 from Dokploy/feat/ai-improvements
feat: add AI log analysis component and integrate into deployment views
2 parents 4a1b428 + 6c3578a commit c1b1937

17 files changed

Lines changed: 1042 additions & 242 deletions

File tree

apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import copy from "copy-to-clipboard";
22
import { Check, Copy, Loader2 } from "lucide-react";
33
import { useEffect, useRef, useState } from "react";
4+
import { AnalyzeLogs } from "@/components/dashboard/docker/logs/analyze-logs";
45
import { Badge } from "@/components/ui/badge";
56
import { Button } from "@/components/ui/button";
67
import { Checkbox } from "@/components/ui/checkbox";
@@ -165,6 +166,7 @@ export const ShowDeployment = ({
165166
<Copy className="h-3.5 w-3.5" />
166167
)}
167168
</Button>
169+
<AnalyzeLogs logs={filteredLogs} context="build" />
168170

169171
{serverId && (
170172
<div className="flex items-center space-x-2">
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
"use client";
2+
import { Bot, Loader2, RotateCcw, Settings, X } from "lucide-react";
3+
import Link from "next/link";
4+
import { useState } from "react";
5+
import ReactMarkdown from "react-markdown";
6+
import { toast } from "sonner";
7+
import { Button } from "@/components/ui/button";
8+
import {
9+
Popover,
10+
PopoverContent,
11+
PopoverTrigger,
12+
} from "@/components/ui/popover";
13+
import {
14+
Select,
15+
SelectContent,
16+
SelectItem,
17+
SelectTrigger,
18+
SelectValue,
19+
} from "@/components/ui/select";
20+
import { api } from "@/utils/api";
21+
import type { LogLine } from "./utils";
22+
23+
interface Props {
24+
logs: LogLine[];
25+
context: "build" | "runtime";
26+
}
27+
28+
const MAX_LOG_LINES = 200;
29+
30+
export function AnalyzeLogs({ logs, context }: Props) {
31+
const [open, setOpen] = useState(false);
32+
const [aiId, setAiId] = useState<string>("");
33+
const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, {
34+
enabled: open,
35+
});
36+
const { mutate, isPending, data, reset } = api.ai.analyzeLogs.useMutation({
37+
onError: (error) => {
38+
toast.error("Analysis failed", {
39+
description: error.message,
40+
});
41+
},
42+
});
43+
44+
const handleAnalyze = () => {
45+
if (!aiId || logs.length === 0) return;
46+
47+
const logsText = logs
48+
.slice(-MAX_LOG_LINES)
49+
.map((l) => l.message)
50+
.join("\n");
51+
52+
mutate({ aiId, logs: logsText, context });
53+
};
54+
55+
return (
56+
<Popover
57+
open={open}
58+
onOpenChange={(isOpen) => {
59+
setOpen(isOpen);
60+
if (!isOpen) {
61+
reset();
62+
setAiId("");
63+
}
64+
}}
65+
>
66+
<PopoverTrigger asChild>
67+
<Button
68+
variant="outline"
69+
size="sm"
70+
className="h-9"
71+
disabled={logs.length === 0}
72+
title="Analyze logs with AI"
73+
>
74+
<Bot className="mr-2 h-4 w-4" />
75+
AI
76+
</Button>
77+
</PopoverTrigger>
78+
<PopoverContent className="w-[550px] p-0" align="end">
79+
<div className="flex items-center justify-between border-b px-4 py-3">
80+
<div className="flex items-center gap-2">
81+
<Bot className="h-4 w-4" />
82+
<span className="text-sm font-medium">Log Analysis</span>
83+
</div>
84+
<Button
85+
variant="ghost"
86+
size="icon"
87+
className="h-6 w-6"
88+
onClick={() => setOpen(false)}
89+
>
90+
<X className="h-3.5 w-3.5" />
91+
</Button>
92+
</div>
93+
<div className="p-4 space-y-3">
94+
{!data?.analysis ? (
95+
providers && providers.length === 0 ? (
96+
<div className="flex flex-col items-center gap-3 py-2 text-center">
97+
<p className="text-sm text-muted-foreground">
98+
No AI providers configured. Set up a provider to start
99+
analyzing logs.
100+
</p>
101+
<Button size="sm" variant="outline" asChild>
102+
<Link href="/dashboard/settings/ai">
103+
<Settings className="mr-2 h-3.5 w-3.5" />
104+
Configure AI Provider
105+
</Link>
106+
</Button>
107+
</div>
108+
) : (
109+
<>
110+
<Select value={aiId} onValueChange={setAiId}>
111+
<SelectTrigger className="h-9 text-sm">
112+
<SelectValue placeholder="Select AI provider..." />
113+
</SelectTrigger>
114+
<SelectContent>
115+
{providers?.map((p) => (
116+
<SelectItem key={p.aiId} value={p.aiId}>
117+
{p.name} ({p.model})
118+
</SelectItem>
119+
))}
120+
</SelectContent>
121+
</Select>
122+
<Button
123+
size="sm"
124+
className="w-full"
125+
disabled={!aiId || isPending || logs.length === 0}
126+
onClick={handleAnalyze}
127+
>
128+
{isPending ? (
129+
<>
130+
<Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" />
131+
Analyzing...
132+
</>
133+
) : (
134+
<>
135+
<Bot className="mr-2 h-3.5 w-3.5" />
136+
Analyze{" "}
137+
{logs.length > MAX_LOG_LINES
138+
? `last ${MAX_LOG_LINES}`
139+
: logs.length}{" "}
140+
lines
141+
</>
142+
)}
143+
</Button>
144+
</>
145+
)
146+
) : (
147+
<>
148+
<div className="max-h-[400px] overflow-y-auto">
149+
<div className="prose prose-sm dark:prose-invert max-w-none text-sm break-words">
150+
<ReactMarkdown>{data.analysis}</ReactMarkdown>
151+
</div>
152+
</div>
153+
<div className="flex gap-2">
154+
<Button
155+
size="sm"
156+
variant="outline"
157+
className="flex-1"
158+
onClick={() => {
159+
reset();
160+
handleAnalyze();
161+
}}
162+
disabled={isPending}
163+
>
164+
{isPending ? (
165+
<Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" />
166+
) : (
167+
<RotateCcw className="mr-2 h-3.5 w-3.5" />
168+
)}
169+
Re-analyze
170+
</Button>
171+
<Button
172+
size="sm"
173+
variant="ghost"
174+
onClick={() => {
175+
reset();
176+
setAiId("");
177+
}}
178+
title="Change provider"
179+
>
180+
<X className="h-3.5 w-3.5" />
181+
</Button>
182+
</div>
183+
</>
184+
)}
185+
</div>
186+
</PopoverContent>
187+
</Popover>
188+
);
189+
}

apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { AlertBlock } from "@/components/shared/alert-block";
1212
import { Button } from "@/components/ui/button";
1313
import { Input } from "@/components/ui/input";
1414
import { api } from "@/utils/api";
15+
import { AnalyzeLogs } from "./analyze-logs";
1516
import { LineCountFilter } from "./line-count-filter";
1617
import { SinceLogsFilter, type TimeFilter } from "./since-logs-filter";
1718
import { StatusLogsFilter } from "./status-logs-filter";
@@ -377,6 +378,7 @@ export const DockerLogsId: React.FC<Props> = ({
377378
<DownloadIcon className="mr-2 h-4 w-4" />
378379
Download logs
379380
</Button>
381+
<AnalyzeLogs logs={filteredLogs} context="runtime" />
380382
</div>
381383
</div>
382384
{isPaused && (

apps/dokploy/components/dashboard/project/ai/template-generator.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,19 @@ export const TemplateGenerator = ({ environmentId }: Props) => {
298298
<div className="flex items-center justify-between w-full">
299299
<div className="flex items-center gap-2 w-full justify-end">
300300
<Button
301-
onClick={stepper.prev}
301+
onClick={() => {
302+
if (
303+
stepper.current.id === "variant" &&
304+
templateInfo.details
305+
) {
306+
setTemplateInfo((prev) => ({
307+
...prev,
308+
details: null,
309+
}));
310+
return;
311+
}
312+
stepper.prev();
313+
}}
302314
disabled={stepper.isFirst}
303315
variant="secondary"
304316
>

0 commit comments

Comments
 (0)