diff --git a/apps/dokploy/components/dashboard/project/ai/step-one.tsx b/apps/dokploy/components/dashboard/project/ai/step-one.tsx index 56156bbe..0c80ec1d 100644 --- a/apps/dokploy/components/dashboard/project/ai/step-one.tsx +++ b/apps/dokploy/components/dashboard/project/ai/step-one.tsx @@ -2,7 +2,6 @@ import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, @@ -12,8 +11,9 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { useState } from "react"; +import { Textarea } from "@/components/ui/textarea"; import { api } from "@/utils/api"; +import { useState } from "react"; const examples = [ "Make a personal blog", diff --git a/apps/dokploy/components/dashboard/project/ai/step-two.tsx b/apps/dokploy/components/dashboard/project/ai/step-two.tsx index d87f9519..7d644022 100644 --- a/apps/dokploy/components/dashboard/project/ai/step-two.tsx +++ b/apps/dokploy/components/dashboard/project/ai/step-two.tsx @@ -136,10 +136,7 @@ export const StepTwo = ({ setSelectedVariant({ ...selectedVariant, - envVariables: [ - ...selectedVariant.envVariables, - { name: "", value: "" }, - ], + envVariables: [...selectedVariant.envVariables, { name: "", value: "" }], }); }; diff --git a/apps/dokploy/server/api/routers/ai.ts b/apps/dokploy/server/api/routers/ai.ts index 605abc80..ec4ce16d 100644 --- a/apps/dokploy/server/api/routers/ai.ts +++ b/apps/dokploy/server/api/routers/ai.ts @@ -1,145 +1,145 @@ import { slugify } from "@/lib/slug"; import { - adminProcedure, - createTRPCRouter, - protectedProcedure, + adminProcedure, + createTRPCRouter, + protectedProcedure, } from "@/server/api/trpc"; import { generatePassword } from "@/templates/utils"; import { IS_CLOUD } from "@dokploy/server/constants"; import { - apiCreateAi, - apiUpdateAi, - deploySuggestionSchema, + apiCreateAi, + apiUpdateAi, + deploySuggestionSchema, } from "@dokploy/server/db/schema/ai"; import { createDomain } from "@dokploy/server/index"; import { - deleteAiSettings, - getAiSettingById, - getAiSettingsByAdminId, - saveAiSettings, - suggestVariants, + deleteAiSettings, + getAiSettingById, + getAiSettingsByAdminId, + saveAiSettings, + suggestVariants, } from "@dokploy/server/services/ai"; import { createComposeByTemplate } from "@dokploy/server/services/compose"; import { findProjectById } from "@dokploy/server/services/project"; import { - addNewService, - checkServiceAccess, + addNewService, + checkServiceAccess, } from "@dokploy/server/services/user"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; export const aiRouter = createTRPCRouter({ - one: protectedProcedure - .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); - }), + one: protectedProcedure + .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 }) => { - return await saveAiSettings(ctx.user.adminId, input); - }), + update: protectedProcedure + .input(apiUpdateAi) + .mutation(async ({ ctx, input }) => { + return await saveAiSettings(ctx.user.adminId, input); + }), - getAll: adminProcedure.query(async ({ ctx }) => { - return await getAiSettingsByAdminId(ctx.user.adminId); - }), + 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; - }), + 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); - }), + 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 - .input( - z.object({ - aiId: z.string(), - input: z.string(), - serverId: z.string().optional(), - }) - ) - .mutation(async ({ ctx, input }) => { - return await suggestVariants({ - ...input, - adminId: ctx.user.adminId, - }); - }), - deploy: protectedProcedure - .input(deploySuggestionSchema) - .mutation(async ({ ctx, input }) => { - if (ctx.user.rol === "user") { - await checkServiceAccess(ctx.user.adminId, input.projectId, "create"); - } + suggest: protectedProcedure + .input( + z.object({ + aiId: z.string(), + input: z.string(), + serverId: z.string().optional(), + }), + ) + .mutation(async ({ ctx, input }) => { + return await suggestVariants({ + ...input, + adminId: ctx.user.adminId, + }); + }), + deploy: protectedProcedure + .input(deploySuggestionSchema) + .mutation(async ({ ctx, input }) => { + if (ctx.user.rol === "user") { + await checkServiceAccess(ctx.user.adminId, input.projectId, "create"); + } - if (IS_CLOUD && !input.serverId) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "You need to use a server to create a compose", - }); - } + if (IS_CLOUD && !input.serverId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You need to use a server to create a compose", + }); + } - const project = await findProjectById(input.projectId); + const project = await findProjectById(input.projectId); - const projectName = slugify(`${project.name} ${input.id}`); + const projectName = slugify(`${project.name} ${input.id}`); - console.log(input); + console.log(input); - const compose = await createComposeByTemplate({ - ...input, - composeFile: input.dockerCompose, - env: input.envVariables, - serverId: input.serverId, - name: input.name, - sourceType: "raw", - appName: `${projectName}-${generatePassword(6)}`, - }); + const compose = await createComposeByTemplate({ + ...input, + composeFile: input.dockerCompose, + env: input.envVariables, + serverId: input.serverId, + name: input.name, + sourceType: "raw", + appName: `${projectName}-${generatePassword(6)}`, + }); - if (input.domains && input.domains?.length > 0) { - for (const domain of input.domains) { - await createDomain({ - ...domain, - domainType: "compose", - certificateType: "none", - composeId: compose.composeId, - }); - } - } + if (input.domains && input.domains?.length > 0) { + for (const domain of input.domains) { + await createDomain({ + ...domain, + domainType: "compose", + certificateType: "none", + composeId: compose.composeId, + }); + } + } - if (ctx.user.rol === "user") { - await addNewService(ctx.user.authId, compose.composeId); - } + if (ctx.user.rol === "user") { + await addNewService(ctx.user.authId, compose.composeId); + } - return null; - }), + return null; + }), }); diff --git a/packages/server/src/db/schema/ai.ts b/packages/server/src/db/schema/ai.ts index 43f03dd0..5da6e03c 100644 --- a/packages/server/src/db/schema/ai.ts +++ b/packages/server/src/db/schema/ai.ts @@ -6,70 +6,70 @@ import { z } from "zod"; import { admins } from "./admin"; export const ai = pgTable("ai", { - aiId: text("aiId") - .notNull() - .primaryKey() - .$defaultFn(() => nanoid()), - name: text("name").notNull(), - apiUrl: text("apiUrl").notNull(), - apiKey: text("apiKey").notNull(), - model: text("model").notNull(), - 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()), + aiId: text("aiId") + .notNull() + .primaryKey() + .$defaultFn(() => nanoid()), + name: text("name").notNull(), + apiUrl: text("apiUrl").notNull(), + apiKey: text("apiKey").notNull(), + model: text("model").notNull(), + 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], - }), + admin: one(admins, { + fields: [ai.adminId], + references: [admins.adminId], + }), })); const createSchema = createInsertSchema(ai, { - 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().optional(), + 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().optional(), }); export const apiCreateAi = createSchema - .pick({ - name: true, - apiUrl: true, - apiKey: true, - model: true, - isEnabled: true, - }) - .required(); + .pick({ + name: true, + apiUrl: true, + apiKey: true, + model: true, + isEnabled: true, + }) + .required(); export const apiUpdateAi = createSchema - .partial() - .extend({ - aiId: z.string().min(1), - }) - .omit({ adminId: true }); + .partial() + .extend({ + aiId: z.string().min(1), + }) + .omit({ adminId: true }); export const deploySuggestionSchema = z.object({ - projectId: z.string().min(1), - id: z.string().min(1), - dockerCompose: z.string().min(1), - envVariables: z.string(), - serverId: z.string().optional(), - name: z.string().min(1), - description: z.string(), - domains: z - .array( - z.object({ - host: z.string().min(1), - port: z.number().min(1), - serviceName: z.string().min(1), - }) - ) - .optional(), + projectId: z.string().min(1), + id: z.string().min(1), + dockerCompose: z.string().min(1), + envVariables: z.string(), + serverId: z.string().optional(), + name: z.string().min(1), + description: z.string(), + domains: z + .array( + z.object({ + host: z.string().min(1), + port: z.number().min(1), + serviceName: z.string().min(1), + }), + ) + .optional(), }); diff --git a/packages/server/src/services/ai.ts b/packages/server/src/services/ai.ts index f810ff57..92f7f5e1 100644 --- a/packages/server/src/services/ai.ts +++ b/packages/server/src/services/ai.ts @@ -10,128 +10,128 @@ import { findAdminById } from "./admin"; import { findServerById } from "./server"; export const getAiSettingsByAdminId = async (adminId: string) => { - const aiSettings = await db.query.ai.findMany({ - where: eq(ai.adminId, adminId), - orderBy: desc(ai.createdAt), - }); - return aiSettings; + const aiSettings = await db.query.ai.findMany({ + where: eq(ai.adminId, adminId), + orderBy: desc(ai.createdAt), + }); + return aiSettings; }; 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; + 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; + const aiId = settings.aiId; - return db - .insert(ai) - .values({ - aiId, - adminId, - ...settings, - }) - .onConflictDoUpdate({ - target: ai.aiId, - set: { - ...settings, - }, - }); + return db + .insert(ai) + .values({ + aiId, + adminId, + ...settings, + }) + .onConflictDoUpdate({ + target: ai.aiId, + set: { + ...settings, + }, + }); }; export const deleteAiSettings = async (aiId: string) => { - return db.delete(ai).where(eq(ai.aiId, aiId)); + return db.delete(ai).where(eq(ai.aiId, aiId)); }; interface Props { - adminId: string; - aiId: string; - input: string; - serverId?: string | undefined; + adminId: string; + aiId: string; + input: string; + serverId?: string | undefined; } export const suggestVariants = async ({ - adminId, - aiId, - input, - serverId, + adminId, + aiId, + input, + serverId, }: Props) => { - try { - const aiSettings = await getAiSettingById(aiId); - if (!aiSettings || !aiSettings.isEnabled) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "AI features are not enabled for this configuration", - }); - } + try { + const aiSettings = await getAiSettingById(aiId); + if (!aiSettings || !aiSettings.isEnabled) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "AI features are not enabled for this configuration", + }); + } - const provider = selectAIProvider(aiSettings); - const model = provider(aiSettings.model); + const provider = selectAIProvider(aiSettings); + const model = provider(aiSettings.model); - let ip = ""; - if (!IS_CLOUD) { - const admin = await findAdminById(adminId); - ip = admin?.serverIp || ""; - } + let ip = ""; + if (!IS_CLOUD) { + const admin = await findAdminById(adminId); + ip = admin?.serverIp || ""; + } - if (serverId) { - const server = await findServerById(serverId); - ip = server.ipAddress; - } else if (process.env.NODE_ENV === "development") { - ip = "127.0.0.1"; - } + if (serverId) { + const server = await findServerById(serverId); + ip = server.ipAddress; + } else if (process.env.NODE_ENV === "development") { + ip = "127.0.0.1"; + } - const { object } = await generateObject({ - model, - output: "array", - schema: z.object({ - id: z.string(), - name: z.string(), - shortDescription: z.string(), - description: z.string(), - }), - prompt: ` + const { object } = await generateObject({ + model, + output: "array", + schema: z.object({ + id: z.string(), + name: z.string(), + shortDescription: z.string(), + description: z.string(), + }), + prompt: ` Act as advanced DevOps engineer and generate a list of open source projects what can cover users needs(up to 3 items), the suggestion should include id, name, shortDescription, and description. Use slug of title for id. The description should be in markdown format with full description of suggested stack. The shortDescription should be in plain text and have short information about used technologies. User wants to create a new project with the following details, it should be installable in docker and can be docker compose generated for it: ${input} `, - }); + }); - if (object?.length) { - const result = []; - for (const suggestion of object) { - try { - const { object: docker } = await generateObject({ - model, - output: "object", - schema: z.object({ - dockerCompose: z.string(), - envVariables: z.array( - z.object({ - name: z.string(), - value: z.string(), - }) - ), - domains: z.array( - z.object({ - host: z.string(), - port: z.number(), - serviceName: z.string(), - }) - ), - }), - prompt: ` + if (object?.length) { + const result = []; + for (const suggestion of object) { + try { + const { object: docker } = await generateObject({ + model, + output: "object", + schema: z.object({ + dockerCompose: z.string(), + envVariables: z.array( + z.object({ + name: z.string(), + value: z.string(), + }), + ), + domains: z.array( + z.object({ + host: z.string(), + port: z.number(), + serviceName: z.string(), + }), + ), + }), + prompt: ` Act as advanced DevOps engineer and generate docker compose with environment variables and domain configurations needed to install the following project. Return the docker compose as a YAML string. Follow these rules: 1. Use placeholder like \${VARIABLE_NAME-default} for generated variables @@ -152,26 +152,26 @@ export const suggestVariants = async ({ Project details: ${suggestion?.description} `, - }); - if (!!docker && !!docker.dockerCompose) { - result.push({ - ...suggestion, - ...docker, - }); - } - } catch (error) { - console.error("Error in docker compose generation:", error); - } - } - return result; - } + }); + if (!!docker && !!docker.dockerCompose) { + result.push({ + ...suggestion, + ...docker, + }); + } + } catch (error) { + console.error("Error in docker compose generation:", error); + } + } + return result; + } - throw new TRPCError({ - code: "NOT_FOUND", - message: "No suggestions found", - }); - } catch (error) { - console.error("Error in suggestVariants:", error); - throw error; - } + throw new TRPCError({ + code: "NOT_FOUND", + message: "No suggestions found", + }); + } catch (error) { + console.error("Error in suggestVariants:", error); + throw error; + } };