diff --git a/bun.lock b/bun.lock index 8f11f97..279025d 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "forgery", @@ -48,6 +47,7 @@ "@types/bun": "^1.3.8", "@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/parser": "^8.54.0", + "dotenv": "^17.2.4", "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "prettier": "^3.8.1", @@ -534,7 +534,7 @@ "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], - "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "dotenv": ["dotenv@17.2.4", "", {}, "sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -1170,6 +1170,8 @@ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + "c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "eslint/ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], diff --git a/package.json b/package.json index 5127e7b..d11b873 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@types/bun": "^1.3.8", "@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/parser": "^8.54.0", + "dotenv": "^17.2.4", "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "prettier": "^3.8.1", diff --git a/prisma/migrations/20260210021646_add_user_prompt_to_generation/migration.sql b/prisma/migrations/20260210021646_add_user_prompt_to_generation/migration.sql new file mode 100644 index 0000000..f483c04 --- /dev/null +++ b/prisma/migrations/20260210021646_add_user_prompt_to_generation/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "generations" ADD COLUMN "user_prompt" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e18830e..39d4534 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -65,6 +65,7 @@ model Generation { id String @id characterId String @map("character_id") imageId String @map("image_id") + userPrompt String? @map("user_prompt") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") diff --git a/src/api/generation.test.ts b/src/api/generation.test.ts index 24ed3f1..3051e85 100644 --- a/src/api/generation.test.ts +++ b/src/api/generation.test.ts @@ -42,6 +42,33 @@ describe('Generation API', () => { const res = await generation.fetch(req) expect(res.status).toBe(201) expect(await res.json()).toEqual({ id: '1' }) - expect(mockGenerationService.createGeneration).toHaveBeenCalledWith('char1', ['eq1', 'eq2']) + expect(mockGenerationService.createGeneration).toHaveBeenCalledWith( + 'char1', + ['eq1', 'eq2'], + undefined, + ) + }) + + it('POST / should create generation with userPrompt', async () => { + mockGenerationService.createGeneration.mockResolvedValue({ id: '1' }) + + const req = new Request('http://localhost/', { + method: 'POST', + body: JSON.stringify({ + characterId: 'char1', + equipmentIds: ['eq1', 'eq2'], + userPrompt: 'test prompt', + }), + headers: { 'Content-Type': 'application/json' }, + }) + + const res = await generation.fetch(req) + expect(res.status).toBe(201) + expect(await res.json()).toEqual({ id: '1' }) + expect(mockGenerationService.createGeneration).toHaveBeenCalledWith( + 'char1', + ['eq1', 'eq2'], + 'test prompt', + ) }) }) diff --git a/src/api/generation.ts b/src/api/generation.ts index a2a5cd3..32b3ca3 100644 --- a/src/api/generation.ts +++ b/src/api/generation.ts @@ -15,6 +15,7 @@ const listSchema = z.object({ const createSchema = z.object({ characterId: z.string().min(1), equipmentIds: z.array(z.string()), + userPrompt: z.string().optional(), }) const route = app @@ -28,9 +29,9 @@ const route = app return c.json(result) }) .post('/', zValidator('json', createSchema), async (c) => { - const { characterId, equipmentIds } = c.req.valid('json') + const { characterId, equipmentIds, userPrompt } = c.req.valid('json') try { - const result = await generationService.createGeneration(characterId, equipmentIds) + const result = await generationService.createGeneration(characterId, equipmentIds, userPrompt) return c.json(result, 201) } catch (e) { console.error('Failed to create generation', e) diff --git a/src/generated/prisma/internal/class.ts b/src/generated/prisma/internal/class.ts index 8b91e0f..e8267ab 100644 --- a/src/generated/prisma/internal/class.ts +++ b/src/generated/prisma/internal/class.ts @@ -16,13 +16,11 @@ import type * as Prisma from "./prismaNamespace.ts" const config: runtime.GetPrismaClientConfig = { - "previewFeatures": [ - "driverAdapters" - ], + "previewFeatures": [], "clientVersion": "7.3.0", "engineVersion": "9d6ad21cbbceab97458517b147a6a09ff43aa735", "activeProvider": "sqlite", - "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../src/generated/prisma\"\n previewFeatures = [\"driverAdapters\"]\n}\n\ndatasource db {\n provider = \"sqlite\"\n}\n\nmodel Asset {\n id String @id\n name String\n type String\n path String\n createdAt DateTime @default(now()) @map(\"created_at\")\n updatedAt DateTime @updatedAt @map(\"updated_at\")\n\n // Relations\n characters Character[]\n equipments Equipment[]\n generations Generation[]\n\n @@map(\"assets\")\n}\n\nmodel Character {\n id String @id\n name String\n description String?\n imageId String @map(\"image_id\")\n createdAt DateTime @default(now()) @map(\"created_at\")\n updatedAt DateTime @updatedAt @map(\"updated_at\")\n\n // Relations\n image Asset @relation(fields: [imageId], references: [id])\n generations Generation[]\n\n @@map(\"characters\")\n}\n\nmodel Equipment {\n id String @id\n name String\n description String\n category String\n subCategory String? @map(\"sub_category\")\n imageId String @map(\"image_id\")\n createdAt DateTime @default(now()) @map(\"created_at\")\n updatedAt DateTime @updatedAt @map(\"updated_at\")\n\n // Relations\n image Asset @relation(fields: [imageId], references: [id])\n generations GenerationEquipment[]\n\n @@index([category, subCategory])\n @@map(\"equipments\")\n}\n\nmodel Generation {\n id String @id\n characterId String @map(\"character_id\")\n imageId String @map(\"image_id\")\n createdAt DateTime @default(now()) @map(\"created_at\")\n updatedAt DateTime @updatedAt @map(\"updated_at\")\n\n // Relations\n character Character @relation(fields: [characterId], references: [id])\n image Asset @relation(fields: [imageId], references: [id])\n equipments GenerationEquipment[]\n\n @@map(\"generations\")\n}\n\nmodel GenerationEquipment {\n generationId String @map(\"generation_id\")\n equipmentId String @map(\"equipment_id\")\n\n generation Generation @relation(fields: [generationId], references: [id])\n equipment Equipment @relation(fields: [equipmentId], references: [id])\n\n @@id([generationId, equipmentId])\n @@map(\"generation_equipments\")\n}\n\nmodel Setting {\n key String @id\n value String\n\n createdAt DateTime @default(now()) @map(\"created_at\")\n updatedAt DateTime @updatedAt @map(\"updated_at\")\n\n @@map(\"settings\")\n}\n", + "inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"../src/generated/prisma\"\n engineType = \"client\"\n runtime = \"bun\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n}\n\nmodel Asset {\n id String @id\n name String\n type String\n path String\n createdAt DateTime @default(now()) @map(\"created_at\")\n updatedAt DateTime @updatedAt @map(\"updated_at\")\n\n // Relations\n characters Character[]\n equipments Equipment[]\n generations Generation[]\n\n @@map(\"assets\")\n}\n\nmodel Character {\n id String @id\n name String\n description String?\n imageId String @map(\"image_id\")\n createdAt DateTime @default(now()) @map(\"created_at\")\n updatedAt DateTime @updatedAt @map(\"updated_at\")\n\n // Relations\n image Asset @relation(fields: [imageId], references: [id])\n generations Generation[]\n\n @@map(\"characters\")\n}\n\nmodel Equipment {\n id String @id\n name String\n description String\n category String\n subCategory String? @map(\"sub_category\")\n imageId String @map(\"image_id\")\n createdAt DateTime @default(now()) @map(\"created_at\")\n updatedAt DateTime @updatedAt @map(\"updated_at\")\n\n // Relations\n image Asset @relation(fields: [imageId], references: [id])\n generations GenerationEquipment[]\n\n @@index([category, subCategory])\n @@map(\"equipments\")\n}\n\nmodel Generation {\n id String @id\n characterId String @map(\"character_id\")\n imageId String @map(\"image_id\")\n userPrompt String? @map(\"user_prompt\")\n createdAt DateTime @default(now()) @map(\"created_at\")\n updatedAt DateTime @updatedAt @map(\"updated_at\")\n\n // Relations\n character Character @relation(fields: [characterId], references: [id])\n image Asset @relation(fields: [imageId], references: [id])\n equipments GenerationEquipment[]\n\n @@map(\"generations\")\n}\n\nmodel GenerationEquipment {\n generationId String @map(\"generation_id\")\n equipmentId String @map(\"equipment_id\")\n\n generation Generation @relation(fields: [generationId], references: [id])\n equipment Equipment @relation(fields: [equipmentId], references: [id])\n\n @@id([generationId, equipmentId])\n @@map(\"generation_equipments\")\n}\n\nmodel Setting {\n key String @id\n value String\n\n createdAt DateTime @default(now()) @map(\"created_at\")\n updatedAt DateTime @updatedAt @map(\"updated_at\")\n\n @@map(\"settings\")\n}\n", "runtimeDataModel": { "models": {}, "enums": {}, @@ -30,7 +28,7 @@ const config: runtime.GetPrismaClientConfig = { } } -config.runtimeDataModel = JSON.parse("{\"models\":{\"Asset\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"path\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"},{\"name\":\"characters\",\"kind\":\"object\",\"type\":\"Character\",\"relationName\":\"AssetToCharacter\"},{\"name\":\"equipments\",\"kind\":\"object\",\"type\":\"Equipment\",\"relationName\":\"AssetToEquipment\"},{\"name\":\"generations\",\"kind\":\"object\",\"type\":\"Generation\",\"relationName\":\"AssetToGeneration\"}],\"dbName\":\"assets\"},\"Character\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"imageId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"image_id\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"},{\"name\":\"image\",\"kind\":\"object\",\"type\":\"Asset\",\"relationName\":\"AssetToCharacter\"},{\"name\":\"generations\",\"kind\":\"object\",\"type\":\"Generation\",\"relationName\":\"CharacterToGeneration\"}],\"dbName\":\"characters\"},\"Equipment\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"category\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"subCategory\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"sub_category\"},{\"name\":\"imageId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"image_id\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"},{\"name\":\"image\",\"kind\":\"object\",\"type\":\"Asset\",\"relationName\":\"AssetToEquipment\"},{\"name\":\"generations\",\"kind\":\"object\",\"type\":\"GenerationEquipment\",\"relationName\":\"EquipmentToGenerationEquipment\"}],\"dbName\":\"equipments\"},\"Generation\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"characterId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"character_id\"},{\"name\":\"imageId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"image_id\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"},{\"name\":\"character\",\"kind\":\"object\",\"type\":\"Character\",\"relationName\":\"CharacterToGeneration\"},{\"name\":\"image\",\"kind\":\"object\",\"type\":\"Asset\",\"relationName\":\"AssetToGeneration\"},{\"name\":\"equipments\",\"kind\":\"object\",\"type\":\"GenerationEquipment\",\"relationName\":\"GenerationToGenerationEquipment\"}],\"dbName\":\"generations\"},\"GenerationEquipment\":{\"fields\":[{\"name\":\"generationId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"generation_id\"},{\"name\":\"equipmentId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"equipment_id\"},{\"name\":\"generation\",\"kind\":\"object\",\"type\":\"Generation\",\"relationName\":\"GenerationToGenerationEquipment\"},{\"name\":\"equipment\",\"kind\":\"object\",\"type\":\"Equipment\",\"relationName\":\"EquipmentToGenerationEquipment\"}],\"dbName\":\"generation_equipments\"},\"Setting\":{\"fields\":[{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"}],\"dbName\":\"settings\"}},\"enums\":{},\"types\":{}}") +config.runtimeDataModel = JSON.parse("{\"models\":{\"Asset\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"path\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"},{\"name\":\"characters\",\"kind\":\"object\",\"type\":\"Character\",\"relationName\":\"AssetToCharacter\"},{\"name\":\"equipments\",\"kind\":\"object\",\"type\":\"Equipment\",\"relationName\":\"AssetToEquipment\"},{\"name\":\"generations\",\"kind\":\"object\",\"type\":\"Generation\",\"relationName\":\"AssetToGeneration\"}],\"dbName\":\"assets\"},\"Character\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"imageId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"image_id\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"},{\"name\":\"image\",\"kind\":\"object\",\"type\":\"Asset\",\"relationName\":\"AssetToCharacter\"},{\"name\":\"generations\",\"kind\":\"object\",\"type\":\"Generation\",\"relationName\":\"CharacterToGeneration\"}],\"dbName\":\"characters\"},\"Equipment\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"category\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"subCategory\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"sub_category\"},{\"name\":\"imageId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"image_id\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"},{\"name\":\"image\",\"kind\":\"object\",\"type\":\"Asset\",\"relationName\":\"AssetToEquipment\"},{\"name\":\"generations\",\"kind\":\"object\",\"type\":\"GenerationEquipment\",\"relationName\":\"EquipmentToGenerationEquipment\"}],\"dbName\":\"equipments\"},\"Generation\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"characterId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"character_id\"},{\"name\":\"imageId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"image_id\"},{\"name\":\"userPrompt\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"user_prompt\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"},{\"name\":\"character\",\"kind\":\"object\",\"type\":\"Character\",\"relationName\":\"CharacterToGeneration\"},{\"name\":\"image\",\"kind\":\"object\",\"type\":\"Asset\",\"relationName\":\"AssetToGeneration\"},{\"name\":\"equipments\",\"kind\":\"object\",\"type\":\"GenerationEquipment\",\"relationName\":\"GenerationToGenerationEquipment\"}],\"dbName\":\"generations\"},\"GenerationEquipment\":{\"fields\":[{\"name\":\"generationId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"generation_id\"},{\"name\":\"equipmentId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"equipment_id\"},{\"name\":\"generation\",\"kind\":\"object\",\"type\":\"Generation\",\"relationName\":\"GenerationToGenerationEquipment\"},{\"name\":\"equipment\",\"kind\":\"object\",\"type\":\"Equipment\",\"relationName\":\"EquipmentToGenerationEquipment\"}],\"dbName\":\"generation_equipments\"},\"Setting\":{\"fields\":[{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"value\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"}],\"dbName\":\"settings\"}},\"enums\":{},\"types\":{}}") async function decodeBase64AsWasm(wasmBase64: string): Promise { const { Buffer } = await import('node:buffer') diff --git a/src/generated/prisma/internal/prismaNamespace.ts b/src/generated/prisma/internal/prismaNamespace.ts index c9f37bd..b2e817b 100644 --- a/src/generated/prisma/internal/prismaNamespace.ts +++ b/src/generated/prisma/internal/prismaNamespace.ts @@ -931,6 +931,7 @@ export const GenerationScalarFieldEnum = { id: 'id', characterId: 'characterId', imageId: 'imageId', + userPrompt: 'userPrompt', createdAt: 'createdAt', updatedAt: 'updatedAt' } as const diff --git a/src/generated/prisma/internal/prismaNamespaceBrowser.ts b/src/generated/prisma/internal/prismaNamespaceBrowser.ts index 44aef08..dad148d 100644 --- a/src/generated/prisma/internal/prismaNamespaceBrowser.ts +++ b/src/generated/prisma/internal/prismaNamespaceBrowser.ts @@ -114,6 +114,7 @@ export const GenerationScalarFieldEnum = { id: 'id', characterId: 'characterId', imageId: 'imageId', + userPrompt: 'userPrompt', createdAt: 'createdAt', updatedAt: 'updatedAt' } as const diff --git a/src/generated/prisma/models/Generation.ts b/src/generated/prisma/models/Generation.ts index 80a842d..1a78cad 100644 --- a/src/generated/prisma/models/Generation.ts +++ b/src/generated/prisma/models/Generation.ts @@ -28,6 +28,7 @@ export type GenerationMinAggregateOutputType = { id: string | null characterId: string | null imageId: string | null + userPrompt: string | null createdAt: Date | null updatedAt: Date | null } @@ -36,6 +37,7 @@ export type GenerationMaxAggregateOutputType = { id: string | null characterId: string | null imageId: string | null + userPrompt: string | null createdAt: Date | null updatedAt: Date | null } @@ -44,6 +46,7 @@ export type GenerationCountAggregateOutputType = { id: number characterId: number imageId: number + userPrompt: number createdAt: number updatedAt: number _all: number @@ -54,6 +57,7 @@ export type GenerationMinAggregateInputType = { id?: true characterId?: true imageId?: true + userPrompt?: true createdAt?: true updatedAt?: true } @@ -62,6 +66,7 @@ export type GenerationMaxAggregateInputType = { id?: true characterId?: true imageId?: true + userPrompt?: true createdAt?: true updatedAt?: true } @@ -70,6 +75,7 @@ export type GenerationCountAggregateInputType = { id?: true characterId?: true imageId?: true + userPrompt?: true createdAt?: true updatedAt?: true _all?: true @@ -151,6 +157,7 @@ export type GenerationGroupByOutputType = { id: string characterId: string imageId: string + userPrompt: string | null createdAt: Date updatedAt: Date _count: GenerationCountAggregateOutputType | null @@ -180,6 +187,7 @@ export type GenerationWhereInput = { id?: Prisma.StringFilter<"Generation"> | string characterId?: Prisma.StringFilter<"Generation"> | string imageId?: Prisma.StringFilter<"Generation"> | string + userPrompt?: Prisma.StringNullableFilter<"Generation"> | string | null createdAt?: Prisma.DateTimeFilter<"Generation"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Generation"> | Date | string character?: Prisma.XOR @@ -191,6 +199,7 @@ export type GenerationOrderByWithRelationInput = { id?: Prisma.SortOrder characterId?: Prisma.SortOrder imageId?: Prisma.SortOrder + userPrompt?: Prisma.SortOrderInput | Prisma.SortOrder createdAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder character?: Prisma.CharacterOrderByWithRelationInput @@ -205,6 +214,7 @@ export type GenerationWhereUniqueInput = Prisma.AtLeast<{ NOT?: Prisma.GenerationWhereInput | Prisma.GenerationWhereInput[] characterId?: Prisma.StringFilter<"Generation"> | string imageId?: Prisma.StringFilter<"Generation"> | string + userPrompt?: Prisma.StringNullableFilter<"Generation"> | string | null createdAt?: Prisma.DateTimeFilter<"Generation"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Generation"> | Date | string character?: Prisma.XOR @@ -216,6 +226,7 @@ export type GenerationOrderByWithAggregationInput = { id?: Prisma.SortOrder characterId?: Prisma.SortOrder imageId?: Prisma.SortOrder + userPrompt?: Prisma.SortOrderInput | Prisma.SortOrder createdAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder _count?: Prisma.GenerationCountOrderByAggregateInput @@ -230,12 +241,14 @@ export type GenerationScalarWhereWithAggregatesInput = { id?: Prisma.StringWithAggregatesFilter<"Generation"> | string characterId?: Prisma.StringWithAggregatesFilter<"Generation"> | string imageId?: Prisma.StringWithAggregatesFilter<"Generation"> | string + userPrompt?: Prisma.StringNullableWithAggregatesFilter<"Generation"> | string | null createdAt?: Prisma.DateTimeWithAggregatesFilter<"Generation"> | Date | string updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Generation"> | Date | string } export type GenerationCreateInput = { id: string + userPrompt?: string | null createdAt?: Date | string updatedAt?: Date | string character: Prisma.CharacterCreateNestedOneWithoutGenerationsInput @@ -247,6 +260,7 @@ export type GenerationUncheckedCreateInput = { id: string characterId: string imageId: string + userPrompt?: string | null createdAt?: Date | string updatedAt?: Date | string equipments?: Prisma.GenerationEquipmentUncheckedCreateNestedManyWithoutGenerationInput @@ -254,6 +268,7 @@ export type GenerationUncheckedCreateInput = { export type GenerationUpdateInput = { id?: Prisma.StringFieldUpdateOperationsInput | string + userPrompt?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string character?: Prisma.CharacterUpdateOneRequiredWithoutGenerationsNestedInput @@ -265,6 +280,7 @@ export type GenerationUncheckedUpdateInput = { id?: Prisma.StringFieldUpdateOperationsInput | string characterId?: Prisma.StringFieldUpdateOperationsInput | string imageId?: Prisma.StringFieldUpdateOperationsInput | string + userPrompt?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string equipments?: Prisma.GenerationEquipmentUncheckedUpdateManyWithoutGenerationNestedInput @@ -274,12 +290,14 @@ export type GenerationCreateManyInput = { id: string characterId: string imageId: string + userPrompt?: string | null createdAt?: Date | string updatedAt?: Date | string } export type GenerationUpdateManyMutationInput = { id?: Prisma.StringFieldUpdateOperationsInput | string + userPrompt?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string } @@ -288,6 +306,7 @@ export type GenerationUncheckedUpdateManyInput = { id?: Prisma.StringFieldUpdateOperationsInput | string characterId?: Prisma.StringFieldUpdateOperationsInput | string imageId?: Prisma.StringFieldUpdateOperationsInput | string + userPrompt?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string } @@ -306,6 +325,7 @@ export type GenerationCountOrderByAggregateInput = { id?: Prisma.SortOrder characterId?: Prisma.SortOrder imageId?: Prisma.SortOrder + userPrompt?: Prisma.SortOrder createdAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder } @@ -314,6 +334,7 @@ export type GenerationMaxOrderByAggregateInput = { id?: Prisma.SortOrder characterId?: Prisma.SortOrder imageId?: Prisma.SortOrder + userPrompt?: Prisma.SortOrder createdAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder } @@ -322,6 +343,7 @@ export type GenerationMinOrderByAggregateInput = { id?: Prisma.SortOrder characterId?: Prisma.SortOrder imageId?: Prisma.SortOrder + userPrompt?: Prisma.SortOrder createdAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder } @@ -431,6 +453,7 @@ export type GenerationUpdateOneRequiredWithoutEquipmentsNestedInput = { export type GenerationCreateWithoutImageInput = { id: string + userPrompt?: string | null createdAt?: Date | string updatedAt?: Date | string character: Prisma.CharacterCreateNestedOneWithoutGenerationsInput @@ -440,6 +463,7 @@ export type GenerationCreateWithoutImageInput = { export type GenerationUncheckedCreateWithoutImageInput = { id: string characterId: string + userPrompt?: string | null createdAt?: Date | string updatedAt?: Date | string equipments?: Prisma.GenerationEquipmentUncheckedCreateNestedManyWithoutGenerationInput @@ -477,12 +501,14 @@ export type GenerationScalarWhereInput = { id?: Prisma.StringFilter<"Generation"> | string characterId?: Prisma.StringFilter<"Generation"> | string imageId?: Prisma.StringFilter<"Generation"> | string + userPrompt?: Prisma.StringNullableFilter<"Generation"> | string | null createdAt?: Prisma.DateTimeFilter<"Generation"> | Date | string updatedAt?: Prisma.DateTimeFilter<"Generation"> | Date | string } export type GenerationCreateWithoutCharacterInput = { id: string + userPrompt?: string | null createdAt?: Date | string updatedAt?: Date | string image: Prisma.AssetCreateNestedOneWithoutGenerationsInput @@ -492,6 +518,7 @@ export type GenerationCreateWithoutCharacterInput = { export type GenerationUncheckedCreateWithoutCharacterInput = { id: string imageId: string + userPrompt?: string | null createdAt?: Date | string updatedAt?: Date | string equipments?: Prisma.GenerationEquipmentUncheckedCreateNestedManyWithoutGenerationInput @@ -524,6 +551,7 @@ export type GenerationUpdateManyWithWhereWithoutCharacterInput = { export type GenerationCreateWithoutEquipmentsInput = { id: string + userPrompt?: string | null createdAt?: Date | string updatedAt?: Date | string character: Prisma.CharacterCreateNestedOneWithoutGenerationsInput @@ -534,6 +562,7 @@ export type GenerationUncheckedCreateWithoutEquipmentsInput = { id: string characterId: string imageId: string + userPrompt?: string | null createdAt?: Date | string updatedAt?: Date | string } @@ -556,6 +585,7 @@ export type GenerationUpdateToOneWithWhereWithoutEquipmentsInput = { export type GenerationUpdateWithoutEquipmentsInput = { id?: Prisma.StringFieldUpdateOperationsInput | string + userPrompt?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string character?: Prisma.CharacterUpdateOneRequiredWithoutGenerationsNestedInput @@ -566,6 +596,7 @@ export type GenerationUncheckedUpdateWithoutEquipmentsInput = { id?: Prisma.StringFieldUpdateOperationsInput | string characterId?: Prisma.StringFieldUpdateOperationsInput | string imageId?: Prisma.StringFieldUpdateOperationsInput | string + userPrompt?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string } @@ -573,12 +604,14 @@ export type GenerationUncheckedUpdateWithoutEquipmentsInput = { export type GenerationCreateManyImageInput = { id: string characterId: string + userPrompt?: string | null createdAt?: Date | string updatedAt?: Date | string } export type GenerationUpdateWithoutImageInput = { id?: Prisma.StringFieldUpdateOperationsInput | string + userPrompt?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string character?: Prisma.CharacterUpdateOneRequiredWithoutGenerationsNestedInput @@ -588,6 +621,7 @@ export type GenerationUpdateWithoutImageInput = { export type GenerationUncheckedUpdateWithoutImageInput = { id?: Prisma.StringFieldUpdateOperationsInput | string characterId?: Prisma.StringFieldUpdateOperationsInput | string + userPrompt?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string equipments?: Prisma.GenerationEquipmentUncheckedUpdateManyWithoutGenerationNestedInput @@ -596,6 +630,7 @@ export type GenerationUncheckedUpdateWithoutImageInput = { export type GenerationUncheckedUpdateManyWithoutImageInput = { id?: Prisma.StringFieldUpdateOperationsInput | string characterId?: Prisma.StringFieldUpdateOperationsInput | string + userPrompt?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string } @@ -603,12 +638,14 @@ export type GenerationUncheckedUpdateManyWithoutImageInput = { export type GenerationCreateManyCharacterInput = { id: string imageId: string + userPrompt?: string | null createdAt?: Date | string updatedAt?: Date | string } export type GenerationUpdateWithoutCharacterInput = { id?: Prisma.StringFieldUpdateOperationsInput | string + userPrompt?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string image?: Prisma.AssetUpdateOneRequiredWithoutGenerationsNestedInput @@ -618,6 +655,7 @@ export type GenerationUpdateWithoutCharacterInput = { export type GenerationUncheckedUpdateWithoutCharacterInput = { id?: Prisma.StringFieldUpdateOperationsInput | string imageId?: Prisma.StringFieldUpdateOperationsInput | string + userPrompt?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string equipments?: Prisma.GenerationEquipmentUncheckedUpdateManyWithoutGenerationNestedInput @@ -626,6 +664,7 @@ export type GenerationUncheckedUpdateWithoutCharacterInput = { export type GenerationUncheckedUpdateManyWithoutCharacterInput = { id?: Prisma.StringFieldUpdateOperationsInput | string imageId?: Prisma.StringFieldUpdateOperationsInput | string + userPrompt?: Prisma.NullableStringFieldUpdateOperationsInput | string | null createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string } @@ -665,6 +704,7 @@ export type GenerationSelect @@ -677,6 +717,7 @@ export type GenerationSelectCreateManyAndReturn @@ -687,6 +728,7 @@ export type GenerationSelectUpdateManyAndReturn @@ -697,11 +739,12 @@ export type GenerationSelectScalar = { id?: boolean characterId?: boolean imageId?: boolean + userPrompt?: boolean createdAt?: boolean updatedAt?: boolean } -export type GenerationOmit = runtime.Types.Extensions.GetOmit<"id" | "characterId" | "imageId" | "createdAt" | "updatedAt", ExtArgs["result"]["generation"]> +export type GenerationOmit = runtime.Types.Extensions.GetOmit<"id" | "characterId" | "imageId" | "userPrompt" | "createdAt" | "updatedAt", ExtArgs["result"]["generation"]> export type GenerationInclude = { character?: boolean | Prisma.CharacterDefaultArgs image?: boolean | Prisma.AssetDefaultArgs @@ -728,6 +771,7 @@ export type $GenerationPayload @@ -1159,6 +1203,7 @@ export interface GenerationFieldRefs { readonly id: Prisma.FieldRef<"Generation", 'String'> readonly characterId: Prisma.FieldRef<"Generation", 'String'> readonly imageId: Prisma.FieldRef<"Generation", 'String'> + readonly userPrompt: Prisma.FieldRef<"Generation", 'String'> readonly createdAt: Prisma.FieldRef<"Generation", 'DateTime'> readonly updatedAt: Prisma.FieldRef<"Generation", 'DateTime'> } diff --git a/src/service/generation.test.ts b/src/service/generation.test.ts index d129de2..a823e05 100644 --- a/src/service/generation.test.ts +++ b/src/service/generation.test.ts @@ -4,6 +4,17 @@ const mockPrisma = { generation: { findMany: mock(), count: mock(), + create: mock(), + findUnique: mock(), + }, + character: { + findUnique: mock(), + }, + equipment: { + findMany: mock(), + }, + generationEquipment: { + createMany: mock(), }, } @@ -11,10 +22,74 @@ mock.module('../db', () => ({ prisma: mockPrisma, })) +const mockAiService = { + generateImage: mock().mockResolvedValue('data:image/png;base64,mockedbase64data'), +} + +const mockAssetService = { + createAssetFromBuffer: mock().mockResolvedValue({ id: 'asset1' }), +} + +mock.module('./ai', () => ({ + aiService: mockAiService, +})) + +mock.module('./asset', () => ({ + assetService: mockAssetService, +})) + describe('GenerationService', () => { + const originalFile = Bun.file + afterEach(() => { mockPrisma.generation.findMany.mockClear() mockPrisma.generation.count.mockClear() + mockPrisma.generation.create.mockClear() + mockAiService.generateImage.mockClear() + Bun.file = originalFile + }) + + it('createGeneration should include userPrompt', async () => { + // @ts-ignore + const { generationService } = await import(`./generation?v=${Date.now()}`) + + mockPrisma.character.findUnique.mockResolvedValue({ + id: 'char1', + name: 'Char1', + image: { path: 'char.png', type: 'image/png' }, + }) + mockPrisma.equipment.findMany.mockResolvedValue([]) + + // Mock Bun.file + // @ts-ignore + Bun.file = () => ({ + arrayBuffer: async () => new ArrayBuffer(8), + }) + + mockPrisma.generation.create.mockResolvedValue({ id: 'gen1' }) + mockPrisma.generation.findUnique.mockResolvedValue({ id: 'gen1' }) + + await generationService.createGeneration('char1', [], 'Make it cool') + + // Verify AI service call includes user prompt + expect(mockAiService.generateImage).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + text: expect.stringContaining('Make it cool'), + }), + ]), + undefined, + 'step_generation_model', + ) + + // Verify DB creation includes userPrompt + expect(mockPrisma.generation.create).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + userPrompt: 'Make it cool', + }), + }), + ) }) it('listGenerations should filter by equipment', async () => { diff --git a/src/service/generation.ts b/src/service/generation.ts index e19db31..0465324 100644 --- a/src/service/generation.ts +++ b/src/service/generation.ts @@ -6,7 +6,7 @@ import { aiService, type AIPart } from './ai' import { assetService } from './asset' export class GenerationService { - async createGeneration(characterId: string, equipmentIds: string[]) { + async createGeneration(characterId: string, equipmentIds: string[], userPrompt?: string) { // 1. Fetch character and its image const character = await prisma.character.findUnique({ where: { id: characterId }, @@ -64,7 +64,7 @@ category: ${eq.category} }) } - const prompt = ` + let prompt = ` Task: Character Synthesis and Equipment Modification. 1. Reference the 'Base Character' for pose, body type, facial features, and general art style. @@ -76,6 +76,14 @@ category: ${eq.category} 7. High quality, detailed, game art style. ` + if (userPrompt) { + prompt += ` + User Requirements: + ${userPrompt} + IMPORTANT: You must strictly adhere to the User Requirements above. + ` + } + parts.push({ text: prompt }) // 4. Generate Image @@ -109,6 +117,7 @@ category: ${eq.category} id: generationId, characterId: character.id, imageId: asset.id, + userPrompt: userPrompt, }, }) diff --git a/src/ui/components/CreateLookDialog.tsx b/src/ui/components/CreateLookDialog.tsx index 7bead77..562af4f 100644 --- a/src/ui/components/CreateLookDialog.tsx +++ b/src/ui/components/CreateLookDialog.tsx @@ -4,6 +4,8 @@ import { InferResponseType } from 'hono/client' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog' import { Button } from './ui/button' import { Badge } from './ui/badge' +import { Textarea } from './ui/textarea' +import { Label } from './ui/label' import { ScrollArea, ScrollBar } from './ui/scroll-area' import { EQUIPMENT_CATEGORIES } from '../../lib/categories' import { cn } from '../lib/utils' @@ -29,6 +31,7 @@ export const CreateLookDialog: React.FC = ({ const [loading, setLoading] = useState(true) const [selectedCategories, setSelectedCategories] = useState([]) const [selectedEquipments, setSelectedEquipments] = useState([]) + const [userPrompt, setUserPrompt] = useState('') const [submitting, setSubmitting] = useState(false) const [error, setError] = useState(null) @@ -86,6 +89,7 @@ export const CreateLookDialog: React.FC = ({ json: { characterId, equipmentIds: selectedEquipments.map((e) => e.id), + userPrompt: userPrompt.trim() || undefined, }, }) @@ -97,6 +101,7 @@ export const CreateLookDialog: React.FC = ({ onSuccess() onOpenChange(false) setSelectedEquipments([]) + setUserPrompt('') } catch (e) { console.error(e) setError(e instanceof Error ? e.message : 'Something went wrong') @@ -192,6 +197,23 @@ export const CreateLookDialog: React.FC = ({ )} + {/* User Prompt Area */} +
+ +