mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: add AI assistant to dokploy
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { TemplateGenerator } from "@/components/dashboard/project/ai/template-generator";
|
||||
|
||||
interface Props {
|
||||
projectId: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
export const AddAiAssistant = ({ projectId }: Props) => {
|
||||
return <TemplateGenerator projectId={projectId} />;
|
||||
};
|
||||
92
apps/dokploy/components/dashboard/project/ai/step-four.tsx
Normal file
92
apps/dokploy/components/dashboard/project/ai/step-four.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import dynamic from "next/dynamic";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
const MonacoEditor = dynamic(() => import("@monaco-editor/react"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export function StepFour({
|
||||
prevStep,
|
||||
templateInfo,
|
||||
setOpen,
|
||||
setTemplateInfo,
|
||||
}: any) {
|
||||
const handleSubmit = () => {
|
||||
setTemplateInfo(templateInfo); // Update the template info
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-grow">
|
||||
<div className="space-y-6 pb-20">
|
||||
<h2 className="text-lg font-semibold">Step 4: Review and Finalize</h2>
|
||||
<ScrollArea className="h-[400px] p-5">
|
||||
<div className="space-y-4">
|
||||
<div className="p-4">
|
||||
<ReactMarkdown className="prose dark:prose-invert">
|
||||
{templateInfo.details.description}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-md font-semibold">Name</h3>
|
||||
<p>{templateInfo.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-md font-semibold">Server</h3>
|
||||
<p>{templateInfo.server || "localhost"}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-md font-semibold">Docker Compose</h3>
|
||||
<MonacoEditor
|
||||
height="200px"
|
||||
language="yaml"
|
||||
theme="vs-dark"
|
||||
value={templateInfo.details.dockerCompose}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
fontSize: 14,
|
||||
lineNumbers: "on",
|
||||
readOnly: true,
|
||||
wordWrap: "on",
|
||||
automaticLayout: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-md font-semibold">Environment Variables</h3>
|
||||
<ul className="list-disc pl-5">
|
||||
{templateInfo.details.envVariables.map(
|
||||
(
|
||||
env: {
|
||||
name: string;
|
||||
value: string;
|
||||
},
|
||||
index: number,
|
||||
) => (
|
||||
<li key={index}>
|
||||
<strong>{env.name}</strong>:
|
||||
<span className="ml-2 font-mono">{env.value}</span>
|
||||
</li>
|
||||
),
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sticky bottom-0 bg-background pt-2 border-t">
|
||||
<div className="flex justify-between">
|
||||
<Button onClick={prevStep} variant="outline">
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={handleSubmit}>Create</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
69
apps/dokploy/components/dashboard/project/ai/step-one.tsx
Normal file
69
apps/dokploy/components/dashboard/project/ai/step-one.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useState } from "react";
|
||||
|
||||
const examples = [
|
||||
"Make a personal blog",
|
||||
"Add a photo studio portfolio",
|
||||
"Create a personal ad blocker",
|
||||
"Build a social media dashboard",
|
||||
"Sendgrid service opensource analogue",
|
||||
];
|
||||
|
||||
export function StepOne({ nextStep, setTemplateInfo, templateInfo }: any) {
|
||||
const [userInput, setUserInput] = useState(templateInfo.userInput);
|
||||
|
||||
const handleNext = () => {
|
||||
setTemplateInfo({ ...templateInfo, userInput });
|
||||
nextStep();
|
||||
};
|
||||
|
||||
const handleExampleClick = (example: string) => {
|
||||
setUserInput(example);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-grow overflow-auto">
|
||||
<div className="space-y-4 pb-20">
|
||||
<h2 className="text-lg font-semibold">Step 1: Describe Your Needs</h2>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="user-needs">Describe your template needs</Label>
|
||||
<Textarea
|
||||
id="user-needs"
|
||||
placeholder="Describe the type of template you need, its purpose, and any specific features you'd like to include."
|
||||
value={userInput}
|
||||
onChange={(e) => setUserInput(e.target.value)}
|
||||
className="min-h-[100px]"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Examples:</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{examples.map((example, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleExampleClick(example)}
|
||||
>
|
||||
{example}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sticky bottom-0 bg-background pt-2 border-t">
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleNext} disabled={!userInput.trim()}>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
84
apps/dokploy/components/dashboard/project/ai/step-three.tsx
Normal file
84
apps/dokploy/components/dashboard/project/ai/step-three.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { api } from "@/utils/api";
|
||||
import { useState } from "react";
|
||||
|
||||
export function StepThree({
|
||||
nextStep,
|
||||
prevStep,
|
||||
templateInfo,
|
||||
setTemplateInfo,
|
||||
}: any) {
|
||||
const [name, setName] = useState(templateInfo.name);
|
||||
const [server, setServer] = useState(templateInfo.server);
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
|
||||
const handleNext = () => {
|
||||
const updatedInfo = { ...templateInfo, name };
|
||||
if (server?.trim()) {
|
||||
updatedInfo.server = server;
|
||||
}
|
||||
setTemplateInfo(updatedInfo);
|
||||
nextStep();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-grow overflow-auto">
|
||||
<div className="space-y-4 pb-20">
|
||||
<h2 className="text-lg font-semibold">Step 3: Additional Details</h2>
|
||||
<div>
|
||||
<Label htmlFor="name">App Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter app name"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="server">Server Attachment (Optional)</Label>
|
||||
<Select value={server} onValueChange={setServer}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a Server" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{servers?.map((server) => (
|
||||
<SelectItem key={server.serverId} value={server.serverId}>
|
||||
{server.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sticky bottom-0 bg-background pt-2 border-t">
|
||||
<div className="flex justify-between">
|
||||
<Button onClick={prevStep} variant="outline">
|
||||
Back
|
||||
</Button>
|
||||
<Button onClick={handleNext} disabled={!name.trim()}>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
326
apps/dokploy/components/dashboard/project/ai/step-two.tsx
Normal file
326
apps/dokploy/components/dashboard/project/ai/step-two.tsx
Normal file
@@ -0,0 +1,326 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { api } from "@/utils/api";
|
||||
import { Bot, Eye, EyeOff, PlusCircle, Trash2 } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useEffect, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const MonacoEditor = dynamic(() => import("@monaco-editor/react"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
interface EnvVariable {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface TemplateInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
shortDescription: string;
|
||||
description: string;
|
||||
dockerCompose: string;
|
||||
envVariables: EnvVariable[];
|
||||
}
|
||||
|
||||
export function StepTwo({
|
||||
nextStep,
|
||||
prevStep,
|
||||
templateInfo,
|
||||
setTemplateInfo,
|
||||
}: any) {
|
||||
const [suggestions, setSuggestions] = useState<Array<TemplateInfo>>([]);
|
||||
const [selectedVariant, setSelectedVariant] = useState("");
|
||||
const [dockerCompose, setDockerCompose] = useState("");
|
||||
const [envVariables, setEnvVariables] = useState<Array<EnvVariable>>([]);
|
||||
const [showValues, setShowValues] = useState<Record<string, boolean>>({});
|
||||
|
||||
const { mutateAsync, isLoading } = api.ai.suggest.useMutation();
|
||||
|
||||
useEffect(() => {
|
||||
mutateAsync(templateInfo.userInput)
|
||||
.then((data) => {
|
||||
setSuggestions(data);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating AI settings");
|
||||
});
|
||||
}, [templateInfo.userInput]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedVariant) {
|
||||
const selected = suggestions.find(
|
||||
(s: { id: string }) => s.id === selectedVariant,
|
||||
);
|
||||
if (selected) {
|
||||
setDockerCompose(selected.dockerCompose);
|
||||
setEnvVariables(selected.envVariables);
|
||||
setShowValues(
|
||||
selected.envVariables.reduce((acc: Record<string, boolean>, env) => {
|
||||
acc[env.name] = false;
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [selectedVariant, suggestions]);
|
||||
|
||||
const handleNext = () => {
|
||||
const selected = suggestions.find(
|
||||
(s: { id: string }) => s.id === selectedVariant,
|
||||
);
|
||||
if (selected) {
|
||||
setTemplateInfo({
|
||||
...templateInfo,
|
||||
type: selectedVariant,
|
||||
details: {
|
||||
...selected,
|
||||
dockerCompose,
|
||||
envVariables,
|
||||
},
|
||||
});
|
||||
}
|
||||
nextStep();
|
||||
};
|
||||
|
||||
const handleEnvVariableChange = (
|
||||
index: number,
|
||||
field: string,
|
||||
value: string,
|
||||
) => {
|
||||
const updatedEnvVariables = [...envVariables];
|
||||
if (updatedEnvVariables[index]) {
|
||||
updatedEnvVariables[index] = {
|
||||
...updatedEnvVariables[index],
|
||||
[field]: value,
|
||||
};
|
||||
setEnvVariables(updatedEnvVariables);
|
||||
}
|
||||
};
|
||||
|
||||
const addEnvVariable = () => {
|
||||
setEnvVariables([...envVariables, { name: "", value: "" }]);
|
||||
setShowValues((prev) => ({ ...prev, "": false }));
|
||||
};
|
||||
|
||||
const removeEnvVariable = (index: number) => {
|
||||
const updatedEnvVariables = envVariables.filter((_, i) => i !== index);
|
||||
setEnvVariables(updatedEnvVariables);
|
||||
};
|
||||
|
||||
const toggleShowValue = (name: string) => {
|
||||
setShowValues((prev) => ({ ...prev, [name]: !prev[name] }));
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full space-y-4">
|
||||
<Bot className="w-16 h-16 text-primary animate-pulse" />
|
||||
<h2 className="text-2xl font-semibold animate-pulse">
|
||||
AI is processing your request
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Generating template suggestions based on your input...
|
||||
</p>
|
||||
<pre>{templateInfo.userInput}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const selectedTemplate = suggestions.find(
|
||||
(s: { id: string }) => s.id === selectedVariant,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-grow overflow-auto">
|
||||
<div className="space-y-6 pb-20">
|
||||
<h2 className="text-lg font-semibold">Step 2: Choose a Variant</h2>
|
||||
{!selectedVariant && (
|
||||
<div className="space-y-4">
|
||||
<div>Based on your input, we suggest the following variants:</div>
|
||||
<RadioGroup
|
||||
value={selectedVariant}
|
||||
onValueChange={setSelectedVariant}
|
||||
className="space-y-4"
|
||||
>
|
||||
{suggestions.map((suggestion) => (
|
||||
<div
|
||||
key={suggestion.id}
|
||||
className="flex items-start space-x-3"
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={suggestion.id}
|
||||
id={suggestion.id}
|
||||
className="mt-1"
|
||||
/>
|
||||
<div>
|
||||
<Label htmlFor={suggestion.id} className="font-medium">
|
||||
{suggestion.name}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{suggestion.shortDescription}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
)}
|
||||
{selectedVariant && (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<h3 className="text-xl font-bold">{selectedTemplate?.name}</h3>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
{selectedTemplate?.shortDescription}
|
||||
</p>
|
||||
</div>
|
||||
<ScrollArea className="h-[400px] p-5">
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="description">
|
||||
<AccordionTrigger>Description</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<ScrollArea className="h-[300px] w-full rounded-md border">
|
||||
<div className="p-4">
|
||||
<ReactMarkdown className="prose dark:prose-invert">
|
||||
{selectedTemplate?.description}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="docker-compose">
|
||||
<AccordionTrigger>Docker Compose</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="h-[400px] w-full rounded-md border overflow-hidden">
|
||||
<MonacoEditor
|
||||
height="100%"
|
||||
language="yaml"
|
||||
theme="vs-dark"
|
||||
value={dockerCompose}
|
||||
onChange={(value) =>
|
||||
setDockerCompose(value as string)
|
||||
}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
fontSize: 14,
|
||||
lineNumbers: "on",
|
||||
readOnly: false,
|
||||
wordWrap: "on",
|
||||
automaticLayout: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="env-variables">
|
||||
<AccordionTrigger>Environment Variables</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<ScrollArea className="h-[300px] w-full rounded-md border">
|
||||
<div className="p-4 space-y-4">
|
||||
{envVariables.map((env, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Input
|
||||
value={env.name}
|
||||
onChange={(e) =>
|
||||
handleEnvVariableChange(
|
||||
index,
|
||||
"name",
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
placeholder="Variable Name"
|
||||
className="flex-1"
|
||||
/>
|
||||
<div className="flex-1 relative">
|
||||
<Input
|
||||
type={
|
||||
showValues[env.name] ? "text" : "password"
|
||||
}
|
||||
value={env.value}
|
||||
onChange={(e) =>
|
||||
handleEnvVariableChange(
|
||||
index,
|
||||
"value",
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
placeholder="Variable Value"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-2 top-1/2 transform -translate-y-1/2"
|
||||
onClick={() => toggleShowValue(env.name)}
|
||||
>
|
||||
{showValues[env.name] ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeEnvVariable(index)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-2"
|
||||
onClick={addEnvVariable}
|
||||
>
|
||||
<PlusCircle className="h-4 w-4 mr-2" />
|
||||
Add Variable
|
||||
</Button>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</ScrollArea>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="sticky bottom-0 bg-background pt-2 border-t">
|
||||
<div className="flex justify-between">
|
||||
<Button
|
||||
onClick={() =>
|
||||
selectedVariant ? setSelectedVariant("") : prevStep()
|
||||
}
|
||||
variant="outline"
|
||||
>
|
||||
{selectedVariant ? "Change Variant" : "Back"}
|
||||
</Button>
|
||||
<Button onClick={handleNext} disabled={!selectedVariant}>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
import { api } from "@/utils/api";
|
||||
import { AlertCircle, Bot } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { StepFour } from "./step-four";
|
||||
import { StepOne } from "./step-one";
|
||||
import { StepThree } from "./step-three";
|
||||
import { StepTwo } from "./step-two";
|
||||
|
||||
const emptyState = {
|
||||
userInput: "",
|
||||
type: "",
|
||||
details: {
|
||||
id: "",
|
||||
dockerCompose: "",
|
||||
envVariables: [],
|
||||
shortDescription: "",
|
||||
},
|
||||
name: "",
|
||||
server: undefined,
|
||||
description: "",
|
||||
};
|
||||
|
||||
interface Props {
|
||||
projectId: string;
|
||||
projectName?: string;
|
||||
}
|
||||
|
||||
export function TemplateGenerator({ projectId }: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [step, setStep] = useState(1);
|
||||
const { data: aiSettings } = api.ai.get.useQuery();
|
||||
const { mutateAsync } = api.ai.deploy.useMutation();
|
||||
const [templateInfo, setTemplateInfo] = useState(emptyState);
|
||||
const utils = api.useUtils();
|
||||
|
||||
const totalSteps = 4;
|
||||
|
||||
const nextStep = () => setStep((prev) => Math.min(prev + 1, totalSteps));
|
||||
const prevStep = () => setStep((prev) => Math.max(prev - 1, 1));
|
||||
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
setOpen(newOpen);
|
||||
if (!newOpen) {
|
||||
// Reset to the first step when closing the dialog
|
||||
setStep(1);
|
||||
setTemplateInfo(emptyState);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogTrigger asChild className="w-full">
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer space-x-3"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
<Bot className="size-4 text-muted-foreground" />
|
||||
<span>AI Assistant</span>
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-[800px] w-full max-h-[90vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>AI Assistant</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a custom template based on your needs
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="mt-4 flex-grow overflow-auto">
|
||||
{step === 1 && (
|
||||
<>
|
||||
{(!aiSettings || !aiSettings?.isEnabled) && (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>AI features are not enabled</AlertTitle>
|
||||
<AlertDescription>
|
||||
To use AI-powered template generation, please{" "}
|
||||
<a
|
||||
href="/dashboard/settings/ai"
|
||||
className="font-medium underline underline-offset-4"
|
||||
>
|
||||
enable AI in your settings
|
||||
</a>
|
||||
.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{!!aiSettings && !!aiSettings?.isEnabled && (
|
||||
<StepOne
|
||||
nextStep={nextStep}
|
||||
setTemplateInfo={setTemplateInfo}
|
||||
templateInfo={templateInfo}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{step === 2 && (
|
||||
<StepTwo
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
templateInfo={templateInfo}
|
||||
setTemplateInfo={setTemplateInfo}
|
||||
/>
|
||||
)}
|
||||
{step === 3 && (
|
||||
<StepThree
|
||||
nextStep={nextStep}
|
||||
prevStep={prevStep}
|
||||
templateInfo={templateInfo}
|
||||
setTemplateInfo={setTemplateInfo}
|
||||
/>
|
||||
)}
|
||||
{step === 4 && (
|
||||
<StepFour
|
||||
prevStep={prevStep}
|
||||
templateInfo={templateInfo}
|
||||
setTemplateInfo={async (data: any) => {
|
||||
console.log("Submitting template:", data);
|
||||
setTemplateInfo(data);
|
||||
await mutateAsync({
|
||||
projectId,
|
||||
id: templateInfo.details?.id,
|
||||
name: templateInfo.name,
|
||||
description: data.details.shortDescription,
|
||||
dockerCompose: data.details.dockerCompose,
|
||||
envVariables: (data.details?.envVariables || [])
|
||||
.map((env: any) => `${env.name}=${env.value}`)
|
||||
.join("\n"),
|
||||
serverId: data.server,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Compose Created");
|
||||
setOpen(false);
|
||||
await utils.project.one.invalidate({
|
||||
projectId,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error creating the compose");
|
||||
});
|
||||
}}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user