refactor: add many AI providers & improve prompt

This commit is contained in:
Mauricio Siu
2025-01-18 21:35:03 -06:00
parent ad642ab4e0
commit 08ab18eebf
17 changed files with 1158 additions and 667 deletions

View File

@@ -2,13 +2,14 @@ import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import ReactMarkdown from "react-markdown";
import type { StepProps } from "./step-two";
export const StepFour = ({
prevStep,
templateInfo,
setOpen,
setTemplateInfo,
}: any) => {
}: StepProps) => {
const handleSubmit = () => {
setTemplateInfo(templateInfo); // Update the template info
setOpen(false);
@@ -17,36 +18,41 @@ export const StepFour = ({
return (
<div className="flex flex-col h-full">
<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>
<ScrollArea className="h-[400px] p-5">
<div className="space-y-4">
<div className="p-4">
<ReactMarkdown className="prose dark:prose-invert">
{templateInfo.details.description}
<div>
<h3 className="text-sm font-semibold">Name</h3>
<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>
</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>
<p className="text-sm text-muted-foreground">
{templateInfo?.serverId || "Dokploy Server"}
</p>
</div>
<div>
<h3 className="text-md font-semibold">Docker Compose</h3>
<h3 className="text-sm font-semibold">Docker Compose</h3>
<CodeEditor
lineWrapping
value={templateInfo.details.dockerCompose}
value={templateInfo?.details?.dockerCompose}
disabled
className="font-mono"
/>
</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">
{templateInfo.details.envVariables.map(
{templateInfo?.details?.envVariables.map(
(
env: {
name: string;
@@ -55,8 +61,13 @@ export const StepFour = ({
index: number,
) => (
<li key={index}>
<strong>{env.name}</strong>:
<span className="ml-2 font-mono">{env.value}</span>
<strong className="text-sm font-semibold">
{env.name}
</strong>
:
<span className="text-sm ml-2 text-muted-foreground">
{env.value}
</span>
</li>
),
)}

View File

@@ -14,21 +14,21 @@ import {
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { useState } from "react";
import type { StepProps } from "./step-two";
export const StepThree = ({
nextStep,
prevStep,
templateInfo,
setTemplateInfo,
}: any) => {
const [name, setName] = useState(templateInfo.name);
const [server, setServer] = useState(templateInfo.server);
}: StepProps) => {
const [server, setServer] = useState(templateInfo.serverId);
const { data: servers } = api.server.withSSHKey.useQuery();
const handleNext = () => {
const updatedInfo = { ...templateInfo, name };
if (server?.trim()) {
updatedInfo.server = server;
updatedInfo.serverId = server;
}
setTemplateInfo(updatedInfo);
nextStep();
@@ -43,8 +43,16 @@ export const StepThree = ({
<Label htmlFor="name">App Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
value={templateInfo?.details?.name || ""}
onChange={(e) => {
setTemplateInfo({
...templateInfo,
details: {
...templateInfo?.details,
name: e.target.value,
},
});
}}
placeholder="Enter app name"
className="mt-1"
/>
@@ -74,7 +82,10 @@ export const StepThree = ({
<Button onClick={prevStep} variant="outline">
Back
</Button>
<Button onClick={handleNext} disabled={!name.trim()}>
<Button
onClick={handleNext}
disabled={!templateInfo?.details?.name?.trim()}
>
Next
</Button>
</div>

View File

@@ -15,19 +15,13 @@ import { Bot, Eye, EyeOff, PlusCircle, Trash2 } from "lucide-react";
import { useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import { toast } from "sonner";
import type { TemplateInfo } from "./template-generator";
interface EnvVariable {
name: string;
value: string;
}
interface TemplateInfo {
id: string;
name: string;
shortDescription: string;
description: string;
dockerCompose: string;
envVariables: EnvVariable[];
export interface StepProps {
nextStep: () => void;
prevStep: () => void;
templateInfo: TemplateInfo;
setTemplateInfo: React.Dispatch<React.SetStateAction<TemplateInfo>>;
}
export const StepTwo = ({
@@ -35,56 +29,34 @@ export const StepTwo = ({
prevStep,
templateInfo,
setTemplateInfo,
}: any) => {
const [suggestions, setSuggestions] = useState<Array<TemplateInfo>>([]);
const [selectedVariant, setSelectedVariant] = useState("");
const [dockerCompose, setDockerCompose] = useState("");
const [envVariables, setEnvVariables] = useState<Array<EnvVariable>>([]);
}: StepProps) => {
const [suggestions, setSuggestions] = useState<TemplateInfo["details"][]>();
const [selectedVariant, setSelectedVariant] =
useState<TemplateInfo["details"]>();
const [showValues, setShowValues] = useState<Record<string, boolean>>({});
const { mutateAsync, isLoading } = api.ai.suggest.useMutation();
useEffect(() => {
mutateAsync(templateInfo.userInput)
mutateAsync({
aiId: templateInfo.aiId,
prompt: templateInfo.userInput,
})
.then((data) => {
console.log(data);
setSuggestions(data);
})
.catch(() => {
toast.error("Error updating AI settings");
.catch((error) => {
console.error("Error details:", error);
toast.error("Error generating suggestions");
});
}, [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) {
if (selectedVariant) {
setTemplateInfo({
...templateInfo,
type: selectedVariant,
details: {
...selected,
dockerCompose,
envVariables,
},
details: selectedVariant,
});
}
nextStep();
@@ -95,25 +67,49 @@ export const StepTwo = ({
field: string,
value: string,
) => {
const updatedEnvVariables = [...envVariables];
if (updatedEnvVariables[index]) {
updatedEnvVariables[index] = {
...updatedEnvVariables[index],
[field]: value,
};
setEnvVariables(updatedEnvVariables);
}
// 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 addEnvVariable = () => {
// setEnvVariables([...envVariables, { name: "", value: "" }]);
// setShowValues((prev) => ({ ...prev, "": false }));
// };
const removeEnvVariable = (index: number) => {
const updatedEnvVariables = envVariables.filter((_, i) => i !== index);
setEnvVariables(updatedEnvVariables);
};
// const removeEnvVariable = (index: number) => {
// const updatedEnvVariables = envVariables.filter((_, i) => i !== index);
// 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) => {
setShowValues((prev) => ({ ...prev, [name]: !prev[name] }));
@@ -134,39 +130,38 @@ export const StepTwo = ({
);
}
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">
<div className="flex-grow overflow-auto pb-8">
<div className="space-y-6">
<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}
onValueChange={(value) => {
const element = suggestions?.find((s) => s?.id === value);
setSelectedVariant(element);
}}
className="space-y-4"
>
{suggestions.map((suggestion) => (
{suggestions?.map((suggestion) => (
<div
key={suggestion.id}
key={suggestion?.id}
className="flex items-start space-x-3"
>
<RadioGroupItem
value={suggestion.id}
id={suggestion.id}
value={suggestion?.id || ""}
id={suggestion?.id}
className="mt-1"
/>
<div>
<Label htmlFor={suggestion.id} className="font-medium">
{suggestion.name}
<Label htmlFor={suggestion?.id} className="font-medium">
{suggestion?.name}
</Label>
<p className="text-sm text-muted-foreground">
{suggestion.shortDescription}
{suggestion?.shortDescription}
</p>
</div>
</div>
@@ -177,22 +172,20 @@ export const StepTwo = ({
{selectedVariant && (
<>
<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">
{selectedTemplate?.shortDescription}
{selectedVariant?.shortDescription}
</p>
</div>
<ScrollArea className="h-[400px] p-5">
<ScrollArea>
<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 className=" w-full rounded-md border p-4">
<ReactMarkdown className="text-muted-foreground text-sm">
{selectedVariant?.description}
</ReactMarkdown>
</ScrollArea>
</AccordionContent>
</AccordionItem>
@@ -200,9 +193,14 @@ export const StepTwo = ({
<AccordionTrigger>Docker Compose</AccordionTrigger>
<AccordionContent>
<CodeEditor
value={dockerCompose}
value={selectedVariant?.dockerCompose}
className="font-mono"
onChange={(value) => setDockerCompose(value)}
onChange={(value) => {
setSelectedVariant({
...selectedVariant,
dockerCompose: value,
});
}}
/>
</AccordionContent>
</AccordionItem>
@@ -211,7 +209,7 @@ export const StepTwo = ({
<AccordionContent>
<ScrollArea className="h-[300px] w-full rounded-md border">
<div className="p-4 space-y-4">
{envVariables.map((env, index) => (
{selectedVariant?.envVariables.map((env, index) => (
<div
key={index}
className="flex items-center space-x-2"
@@ -272,7 +270,7 @@ export const StepTwo = ({
variant="outline"
size="sm"
className="mt-2"
onClick={addEnvVariable}
// onClick={addEnvVariable}
>
<PlusCircle className="h-4 w-4 mr-2" />
Add Variable
@@ -281,17 +279,88 @@ export const StepTwo = ({
</ScrollArea>
</AccordionContent>
</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>
</ScrollArea>
</>
)}
</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">
<Button
onClick={() =>
selectedVariant ? setSelectedVariant("") : prevStep()
selectedVariant ? setSelectedVariant(undefined) : prevStep()
}
variant="outline"
>

View File

@@ -8,27 +8,65 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { api } from "@/utils/api";
import { AlertCircle, Bot } from "lucide-react";
import { 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";
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: "",
type: "",
details: {
id: "",
name: "",
description: "",
dockerCompose: "",
envVariables: [],
shortDescription: "",
domains: [],
},
name: "",
server: undefined,
description: "",
};
interface Props {
@@ -39,9 +77,10 @@ interface Props {
export const TemplateGenerator = ({ projectId }: Props) => {
const [open, setOpen] = useState(false);
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 [templateInfo, setTemplateInfo] = useState(emptyState);
const [templateInfo, setTemplateInfo] =
useState<TemplateInfo>(defaultTemplateInfo);
const utils = api.useUtils();
const totalSteps = 4;
@@ -52,12 +91,15 @@ export const TemplateGenerator = ({ projectId }: Props) => {
const handleOpenChange = (newOpen: boolean) => {
setOpen(newOpen);
if (!newOpen) {
// Reset to the first step when closing the dialog
setStep(1);
setTemplateInfo(emptyState);
setTemplateInfo(defaultTemplateInfo);
}
};
const haveAtleasOneProviderEnabled = aiSettings?.some(
(ai) => ai.isEnabled === true,
);
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild className="w-full">
@@ -69,7 +111,7 @@ export const TemplateGenerator = ({ projectId }: Props) => {
<span>AI Assistant</span>
</DropdownMenuItem>
</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>
<DialogTitle>AI Assistant</DialogTitle>
<DialogDescription>
@@ -79,29 +121,65 @@ export const TemplateGenerator = ({ projectId }: Props) => {
<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}
/>
{!haveAtleasOneProviderEnabled && (
<AlertBlock type="warning">
<div className="flex flex-col w-full">
<span>AI features are not enabled</span>
<span>
To use AI-powered template generation, please{" "}
<Link
href="/dashboard/settings/ai"
className="font-medium underline underline-offset-4"
>
enable AI in your settings
</Link>
.
</span>
</div>
</AlertBlock>
)}
{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
nextStep={nextStep}
setTemplateInfo={setTemplateInfo}
templateInfo={templateInfo}
/>
)}
</div>
)}
</>
)}
{step === 2 && (
@@ -129,14 +207,14 @@ export const TemplateGenerator = ({ projectId }: Props) => {
setTemplateInfo(data);
await mutateAsync({
projectId,
id: templateInfo.details?.id,
name: templateInfo.name,
description: data.details.shortDescription,
dockerCompose: data.details.dockerCompose,
envVariables: (data.details?.envVariables || [])
id: templateInfo.details?.id || "",
name: templateInfo?.details?.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,
serverId: data.server || "",
})
.then(async () => {
toast.success("Compose Created");