feat: add missing functions

This commit is contained in:
Mauricio Siu
2025-01-18 22:58:27 -06:00
parent 87546b4558
commit 5e7d344110
5 changed files with 284 additions and 287 deletions

View File

@@ -2,7 +2,6 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -12,8 +11,9 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { useState } from "react"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { useState } from "react";
const examples = [ const examples = [
"Make a personal blog", "Make a personal blog",

View File

@@ -136,10 +136,7 @@ export const StepTwo = ({
setSelectedVariant({ setSelectedVariant({
...selectedVariant, ...selectedVariant,
envVariables: [ envVariables: [...selectedVariant.envVariables, { name: "", value: "" }],
...selectedVariant.envVariables,
{ name: "", value: "" },
],
}); });
}; };

View File

@@ -1,145 +1,145 @@
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { import {
adminProcedure, adminProcedure,
createTRPCRouter, createTRPCRouter,
protectedProcedure, protectedProcedure,
} from "@/server/api/trpc"; } 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 {
apiCreateAi, apiCreateAi,
apiUpdateAi, apiUpdateAi,
deploySuggestionSchema, deploySuggestionSchema,
} from "@dokploy/server/db/schema/ai"; } from "@dokploy/server/db/schema/ai";
import { createDomain } from "@dokploy/server/index"; import { createDomain } from "@dokploy/server/index";
import { import {
deleteAiSettings, deleteAiSettings,
getAiSettingById, getAiSettingById,
getAiSettingsByAdminId, getAiSettingsByAdminId,
saveAiSettings, saveAiSettings,
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";
import { findProjectById } from "@dokploy/server/services/project"; import { findProjectById } from "@dokploy/server/services/project";
import { import {
addNewService, addNewService,
checkServiceAccess, checkServiceAccess,
} from "@dokploy/server/services/user"; } from "@dokploy/server/services/user";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
export const aiRouter = createTRPCRouter({ export const aiRouter = createTRPCRouter({
one: protectedProcedure one: protectedProcedure
.input(z.object({ aiId: z.string() })) .input(z.object({ aiId: z.string() }))
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const aiSetting = await getAiSettingById(input.aiId); const aiSetting = await getAiSettingById(input.aiId);
if (aiSetting.adminId !== ctx.user.adminId) { if (aiSetting.adminId !== ctx.user.adminId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You don't have access to this AI configuration", message: "You don't have access to this AI configuration",
}); });
} }
return aiSetting; return aiSetting;
}), }),
create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => { create: adminProcedure.input(apiCreateAi).mutation(async ({ ctx, input }) => {
return await saveAiSettings(ctx.user.adminId, input); return await saveAiSettings(ctx.user.adminId, input);
}), }),
update: protectedProcedure update: protectedProcedure
.input(apiUpdateAi) .input(apiUpdateAi)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
return await saveAiSettings(ctx.user.adminId, input); return await saveAiSettings(ctx.user.adminId, input);
}), }),
getAll: adminProcedure.query(async ({ ctx }) => { getAll: adminProcedure.query(async ({ ctx }) => {
return await getAiSettingsByAdminId(ctx.user.adminId); return await getAiSettingsByAdminId(ctx.user.adminId);
}), }),
get: protectedProcedure get: protectedProcedure
.input(z.object({ aiId: z.string() })) .input(z.object({ aiId: z.string() }))
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const aiSetting = await getAiSettingById(input.aiId); const aiSetting = await getAiSettingById(input.aiId);
if (aiSetting.adminId !== ctx.user.authId) { if (aiSetting.adminId !== ctx.user.authId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You don't have access to this AI configuration", message: "You don't have access to this AI configuration",
}); });
} }
return aiSetting; return aiSetting;
}), }),
delete: protectedProcedure delete: protectedProcedure
.input(z.object({ aiId: z.string() })) .input(z.object({ aiId: z.string() }))
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const aiSetting = await getAiSettingById(input.aiId); const aiSetting = await getAiSettingById(input.aiId);
if (aiSetting.adminId !== ctx.user.adminId) { if (aiSetting.adminId !== ctx.user.adminId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You don't have access to this AI configuration", message: "You don't have access to this AI configuration",
}); });
} }
return await deleteAiSettings(input.aiId); return await deleteAiSettings(input.aiId);
}), }),
suggest: protectedProcedure suggest: protectedProcedure
.input( .input(
z.object({ z.object({
aiId: z.string(), aiId: z.string(),
input: z.string(), input: z.string(),
serverId: z.string().optional(), serverId: z.string().optional(),
}) }),
) )
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
return await suggestVariants({ return await suggestVariants({
...input, ...input,
adminId: ctx.user.adminId, adminId: ctx.user.adminId,
}); });
}), }),
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.adminId, input.projectId, "create"); await checkServiceAccess(ctx.user.adminId, input.projectId, "create");
} }
if (IS_CLOUD && !input.serverId) { if (IS_CLOUD && !input.serverId) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
message: "You need to use a server to create a compose", 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({ const compose = await createComposeByTemplate({
...input, ...input,
composeFile: input.dockerCompose, composeFile: input.dockerCompose,
env: input.envVariables, env: input.envVariables,
serverId: input.serverId, serverId: input.serverId,
name: input.name, name: input.name,
sourceType: "raw", sourceType: "raw",
appName: `${projectName}-${generatePassword(6)}`, appName: `${projectName}-${generatePassword(6)}`,
}); });
if (input.domains && input.domains?.length > 0) { if (input.domains && input.domains?.length > 0) {
for (const domain of input.domains) { for (const domain of input.domains) {
await createDomain({ await createDomain({
...domain, ...domain,
domainType: "compose", domainType: "compose",
certificateType: "none", certificateType: "none",
composeId: compose.composeId, composeId: compose.composeId,
}); });
} }
} }
if (ctx.user.rol === "user") { if (ctx.user.rol === "user") {
await addNewService(ctx.user.authId, compose.composeId); await addNewService(ctx.user.authId, compose.composeId);
} }
return null; return null;
}), }),
}); });

View File

@@ -6,70 +6,70 @@ import { z } from "zod";
import { admins } from "./admin"; import { admins } from "./admin";
export const ai = pgTable("ai", { export const ai = pgTable("ai", {
aiId: text("aiId") aiId: text("aiId")
.notNull() .notNull()
.primaryKey() .primaryKey()
.$defaultFn(() => nanoid()), .$defaultFn(() => nanoid()),
name: text("name").notNull(), 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") adminId: text("adminId")
.notNull() .notNull()
.references(() => admins.adminId, { onDelete: "cascade" }), // Admin ID who created the AI settings .references(() => admins.adminId, { onDelete: "cascade" }), // Admin ID who created the AI settings
createdAt: text("createdAt") createdAt: text("createdAt")
.notNull() .notNull()
.$defaultFn(() => new Date().toISOString()), .$defaultFn(() => new Date().toISOString()),
}); });
export const aiRelations = relations(ai, ({ one }) => ({ export const aiRelations = relations(ai, ({ one }) => ({
admin: one(admins, { admin: one(admins, {
fields: [ai.adminId], fields: [ai.adminId],
references: [admins.adminId], references: [admins.adminId],
}), }),
})); }));
const createSchema = createInsertSchema(ai, { const createSchema = createInsertSchema(ai, {
name: z.string().min(1, { message: "Name is required" }), 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 apiCreateAi = createSchema export const apiCreateAi = createSchema
.pick({ .pick({
name: true, name: true,
apiUrl: true, apiUrl: true,
apiKey: true, apiKey: true,
model: true, model: true,
isEnabled: true, isEnabled: true,
}) })
.required(); .required();
export const apiUpdateAi = createSchema export const apiUpdateAi = createSchema
.partial() .partial()
.extend({ .extend({
aiId: z.string().min(1), aiId: z.string().min(1),
}) })
.omit({ adminId: true }); .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),
dockerCompose: z.string().min(1), dockerCompose: z.string().min(1),
envVariables: z.string(), envVariables: z.string(),
serverId: z.string().optional(), serverId: z.string().optional(),
name: z.string().min(1), name: z.string().min(1),
description: z.string(), description: z.string(),
domains: z domains: z
.array( .array(
z.object({ z.object({
host: z.string().min(1), host: z.string().min(1),
port: z.number().min(1), port: z.number().min(1),
serviceName: z.string().min(1), serviceName: z.string().min(1),
}) }),
) )
.optional(), .optional(),
}); });

View File

@@ -10,128 +10,128 @@ import { findAdminById } from "./admin";
import { findServerById } from "./server"; import { findServerById } from "./server";
export const getAiSettingsByAdminId = async (adminId: string) => { export const getAiSettingsByAdminId = async (adminId: string) => {
const aiSettings = await db.query.ai.findMany({ const aiSettings = await db.query.ai.findMany({
where: eq(ai.adminId, adminId), where: eq(ai.adminId, adminId),
orderBy: desc(ai.createdAt), orderBy: desc(ai.createdAt),
}); });
return aiSettings; return aiSettings;
}; };
export const getAiSettingById = async (aiId: string) => { export const getAiSettingById = async (aiId: string) => {
const aiSetting = await db.query.ai.findFirst({ const aiSetting = await db.query.ai.findFirst({
where: eq(ai.aiId, aiId), where: eq(ai.aiId, aiId),
}); });
if (!aiSetting) { if (!aiSetting) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "AI settings not found", message: "AI settings not found",
}); });
} }
return aiSetting; return aiSetting;
}; };
export const saveAiSettings = async (adminId: string, settings: any) => { export const saveAiSettings = async (adminId: string, settings: any) => {
const aiId = settings.aiId; const aiId = settings.aiId;
return db return db
.insert(ai) .insert(ai)
.values({ .values({
aiId, aiId,
adminId, adminId,
...settings, ...settings,
}) })
.onConflictDoUpdate({ .onConflictDoUpdate({
target: ai.aiId, target: ai.aiId,
set: { set: {
...settings, ...settings,
}, },
}); });
}; };
export const deleteAiSettings = async (aiId: string) => { 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 { interface Props {
adminId: string; adminId: string;
aiId: string; aiId: string;
input: string; input: string;
serverId?: string | undefined; serverId?: string | undefined;
} }
export const suggestVariants = async ({ export const suggestVariants = async ({
adminId, adminId,
aiId, aiId,
input, input,
serverId, serverId,
}: Props) => { }: Props) => {
try { try {
const aiSettings = await getAiSettingById(aiId); 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 for this configuration", 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 = ""; let ip = "";
if (!IS_CLOUD) { if (!IS_CLOUD) {
const admin = await findAdminById(adminId); const admin = await findAdminById(adminId);
ip = admin?.serverIp || ""; ip = admin?.serverIp || "";
} }
if (serverId) { if (serverId) {
const server = await findServerById(serverId); const server = await findServerById(serverId);
ip = server.ipAddress; ip = server.ipAddress;
} else if (process.env.NODE_ENV === "development") { } else if (process.env.NODE_ENV === "development") {
ip = "127.0.0.1"; ip = "127.0.0.1";
} }
const { object } = await generateObject({ const { object } = await generateObject({
model, model,
output: "array", output: "array",
schema: z.object({ schema: z.object({
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
shortDescription: z.string(), shortDescription: z.string(),
description: z.string(), description: z.string(),
}), }),
prompt: ` 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 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. 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: 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} ${input}
`, `,
}); });
if (object?.length) { if (object?.length) {
const result = []; const result = [];
for (const suggestion of object) { for (const suggestion of object) {
try { try {
const { object: docker } = await generateObject({ const { object: docker } = await generateObject({
model, model,
output: "object", output: "object",
schema: z.object({ schema: z.object({
dockerCompose: z.string(), dockerCompose: z.string(),
envVariables: z.array( envVariables: z.array(
z.object({ z.object({
name: z.string(), name: z.string(),
value: z.string(), value: z.string(),
}) }),
), ),
domains: z.array( domains: z.array(
z.object({ z.object({
host: z.string(), host: z.string(),
port: z.number(), port: z.number(),
serviceName: z.string(), serviceName: z.string(),
}) }),
), ),
}), }),
prompt: ` prompt: `
Act as advanced DevOps engineer and generate docker compose with environment variables and domain configurations 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.
Return the docker compose as a YAML string. Follow these rules: Return the docker compose as a YAML string. Follow these rules:
1. Use placeholder like \${VARIABLE_NAME-default} for generated variables 1. Use placeholder like \${VARIABLE_NAME-default} for generated variables
@@ -152,26 +152,26 @@ export const suggestVariants = async ({
Project details: Project details:
${suggestion?.description} ${suggestion?.description}
`, `,
}); });
if (!!docker && !!docker.dockerCompose) { if (!!docker && !!docker.dockerCompose) {
result.push({ result.push({
...suggestion, ...suggestion,
...docker, ...docker,
}); });
} }
} catch (error) { } catch (error) {
console.error("Error in docker compose generation:", error); console.error("Error in docker compose generation:", error);
} }
} }
return result; return result;
} }
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",
message: "No suggestions found", message: "No suggestions found",
}); });
} catch (error) { } catch (error) {
console.error("Error in suggestVariants:", error); console.error("Error in suggestVariants:", error);
throw error; throw error;
} }
}; };