mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
refactor: add many AI providers & improve prompt
This commit is contained in:
@@ -2,13 +2,14 @@ import { CodeEditor } from "@/components/shared/code-editor";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import type { StepProps } from "./step-two";
|
||||||
|
|
||||||
export const StepFour = ({
|
export const StepFour = ({
|
||||||
prevStep,
|
prevStep,
|
||||||
templateInfo,
|
templateInfo,
|
||||||
setOpen,
|
setOpen,
|
||||||
setTemplateInfo,
|
setTemplateInfo,
|
||||||
}: any) => {
|
}: StepProps) => {
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
setTemplateInfo(templateInfo); // Update the template info
|
setTemplateInfo(templateInfo); // Update the template info
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -17,36 +18,41 @@ export const StepFour = ({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
<div className="space-y-6 pb-20">
|
<div className="space-y-6">
|
||||||
<h2 className="text-lg font-semibold">Step 4: Review and Finalize</h2>
|
<h2 className="text-lg font-semibold">Step 4: Review and Finalize</h2>
|
||||||
<ScrollArea className="h-[400px] p-5">
|
<ScrollArea className="h-[400px] p-5">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="p-4">
|
<div>
|
||||||
<ReactMarkdown className="prose dark:prose-invert">
|
<h3 className="text-sm font-semibold">Name</h3>
|
||||||
{templateInfo.details.description}
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{templateInfo?.details?.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold">Description</h3>
|
||||||
|
<ReactMarkdown className="text-sm text-muted-foreground">
|
||||||
|
{templateInfo?.details?.description}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<h3 className="text-md font-semibold">Name</h3>
|
|
||||||
<p>{templateInfo.name}</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-md font-semibold">Server</h3>
|
<h3 className="text-md font-semibold">Server</h3>
|
||||||
<p>{templateInfo.server || "localhost"}</p>
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{templateInfo?.serverId || "Dokploy Server"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-md font-semibold">Docker Compose</h3>
|
<h3 className="text-sm font-semibold">Docker Compose</h3>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
lineWrapping
|
lineWrapping
|
||||||
value={templateInfo.details.dockerCompose}
|
value={templateInfo?.details?.dockerCompose}
|
||||||
disabled
|
disabled
|
||||||
className="font-mono"
|
className="font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-md font-semibold">Environment Variables</h3>
|
<h3 className="text-sm font-semibold">Environment Variables</h3>
|
||||||
<ul className="list-disc pl-5">
|
<ul className="list-disc pl-5">
|
||||||
{templateInfo.details.envVariables.map(
|
{templateInfo?.details?.envVariables.map(
|
||||||
(
|
(
|
||||||
env: {
|
env: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -55,8 +61,13 @@ export const StepFour = ({
|
|||||||
index: number,
|
index: number,
|
||||||
) => (
|
) => (
|
||||||
<li key={index}>
|
<li key={index}>
|
||||||
<strong>{env.name}</strong>:
|
<strong className="text-sm font-semibold">
|
||||||
<span className="ml-2 font-mono">{env.value}</span>
|
{env.name}
|
||||||
|
</strong>
|
||||||
|
:
|
||||||
|
<span className="text-sm ml-2 text-muted-foreground">
|
||||||
|
{env.value}
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -14,21 +14,21 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import type { StepProps } from "./step-two";
|
||||||
|
|
||||||
export const StepThree = ({
|
export const StepThree = ({
|
||||||
nextStep,
|
nextStep,
|
||||||
prevStep,
|
prevStep,
|
||||||
templateInfo,
|
templateInfo,
|
||||||
setTemplateInfo,
|
setTemplateInfo,
|
||||||
}: any) => {
|
}: StepProps) => {
|
||||||
const [name, setName] = useState(templateInfo.name);
|
const [server, setServer] = useState(templateInfo.serverId);
|
||||||
const [server, setServer] = useState(templateInfo.server);
|
|
||||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
const updatedInfo = { ...templateInfo, name };
|
const updatedInfo = { ...templateInfo, name };
|
||||||
if (server?.trim()) {
|
if (server?.trim()) {
|
||||||
updatedInfo.server = server;
|
updatedInfo.serverId = server;
|
||||||
}
|
}
|
||||||
setTemplateInfo(updatedInfo);
|
setTemplateInfo(updatedInfo);
|
||||||
nextStep();
|
nextStep();
|
||||||
@@ -43,8 +43,16 @@ export const StepThree = ({
|
|||||||
<Label htmlFor="name">App Name</Label>
|
<Label htmlFor="name">App Name</Label>
|
||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
value={name}
|
value={templateInfo?.details?.name || ""}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setTemplateInfo({
|
||||||
|
...templateInfo,
|
||||||
|
details: {
|
||||||
|
...templateInfo?.details,
|
||||||
|
name: e.target.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
placeholder="Enter app name"
|
placeholder="Enter app name"
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
@@ -74,7 +82,10 @@ export const StepThree = ({
|
|||||||
<Button onClick={prevStep} variant="outline">
|
<Button onClick={prevStep} variant="outline">
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleNext} disabled={!name.trim()}>
|
<Button
|
||||||
|
onClick={handleNext}
|
||||||
|
disabled={!templateInfo?.details?.name?.trim()}
|
||||||
|
>
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,19 +15,13 @@ import { Bot, Eye, EyeOff, PlusCircle, Trash2 } from "lucide-react";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import type { TemplateInfo } from "./template-generator";
|
||||||
|
|
||||||
interface EnvVariable {
|
export interface StepProps {
|
||||||
name: string;
|
nextStep: () => void;
|
||||||
value: string;
|
prevStep: () => void;
|
||||||
}
|
templateInfo: TemplateInfo;
|
||||||
|
setTemplateInfo: React.Dispatch<React.SetStateAction<TemplateInfo>>;
|
||||||
interface TemplateInfo {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
shortDescription: string;
|
|
||||||
description: string;
|
|
||||||
dockerCompose: string;
|
|
||||||
envVariables: EnvVariable[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StepTwo = ({
|
export const StepTwo = ({
|
||||||
@@ -35,56 +29,34 @@ export const StepTwo = ({
|
|||||||
prevStep,
|
prevStep,
|
||||||
templateInfo,
|
templateInfo,
|
||||||
setTemplateInfo,
|
setTemplateInfo,
|
||||||
}: any) => {
|
}: StepProps) => {
|
||||||
const [suggestions, setSuggestions] = useState<Array<TemplateInfo>>([]);
|
const [suggestions, setSuggestions] = useState<TemplateInfo["details"][]>();
|
||||||
const [selectedVariant, setSelectedVariant] = useState("");
|
const [selectedVariant, setSelectedVariant] =
|
||||||
const [dockerCompose, setDockerCompose] = useState("");
|
useState<TemplateInfo["details"]>();
|
||||||
const [envVariables, setEnvVariables] = useState<Array<EnvVariable>>([]);
|
|
||||||
const [showValues, setShowValues] = useState<Record<string, boolean>>({});
|
const [showValues, setShowValues] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = api.ai.suggest.useMutation();
|
const { mutateAsync, isLoading } = api.ai.suggest.useMutation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
mutateAsync(templateInfo.userInput)
|
mutateAsync({
|
||||||
|
aiId: templateInfo.aiId,
|
||||||
|
prompt: templateInfo.userInput,
|
||||||
|
})
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
console.log(data);
|
||||||
setSuggestions(data);
|
setSuggestions(data);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((error) => {
|
||||||
toast.error("Error updating AI settings");
|
console.error("Error details:", error);
|
||||||
|
toast.error("Error generating suggestions");
|
||||||
});
|
});
|
||||||
}, [templateInfo.userInput]);
|
}, [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 handleNext = () => {
|
||||||
const selected = suggestions.find(
|
if (selectedVariant) {
|
||||||
(s: { id: string }) => s.id === selectedVariant,
|
|
||||||
);
|
|
||||||
if (selected) {
|
|
||||||
setTemplateInfo({
|
setTemplateInfo({
|
||||||
...templateInfo,
|
...templateInfo,
|
||||||
type: selectedVariant,
|
details: selectedVariant,
|
||||||
details: {
|
|
||||||
...selected,
|
|
||||||
dockerCompose,
|
|
||||||
envVariables,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
nextStep();
|
nextStep();
|
||||||
@@ -95,25 +67,49 @@ export const StepTwo = ({
|
|||||||
field: string,
|
field: string,
|
||||||
value: string,
|
value: string,
|
||||||
) => {
|
) => {
|
||||||
const updatedEnvVariables = [...envVariables];
|
// const updatedEnvVariables = [...envVariables];
|
||||||
if (updatedEnvVariables[index]) {
|
// if (updatedEnvVariables[index]) {
|
||||||
updatedEnvVariables[index] = {
|
// updatedEnvVariables[index] = {
|
||||||
...updatedEnvVariables[index],
|
// ...updatedEnvVariables[index],
|
||||||
[field]: value,
|
// [field]: value,
|
||||||
};
|
// };
|
||||||
setEnvVariables(updatedEnvVariables);
|
// setEnvVariables(updatedEnvVariables);
|
||||||
}
|
// }
|
||||||
};
|
};
|
||||||
|
|
||||||
const addEnvVariable = () => {
|
// const addEnvVariable = () => {
|
||||||
setEnvVariables([...envVariables, { name: "", value: "" }]);
|
// setEnvVariables([...envVariables, { name: "", value: "" }]);
|
||||||
setShowValues((prev) => ({ ...prev, "": false }));
|
// setShowValues((prev) => ({ ...prev, "": false }));
|
||||||
};
|
// };
|
||||||
|
|
||||||
const removeEnvVariable = (index: number) => {
|
// const removeEnvVariable = (index: number) => {
|
||||||
const updatedEnvVariables = envVariables.filter((_, i) => i !== index);
|
// const updatedEnvVariables = envVariables.filter((_, i) => i !== index);
|
||||||
setEnvVariables(updatedEnvVariables);
|
// setEnvVariables(updatedEnvVariables);
|
||||||
};
|
// };
|
||||||
|
|
||||||
|
// const handleDomainChange = (
|
||||||
|
// index: number,
|
||||||
|
// field: string,
|
||||||
|
// value: string | number,
|
||||||
|
// ) => {
|
||||||
|
// const updatedDomains = [...domains];
|
||||||
|
// if (updatedDomains[index]) {
|
||||||
|
// updatedDomains[index] = {
|
||||||
|
// ...updatedDomains[index],
|
||||||
|
// [field]: value,
|
||||||
|
// };
|
||||||
|
// setDomains(updatedDomains);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const addDomain = () => {
|
||||||
|
// setDomains([...domains, { host: "", port: 0, serviceName: "" }]);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const removeDomain = (index: number) => {
|
||||||
|
// const updatedDomains = domains.filter((_, i) => i !== index);
|
||||||
|
// setDomains(updatedDomains);
|
||||||
|
// };
|
||||||
|
|
||||||
const toggleShowValue = (name: string) => {
|
const toggleShowValue = (name: string) => {
|
||||||
setShowValues((prev) => ({ ...prev, [name]: !prev[name] }));
|
setShowValues((prev) => ({ ...prev, [name]: !prev[name] }));
|
||||||
@@ -134,39 +130,38 @@ export const StepTwo = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedTemplate = suggestions.find(
|
|
||||||
(s: { id: string }) => s.id === selectedVariant,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex-grow overflow-auto">
|
<div className="flex-grow overflow-auto pb-8">
|
||||||
<div className="space-y-6 pb-20">
|
<div className="space-y-6">
|
||||||
<h2 className="text-lg font-semibold">Step 2: Choose a Variant</h2>
|
<h2 className="text-lg font-semibold">Step 2: Choose a Variant</h2>
|
||||||
{!selectedVariant && (
|
{!selectedVariant && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>Based on your input, we suggest the following variants:</div>
|
<div>Based on your input, we suggest the following variants:</div>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={selectedVariant}
|
value={selectedVariant}
|
||||||
onValueChange={setSelectedVariant}
|
onValueChange={(value) => {
|
||||||
|
const element = suggestions?.find((s) => s?.id === value);
|
||||||
|
setSelectedVariant(element);
|
||||||
|
}}
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
>
|
>
|
||||||
{suggestions.map((suggestion) => (
|
{suggestions?.map((suggestion) => (
|
||||||
<div
|
<div
|
||||||
key={suggestion.id}
|
key={suggestion?.id}
|
||||||
className="flex items-start space-x-3"
|
className="flex items-start space-x-3"
|
||||||
>
|
>
|
||||||
<RadioGroupItem
|
<RadioGroupItem
|
||||||
value={suggestion.id}
|
value={suggestion?.id || ""}
|
||||||
id={suggestion.id}
|
id={suggestion?.id}
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor={suggestion.id} className="font-medium">
|
<Label htmlFor={suggestion?.id} className="font-medium">
|
||||||
{suggestion.name}
|
{suggestion?.name}
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{suggestion.shortDescription}
|
{suggestion?.shortDescription}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,22 +172,20 @@ export const StepTwo = ({
|
|||||||
{selectedVariant && (
|
{selectedVariant && (
|
||||||
<>
|
<>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h3 className="text-xl font-bold">{selectedTemplate?.name}</h3>
|
<h3 className="text-xl font-bold">{selectedVariant?.name}</h3>
|
||||||
<p className="text-muted-foreground mt-2">
|
<p className="text-muted-foreground mt-2">
|
||||||
{selectedTemplate?.shortDescription}
|
{selectedVariant?.shortDescription}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<ScrollArea className="h-[400px] p-5">
|
<ScrollArea>
|
||||||
<Accordion type="single" collapsible className="w-full">
|
<Accordion type="single" collapsible className="w-full">
|
||||||
<AccordionItem value="description">
|
<AccordionItem value="description">
|
||||||
<AccordionTrigger>Description</AccordionTrigger>
|
<AccordionTrigger>Description</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<ScrollArea className="h-[300px] w-full rounded-md border">
|
<ScrollArea className=" w-full rounded-md border p-4">
|
||||||
<div className="p-4">
|
<ReactMarkdown className="text-muted-foreground text-sm">
|
||||||
<ReactMarkdown className="prose dark:prose-invert">
|
{selectedVariant?.description}
|
||||||
{selectedTemplate?.description}
|
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
@@ -200,9 +193,14 @@ export const StepTwo = ({
|
|||||||
<AccordionTrigger>Docker Compose</AccordionTrigger>
|
<AccordionTrigger>Docker Compose</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
value={dockerCompose}
|
value={selectedVariant?.dockerCompose}
|
||||||
className="font-mono"
|
className="font-mono"
|
||||||
onChange={(value) => setDockerCompose(value)}
|
onChange={(value) => {
|
||||||
|
setSelectedVariant({
|
||||||
|
...selectedVariant,
|
||||||
|
dockerCompose: value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
@@ -211,7 +209,7 @@ export const StepTwo = ({
|
|||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<ScrollArea className="h-[300px] w-full rounded-md border">
|
<ScrollArea className="h-[300px] w-full rounded-md border">
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
{envVariables.map((env, index) => (
|
{selectedVariant?.envVariables.map((env, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex items-center space-x-2"
|
className="flex items-center space-x-2"
|
||||||
@@ -272,7 +270,7 @@ export const StepTwo = ({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
onClick={addEnvVariable}
|
// onClick={addEnvVariable}
|
||||||
>
|
>
|
||||||
<PlusCircle className="h-4 w-4 mr-2" />
|
<PlusCircle className="h-4 w-4 mr-2" />
|
||||||
Add Variable
|
Add Variable
|
||||||
@@ -281,17 +279,88 @@ export const StepTwo = ({
|
|||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
<AccordionItem value="domains">
|
||||||
|
<AccordionTrigger>Domains</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<ScrollArea className=" w-full rounded-md border">
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{selectedVariant?.domains.map((domain, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={domain.host}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleDomainChange(
|
||||||
|
index,
|
||||||
|
"host",
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder="Domain Host"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={domain.port}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleDomainChange(
|
||||||
|
index,
|
||||||
|
"port",
|
||||||
|
parseInt(e.target.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder="Port"
|
||||||
|
className="w-24"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={domain.serviceName}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleDomainChange(
|
||||||
|
index,
|
||||||
|
"serviceName",
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder="Service Name"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => removeDomain(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="mt-2"
|
||||||
|
// onClick={addDomain}
|
||||||
|
>
|
||||||
|
<PlusCircle className="h-4 w-4 mr-2" />
|
||||||
|
Add Domain
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sticky bottom-0 bg-background pt-2 border-t">
|
<div className="sticky bottom-0 bg-background pt-5 border-t">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<Button
|
<Button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
selectedVariant ? setSelectedVariant("") : prevStep()
|
selectedVariant ? setSelectedVariant(undefined) : prevStep()
|
||||||
}
|
}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -8,27 +8,65 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { AlertCircle, Bot } from "lucide-react";
|
import { Bot } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { StepFour } from "./step-four";
|
import { StepFour } from "./step-four";
|
||||||
import { StepOne } from "./step-one";
|
import { StepOne } from "./step-one";
|
||||||
import { StepThree } from "./step-three";
|
import { StepThree } from "./step-three";
|
||||||
import { StepTwo } from "./step-two";
|
import { StepTwo } from "./step-two";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
const emptyState = {
|
interface EnvVariable {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Domain {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
serviceName: string;
|
||||||
|
}
|
||||||
|
export interface TemplateInfo {
|
||||||
|
id: string;
|
||||||
|
userInput: string;
|
||||||
|
type: string;
|
||||||
|
details?: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
dockerCompose: string;
|
||||||
|
envVariables: EnvVariable[];
|
||||||
|
shortDescription: string;
|
||||||
|
domains: Domain[];
|
||||||
|
};
|
||||||
|
serverId?: string;
|
||||||
|
aiId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTemplateInfo: TemplateInfo = {
|
||||||
|
id: "",
|
||||||
|
aiId: "",
|
||||||
userInput: "",
|
userInput: "",
|
||||||
type: "",
|
type: "",
|
||||||
details: {
|
details: {
|
||||||
id: "",
|
id: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
dockerCompose: "",
|
dockerCompose: "",
|
||||||
envVariables: [],
|
envVariables: [],
|
||||||
shortDescription: "",
|
shortDescription: "",
|
||||||
|
domains: [],
|
||||||
},
|
},
|
||||||
name: "",
|
|
||||||
server: undefined,
|
|
||||||
description: "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -39,9 +77,10 @@ interface Props {
|
|||||||
export const TemplateGenerator = ({ projectId }: Props) => {
|
export const TemplateGenerator = ({ projectId }: Props) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
const { data: aiSettings } = api.ai.get.useQuery();
|
const { data: aiSettings } = api.ai.getAll.useQuery();
|
||||||
const { mutateAsync } = api.ai.deploy.useMutation();
|
const { mutateAsync } = api.ai.deploy.useMutation();
|
||||||
const [templateInfo, setTemplateInfo] = useState(emptyState);
|
const [templateInfo, setTemplateInfo] =
|
||||||
|
useState<TemplateInfo>(defaultTemplateInfo);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const totalSteps = 4;
|
const totalSteps = 4;
|
||||||
@@ -52,12 +91,15 @@ export const TemplateGenerator = ({ projectId }: Props) => {
|
|||||||
const handleOpenChange = (newOpen: boolean) => {
|
const handleOpenChange = (newOpen: boolean) => {
|
||||||
setOpen(newOpen);
|
setOpen(newOpen);
|
||||||
if (!newOpen) {
|
if (!newOpen) {
|
||||||
// Reset to the first step when closing the dialog
|
|
||||||
setStep(1);
|
setStep(1);
|
||||||
setTemplateInfo(emptyState);
|
setTemplateInfo(defaultTemplateInfo);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const haveAtleasOneProviderEnabled = aiSettings?.some(
|
||||||
|
(ai) => ai.isEnabled === true,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<DialogTrigger asChild className="w-full">
|
<DialogTrigger asChild className="w-full">
|
||||||
@@ -69,7 +111,7 @@ export const TemplateGenerator = ({ projectId }: Props) => {
|
|||||||
<span>AI Assistant</span>
|
<span>AI Assistant</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-[800px] w-full max-h-[90vh] flex flex-col">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl w-full flex flex-col">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>AI Assistant</DialogTitle>
|
<DialogTitle>AI Assistant</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -79,29 +121,65 @@ export const TemplateGenerator = ({ projectId }: Props) => {
|
|||||||
<div className="mt-4 flex-grow overflow-auto">
|
<div className="mt-4 flex-grow overflow-auto">
|
||||||
{step === 1 && (
|
{step === 1 && (
|
||||||
<>
|
<>
|
||||||
{(!aiSettings || !aiSettings?.isEnabled) && (
|
{!haveAtleasOneProviderEnabled && (
|
||||||
<Alert variant="destructive" className="mb-4">
|
<AlertBlock type="warning">
|
||||||
<AlertCircle className="h-4 w-4" />
|
<div className="flex flex-col w-full">
|
||||||
<AlertTitle>AI features are not enabled</AlertTitle>
|
<span>AI features are not enabled</span>
|
||||||
<AlertDescription>
|
<span>
|
||||||
To use AI-powered template generation, please{" "}
|
To use AI-powered template generation, please{" "}
|
||||||
<a
|
<Link
|
||||||
href="/dashboard/settings/ai"
|
href="/dashboard/settings/ai"
|
||||||
className="font-medium underline underline-offset-4"
|
className="font-medium underline underline-offset-4"
|
||||||
>
|
>
|
||||||
enable AI in your settings
|
enable AI in your settings
|
||||||
</a>
|
</Link>
|
||||||
.
|
.
|
||||||
</AlertDescription>
|
</span>
|
||||||
</Alert>
|
</div>
|
||||||
|
</AlertBlock>
|
||||||
)}
|
)}
|
||||||
{!!aiSettings && !!aiSettings?.isEnabled && (
|
|
||||||
|
{haveAtleasOneProviderEnabled &&
|
||||||
|
aiSettings &&
|
||||||
|
aiSettings?.length > 0 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label
|
||||||
|
htmlFor="user-needs"
|
||||||
|
className="text-sm font-medium"
|
||||||
|
>
|
||||||
|
Select AI Provider
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={templateInfo.aiId}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setTemplateInfo((prev) => ({
|
||||||
|
...prev,
|
||||||
|
aiId: value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select an AI provider" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{aiSettings.map((ai) => (
|
||||||
|
<SelectItem key={ai.aiId} value={ai.aiId}>
|
||||||
|
{ai.name} ({ai.model})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
{templateInfo.aiId && (
|
||||||
<StepOne
|
<StepOne
|
||||||
nextStep={nextStep}
|
nextStep={nextStep}
|
||||||
setTemplateInfo={setTemplateInfo}
|
setTemplateInfo={setTemplateInfo}
|
||||||
templateInfo={templateInfo}
|
templateInfo={templateInfo}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{step === 2 && (
|
{step === 2 && (
|
||||||
@@ -129,14 +207,14 @@ export const TemplateGenerator = ({ projectId }: Props) => {
|
|||||||
setTemplateInfo(data);
|
setTemplateInfo(data);
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
projectId,
|
projectId,
|
||||||
id: templateInfo.details?.id,
|
id: templateInfo.details?.id || "",
|
||||||
name: templateInfo.name,
|
name: templateInfo?.details?.name || "",
|
||||||
description: data.details.shortDescription,
|
description: data?.details?.shortDescription || "",
|
||||||
dockerCompose: data.details.dockerCompose,
|
dockerCompose: data?.details?.dockerCompose || "",
|
||||||
envVariables: (data.details?.envVariables || [])
|
envVariables: (data?.details?.envVariables || [])
|
||||||
.map((env: any) => `${env.name}=${env.value}`)
|
.map((env: any) => `${env.name}=${env.value}`)
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
serverId: data.server,
|
serverId: data.server || "",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Compose Created");
|
toast.success("Compose Created");
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -9,268 +8,99 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { BotIcon, Loader2, Trash2 } from "lucide-react";
|
||||||
import { AlertCircle } from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { HandleAi } from "./handle-ai";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
const aiSettingsSchema = z.object({
|
|
||||||
apiUrl: z.string().url({ message: "Please enter a valid URL" }),
|
|
||||||
apiKey: z.string().min(1, { message: "API Key is required" }),
|
|
||||||
model: z.string().optional(),
|
|
||||||
isEnabled: z.boolean(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type AISettings = z.infer<typeof aiSettingsSchema>;
|
|
||||||
|
|
||||||
interface Model {
|
|
||||||
id: string;
|
|
||||||
object: string;
|
|
||||||
created: number;
|
|
||||||
owned_by: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AiForm = () => {
|
export const AiForm = () => {
|
||||||
const [models, setModels] = useState<Model[]>([]);
|
const { data: aiConfigs, refetch, isLoading } = api.ai.getAll.useQuery();
|
||||||
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
const { mutateAsync, isLoading: isRemoving } = api.ai.delete.useMutation();
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const { data, refetch } = api.ai.get.useQuery();
|
|
||||||
const { mutateAsync, isLoading } = api.ai.save.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<AISettings>({
|
|
||||||
resolver: zodResolver(aiSettingsSchema),
|
|
||||||
defaultValues: {
|
|
||||||
apiUrl: data?.apiUrl ?? "https://api.openai.com/v1",
|
|
||||||
apiKey: data?.apiKey ?? "",
|
|
||||||
model: data?.model ?? "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchModels = async (apiUrl: string, apiKey: string) => {
|
|
||||||
setIsLoadingModels(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${apiUrl}/models`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${apiKey}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to fetch models");
|
|
||||||
}
|
|
||||||
const res = await response.json();
|
|
||||||
setModels(res.data);
|
|
||||||
|
|
||||||
// Set default model to o1-mini if present
|
|
||||||
const defaultModel = res.data.find(
|
|
||||||
(model: Model) => model.id === "gpt-4o",
|
|
||||||
);
|
|
||||||
if (defaultModel) {
|
|
||||||
form.setValue("model", defaultModel.id);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
setError("Failed to fetch models. Please check your API URL and Key.");
|
|
||||||
setModels([]);
|
|
||||||
} finally {
|
|
||||||
setIsLoadingModels(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
form.reset({
|
|
||||||
apiUrl: data?.apiUrl ?? "https://api.openai.com/v1",
|
|
||||||
apiKey: data?.apiKey ?? "",
|
|
||||||
model: data?.model ?? "",
|
|
||||||
isEnabled: !!data.isEnabled,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
form.reset();
|
|
||||||
}, [form, form.reset, data]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const apiUrl = form.watch("apiUrl");
|
|
||||||
const apiKey = form.watch("apiKey");
|
|
||||||
if (apiUrl && apiKey) {
|
|
||||||
form.setValue("model", undefined); // Reset model when API URL or Key changes
|
|
||||||
fetchModels(apiUrl, apiKey);
|
|
||||||
}
|
|
||||||
}, [form.watch("apiUrl"), form.watch("apiKey")]);
|
|
||||||
|
|
||||||
const onSubmit = async (values: AISettings) => {
|
|
||||||
await mutateAsync({
|
|
||||||
apiUrl: values.apiUrl,
|
|
||||||
apiKey: values.apiKey,
|
|
||||||
model: values.model || "",
|
|
||||||
isEnabled: !!values.isEnabled,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await refetch();
|
|
||||||
toast.success("AI Settings Updated");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error updating AI settings");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-transparent">
|
<div className="w-full">
|
||||||
<CardHeader className="flex flex-row gap-2 flex-wrap justify-between items-center">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||||
<CardTitle className="text-xl">AI Settings</CardTitle>
|
<div className="rounded-xl bg-background shadow-md ">
|
||||||
<CardDescription>
|
<CardHeader className="flex flex-row gap-2 justify-between">
|
||||||
Configure your AI model settings here.
|
<div>
|
||||||
</CardDescription>
|
<CardTitle className="text-xl flex flex-row gap-2">
|
||||||
|
<BotIcon className="size-6 text-muted-foreground self-center" />
|
||||||
|
AI Settings
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Manage your AI configurations</CardDescription>
|
||||||
|
</div>
|
||||||
|
{aiConfigs && aiConfigs?.length > 0 && <HandleAi />}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
<Form {...form}>
|
{isLoading ? (
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]">
|
||||||
<FormField
|
<span>Loading...</span>
|
||||||
control={form.control}
|
<Loader2 className="animate-spin size-4" />
|
||||||
name="isEnabled"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel className="text-base">Enable AI</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Turn on or off AI functionality
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
</div>
|
||||||
<FormControl>
|
) : (
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{!!form.watch("isEnabled") && (
|
|
||||||
<>
|
<>
|
||||||
<FormField
|
{aiConfigs?.length === 0 ? (
|
||||||
control={form.control}
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
name="apiUrl"
|
<BotIcon className="size-8 self-center text-muted-foreground" />
|
||||||
render={({ field }) => (
|
<span className="text-base text-muted-foreground text-center">
|
||||||
<FormItem>
|
You don't have any AI configurations
|
||||||
<FormLabel>API URL</FormLabel>
|
</span>
|
||||||
<FormControl>
|
<HandleAi />
|
||||||
<Input
|
</div>
|
||||||
placeholder="https://api.openai.com/v1"
|
) : (
|
||||||
{...field}
|
<div className="flex flex-col gap-4 rounded-lg min-h-[25vh]">
|
||||||
/>
|
{aiConfigs?.map((config) => (
|
||||||
</FormControl>
|
<div
|
||||||
<FormMessage />
|
key={config.aiId}
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
||||||
By default, the OpenAI API URL is used. Only change this
|
>
|
||||||
if you're using a different API.
|
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||||
</p>
|
<div>
|
||||||
</FormItem>
|
<span className="text-sm font-medium">
|
||||||
)}
|
{config.name}
|
||||||
/>
|
</span>
|
||||||
<FormField
|
<CardDescription>{config.model}</CardDescription>
|
||||||
control={form.control}
|
</div>
|
||||||
name="apiKey"
|
<div className="flex justify-between items-center">
|
||||||
render={({ field }) => (
|
<HandleAi aiId={config.aiId} />
|
||||||
<FormItem>
|
<DialogAction
|
||||||
<FormLabel>API Key</FormLabel>
|
title="Delete AI"
|
||||||
<FormControl>
|
description="Are you sure you want to delete this AI?"
|
||||||
<Input
|
type="destructive"
|
||||||
type="password"
|
onClick={async () => {
|
||||||
placeholder="Enter your API key"
|
await mutateAsync({
|
||||||
{...field}
|
aiId: config.aiId,
|
||||||
/>
|
})
|
||||||
</FormControl>
|
.then(() => {
|
||||||
<FormMessage />
|
toast.success("AI deleted successfully");
|
||||||
{form.watch("apiUrl") === "https://api.openai.com/v1" && (
|
refetch();
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
})
|
||||||
You can find your API key on the{" "}
|
.catch(() => {
|
||||||
<a
|
toast.error("Error deleting AI");
|
||||||
href="https://platform.openai.com/settings/organization/api-keys"
|
});
|
||||||
target="_blank"
|
}}
|
||||||
rel="noopener noreferrer"
|
>
|
||||||
className="underline hover:text-primary"
|
<Button
|
||||||
>
|
variant="ghost"
|
||||||
OpenAI account page
|
size="icon"
|
||||||
</a>
|
className="group hover:bg-red-500/10 "
|
||||||
.
|
isLoading={isRemoving}
|
||||||
</p>
|
>
|
||||||
)}
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
</FormItem>
|
</Button>
|
||||||
)}
|
</DialogAction>
|
||||||
/>
|
</div>
|
||||||
{!isLoadingModels && models.length > 0 && (
|
</div>
|
||||||
<FormField
|
</div>
|
||||||
control={form.control}
|
))}
|
||||||
name="model"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Model</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
value={field.value || ""}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a model" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{models.map((model) => (
|
|
||||||
<SelectItem key={model.id} value={model.id}>
|
|
||||||
{model.id}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{isLoadingModels && (
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
Loading models...
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{error && (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertCircle className="h-4 w-4" />
|
|
||||||
<AlertTitle>Error</AlertTitle>
|
|
||||||
<AlertDescription>{error}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
isLoading={isLoading}
|
|
||||||
disabled={!!error || isLoadingModels || !form.watch("model")}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
305
apps/dokploy/components/dashboard/settings/handle-ai.tsx
Normal file
305
apps/dokploy/components/dashboard/settings/handle-ai.tsx
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
"use client";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
|
||||||
|
const Schema = z.object({
|
||||||
|
name: z.string().min(1, { message: "Name is required" }),
|
||||||
|
apiUrl: z.string().url({ message: "Please enter a valid URL" }),
|
||||||
|
apiKey: z.string().min(1, { message: "API Key is required" }),
|
||||||
|
model: z.string().min(1, { message: "Model is required" }),
|
||||||
|
isEnabled: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof Schema>;
|
||||||
|
|
||||||
|
interface Model {
|
||||||
|
id: string;
|
||||||
|
object: string;
|
||||||
|
created: number;
|
||||||
|
owned_by: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
aiId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HandleAi = ({ aiId }: Props) => {
|
||||||
|
const [models, setModels] = useState<Model[]>([]);
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const [isLoadingModels, setIsLoadingModels] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const { data, refetch } = api.ai.one.useQuery(
|
||||||
|
{
|
||||||
|
aiId: aiId || "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!aiId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const { mutateAsync, isLoading } = aiId
|
||||||
|
? api.ai.update.useMutation()
|
||||||
|
: api.ai.create.useMutation();
|
||||||
|
const form = useForm<Schema>({
|
||||||
|
resolver: zodResolver(Schema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
apiUrl: "",
|
||||||
|
apiKey: "",
|
||||||
|
model: "gpt-3.5-turbo",
|
||||||
|
isEnabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset({
|
||||||
|
name: data?.name ?? "",
|
||||||
|
apiUrl: data?.apiUrl ?? "https://api.openai.com/v1",
|
||||||
|
apiKey: data?.apiKey ?? "",
|
||||||
|
model: data?.model ?? "gpt-3.5-turbo",
|
||||||
|
isEnabled: data?.isEnabled ?? true,
|
||||||
|
});
|
||||||
|
}, [aiId, form, data]);
|
||||||
|
|
||||||
|
const fetchModels = async (apiUrl: string, apiKey: string) => {
|
||||||
|
setIsLoadingModels(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${apiUrl}/models`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch models");
|
||||||
|
}
|
||||||
|
const res = await response.json();
|
||||||
|
setModels(res.data);
|
||||||
|
|
||||||
|
// Set default model to gpt-4 if present
|
||||||
|
const defaultModel = res.data.find(
|
||||||
|
(model: Model) => model.id === "gpt-4",
|
||||||
|
);
|
||||||
|
if (defaultModel) {
|
||||||
|
form.setValue("model", defaultModel.id);
|
||||||
|
return defaultModel.id;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setError("Failed to fetch models. Please check your API URL and Key.");
|
||||||
|
setModels([]);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingModels(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const apiUrl = form.watch("apiUrl");
|
||||||
|
const apiKey = form.watch("apiKey");
|
||||||
|
if (apiUrl && apiKey) {
|
||||||
|
form.setValue("model", "");
|
||||||
|
fetchModels(apiUrl, apiKey);
|
||||||
|
}
|
||||||
|
}, [form.watch("apiUrl"), form.watch("apiKey")]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: Schema) => {
|
||||||
|
try {
|
||||||
|
console.log("Form data:", data);
|
||||||
|
console.log("Current model value:", form.getValues("model"));
|
||||||
|
await mutateAsync({
|
||||||
|
...data,
|
||||||
|
aiId: aiId || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
utils.ai.getAll.invalidate();
|
||||||
|
toast.success("AI settings saved successfully");
|
||||||
|
refetch();
|
||||||
|
setOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Submit error:", error);
|
||||||
|
toast.error("Failed to save AI settings");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger className="" asChild>
|
||||||
|
{aiId ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10 "
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button className="cursor-pointer space-x-3">
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
Add AI
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{aiId ? "Edit AI" : "Add AI"}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Configure your AI provider settings
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...form}>
|
||||||
|
{error && <AlertBlock type="error">{error}</AlertBlock>}
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-2">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="My OpenAI Config" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
A name to identify this configuration
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="apiUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>API URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="https://api.openai.com/v1" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
The base URL for your AI provider's API
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="apiKey"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>API Key</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="sk-..." {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Your API key for authentication
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isLoadingModels && (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Loading models...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoadingModels && models.length > 0 && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="model"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Model</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
value={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a model" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{models.map((model) => (
|
||||||
|
<SelectItem key={model.id} value={model.id}>
|
||||||
|
{model.id}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>Select an AI model to use</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isEnabled"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel className="text-base">
|
||||||
|
Enable AI Features
|
||||||
|
</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Turn on/off AI functionality
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 pt-4">
|
||||||
|
<Button type="submit" isLoading={isLoading}>
|
||||||
|
{aiId ? "Update" : "Create"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -128,7 +128,6 @@ export const HandleSSHKeys = ({ sshKeyId }: Props) => {
|
|||||||
Add SSH Key
|
Add SSH Key
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{/* {children} */}
|
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
GitBranch,
|
GitBranch,
|
||||||
HeartIcon,
|
HeartIcon,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
|
BotIcon,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
Package,
|
Package,
|
||||||
PieChart,
|
PieChart,
|
||||||
@@ -249,7 +250,13 @@ const data = {
|
|||||||
isSingle: true,
|
isSingle: true,
|
||||||
isActive: false,
|
isActive: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "AI",
|
||||||
|
icon: BotIcon,
|
||||||
|
url: "/dashboard/settings/ai",
|
||||||
|
isSingle: true,
|
||||||
|
isActive: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Git",
|
title: "Git",
|
||||||
url: "/dashboard/settings/git-providers",
|
url: "/dashboard/settings/git-providers",
|
||||||
|
|||||||
16
apps/dokploy/drizzle/0057_damp_prism.sql
Normal file
16
apps/dokploy/drizzle/0057_damp_prism.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "ai" (
|
||||||
|
"aiId" text PRIMARY KEY NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"apiUrl" text NOT NULL,
|
||||||
|
"apiKey" text NOT NULL,
|
||||||
|
"model" text NOT NULL,
|
||||||
|
"isEnabled" boolean DEFAULT true NOT NULL,
|
||||||
|
"adminId" text NOT NULL,
|
||||||
|
"createdAt" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "ai" ADD CONSTRAINT "ai_adminId_admin_adminId_fk" FOREIGN KEY ("adminId") REFERENCES "public"."admin"("adminId") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS "ai" (
|
|
||||||
"authId" text PRIMARY KEY NOT NULL,
|
|
||||||
"apiUrl" text NOT NULL,
|
|
||||||
"apiKey" text NOT NULL,
|
|
||||||
"model" text NOT NULL,
|
|
||||||
"isEnabled" boolean DEFAULT true NOT NULL
|
|
||||||
);
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"id": "03ce8887-3824-49b9-ad47-12c25aa4c090",
|
"id": "841960d7-0573-41e4-8529-fd9960f726d5",
|
||||||
"prevId": "24787a88-0754-437a-b077-03a3265b8ef5",
|
"prevId": "24787a88-0754-437a-b077-03a3265b8ef5",
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
@@ -4144,12 +4144,18 @@
|
|||||||
"name": "ai",
|
"name": "ai",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
"authId": {
|
"aiId": {
|
||||||
"name": "authId",
|
"name": "aiId",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": true,
|
"primaryKey": true,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
"apiUrl": {
|
"apiUrl": {
|
||||||
"name": "apiUrl",
|
"name": "apiUrl",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -4174,10 +4180,36 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
"default": true
|
"default": true
|
||||||
|
},
|
||||||
|
"adminId": {
|
||||||
|
"name": "adminId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {},
|
"foreignKeys": {
|
||||||
|
"ai_adminId_admin_adminId_fk": {
|
||||||
|
"name": "ai_adminId_admin_adminId_fk",
|
||||||
|
"tableFrom": "ai",
|
||||||
|
"tableTo": "admin",
|
||||||
|
"columnsFrom": [
|
||||||
|
"adminId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"adminId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {}
|
"uniqueConstraints": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -404,8 +404,8 @@
|
|||||||
{
|
{
|
||||||
"idx": 57,
|
"idx": 57,
|
||||||
"version": "6",
|
"version": "6",
|
||||||
"when": 1737246538368,
|
"when": 1737251708859,
|
||||||
"tag": "0057_mature_thaddeus_ross",
|
"tag": "0057_damp_prism",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { AiForm } from "@/components/dashboard/settings/ai-form";
|
import { AiForm } from "@/components/dashboard/settings/ai-form";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
import { getLocale, serverSideTranslations } from "@/utils/i18n";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
@@ -20,11 +19,7 @@ const Page = () => {
|
|||||||
export default Page;
|
export default Page;
|
||||||
|
|
||||||
Page.getLayout = (page: ReactElement) => {
|
Page.getLayout = (page: ReactElement) => {
|
||||||
return (
|
return <DashboardLayout>{page}</DashboardLayout>;
|
||||||
<DashboardLayout tab={"settings"} metaName="AI">
|
|
||||||
<SettingsLayout>{page}</SettingsLayout>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import { slugify } from "@/lib/slug";
|
import { slugify } from "@/lib/slug";
|
||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import {
|
||||||
|
adminProcedure,
|
||||||
|
createTRPCRouter,
|
||||||
|
protectedProcedure,
|
||||||
|
} from "@/server/api/trpc";
|
||||||
import { generatePassword } from "@/templates/utils";
|
import { generatePassword } from "@/templates/utils";
|
||||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||||
import {
|
import {
|
||||||
apiAiSettingsSchema,
|
apiCreateAi,
|
||||||
|
apiUpdateAi,
|
||||||
deploySuggestionSchema,
|
deploySuggestionSchema,
|
||||||
} from "@dokploy/server/db/schema/ai";
|
} from "@dokploy/server/db/schema/ai";
|
||||||
import {
|
import {
|
||||||
getAiSettingsByAuthId,
|
getAiSettingsByAdminId,
|
||||||
|
getAiSettingById,
|
||||||
saveAiSettings,
|
saveAiSettings,
|
||||||
|
deleteAiSettings,
|
||||||
suggestVariants,
|
suggestVariants,
|
||||||
} from "@dokploy/server/services/ai";
|
} from "@dokploy/server/services/ai";
|
||||||
import { createComposeByTemplate } from "@dokploy/server/services/compose";
|
import { createComposeByTemplate } from "@dokploy/server/services/compose";
|
||||||
@@ -21,24 +28,73 @@ import { TRPCError } from "@trpc/server";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const aiRouter = createTRPCRouter({
|
export const aiRouter = createTRPCRouter({
|
||||||
save: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiAiSettingsSchema)
|
.input(z.object({ aiId: z.string() }))
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const aiSetting = await getAiSettingById(input.aiId);
|
||||||
|
if (aiSetting.adminId !== ctx.user.adminId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this AI configuration",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return aiSetting;
|
||||||
|
}),
|
||||||
|
create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => {
|
||||||
|
return await saveAiSettings(ctx.user.adminId, input);
|
||||||
|
}),
|
||||||
|
|
||||||
|
update: protectedProcedure
|
||||||
|
.input(apiUpdateAi)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
return await saveAiSettings(ctx.user.authId, input);
|
return await saveAiSettings(ctx.user.adminId, input);
|
||||||
}),
|
}),
|
||||||
get: protectedProcedure.query(async ({ ctx }) => {
|
|
||||||
return await getAiSettingsByAuthId(ctx.user.authId);
|
getAll: adminProcedure.query(async ({ ctx }) => {
|
||||||
|
return await getAiSettingsByAdminId(ctx.user.adminId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
get: protectedProcedure
|
||||||
|
.input(z.object({ aiId: z.string() }))
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const aiSetting = await getAiSettingById(input.aiId);
|
||||||
|
if (aiSetting.adminId !== ctx.user.authId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this AI configuration",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return aiSetting;
|
||||||
|
}),
|
||||||
|
|
||||||
|
delete: protectedProcedure
|
||||||
|
.input(z.object({ aiId: z.string() }))
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const aiSetting = await getAiSettingById(input.aiId);
|
||||||
|
if (aiSetting.adminId !== ctx.user.adminId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this AI configuration",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return await deleteAiSettings(input.aiId);
|
||||||
|
}),
|
||||||
|
|
||||||
suggest: protectedProcedure
|
suggest: protectedProcedure
|
||||||
.input(z.string())
|
.input(
|
||||||
|
z.object({
|
||||||
|
aiId: z.string(),
|
||||||
|
prompt: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
return await suggestVariants(ctx.user.authId, input);
|
return await suggestVariants(ctx.user.adminId, input.aiId, input.prompt);
|
||||||
}),
|
}),
|
||||||
deploy: protectedProcedure
|
deploy: protectedProcedure
|
||||||
.input(deploySuggestionSchema)
|
.input(deploySuggestionSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
await checkServiceAccess(ctx.user.authId, input.projectId, "create");
|
await checkServiceAccess(ctx.user.adminId, input.projectId, "create");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IS_CLOUD && !input.serverId) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { registry } from "./registry";
|
|||||||
import { certificateType } from "./shared";
|
import { certificateType } from "./shared";
|
||||||
import { sshKeys } from "./ssh-key";
|
import { sshKeys } from "./ssh-key";
|
||||||
import { users } from "./user";
|
import { users } from "./user";
|
||||||
|
import { ai } from "./ai";
|
||||||
|
|
||||||
export const admins = pgTable("admin", {
|
export const admins = pgTable("admin", {
|
||||||
adminId: text("adminId")
|
adminId: text("adminId")
|
||||||
@@ -42,6 +43,7 @@ export const adminsRelations = relations(admins, ({ one, many }) => ({
|
|||||||
registry: many(registry),
|
registry: many(registry),
|
||||||
sshKeys: many(sshKeys),
|
sshKeys: many(sshKeys),
|
||||||
certificates: many(certificates),
|
certificates: many(certificates),
|
||||||
|
ai: many(ai),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const createSchema = createInsertSchema(admins, {
|
const createSchema = createInsertSchema(admins, {
|
||||||
|
|||||||
@@ -1,24 +1,46 @@
|
|||||||
import { boolean, pgTable, text } from "drizzle-orm/pg-core";
|
import { boolean, pgTable, text } from "drizzle-orm/pg-core";
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { admins } from "./admin";
|
||||||
|
import { relations } from "drizzle-orm";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
export const ai = pgTable("ai", {
|
export const ai = pgTable("ai", {
|
||||||
authId: text("authId").notNull().primaryKey(),
|
aiId: text("aiId")
|
||||||
|
.notNull()
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => nanoid()),
|
||||||
|
name: text("name").notNull(),
|
||||||
apiUrl: text("apiUrl").notNull(),
|
apiUrl: text("apiUrl").notNull(),
|
||||||
apiKey: text("apiKey").notNull(),
|
apiKey: text("apiKey").notNull(),
|
||||||
model: text("model").notNull(),
|
model: text("model").notNull(),
|
||||||
isEnabled: boolean("isEnabled").notNull().default(true),
|
isEnabled: boolean("isEnabled").notNull().default(true),
|
||||||
|
adminId: text("adminId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => admins.adminId, { onDelete: "cascade" }), // Admin ID who created the AI settings
|
||||||
|
createdAt: text("createdAt")
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const aiRelations = relations(ai, ({ one }) => ({
|
||||||
|
admin: one(admins, {
|
||||||
|
fields: [ai.adminId],
|
||||||
|
references: [admins.adminId],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
const createSchema = createInsertSchema(ai, {
|
const createSchema = createInsertSchema(ai, {
|
||||||
|
name: z.string().min(1, { message: "Name is required" }),
|
||||||
apiUrl: z.string().url({ message: "Please enter a valid URL" }),
|
apiUrl: z.string().url({ message: "Please enter a valid URL" }),
|
||||||
apiKey: z.string().min(1, { message: "API Key is required" }),
|
apiKey: z.string().min(1, { message: "API Key is required" }),
|
||||||
model: z.string().min(1, { message: "Model is required" }),
|
model: z.string().min(1, { message: "Model is required" }),
|
||||||
isEnabled: z.boolean().optional(),
|
isEnabled: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiAiSettingsSchema = createSchema
|
export const apiCreateAi = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
name: true,
|
||||||
apiUrl: true,
|
apiUrl: true,
|
||||||
apiKey: true,
|
apiKey: true,
|
||||||
model: true,
|
model: true,
|
||||||
@@ -26,6 +48,13 @@ export const apiAiSettingsSchema = createSchema
|
|||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
|
export const apiUpdateAi = createSchema
|
||||||
|
.partial()
|
||||||
|
.extend({
|
||||||
|
aiId: z.string().min(1),
|
||||||
|
})
|
||||||
|
.omit({ adminId: true });
|
||||||
|
|
||||||
export const deploySuggestionSchema = z.object({
|
export const deploySuggestionSchema = z.object({
|
||||||
projectId: z.string().min(1),
|
projectId: z.string().min(1),
|
||||||
id: z.string().min(1),
|
id: z.string().min(1),
|
||||||
|
|||||||
@@ -3,48 +3,77 @@ import { ai } from "@dokploy/server/db/schema";
|
|||||||
import { selectAIProvider } from "@dokploy/server/utils/ai/select-ai-provider";
|
import { selectAIProvider } from "@dokploy/server/utils/ai/select-ai-provider";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { generateObject } from "ai";
|
import { generateObject } from "ai";
|
||||||
import { eq } from "drizzle-orm";
|
import { desc, eq } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { IS_CLOUD } from "../constants";
|
||||||
|
import { findAdminById } from "./admin";
|
||||||
|
|
||||||
export const getAiSettingsByAuthId = async (authId: string) => {
|
export const getAiSettingsByAdminId = async (adminId: string) => {
|
||||||
const aiSettings = await db.query.ai.findFirst({
|
const aiSettings = await db.query.ai.findMany({
|
||||||
where: eq(ai.authId, authId),
|
where: eq(ai.adminId, adminId),
|
||||||
|
orderBy: desc(ai.createdAt),
|
||||||
});
|
});
|
||||||
if (!aiSettings) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "NOT_FOUND",
|
|
||||||
message: "AI settings not found for the user",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return aiSettings;
|
return aiSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const saveAiSettings = async (authId: string, settings: any) => {
|
export const getAiSettingById = async (aiId: string) => {
|
||||||
|
const aiSetting = await db.query.ai.findFirst({
|
||||||
|
where: eq(ai.aiId, aiId),
|
||||||
|
});
|
||||||
|
if (!aiSetting) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "AI settings not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return aiSetting;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveAiSettings = async (adminId: string, settings: any) => {
|
||||||
|
const aiId = settings.aiId;
|
||||||
|
|
||||||
return db
|
return db
|
||||||
.insert(ai)
|
.insert(ai)
|
||||||
.values({
|
.values({
|
||||||
authId,
|
aiId,
|
||||||
|
adminId,
|
||||||
...settings,
|
...settings,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: ai.authId,
|
target: ai.aiId,
|
||||||
set: {
|
set: {
|
||||||
...settings,
|
...settings,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const suggestVariants = async (authId: string, input: string) => {
|
export const deleteAiSettings = async (aiId: string) => {
|
||||||
const aiSettings = await getAiSettingsByAuthId(authId);
|
return db.delete(ai).where(eq(ai.aiId, aiId));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const suggestVariants = async (
|
||||||
|
adminId: string,
|
||||||
|
aiId: string,
|
||||||
|
input: string
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const aiSettings = await getAiSettingById(aiId);
|
||||||
if (!aiSettings || !aiSettings.isEnabled) {
|
if (!aiSettings || !aiSettings.isEnabled) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "AI features are not enabled",
|
message: "AI features are not enabled for this configuration",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const provider = selectAIProvider(aiSettings);
|
const provider = selectAIProvider(aiSettings);
|
||||||
const model = provider(aiSettings.model);
|
const model = provider(aiSettings.model);
|
||||||
|
|
||||||
|
let ip = "";
|
||||||
|
if (!IS_CLOUD) {
|
||||||
|
const admin = await findAdminById(adminId);
|
||||||
|
ip = admin?.serverIp || "";
|
||||||
|
}
|
||||||
|
|
||||||
const { object } = await generateObject({
|
const { object } = await generateObject({
|
||||||
model,
|
model,
|
||||||
output: "array",
|
output: "array",
|
||||||
@@ -66,6 +95,7 @@ export const suggestVariants = async (authId: string, input: string) => {
|
|||||||
if (object?.length) {
|
if (object?.length) {
|
||||||
const result = [];
|
const result = [];
|
||||||
for (const suggestion of object) {
|
for (const suggestion of object) {
|
||||||
|
try {
|
||||||
const { object: docker } = await generateObject({
|
const { object: docker } = await generateObject({
|
||||||
model,
|
model,
|
||||||
output: "object",
|
output: "object",
|
||||||
@@ -75,13 +105,33 @@ export const suggestVariants = async (authId: string, input: string) => {
|
|||||||
z.object({
|
z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
value: z.string(),
|
value: z.string(),
|
||||||
}),
|
})
|
||||||
|
),
|
||||||
|
domains: z.array(
|
||||||
|
z.object({
|
||||||
|
host: z.string(),
|
||||||
|
port: z.number(),
|
||||||
|
serviceName: z.string(),
|
||||||
|
})
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
prompt: `
|
prompt: `
|
||||||
Act as advanced DevOps engineer and generate docker compose with environment variables needed to install the following project,
|
Act as advanced DevOps engineer and generate docker compose with environment variables and domain configurations needed to install the following project.
|
||||||
use placeholder like \${VARIABLE_NAME-default} for generated variables in the docker compose. Use complex values for passwords/secrets variables.
|
Return the docker compose as a YAML string. Follow these rules:
|
||||||
Don\'t set container_name field in services. Don\'t set version field in the docker compose.
|
1. Use placeholder like \${VARIABLE_NAME-default} for generated variables
|
||||||
|
2. Use complex values for passwords/secrets variables
|
||||||
|
3. Don't set container_name field in services
|
||||||
|
4. Don't set version field in the docker compose
|
||||||
|
5. Don't set ports like 'ports: 3000:3000', use 'ports: ["3000"]' instead
|
||||||
|
6. Use dokploy-network in all services
|
||||||
|
7. Add dokploy-network at the end and mark it as external: true
|
||||||
|
|
||||||
|
For each service that needs to be exposed to the internet:
|
||||||
|
1. Define a domain configuration with:
|
||||||
|
- host: the domain name for the service
|
||||||
|
- port: the internal port the service runs on
|
||||||
|
- serviceName: the name of the service in the docker-compose
|
||||||
|
2. Make sure the service is properly configured in the docker-compose to work with the specified port
|
||||||
|
|
||||||
Project details:
|
Project details:
|
||||||
${suggestion?.description}
|
${suggestion?.description}
|
||||||
@@ -93,6 +143,10 @@ export const suggestVariants = async (authId: string, input: string) => {
|
|||||||
...docker,
|
...docker,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in docker compose generation:", error);
|
||||||
|
console.error("Error details:", error.cause?.issues || error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -101,4 +155,8 @@ export const suggestVariants = async (authId: string, input: string) => {
|
|||||||
code: "NOT_FOUND",
|
code: "NOT_FOUND",
|
||||||
message: "No suggestions found",
|
message: "No suggestions found",
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in suggestVariants:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user