fix format
This commit is contained in:
1 parent
79c8840659
commit
51252249eb
34 files changed
+4890
-2356
No files matched your search
@@ -37,12 +37,8 @@ The frontend is built with:
|
||||
|
||||
### Development
|
||||
|
||||
- **Run Backend**: `bun --hot src/index.ts` (Runs on port 3000)
|
||||
- **Run Frontend**: `bun --hot ui/index.html` (Runs on port 3001, make sure port 3000 is running for API)
|
||||
- **Run Dev**: `bun --hot src/index.ts` (Runs on port 3000)
|
||||
|
||||
### Build
|
||||
|
||||
- **Build Frontend**: `bun build ui/index.html --outdir=ui/dist`
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -66,7 +62,14 @@ We use `bun:test` for testing.
|
||||
|
||||
## Submission Rules
|
||||
|
||||
- **Strict Requirement**: You must ensure `lint`, `format`, `typecheck`, and `test` all pass before submitting any changes.
|
||||
- **Strict Requirement**: A submission is considered complete **only** when there is a single final code state in which **all** of the following pass **simultaneously**:
|
||||
- `lint`
|
||||
- `format`
|
||||
- `typecheck`
|
||||
- `test`
|
||||
|
||||
- Fixes must be iterated until **no check causes any other check to fail**.
|
||||
- Do **not** submit intermediate states where some checks pass and others fail, even temporarily.
|
||||
|
||||
## Prisma Configuration
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ equipment.get('/', zValidator('query', listSchema), async (c) => {
|
||||
|
||||
const result = await EquipmentService.listEquipments(
|
||||
{ page, limit },
|
||||
{ categoryId, subCategoryId }
|
||||
{ categoryId, subCategoryId },
|
||||
)
|
||||
|
||||
return c.json(result)
|
||||
|
||||
+34
-29
@@ -23,20 +23,22 @@ extract.post('/', zValidator('form', extractSchema), async (c) => {
|
||||
// 1. Analyze
|
||||
await stream.writeSSE({
|
||||
event: 'status',
|
||||
data: JSON.stringify({ status: 'analyzing', message: 'Analyzing character assets...' })
|
||||
data: JSON.stringify({ status: 'analyzing', message: 'Analyzing character assets...' }),
|
||||
})
|
||||
|
||||
let analysis;
|
||||
let analysis
|
||||
try {
|
||||
analysis = await ExtractionService.analyzeImage(file)
|
||||
} catch (err: unknown) {
|
||||
throw new Error(`Analysis failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
||||
throw new Error(`Analysis failed: ${err instanceof Error ? err.message : String(err)}`, {
|
||||
cause: err,
|
||||
})
|
||||
}
|
||||
|
||||
if (!analysis.assets || analysis.assets.length === 0) {
|
||||
await stream.writeSSE({
|
||||
event: 'complete',
|
||||
data: JSON.stringify({ assets: [] })
|
||||
data: JSON.stringify({ assets: [] }),
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -44,27 +46,31 @@ extract.post('/', zValidator('form', extractSchema), async (c) => {
|
||||
// 2. Generate Texture Sheet
|
||||
await stream.writeSSE({
|
||||
event: 'status',
|
||||
data: JSON.stringify({ status: 'generating', message: 'Generating texture sheet...' })
|
||||
data: JSON.stringify({ status: 'generating', message: 'Generating texture sheet...' }),
|
||||
})
|
||||
|
||||
let sheetBase64: string;
|
||||
let sheetBase64: string
|
||||
try {
|
||||
sheetBase64 = await ExtractionService.generateTextureSheet(file, analysis.assets)
|
||||
} catch (err: unknown) {
|
||||
throw new Error(`Generation failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
||||
throw new Error(`Generation failed: ${err instanceof Error ? err.message : String(err)}`, {
|
||||
cause: err,
|
||||
})
|
||||
}
|
||||
|
||||
// 3. Detect Bounding Boxes
|
||||
await stream.writeSSE({
|
||||
event: 'status',
|
||||
data: JSON.stringify({ status: 'splitting', message: 'Detecting asset boundaries...' })
|
||||
data: JSON.stringify({ status: 'splitting', message: 'Detecting asset boundaries...' }),
|
||||
})
|
||||
|
||||
let boxes;
|
||||
let boxes
|
||||
try {
|
||||
boxes = await ExtractionService.detectBoundingBoxes(sheetBase64)
|
||||
} catch (err: unknown) {
|
||||
throw new Error(`Detection failed: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
||||
throw new Error(`Detection failed: ${err instanceof Error ? err.message : String(err)}`, {
|
||||
cause: err,
|
||||
})
|
||||
}
|
||||
|
||||
// 4. Crop Assets
|
||||
@@ -73,18 +79,18 @@ extract.post('/', zValidator('form', extractSchema), async (c) => {
|
||||
// 5. Refine & Save
|
||||
await stream.writeSSE({
|
||||
event: 'status',
|
||||
data: JSON.stringify({ status: 'refining', message: 'Refining and saving assets...' })
|
||||
data: JSON.stringify({ status: 'refining', message: 'Refining and saving assets...' }),
|
||||
})
|
||||
|
||||
const refinedAssets = []
|
||||
|
||||
for (const crop of crops) {
|
||||
// Find matching asset from analysis to get category/description
|
||||
const originalMeta = analysis.assets.find(a => a.item_name === crop.name) || {
|
||||
item_name: crop.name,
|
||||
description: "Extracted asset",
|
||||
category: "Uncategorized",
|
||||
background_color: "unknown"
|
||||
const originalMeta = analysis.assets.find((a) => a.item_name === crop.name) || {
|
||||
item_name: crop.name,
|
||||
description: 'Extracted asset',
|
||||
category: 'Uncategorized',
|
||||
background_color: 'unknown',
|
||||
}
|
||||
|
||||
// Refine image
|
||||
@@ -95,9 +101,9 @@ extract.post('/', zValidator('form', extractSchema), async (c) => {
|
||||
|
||||
// Create Asset record (AssetService now has createAssetRecord)
|
||||
const asset = await AssetService.createAssetRecord({
|
||||
name: originalMeta.item_name,
|
||||
type: 'image/webp',
|
||||
path: savedFile.filename
|
||||
name: originalMeta.item_name,
|
||||
type: 'image/webp',
|
||||
path: savedFile.filename,
|
||||
})
|
||||
|
||||
// Find or create category
|
||||
@@ -105,29 +111,28 @@ extract.post('/', zValidator('form', extractSchema), async (c) => {
|
||||
|
||||
// Create Equipment record linked to Asset and Category
|
||||
const equipment = await EquipmentService.createEquipment({
|
||||
name: originalMeta.item_name,
|
||||
description: originalMeta.description,
|
||||
imageId: asset.id,
|
||||
categoryId: categoryId
|
||||
name: originalMeta.item_name,
|
||||
description: originalMeta.description,
|
||||
imageId: asset.id,
|
||||
categoryId: categoryId,
|
||||
})
|
||||
|
||||
refinedAssets.push({
|
||||
...equipment,
|
||||
imageUrl: `/files/${savedFile.filename}`, // Frontend needs URL
|
||||
...equipment,
|
||||
imageUrl: `/files/${savedFile.filename}`, // Frontend needs URL
|
||||
})
|
||||
}
|
||||
|
||||
await stream.writeSSE({
|
||||
event: 'complete',
|
||||
data: JSON.stringify({ assets: refinedAssets })
|
||||
data: JSON.stringify({ assets: refinedAssets }),
|
||||
})
|
||||
|
||||
} catch (e: unknown) {
|
||||
console.error(e)
|
||||
const error = e instanceof Error ? e : new Error(String(e));
|
||||
const error = e instanceof Error ? e : new Error(String(e))
|
||||
await stream.writeSSE({
|
||||
event: 'error',
|
||||
data: JSON.stringify({ message: error.message || 'Extraction failed' })
|
||||
data: JSON.stringify({ message: error.message || 'Extraction failed' }),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
Vendored
+3
-3
@@ -1,4 +1,4 @@
|
||||
declare module "*.html" {
|
||||
const content: string;
|
||||
export default content;
|
||||
declare module '*.html' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
@@ -16,7 +15,7 @@
|
||||
import * as Prisma from './internal/prismaNamespaceBrowser.ts'
|
||||
export { Prisma }
|
||||
export * as $Enums from './enums.ts'
|
||||
export * from './enums.ts';
|
||||
export * from './enums.ts'
|
||||
/**
|
||||
* Model Asset
|
||||
*
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
@@ -15,13 +14,13 @@ import * as path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/client"
|
||||
import * as $Enums from "./enums.ts"
|
||||
import * as $Class from "./internal/class.ts"
|
||||
import * as Prisma from "./internal/prismaNamespace.ts"
|
||||
import * as runtime from '@prisma/client/runtime/client'
|
||||
import * as $Enums from './enums.ts'
|
||||
import * as $Class from './internal/class.ts'
|
||||
import * as Prisma from './internal/prismaNamespace.ts'
|
||||
|
||||
export * as $Enums from './enums.ts'
|
||||
export * from "./enums.ts"
|
||||
export * from './enums.ts'
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
@@ -36,7 +35,11 @@ export * from "./enums.ts"
|
||||
* Read more in our [docs](https://pris.ly/d/client).
|
||||
*/
|
||||
export const PrismaClient = $Class.getPrismaClientClass()
|
||||
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||
export type PrismaClient<
|
||||
LogOpts extends Prisma.LogLevel = never,
|
||||
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Prisma.PrismaClientOptions['omit'],
|
||||
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs,
|
||||
> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||
export { Prisma }
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
@@ -9,10 +8,9 @@
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import type * as runtime from "@prisma/client/runtime/client"
|
||||
import * as $Enums from "./enums.ts"
|
||||
import type * as Prisma from "./internal/prismaNamespace.ts"
|
||||
|
||||
import type * as runtime from '@prisma/client/runtime/client'
|
||||
import * as $Enums from './enums.ts'
|
||||
import type * as Prisma from './internal/prismaNamespace.ts'
|
||||
|
||||
export type StringFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file exports all enum related types from the schema.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
|
||||
* This file exports all enum related types from the schema.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
// This file is empty because there are no enums in the schema.
|
||||
export {}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
@@ -11,26 +10,26 @@
|
||||
* Please import the `PrismaClient` class from the `client.ts` file instead.
|
||||
*/
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/client"
|
||||
import type * as Prisma from "./prismaNamespace.ts"
|
||||
|
||||
import * as runtime from '@prisma/client/runtime/client'
|
||||
import type * as Prisma from './prismaNamespace.ts'
|
||||
|
||||
const config: runtime.GetPrismaClientConfig = {
|
||||
"previewFeatures": [
|
||||
"driverAdapters"
|
||||
],
|
||||
"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 Category {\n id String @id\n name String\n parentId String? @map(\"parent_id\")\n createdAt DateTime @default(now()) @map(\"created_at\")\n updatedAt DateTime @updatedAt @map(\"updated_at\")\n\n // Relations\n parent Category? @relation(\"CategoryToSubCategory\", fields: [parentId], references: [id])\n subCategories Category[] @relation(\"CategoryToSubCategory\")\n equipmentsAsMain Equipment[] @relation(\"CategoryMain\")\n equipmentsAsSub Equipment[] @relation(\"CategorySub\")\n\n @@map(\"categories\")\n}\n\nmodel Equipment {\n id String @id\n name String\n description String\n categoryId String @map(\"category_id\")\n subCategoryId String? @map(\"sub_category_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 category Category @relation(\"CategoryMain\", fields: [categoryId], references: [id])\n subCategory Category? @relation(\"CategorySub\", fields: [subCategoryId], references: [id])\n image Asset @relation(fields: [imageId], references: [id])\n generations GenerationEquipment[]\n\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",
|
||||
"runtimeDataModel": {
|
||||
"models": {},
|
||||
"enums": {},
|
||||
"types": {}
|
||||
}
|
||||
previewFeatures: ['driverAdapters'],
|
||||
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 Category {\n id String @id\n name String\n parentId String? @map("parent_id")\n createdAt DateTime @default(now()) @map("created_at")\n updatedAt DateTime @updatedAt @map("updated_at")\n\n // Relations\n parent Category? @relation("CategoryToSubCategory", fields: [parentId], references: [id])\n subCategories Category[] @relation("CategoryToSubCategory")\n equipmentsAsMain Equipment[] @relation("CategoryMain")\n equipmentsAsSub Equipment[] @relation("CategorySub")\n\n @@map("categories")\n}\n\nmodel Equipment {\n id String @id\n name String\n description String\n categoryId String @map("category_id")\n subCategoryId String? @map("sub_category_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 category Category @relation("CategoryMain", fields: [categoryId], references: [id])\n subCategory Category? @relation("CategorySub", fields: [subCategoryId], references: [id])\n image Asset @relation(fields: [imageId], references: [id])\n generations GenerationEquipment[]\n\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',
|
||||
runtimeDataModel: {
|
||||
models: {},
|
||||
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\"},\"Category\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"parentId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"parent_id\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"created_at\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\",\"dbName\":\"updated_at\"},{\"name\":\"parent\",\"kind\":\"object\",\"type\":\"Category\",\"relationName\":\"CategoryToSubCategory\"},{\"name\":\"subCategories\",\"kind\":\"object\",\"type\":\"Category\",\"relationName\":\"CategoryToSubCategory\"},{\"name\":\"equipmentsAsMain\",\"kind\":\"object\",\"type\":\"Equipment\",\"relationName\":\"CategoryMain\"},{\"name\":\"equipmentsAsSub\",\"kind\":\"object\",\"type\":\"Equipment\",\"relationName\":\"CategorySub\"}],\"dbName\":\"categories\"},\"Equipment\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"categoryId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"category_id\"},{\"name\":\"subCategoryId\",\"kind\":\"scalar\",\"type\":\"String\",\"dbName\":\"sub_category_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\":\"category\",\"kind\":\"object\",\"type\":\"Category\",\"relationName\":\"CategoryMain\"},{\"name\":\"subCategory\",\"kind\":\"object\",\"type\":\"Category\",\"relationName\":\"CategorySub\"},{\"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\"}},\"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"},"Category":{"fields":[{"name":"id","kind":"scalar","type":"String"},{"name":"name","kind":"scalar","type":"String"},{"name":"parentId","kind":"scalar","type":"String","dbName":"parent_id"},{"name":"createdAt","kind":"scalar","type":"DateTime","dbName":"created_at"},{"name":"updatedAt","kind":"scalar","type":"DateTime","dbName":"updated_at"},{"name":"parent","kind":"object","type":"Category","relationName":"CategoryToSubCategory"},{"name":"subCategories","kind":"object","type":"Category","relationName":"CategoryToSubCategory"},{"name":"equipmentsAsMain","kind":"object","type":"Equipment","relationName":"CategoryMain"},{"name":"equipmentsAsSub","kind":"object","type":"Equipment","relationName":"CategorySub"}],"dbName":"categories"},"Equipment":{"fields":[{"name":"id","kind":"scalar","type":"String"},{"name":"name","kind":"scalar","type":"String"},{"name":"description","kind":"scalar","type":"String"},{"name":"categoryId","kind":"scalar","type":"String","dbName":"category_id"},{"name":"subCategoryId","kind":"scalar","type":"String","dbName":"sub_category_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":"category","kind":"object","type":"Category","relationName":"CategoryMain"},{"name":"subCategory","kind":"object","type":"Category","relationName":"CategorySub"},{"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"}},"enums":{},"types":{}}',
|
||||
)
|
||||
|
||||
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
|
||||
const { Buffer } = await import('node:buffer')
|
||||
@@ -39,23 +38,26 @@ async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Modul
|
||||
}
|
||||
|
||||
config.compilerWasm = {
|
||||
getRuntime: async () => await import("@prisma/client/runtime/query_compiler_fast_bg.sqlite.mjs"),
|
||||
getRuntime: async () => await import('@prisma/client/runtime/query_compiler_fast_bg.sqlite.mjs'),
|
||||
|
||||
getQueryCompilerWasmModule: async () => {
|
||||
const { wasm } = await import("@prisma/client/runtime/query_compiler_fast_bg.sqlite.wasm-base64.mjs")
|
||||
const { wasm } =
|
||||
await import('@prisma/client/runtime/query_compiler_fast_bg.sqlite.wasm-base64.mjs')
|
||||
return await decodeBase64AsWasm(wasm)
|
||||
},
|
||||
|
||||
importName: "./query_compiler_fast_bg.js"
|
||||
importName: './query_compiler_fast_bg.js',
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type LogOptions<ClientOptions extends Prisma.PrismaClientOptions> =
|
||||
'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition> ? Prisma.GetEvents<ClientOptions['log']> : never : never
|
||||
'log' extends keyof ClientOptions
|
||||
? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition>
|
||||
? Prisma.GetEvents<ClientOptions['log']>
|
||||
: never
|
||||
: never
|
||||
|
||||
export interface PrismaClientConstructor {
|
||||
/**
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
* Type-safe database client for TypeScript
|
||||
@@ -72,9 +74,13 @@ export interface PrismaClientConstructor {
|
||||
new <
|
||||
Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
|
||||
LogOpts extends LogOptions<Options> = LogOptions<Options>,
|
||||
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'],
|
||||
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
||||
>(options: Prisma.Subset<Options, Prisma.PrismaClientOptions> ): PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U }
|
||||
? U
|
||||
: Prisma.PrismaClientOptions['omit'],
|
||||
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs,
|
||||
>(
|
||||
options: Prisma.Subset<Options, Prisma.PrismaClientOptions>,
|
||||
): PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,23 +100,27 @@ export interface PrismaClientConstructor {
|
||||
export interface PrismaClient<
|
||||
in LogOpts extends Prisma.LogLevel = never,
|
||||
in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined,
|
||||
in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
||||
in out ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
||||
runtime.Types.Extensions.DefaultArgs,
|
||||
> {
|
||||
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] }
|
||||
|
||||
$on<V extends LogOpts>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient;
|
||||
$on<V extends LogOpts>(
|
||||
eventType: V,
|
||||
callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void,
|
||||
): PrismaClient
|
||||
|
||||
/**
|
||||
* Connect with the database
|
||||
*/
|
||||
$connect(): runtime.Types.Utils.JsPromise<void>;
|
||||
$connect(): runtime.Types.Utils.JsPromise<void>
|
||||
|
||||
/**
|
||||
* Disconnect from the database
|
||||
*/
|
||||
$disconnect(): runtime.Types.Utils.JsPromise<void>;
|
||||
$disconnect(): runtime.Types.Utils.JsPromise<void>
|
||||
|
||||
/**
|
||||
/**
|
||||
* Executes a prepared raw query and returns the number of affected rows.
|
||||
* @example
|
||||
* ```
|
||||
@@ -119,7 +129,10 @@ export interface PrismaClient<
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||
*/
|
||||
$executeRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<number>;
|
||||
$executeRaw<T = unknown>(
|
||||
query: TemplateStringsArray | Prisma.Sql,
|
||||
...values: any[]
|
||||
): Prisma.PrismaPromise<number>
|
||||
|
||||
/**
|
||||
* Executes a raw query and returns the number of affected rows.
|
||||
@@ -131,7 +144,7 @@ export interface PrismaClient<
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||
*/
|
||||
$executeRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<number>;
|
||||
$executeRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<number>
|
||||
|
||||
/**
|
||||
* Performs a prepared raw query and returns the `SELECT` data.
|
||||
@@ -142,7 +155,10 @@ export interface PrismaClient<
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||
*/
|
||||
$queryRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<T>;
|
||||
$queryRaw<T = unknown>(
|
||||
query: TemplateStringsArray | Prisma.Sql,
|
||||
...values: any[]
|
||||
): Prisma.PrismaPromise<T>
|
||||
|
||||
/**
|
||||
* Performs a raw query and returns the `SELECT` data.
|
||||
@@ -154,8 +170,7 @@ export interface PrismaClient<
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||
*/
|
||||
$queryRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<T>;
|
||||
|
||||
$queryRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<T>
|
||||
|
||||
/**
|
||||
* Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
|
||||
@@ -170,73 +185,91 @@ export interface PrismaClient<
|
||||
*
|
||||
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions).
|
||||
*/
|
||||
$transaction<P extends Prisma.PrismaPromise<any>[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>
|
||||
$transaction<P extends Prisma.PrismaPromise<any>[]>(
|
||||
arg: [...P],
|
||||
options?: { isolationLevel?: Prisma.TransactionIsolationLevel },
|
||||
): runtime.Types.Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>
|
||||
|
||||
$transaction<R>(fn: (prisma: Omit<PrismaClient, runtime.ITXClientDenyList>) => runtime.Types.Utils.JsPromise<R>, options?: { maxWait?: number, timeout?: number, isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<R>
|
||||
$transaction<R>(
|
||||
fn: (prisma: Omit<PrismaClient, runtime.ITXClientDenyList>) => runtime.Types.Utils.JsPromise<R>,
|
||||
options?: {
|
||||
maxWait?: number
|
||||
timeout?: number
|
||||
isolationLevel?: Prisma.TransactionIsolationLevel
|
||||
},
|
||||
): runtime.Types.Utils.JsPromise<R>
|
||||
|
||||
$extends: runtime.Types.Extensions.ExtendsHook<"extends", Prisma.TypeMapCb<OmitOpts>, ExtArgs, runtime.Types.Utils.Call<Prisma.TypeMapCb<OmitOpts>, {
|
||||
extArgs: ExtArgs
|
||||
}>>
|
||||
$extends: runtime.Types.Extensions.ExtendsHook<
|
||||
'extends',
|
||||
Prisma.TypeMapCb<OmitOpts>,
|
||||
ExtArgs,
|
||||
runtime.Types.Utils.Call<
|
||||
Prisma.TypeMapCb<OmitOpts>,
|
||||
{
|
||||
extArgs: ExtArgs
|
||||
}
|
||||
>
|
||||
>
|
||||
|
||||
/**
|
||||
/**
|
||||
* `prisma.asset`: Exposes CRUD operations for the **Asset** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Assets
|
||||
* const assets = await prisma.asset.findMany()
|
||||
* ```
|
||||
*/
|
||||
get asset(): Prisma.AssetDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Assets
|
||||
* const assets = await prisma.asset.findMany()
|
||||
* ```
|
||||
*/
|
||||
get asset(): Prisma.AssetDelegate<ExtArgs, { omit: OmitOpts }>
|
||||
|
||||
/**
|
||||
* `prisma.character`: Exposes CRUD operations for the **Character** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Characters
|
||||
* const characters = await prisma.character.findMany()
|
||||
* ```
|
||||
*/
|
||||
get character(): Prisma.CharacterDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Characters
|
||||
* const characters = await prisma.character.findMany()
|
||||
* ```
|
||||
*/
|
||||
get character(): Prisma.CharacterDelegate<ExtArgs, { omit: OmitOpts }>
|
||||
|
||||
/**
|
||||
* `prisma.category`: Exposes CRUD operations for the **Category** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Categories
|
||||
* const categories = await prisma.category.findMany()
|
||||
* ```
|
||||
*/
|
||||
get category(): Prisma.CategoryDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Categories
|
||||
* const categories = await prisma.category.findMany()
|
||||
* ```
|
||||
*/
|
||||
get category(): Prisma.CategoryDelegate<ExtArgs, { omit: OmitOpts }>
|
||||
|
||||
/**
|
||||
* `prisma.equipment`: Exposes CRUD operations for the **Equipment** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Equipment
|
||||
* const equipment = await prisma.equipment.findMany()
|
||||
* ```
|
||||
*/
|
||||
get equipment(): Prisma.EquipmentDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Equipment
|
||||
* const equipment = await prisma.equipment.findMany()
|
||||
* ```
|
||||
*/
|
||||
get equipment(): Prisma.EquipmentDelegate<ExtArgs, { omit: OmitOpts }>
|
||||
|
||||
/**
|
||||
* `prisma.generation`: Exposes CRUD operations for the **Generation** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Generations
|
||||
* const generations = await prisma.generation.findMany()
|
||||
* ```
|
||||
*/
|
||||
get generation(): Prisma.GenerationDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Generations
|
||||
* const generations = await prisma.generation.findMany()
|
||||
* ```
|
||||
*/
|
||||
get generation(): Prisma.GenerationDelegate<ExtArgs, { omit: OmitOpts }>
|
||||
|
||||
/**
|
||||
* `prisma.generationEquipment`: Exposes CRUD operations for the **GenerationEquipment** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more GenerationEquipments
|
||||
* const generationEquipments = await prisma.generationEquipment.findMany()
|
||||
* ```
|
||||
*/
|
||||
get generationEquipment(): Prisma.GenerationEquipmentDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more GenerationEquipments
|
||||
* const generationEquipments = await prisma.generationEquipment.findMany()
|
||||
* ```
|
||||
*/
|
||||
get generationEquipment(): Prisma.GenerationEquipmentDelegate<ExtArgs, { omit: OmitOpts }>
|
||||
}
|
||||
|
||||
export function getPrismaClientClass(): PrismaClientConstructor {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
@@ -15,9 +14,9 @@
|
||||
* model files in the `model` directory!
|
||||
*/
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/client"
|
||||
import type * as Prisma from "../models.ts"
|
||||
import { type PrismaClient } from "./class.ts"
|
||||
import * as runtime from '@prisma/client/runtime/client'
|
||||
import type * as Prisma from '../models.ts'
|
||||
import { type PrismaClient } from './class.ts'
|
||||
|
||||
export type * from '../models.ts'
|
||||
|
||||
@@ -54,8 +53,6 @@ export const raw = runtime.raw
|
||||
export const Sql = runtime.Sql
|
||||
export type Sql = runtime.Sql
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Decimal.js
|
||||
*/
|
||||
@@ -65,8 +62,8 @@ export type Decimal = runtime.Decimal
|
||||
export type DecimalJsLike = runtime.DecimalJsLike
|
||||
|
||||
/**
|
||||
* Extensions
|
||||
*/
|
||||
* Extensions
|
||||
*/
|
||||
export type Extension = runtime.Types.Extensions.UserArgs
|
||||
export const getExtensionContext = runtime.Extensions.getExtensionContext
|
||||
export type Args<T, F extends runtime.Operation> = runtime.Types.Public.Args<T, F>
|
||||
@@ -84,8 +81,8 @@ export type PrismaVersion = {
|
||||
* Query Engine version: 9d6ad21cbbceab97458517b147a6a09ff43aa735
|
||||
*/
|
||||
export const prismaVersion: PrismaVersion = {
|
||||
client: "7.3.0",
|
||||
engine: "9d6ad21cbbceab97458517b147a6a09ff43aa735"
|
||||
client: '7.3.0',
|
||||
engine: '9d6ad21cbbceab97458517b147a6a09ff43aa735',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,11 +97,10 @@ export type InputJsonObject = runtime.InputJsonObject
|
||||
export type InputJsonArray = runtime.InputJsonArray
|
||||
export type InputJsonValue = runtime.InputJsonValue
|
||||
|
||||
|
||||
export const NullTypes = {
|
||||
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
|
||||
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
|
||||
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
|
||||
DbNull: runtime.NullTypes.DbNull as new (secret: never) => typeof runtime.DbNull,
|
||||
JsonNull: runtime.NullTypes.JsonNull as new (secret: never) => typeof runtime.JsonNull,
|
||||
AnyNull: runtime.NullTypes.AnyNull as new (secret: never) => typeof runtime.AnyNull,
|
||||
}
|
||||
/**
|
||||
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
||||
@@ -127,7 +123,6 @@ export const JsonNull = runtime.JsonNull
|
||||
*/
|
||||
export const AnyNull = runtime.AnyNull
|
||||
|
||||
|
||||
type SelectAndInclude = {
|
||||
select: any
|
||||
include: any
|
||||
@@ -142,18 +137,18 @@ type SelectAndOmit = {
|
||||
* From T, pick a set of properties whose keys are in the union K
|
||||
*/
|
||||
type Prisma__Pick<T, K extends keyof T> = {
|
||||
[P in K]: T[P];
|
||||
};
|
||||
[P in K]: T[P]
|
||||
}
|
||||
|
||||
export type Enumerable<T> = T | Array<T>;
|
||||
export type Enumerable<T> = T | Array<T>
|
||||
|
||||
/**
|
||||
* Subset
|
||||
* @desc From `T` pick properties that exist in `U`. Simple version of Intersection
|
||||
*/
|
||||
export type Subset<T, U> = {
|
||||
[key in keyof T]: key extends keyof U ? T[key] : never;
|
||||
};
|
||||
[key in keyof T]: key extends keyof U ? T[key] : never
|
||||
}
|
||||
|
||||
/**
|
||||
* SelectSubset
|
||||
@@ -162,12 +157,11 @@ export type Subset<T, U> = {
|
||||
*/
|
||||
export type SelectSubset<T, U> = {
|
||||
[key in keyof T]: key extends keyof U ? T[key] : never
|
||||
} &
|
||||
(T extends SelectAndInclude
|
||||
? 'Please either choose `select` or `include`.'
|
||||
: T extends SelectAndOmit
|
||||
? 'Please either choose `select` or `omit`.'
|
||||
: {})
|
||||
} & (T extends SelectAndInclude
|
||||
? 'Please either choose `select` or `include`.'
|
||||
: T extends SelectAndOmit
|
||||
? 'Please either choose `select` or `omit`.'
|
||||
: {})
|
||||
|
||||
/**
|
||||
* Subset + Intersection
|
||||
@@ -175,37 +169,35 @@ export type SelectSubset<T, U> = {
|
||||
*/
|
||||
export type SubsetIntersection<T, U, K> = {
|
||||
[key in keyof T]: key extends keyof U ? T[key] : never
|
||||
} &
|
||||
K
|
||||
} & K
|
||||
|
||||
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
|
||||
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never }
|
||||
|
||||
/**
|
||||
* XOR is needed to have a real mutually exclusive union type
|
||||
* https://stackoverflow.com/questions/42123407/does-typescript-support-mutually-exclusive-types
|
||||
*/
|
||||
export type XOR<T, U> =
|
||||
T extends object ?
|
||||
U extends object ?
|
||||
(Without<T, U> & U) | (Without<U, T> & T)
|
||||
: U : T
|
||||
|
||||
export type XOR<T, U> = T extends object
|
||||
? U extends object
|
||||
? (Without<T, U> & U) | (Without<U, T> & T)
|
||||
: U
|
||||
: T
|
||||
|
||||
/**
|
||||
* Is T a Record?
|
||||
*/
|
||||
type IsObject<T extends any> = T extends Array<any>
|
||||
? False
|
||||
: T extends Date
|
||||
? False
|
||||
: T extends Uint8Array
|
||||
? False
|
||||
: T extends BigInt
|
||||
? False
|
||||
: T extends object
|
||||
? True
|
||||
: False
|
||||
|
||||
type IsObject<T extends any> =
|
||||
T extends Array<any>
|
||||
? False
|
||||
: T extends Date
|
||||
? False
|
||||
: T extends Uint8Array
|
||||
? False
|
||||
: T extends BigInt
|
||||
? False
|
||||
: T extends object
|
||||
? True
|
||||
: False
|
||||
|
||||
/**
|
||||
* If it's T[], return T
|
||||
@@ -226,20 +218,14 @@ type EitherStrict<O extends object, K extends Key> = Strict<__Either<O, K>>
|
||||
|
||||
type EitherLoose<O extends object, K extends Key> = ComputeRaw<__Either<O, K>>
|
||||
|
||||
type _Either<
|
||||
O extends object,
|
||||
K extends Key,
|
||||
strict extends Boolean
|
||||
> = {
|
||||
type _Either<O extends object, K extends Key, strict extends Boolean> = {
|
||||
1: EitherStrict<O, K>
|
||||
0: EitherLoose<O, K>
|
||||
}[strict]
|
||||
|
||||
export type Either<
|
||||
O extends object,
|
||||
K extends Key,
|
||||
strict extends Boolean = 1
|
||||
> = O extends unknown ? _Either<O, K, strict> : never
|
||||
export type Either<O extends object, K extends Key, strict extends Boolean = 1> = O extends unknown
|
||||
? _Either<O, K, strict>
|
||||
: never
|
||||
|
||||
export type Union = any
|
||||
|
||||
@@ -248,56 +234,67 @@ export type PatchUndefined<O extends object, O1 extends object> = {
|
||||
} & {}
|
||||
|
||||
/** Helper Types for "Merge" **/
|
||||
export type IntersectOf<U extends Union> = (
|
||||
U extends unknown ? (k: U) => void : never
|
||||
) extends (k: infer I) => void
|
||||
export type IntersectOf<U extends Union> = (U extends unknown ? (k: U) => void : never) extends (
|
||||
k: infer I,
|
||||
) => void
|
||||
? I
|
||||
: never
|
||||
|
||||
export type Overwrite<O extends object, O1 extends object> = {
|
||||
[K in keyof O]: K extends keyof O1 ? O1[K] : O[K];
|
||||
} & {};
|
||||
[K in keyof O]: K extends keyof O1 ? O1[K] : O[K]
|
||||
} & {}
|
||||
|
||||
type _Merge<U extends object> = IntersectOf<Overwrite<U, {
|
||||
[K in keyof U]-?: At<U, K>;
|
||||
}>>;
|
||||
type _Merge<U extends object> = IntersectOf<
|
||||
Overwrite<
|
||||
U,
|
||||
{
|
||||
[K in keyof U]-?: At<U, K>
|
||||
}
|
||||
>
|
||||
>
|
||||
|
||||
type Key = string | number | symbol;
|
||||
type AtStrict<O extends object, K extends Key> = O[K & keyof O];
|
||||
type AtLoose<O extends object, K extends Key> = O extends unknown ? AtStrict<O, K> : never;
|
||||
type Key = string | number | symbol
|
||||
type AtStrict<O extends object, K extends Key> = O[K & keyof O]
|
||||
type AtLoose<O extends object, K extends Key> = O extends unknown ? AtStrict<O, K> : never
|
||||
export type At<O extends object, K extends Key, strict extends Boolean = 1> = {
|
||||
1: AtStrict<O, K>;
|
||||
0: AtLoose<O, K>;
|
||||
}[strict];
|
||||
1: AtStrict<O, K>
|
||||
0: AtLoose<O, K>
|
||||
}[strict]
|
||||
|
||||
export type ComputeRaw<A extends any> = A extends Function ? A : {
|
||||
[K in keyof A]: A[K];
|
||||
} & {};
|
||||
export type ComputeRaw<A extends any> = A extends Function
|
||||
? A
|
||||
: {
|
||||
[K in keyof A]: A[K]
|
||||
} & {}
|
||||
|
||||
export type OptionalFlat<O> = {
|
||||
[K in keyof O]?: O[K];
|
||||
} & {};
|
||||
[K in keyof O]?: O[K]
|
||||
} & {}
|
||||
|
||||
type _Record<K extends keyof any, T> = {
|
||||
[P in K]: T;
|
||||
};
|
||||
[P in K]: T
|
||||
}
|
||||
|
||||
// cause typescript not to expand types and preserve names
|
||||
type NoExpand<T> = T extends unknown ? T : never;
|
||||
type NoExpand<T> = T extends unknown ? T : never
|
||||
|
||||
// this type assumes the passed object is entirely optional
|
||||
export type AtLeast<O extends object, K extends string> = NoExpand<
|
||||
O extends unknown
|
||||
? | (K extends keyof O ? { [P in K]: O[P] } & O : O)
|
||||
| {[P in keyof O as P extends K ? P : never]-?: O[P]} & O
|
||||
: never>;
|
||||
?
|
||||
| (K extends keyof O ? { [P in K]: O[P] } & O : O)
|
||||
| ({ [P in keyof O as P extends K ? P : never]-?: O[P] } & O)
|
||||
: never
|
||||
>
|
||||
|
||||
type _Strict<U, _U = U> = U extends unknown ? U & OptionalFlat<_Record<Exclude<Keys<_U>, keyof U>, never>> : never;
|
||||
type _Strict<U, _U = U> = U extends unknown
|
||||
? U & OptionalFlat<_Record<Exclude<Keys<_U>, keyof U>, never>>
|
||||
: never
|
||||
|
||||
export type Strict<U extends object> = ComputeRaw<_Strict<U>>;
|
||||
export type Strict<U extends object> = ComputeRaw<_Strict<U>>
|
||||
/** End Helper Types for "Merge" **/
|
||||
|
||||
export type Merge<U extends object> = ComputeRaw<_Merge<Strict<U>>>;
|
||||
export type Merge<U extends object> = ComputeRaw<_Merge<Strict<U>>>
|
||||
|
||||
export type Boolean = True | False
|
||||
|
||||
@@ -313,12 +310,10 @@ export type Not<B extends Boolean> = {
|
||||
export type Extends<A1 extends any, A2 extends any> = [A1] extends [never]
|
||||
? 0 // anything `never` is false
|
||||
: A1 extends A2
|
||||
? 1
|
||||
: 0
|
||||
? 1
|
||||
: 0
|
||||
|
||||
export type Has<U extends Union, U1 extends Union> = Not<
|
||||
Extends<Exclude<U1, U>, U1>
|
||||
>
|
||||
export type Has<U extends Union, U1 extends Union> = Not<Extends<Exclude<U1, U>, U1>>
|
||||
|
||||
export type Or<B1 extends Boolean, B2 extends Boolean> = {
|
||||
0: {
|
||||
@@ -333,22 +328,17 @@ export type Or<B1 extends Boolean, B2 extends Boolean> = {
|
||||
|
||||
export type Keys<U extends Union> = U extends unknown ? keyof U : never
|
||||
|
||||
export type GetScalarType<T, O> = O extends object ? {
|
||||
[P in keyof T]: P extends keyof O
|
||||
? O[P]
|
||||
: never
|
||||
} : never
|
||||
export type GetScalarType<T, O> = O extends object
|
||||
? {
|
||||
[P in keyof T]: P extends keyof O ? O[P] : never
|
||||
}
|
||||
: never
|
||||
|
||||
type FieldPaths<
|
||||
T,
|
||||
U = Omit<T, '_avg' | '_sum' | '_count' | '_min' | '_max'>
|
||||
> = IsObject<T> extends True ? U : T
|
||||
type FieldPaths<T, U = Omit<T, '_avg' | '_sum' | '_count' | '_min' | '_max'>> =
|
||||
IsObject<T> extends True ? U : T
|
||||
|
||||
export type GetHavingFields<T> = {
|
||||
[K in keyof T]: Or<
|
||||
Or<Extends<'OR', K>, Extends<'AND', K>>,
|
||||
Extends<'NOT', K>
|
||||
> extends True
|
||||
[K in keyof T]: Or<Or<Extends<'OR', K>, Extends<'AND', K>>, Extends<'NOT', K>> extends True
|
||||
? // infer is only needed to not hit TS limit
|
||||
// based on the brilliant idea of Pierre-Antoine Mills
|
||||
// https://github.com/microsoft/TypeScript/issues/30188#issuecomment-478938437
|
||||
@@ -356,8 +346,8 @@ export type GetHavingFields<T> = {
|
||||
? GetHavingFields<UnEnumerate<TK> extends object ? Merge<UnEnumerate<TK>> : never>
|
||||
: never
|
||||
: {} extends FieldPaths<T[K]>
|
||||
? never
|
||||
: K
|
||||
? never
|
||||
: K
|
||||
}[keyof T]
|
||||
|
||||
/**
|
||||
@@ -370,42 +360,53 @@ export type MaybeTupleToUnion<T> = T extends any[] ? TupleToUnion<T> : T
|
||||
/**
|
||||
* Like `Pick`, but additionally can also accept an array of keys
|
||||
*/
|
||||
export type PickEnumerable<T, K extends Enumerable<keyof T> | keyof T> = Prisma__Pick<T, MaybeTupleToUnion<K>>
|
||||
export type PickEnumerable<T, K extends Enumerable<keyof T> | keyof T> = Prisma__Pick<
|
||||
T,
|
||||
MaybeTupleToUnion<K>
|
||||
>
|
||||
|
||||
/**
|
||||
* Exclude all keys with underscores
|
||||
*/
|
||||
export type ExcludeUnderscoreKeys<T extends string> = T extends `_${string}` ? never : T
|
||||
|
||||
|
||||
export type FieldRef<Model, FieldType> = runtime.FieldRef<Model, FieldType>
|
||||
|
||||
type FieldRefInputType<Model, FieldType> = Model extends never ? never : FieldRef<Model, FieldType>
|
||||
|
||||
|
||||
export const ModelName = {
|
||||
Asset: 'Asset',
|
||||
Character: 'Character',
|
||||
Category: 'Category',
|
||||
Equipment: 'Equipment',
|
||||
Generation: 'Generation',
|
||||
GenerationEquipment: 'GenerationEquipment'
|
||||
GenerationEquipment: 'GenerationEquipment',
|
||||
} as const
|
||||
|
||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||
|
||||
|
||||
|
||||
export interface TypeMapCb<GlobalOmitOptions = {}> extends runtime.Types.Utils.Fn<{extArgs: runtime.Types.Extensions.InternalArgs }, runtime.Types.Utils.Record<string, any>> {
|
||||
export interface TypeMapCb<GlobalOmitOptions = {}> extends runtime.Types.Utils.Fn<
|
||||
{ extArgs: runtime.Types.Extensions.InternalArgs },
|
||||
runtime.Types.Utils.Record<string, any>
|
||||
> {
|
||||
returns: TypeMap<this['params']['extArgs'], GlobalOmitOptions>
|
||||
}
|
||||
|
||||
export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> = {
|
||||
export type TypeMap<
|
||||
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs,
|
||||
GlobalOmitOptions = {},
|
||||
> = {
|
||||
globalOmitOptions: {
|
||||
omit: GlobalOmitOptions
|
||||
}
|
||||
meta: {
|
||||
modelProps: "asset" | "character" | "category" | "equipment" | "generation" | "generationEquipment"
|
||||
modelProps:
|
||||
| 'asset'
|
||||
| 'character'
|
||||
| 'category'
|
||||
| 'equipment'
|
||||
| 'generation'
|
||||
| 'generationEquipment'
|
||||
txIsolationLevel: TransactionIsolationLevel
|
||||
}
|
||||
model: {
|
||||
@@ -849,7 +850,9 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
||||
}
|
||||
count: {
|
||||
args: Prisma.GenerationEquipmentCountArgs<ExtArgs>
|
||||
result: runtime.Types.Utils.Optional<Prisma.GenerationEquipmentCountAggregateOutputType> | number
|
||||
result:
|
||||
| runtime.Types.Utils.Optional<Prisma.GenerationEquipmentCountAggregateOutputType>
|
||||
| number
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -859,19 +862,19 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
||||
payload: any
|
||||
operations: {
|
||||
$executeRaw: {
|
||||
args: [query: TemplateStringsArray | Sql, ...values: any[]],
|
||||
args: [query: TemplateStringsArray | Sql, ...values: any[]]
|
||||
result: any
|
||||
}
|
||||
$executeRawUnsafe: {
|
||||
args: [query: string, ...values: any[]],
|
||||
args: [query: string, ...values: any[]]
|
||||
result: any
|
||||
}
|
||||
$queryRaw: {
|
||||
args: [query: TemplateStringsArray | Sql, ...values: any[]],
|
||||
args: [query: TemplateStringsArray | Sql, ...values: any[]]
|
||||
result: any
|
||||
}
|
||||
$queryRawUnsafe: {
|
||||
args: [query: string, ...values: any[]],
|
||||
args: [query: string, ...values: any[]]
|
||||
result: any
|
||||
}
|
||||
}
|
||||
@@ -883,11 +886,11 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
||||
*/
|
||||
|
||||
export const TransactionIsolationLevel = runtime.makeStrictEnum({
|
||||
Serializable: 'Serializable'
|
||||
Serializable: 'Serializable',
|
||||
} as const)
|
||||
|
||||
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
||||
|
||||
export type TransactionIsolationLevel =
|
||||
(typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
||||
|
||||
export const AssetScalarFieldEnum = {
|
||||
id: 'id',
|
||||
@@ -895,34 +898,33 @@ export const AssetScalarFieldEnum = {
|
||||
type: 'type',
|
||||
path: 'path',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
updatedAt: 'updatedAt',
|
||||
} as const
|
||||
|
||||
export type AssetScalarFieldEnum = (typeof AssetScalarFieldEnum)[keyof typeof AssetScalarFieldEnum]
|
||||
|
||||
|
||||
export const CharacterScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
imageId: 'imageId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
updatedAt: 'updatedAt',
|
||||
} as const
|
||||
|
||||
export type CharacterScalarFieldEnum = (typeof CharacterScalarFieldEnum)[keyof typeof CharacterScalarFieldEnum]
|
||||
|
||||
export type CharacterScalarFieldEnum =
|
||||
(typeof CharacterScalarFieldEnum)[keyof typeof CharacterScalarFieldEnum]
|
||||
|
||||
export const CategoryScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
parentId: 'parentId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
updatedAt: 'updatedAt',
|
||||
} as const
|
||||
|
||||
export type CategoryScalarFieldEnum = (typeof CategoryScalarFieldEnum)[keyof typeof CategoryScalarFieldEnum]
|
||||
|
||||
export type CategoryScalarFieldEnum =
|
||||
(typeof CategoryScalarFieldEnum)[keyof typeof CategoryScalarFieldEnum]
|
||||
|
||||
export const EquipmentScalarFieldEnum = {
|
||||
id: 'id',
|
||||
@@ -932,73 +934,64 @@ export const EquipmentScalarFieldEnum = {
|
||||
subCategoryId: 'subCategoryId',
|
||||
imageId: 'imageId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
updatedAt: 'updatedAt',
|
||||
} as const
|
||||
|
||||
export type EquipmentScalarFieldEnum = (typeof EquipmentScalarFieldEnum)[keyof typeof EquipmentScalarFieldEnum]
|
||||
|
||||
export type EquipmentScalarFieldEnum =
|
||||
(typeof EquipmentScalarFieldEnum)[keyof typeof EquipmentScalarFieldEnum]
|
||||
|
||||
export const GenerationScalarFieldEnum = {
|
||||
id: 'id',
|
||||
characterId: 'characterId',
|
||||
imageId: 'imageId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
updatedAt: 'updatedAt',
|
||||
} as const
|
||||
|
||||
export type GenerationScalarFieldEnum = (typeof GenerationScalarFieldEnum)[keyof typeof GenerationScalarFieldEnum]
|
||||
|
||||
export type GenerationScalarFieldEnum =
|
||||
(typeof GenerationScalarFieldEnum)[keyof typeof GenerationScalarFieldEnum]
|
||||
|
||||
export const GenerationEquipmentScalarFieldEnum = {
|
||||
generationId: 'generationId',
|
||||
equipmentId: 'equipmentId'
|
||||
equipmentId: 'equipmentId',
|
||||
} as const
|
||||
|
||||
export type GenerationEquipmentScalarFieldEnum = (typeof GenerationEquipmentScalarFieldEnum)[keyof typeof GenerationEquipmentScalarFieldEnum]
|
||||
|
||||
export type GenerationEquipmentScalarFieldEnum =
|
||||
(typeof GenerationEquipmentScalarFieldEnum)[keyof typeof GenerationEquipmentScalarFieldEnum]
|
||||
|
||||
export const SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
desc: 'desc',
|
||||
} as const
|
||||
|
||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||
|
||||
|
||||
export const NullsOrder = {
|
||||
first: 'first',
|
||||
last: 'last'
|
||||
last: 'last',
|
||||
} as const
|
||||
|
||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Field references
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'String'
|
||||
*/
|
||||
export type StringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'DateTime'
|
||||
*/
|
||||
export type DateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'DateTime'>
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reference to a field of type 'Int'
|
||||
*/
|
||||
export type IntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Int'>
|
||||
|
||||
|
||||
/**
|
||||
* Batch Payload for updateMany & deleteMany & createMany
|
||||
*/
|
||||
@@ -1006,22 +999,30 @@ export type BatchPayload = {
|
||||
count: number
|
||||
}
|
||||
|
||||
export const defineExtension = runtime.Extensions.defineExtension as unknown as runtime.Types.Extensions.ExtendsHook<"define", TypeMapCb, runtime.Types.Extensions.DefaultArgs>
|
||||
export const defineExtension = runtime.Extensions
|
||||
.defineExtension as unknown as runtime.Types.Extensions.ExtendsHook<
|
||||
'define',
|
||||
TypeMapCb,
|
||||
runtime.Types.Extensions.DefaultArgs
|
||||
>
|
||||
export type DefaultPrismaClient = PrismaClient
|
||||
export type ErrorFormat = 'pretty' | 'colorless' | 'minimal'
|
||||
export type PrismaClientOptions = ({
|
||||
/**
|
||||
* Instance of a Driver Adapter, e.g., like one provided by `@prisma/adapter-pg`.
|
||||
*/
|
||||
adapter: runtime.SqlDriverAdapterFactory
|
||||
accelerateUrl?: never
|
||||
} | {
|
||||
/**
|
||||
* Prisma Accelerate URL allowing the client to connect through Accelerate instead of a direct database.
|
||||
*/
|
||||
accelerateUrl: string
|
||||
adapter?: never
|
||||
}) & {
|
||||
export type PrismaClientOptions = (
|
||||
| {
|
||||
/**
|
||||
* Instance of a Driver Adapter, e.g., like one provided by `@prisma/adapter-pg`.
|
||||
*/
|
||||
adapter: runtime.SqlDriverAdapterFactory
|
||||
accelerateUrl?: never
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* Prisma Accelerate URL allowing the client to connect through Accelerate instead of a direct database.
|
||||
*/
|
||||
accelerateUrl: string
|
||||
adapter?: never
|
||||
}
|
||||
) & {
|
||||
/**
|
||||
* @default "colorless"
|
||||
*/
|
||||
@@ -1109,15 +1110,12 @@ export type LogDefinition = {
|
||||
emit: 'stdout' | 'event'
|
||||
}
|
||||
|
||||
export type CheckIsLogLevel<T> = T extends LogLevel ? T : never;
|
||||
export type CheckIsLogLevel<T> = T extends LogLevel ? T : never
|
||||
|
||||
export type GetLogType<T> = CheckIsLogLevel<
|
||||
T extends LogDefinition ? T['level'] : T
|
||||
>;
|
||||
export type GetLogType<T> = CheckIsLogLevel<T extends LogDefinition ? T['level'] : T>
|
||||
|
||||
export type GetEvents<T extends any[]> = T extends Array<LogLevel | LogDefinition>
|
||||
? GetLogType<T[number]>
|
||||
: never;
|
||||
export type GetEvents<T extends any[]> =
|
||||
T extends Array<LogLevel | LogDefinition> ? GetLogType<T[number]> : never
|
||||
|
||||
export type QueryEvent = {
|
||||
timestamp: Date
|
||||
@@ -1134,7 +1132,6 @@ export type LogEvent = {
|
||||
}
|
||||
/* End Types for Logging */
|
||||
|
||||
|
||||
export type PrismaAction =
|
||||
| 'findUnique'
|
||||
| 'findUniqueOrThrow'
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
@@ -15,18 +14,17 @@
|
||||
* model files in the `model` directory!
|
||||
*/
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/index-browser"
|
||||
import * as runtime from '@prisma/client/runtime/index-browser'
|
||||
|
||||
export type * from '../models.ts'
|
||||
export type * from './prismaNamespace.ts'
|
||||
|
||||
export const Decimal = runtime.Decimal
|
||||
|
||||
|
||||
export const NullTypes = {
|
||||
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
|
||||
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
|
||||
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
|
||||
DbNull: runtime.NullTypes.DbNull as new (secret: never) => typeof runtime.DbNull,
|
||||
JsonNull: runtime.NullTypes.JsonNull as new (secret: never) => typeof runtime.JsonNull,
|
||||
AnyNull: runtime.NullTypes.AnyNull as new (secret: never) => typeof runtime.AnyNull,
|
||||
}
|
||||
/**
|
||||
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
||||
@@ -49,14 +47,13 @@ export const JsonNull = runtime.JsonNull
|
||||
*/
|
||||
export const AnyNull = runtime.AnyNull
|
||||
|
||||
|
||||
export const ModelName = {
|
||||
Asset: 'Asset',
|
||||
Character: 'Character',
|
||||
Category: 'Category',
|
||||
Equipment: 'Equipment',
|
||||
Generation: 'Generation',
|
||||
GenerationEquipment: 'GenerationEquipment'
|
||||
GenerationEquipment: 'GenerationEquipment',
|
||||
} as const
|
||||
|
||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||
@@ -66,11 +63,11 @@ export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||
*/
|
||||
|
||||
export const TransactionIsolationLevel = runtime.makeStrictEnum({
|
||||
Serializable: 'Serializable'
|
||||
Serializable: 'Serializable',
|
||||
} as const)
|
||||
|
||||
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
||||
|
||||
export type TransactionIsolationLevel =
|
||||
(typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
||||
|
||||
export const AssetScalarFieldEnum = {
|
||||
id: 'id',
|
||||
@@ -78,34 +75,33 @@ export const AssetScalarFieldEnum = {
|
||||
type: 'type',
|
||||
path: 'path',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
updatedAt: 'updatedAt',
|
||||
} as const
|
||||
|
||||
export type AssetScalarFieldEnum = (typeof AssetScalarFieldEnum)[keyof typeof AssetScalarFieldEnum]
|
||||
|
||||
|
||||
export const CharacterScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
imageId: 'imageId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
updatedAt: 'updatedAt',
|
||||
} as const
|
||||
|
||||
export type CharacterScalarFieldEnum = (typeof CharacterScalarFieldEnum)[keyof typeof CharacterScalarFieldEnum]
|
||||
|
||||
export type CharacterScalarFieldEnum =
|
||||
(typeof CharacterScalarFieldEnum)[keyof typeof CharacterScalarFieldEnum]
|
||||
|
||||
export const CategoryScalarFieldEnum = {
|
||||
id: 'id',
|
||||
name: 'name',
|
||||
parentId: 'parentId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
updatedAt: 'updatedAt',
|
||||
} as const
|
||||
|
||||
export type CategoryScalarFieldEnum = (typeof CategoryScalarFieldEnum)[keyof typeof CategoryScalarFieldEnum]
|
||||
|
||||
export type CategoryScalarFieldEnum =
|
||||
(typeof CategoryScalarFieldEnum)[keyof typeof CategoryScalarFieldEnum]
|
||||
|
||||
export const EquipmentScalarFieldEnum = {
|
||||
id: 'id',
|
||||
@@ -115,42 +111,41 @@ export const EquipmentScalarFieldEnum = {
|
||||
subCategoryId: 'subCategoryId',
|
||||
imageId: 'imageId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
updatedAt: 'updatedAt',
|
||||
} as const
|
||||
|
||||
export type EquipmentScalarFieldEnum = (typeof EquipmentScalarFieldEnum)[keyof typeof EquipmentScalarFieldEnum]
|
||||
|
||||
export type EquipmentScalarFieldEnum =
|
||||
(typeof EquipmentScalarFieldEnum)[keyof typeof EquipmentScalarFieldEnum]
|
||||
|
||||
export const GenerationScalarFieldEnum = {
|
||||
id: 'id',
|
||||
characterId: 'characterId',
|
||||
imageId: 'imageId',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
updatedAt: 'updatedAt',
|
||||
} as const
|
||||
|
||||
export type GenerationScalarFieldEnum = (typeof GenerationScalarFieldEnum)[keyof typeof GenerationScalarFieldEnum]
|
||||
|
||||
export type GenerationScalarFieldEnum =
|
||||
(typeof GenerationScalarFieldEnum)[keyof typeof GenerationScalarFieldEnum]
|
||||
|
||||
export const GenerationEquipmentScalarFieldEnum = {
|
||||
generationId: 'generationId',
|
||||
equipmentId: 'equipmentId'
|
||||
equipmentId: 'equipmentId',
|
||||
} as const
|
||||
|
||||
export type GenerationEquipmentScalarFieldEnum = (typeof GenerationEquipmentScalarFieldEnum)[keyof typeof GenerationEquipmentScalarFieldEnum]
|
||||
|
||||
export type GenerationEquipmentScalarFieldEnum =
|
||||
(typeof GenerationEquipmentScalarFieldEnum)[keyof typeof GenerationEquipmentScalarFieldEnum]
|
||||
|
||||
export const SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
desc: 'desc',
|
||||
} as const
|
||||
|
||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||
|
||||
|
||||
export const NullsOrder = {
|
||||
first: 'first',
|
||||
last: 'last'
|
||||
last: 'last',
|
||||
} as const
|
||||
|
||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
@@ -14,4 +13,4 @@ export type * from './models/Category.ts'
|
||||
export type * from './models/Equipment.ts'
|
||||
export type * from './models/Generation.ts'
|
||||
export type * from './models/GenerationEquipment.ts'
|
||||
export type * from './commonInputTypes.ts'
|
||||
export type * from './commonInputTypes.ts'
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+56
-53
@@ -1,110 +1,113 @@
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
import { z } from 'zod';
|
||||
import zodToJsonSchema from "zod-to-json-schema";
|
||||
import { GoogleGenAI } from '@google/genai'
|
||||
import { z } from 'zod'
|
||||
import zodToJsonSchema from 'zod-to-json-schema'
|
||||
|
||||
// Generic AI service interface for flexibility
|
||||
export interface AIService {
|
||||
generateText<T>(prompt: string, images: File[], schema?: z.ZodType<T>): Promise<T>;
|
||||
generateImage(prompt: string, referenceImages?: File[]): Promise<string>;
|
||||
generateText<T>(prompt: string, images: File[], schema?: z.ZodType<T>): Promise<T>
|
||||
generateImage(prompt: string, referenceImages?: File[]): Promise<string>
|
||||
}
|
||||
|
||||
export class GeminiService implements AIService {
|
||||
private genAI: GoogleGenAI;
|
||||
private genAI: GoogleGenAI
|
||||
|
||||
constructor() {
|
||||
this.genAI = new GoogleGenAI({ apiKey: process.env.API_KEY || '' });
|
||||
this.genAI = new GoogleGenAI({ apiKey: process.env.API_KEY || '' })
|
||||
}
|
||||
|
||||
// Generates JSON output from text and images using Gemini
|
||||
async generateText<T>(prompt: string, images: File[] = [], schema?: z.ZodType<T>): Promise<T> {
|
||||
const model = 'gemini-1.5-flash'; // Or 'gemini-2.0-flash-exp' if available for faster responses
|
||||
const model = 'gemini-1.5-flash' // Or 'gemini-2.0-flash-exp' if available for faster responses
|
||||
|
||||
const parts: Array<{ text: string } | { inlineData: { mimeType: string; data: string } }> = [];
|
||||
const parts: Array<{ text: string } | { inlineData: { mimeType: string; data: string } }> = []
|
||||
|
||||
// Convert files to base64 parts
|
||||
for (const image of images) {
|
||||
const buffer = await image.arrayBuffer();
|
||||
const base64 = Buffer.from(buffer).toString('base64');
|
||||
parts.push({
|
||||
inlineData: {
|
||||
mimeType: image.type,
|
||||
data: base64
|
||||
}
|
||||
});
|
||||
const buffer = await image.arrayBuffer()
|
||||
const base64 = Buffer.from(buffer).toString('base64')
|
||||
parts.push({
|
||||
inlineData: {
|
||||
mimeType: image.type,
|
||||
data: base64,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
parts.push({ text: prompt });
|
||||
parts.push({ text: prompt })
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const config: any = {
|
||||
responseMimeType: "application/json",
|
||||
};
|
||||
responseMimeType: 'application/json',
|
||||
}
|
||||
|
||||
if (schema) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
config.responseSchema = zodToJsonSchema(schema as any);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
config.responseSchema = zodToJsonSchema(schema as any)
|
||||
}
|
||||
|
||||
const result = await this.genAI.models.generateContent({
|
||||
model: model,
|
||||
contents: { parts },
|
||||
config: config
|
||||
});
|
||||
config: config,
|
||||
})
|
||||
|
||||
// @ts-ignore - The property exists on the response object
|
||||
const textProp = typeof result.text === 'function' ? result.text() : result.text;
|
||||
const text = typeof textProp === 'string' ? textProp : undefined;
|
||||
if (!text) throw new Error("No text generated");
|
||||
const textProp = typeof result.text === 'function' ? result.text() : result.text
|
||||
const text = typeof textProp === 'string' ? textProp : undefined
|
||||
if (!text) throw new Error('No text generated')
|
||||
|
||||
try {
|
||||
// Clean up markdown if present
|
||||
const cleanText = text.replace(/^```(json)?\n?|\n?```$/g, '').trim();
|
||||
const json = JSON.parse(cleanText);
|
||||
const cleanText = text.replace(/^```(json)?\n?|\n?```$/g, '').trim()
|
||||
const json = JSON.parse(cleanText)
|
||||
|
||||
if (schema) {
|
||||
return schema.parse(json);
|
||||
return schema.parse(json)
|
||||
}
|
||||
return json as T;
|
||||
return json as T
|
||||
} catch (e: unknown) {
|
||||
console.error("Failed to parse JSON", text);
|
||||
throw new Error(`Invalid JSON response from AI: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
|
||||
console.error('Failed to parse JSON', text)
|
||||
throw new Error(
|
||||
`Invalid JSON response from AI: ${e instanceof Error ? e.message : String(e)}`,
|
||||
{ cause: e },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Generates an image based on prompt and optional reference images
|
||||
async generateImage(prompt: string, referenceImages: File[] = []): Promise<string> {
|
||||
const parts: Array<{ text: string } | { inlineData: { mimeType: string; data: string } }> = [];
|
||||
const parts: Array<{ text: string } | { inlineData: { mimeType: string; data: string } }> = []
|
||||
|
||||
for (const image of referenceImages) {
|
||||
const buffer = await image.arrayBuffer();
|
||||
const base64 = Buffer.from(buffer).toString('base64');
|
||||
parts.push({
|
||||
inlineData: {
|
||||
mimeType: image.type,
|
||||
data: base64
|
||||
}
|
||||
});
|
||||
const buffer = await image.arrayBuffer()
|
||||
const base64 = Buffer.from(buffer).toString('base64')
|
||||
parts.push({
|
||||
inlineData: {
|
||||
mimeType: image.type,
|
||||
data: base64,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
parts.push({ text: prompt });
|
||||
parts.push({ text: prompt })
|
||||
|
||||
const result = await this.genAI.models.generateContent({
|
||||
model: 'gemini-2.0-flash-exp', // Using a known capable model
|
||||
contents: { parts },
|
||||
});
|
||||
model: 'gemini-2.0-flash-exp', // Using a known capable model
|
||||
contents: { parts },
|
||||
})
|
||||
|
||||
// Check for image in response
|
||||
const candidate = result.candidates?.[0];
|
||||
const candidate = result.candidates?.[0]
|
||||
if (candidate?.content?.parts) {
|
||||
for (const part of candidate.content.parts) {
|
||||
if (part.inlineData?.mimeType?.startsWith('image/')) {
|
||||
return `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`;
|
||||
}
|
||||
for (const part of candidate.content.parts) {
|
||||
if (part.inlineData?.mimeType?.startsWith('image/')) {
|
||||
return `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("No image generated in response");
|
||||
throw new Error('No image generated in response')
|
||||
}
|
||||
}
|
||||
|
||||
export const aiService = new GeminiService();
|
||||
export const aiService = new GeminiService()
|
||||
@@ -14,10 +14,10 @@ export class CategoryService {
|
||||
where: {
|
||||
name: {
|
||||
equals: normalizedName,
|
||||
// mode: 'insensitive' // SQLite doesn't support 'mode: insensitive' directly like Postgres, usually need manual lower() or collate NOCASE
|
||||
// mode: 'insensitive' // SQLite doesn't support 'mode: insensitive' directly like Postgres, usually need manual lower() or collate NOCASE
|
||||
},
|
||||
parentId: parentId || null
|
||||
}
|
||||
parentId: parentId || null,
|
||||
},
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
@@ -30,8 +30,8 @@ export class CategoryService {
|
||||
data: {
|
||||
id,
|
||||
name: normalizedName,
|
||||
parentId: parentId || null
|
||||
}
|
||||
parentId: parentId || null,
|
||||
},
|
||||
})
|
||||
|
||||
return newCategory.id
|
||||
@@ -43,14 +43,14 @@ export class CategoryService {
|
||||
static async listAll() {
|
||||
return prisma.category.findMany({
|
||||
where: {
|
||||
parentId: null
|
||||
parentId: null,
|
||||
},
|
||||
include: {
|
||||
subCategories: true
|
||||
subCategories: true,
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc'
|
||||
}
|
||||
name: 'asc',
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,7 @@ export class EquipmentService {
|
||||
include: {
|
||||
image: true,
|
||||
category: true,
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,17 @@ import { ExtractionService } from './extraction'
|
||||
mock.module('./ai', () => ({
|
||||
aiService: {
|
||||
generateText: mock(async () => ({
|
||||
assets: [{
|
||||
item_name: 'Helmet',
|
||||
description: 'A rusty helmet',
|
||||
background_color: 'Red',
|
||||
category: 'Head'
|
||||
}]
|
||||
assets: [
|
||||
{
|
||||
item_name: 'Helmet',
|
||||
description: 'A rusty helmet',
|
||||
background_color: 'Red',
|
||||
category: 'Head',
|
||||
},
|
||||
],
|
||||
})),
|
||||
generateImage: mock(async () => 'data:image/png;base64,mockImage'),
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
mock.module('./file', () => ({
|
||||
@@ -21,27 +23,27 @@ mock.module('./file', () => ({
|
||||
saveBase64Image: mock(async () => ({
|
||||
path: 'data/files/123.webp',
|
||||
filename: '123.webp',
|
||||
mimeType: 'image/webp'
|
||||
}))
|
||||
}
|
||||
mimeType: 'image/webp',
|
||||
})),
|
||||
},
|
||||
}))
|
||||
|
||||
mock.module('./category', () => ({
|
||||
CategoryService: {
|
||||
findOrCreate: mock(async () => 'cat_123')
|
||||
}
|
||||
findOrCreate: mock(async () => 'cat_123'),
|
||||
},
|
||||
}))
|
||||
|
||||
mock.module('./asset', () => ({
|
||||
AssetService: {
|
||||
createAssetRecord: mock(async () => ({ id: 'asset_123' }))
|
||||
}
|
||||
createAssetRecord: mock(async () => ({ id: 'asset_123' })),
|
||||
},
|
||||
}))
|
||||
|
||||
mock.module('./equipment', () => ({
|
||||
EquipmentService: {
|
||||
createEquipment: mock(async () => ({ id: 'eq_123', name: 'Helmet' }))
|
||||
}
|
||||
createEquipment: mock(async () => ({ id: 'eq_123', name: 'Helmet' })),
|
||||
},
|
||||
}))
|
||||
|
||||
mock.module('sharp', () => {
|
||||
@@ -49,9 +51,9 @@ mock.module('sharp', () => {
|
||||
metadata: () => Promise.resolve({ width: 100, height: 100 }),
|
||||
clone: () => ({
|
||||
extract: () => ({
|
||||
toBuffer: () => Promise.resolve(Buffer.from('mockCrop'))
|
||||
})
|
||||
})
|
||||
toBuffer: () => Promise.resolve(Buffer.from('mockCrop')),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
+72
-54
@@ -43,18 +43,20 @@ Instructions:
|
||||
|
||||
Output Format (Strict JSON):
|
||||
Return ONLY a JSON object with a list of assets.
|
||||
`;
|
||||
`
|
||||
|
||||
const schema = z.object({
|
||||
assets: z.array(z.object({
|
||||
item_name: z.string().describe("Name of the item"),
|
||||
background_color: z.string().describe("Background color assigned"),
|
||||
description: z.string().describe("Description of the item"),
|
||||
category: z.string().describe("Category of the item"),
|
||||
})),
|
||||
});
|
||||
assets: z.array(
|
||||
z.object({
|
||||
item_name: z.string().describe('Name of the item'),
|
||||
background_color: z.string().describe('Background color assigned'),
|
||||
description: z.string().describe('Description of the item'),
|
||||
category: z.string().describe('Category of the item'),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
return aiService.generateText<AnalysisResponse>(prompt, [file], schema);
|
||||
return aiService.generateText<AnalysisResponse>(prompt, [file], schema)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,16 +64,25 @@ Return ONLY a JSON object with a list of assets.
|
||||
*/
|
||||
static async generateTextureSheet(file: File, assets: ExtractedAsset[]): Promise<string> {
|
||||
const HEX_COLORS: Record<string, string> = {
|
||||
Red: '#FF0000', Yellow: '#FFFF00', Green: '#00FF00', White: '#FFFFFF',
|
||||
Blue: '#0000FF', Black: '#000000', Magenta: '#FF00FF', Cyan: '#00FFFF',
|
||||
Orange: '#FFA500', Gray: '#808080'
|
||||
};
|
||||
Red: '#FF0000',
|
||||
Yellow: '#FFFF00',
|
||||
Green: '#00FF00',
|
||||
White: '#FFFFFF',
|
||||
Blue: '#0000FF',
|
||||
Black: '#000000',
|
||||
Magenta: '#FF00FF',
|
||||
Cyan: '#00FFFF',
|
||||
Orange: '#FFA500',
|
||||
Gray: '#808080',
|
||||
}
|
||||
|
||||
const mappingString = assets.map(asset => {
|
||||
const colorName = asset.background_color;
|
||||
const hex = HEX_COLORS[colorName] || '#000000';
|
||||
return `- ${asset.item_name}: ${colorName} Background (${hex}). ${asset.description}`;
|
||||
}).join('\n');
|
||||
const mappingString = assets
|
||||
.map((asset) => {
|
||||
const colorName = asset.background_color
|
||||
const hex = HEX_COLORS[colorName] || '#000000'
|
||||
return `- ${asset.item_name}: ${colorName} Background (${hex}). ${asset.description}`
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
const prompt = `
|
||||
Task: Character Asset Extraction & Grid Generation
|
||||
@@ -89,17 +100,17 @@ Item & Background Mapping:
|
||||
${mappingString}
|
||||
|
||||
Output Requirement: High-resolution texture sheet, flat lay presentation, sharp edges, and uniform lighting across all assets.
|
||||
`;
|
||||
`
|
||||
|
||||
return aiService.generateImage(prompt, [file]);
|
||||
return aiService.generateImage(prompt, [file])
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects bounding boxes for assets in the texture sheet.
|
||||
*/
|
||||
static async detectBoundingBoxes(sheetBase64: string): Promise<BoundingBoxResponse> {
|
||||
const buffer = Buffer.from(sheetBase64.replace(/^data:image\/\w+;base64,/, ''), 'base64');
|
||||
const file = new File([buffer], 'sheet.png', { type: 'image/png' });
|
||||
const buffer = Buffer.from(sheetBase64.replace(/^data:image\/\w+;base64,/, ''), 'base64')
|
||||
const file = new File([buffer], 'sheet.png', { type: 'image/png' })
|
||||
|
||||
const prompt = `
|
||||
Task: Spatial Asset Detection & Normalized Grid Mapping
|
||||
@@ -114,71 +125,78 @@ Instructions:
|
||||
|
||||
Output Format (Strict JSON):
|
||||
Return ONLY a JSON object. Provide the grid dimensions and a list of detected assets with their coordinates.
|
||||
`;
|
||||
`
|
||||
|
||||
const schema = z.object({
|
||||
grid_dimensions: z.object({
|
||||
rows: z.number(),
|
||||
cols: z.number(),
|
||||
}),
|
||||
assets: z.array(z.object({
|
||||
item_name: z.string(),
|
||||
coordinates: z.tuple([z.number(), z.number(), z.number(), z.number()]).describe("[ymin, xmin, ymax, xmax]"),
|
||||
bg_color_detected: z.string(),
|
||||
})),
|
||||
});
|
||||
assets: z.array(
|
||||
z.object({
|
||||
item_name: z.string(),
|
||||
coordinates: z
|
||||
.tuple([z.number(), z.number(), z.number(), z.number()])
|
||||
.describe('[ymin, xmin, ymax, xmax]'),
|
||||
bg_color_detected: z.string(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
return aiService.generateText<BoundingBoxResponse>(prompt, [file], schema);
|
||||
return aiService.generateText<BoundingBoxResponse>(prompt, [file], schema)
|
||||
}
|
||||
|
||||
/**
|
||||
* Crops assets from the texture sheet based on bounding boxes.
|
||||
*/
|
||||
static async cropAssets(sheetBase64: string, boxes: BoundingBoxResponse): Promise<{ name: string, base64: string }[]> {
|
||||
const buffer = Buffer.from(sheetBase64.replace(/^data:image\/\w+;base64,/, ''), 'base64');
|
||||
const image = sharp(buffer);
|
||||
const metadata = await image.metadata();
|
||||
static async cropAssets(
|
||||
sheetBase64: string,
|
||||
boxes: BoundingBoxResponse,
|
||||
): Promise<{ name: string; base64: string }[]> {
|
||||
const buffer = Buffer.from(sheetBase64.replace(/^data:image\/\w+;base64,/, ''), 'base64')
|
||||
const image = sharp(buffer)
|
||||
const metadata = await image.metadata()
|
||||
|
||||
if (!metadata.width || !metadata.height) throw new Error("Could not determine image dimensions");
|
||||
if (!metadata.width || !metadata.height) throw new Error('Could not determine image dimensions')
|
||||
|
||||
const croppedAssets = [];
|
||||
const croppedAssets = []
|
||||
|
||||
for (const box of boxes.assets) {
|
||||
const [ymin, xmin, ymax, xmax] = box.coordinates;
|
||||
const [ymin, xmin, ymax, xmax] = box.coordinates
|
||||
|
||||
const left = Math.floor(xmin * metadata.width);
|
||||
const top = Math.floor(ymin * metadata.height);
|
||||
const width = Math.floor((xmax - xmin) * metadata.width);
|
||||
const height = Math.floor((ymax - ymin) * metadata.height);
|
||||
const left = Math.floor(xmin * metadata.width)
|
||||
const top = Math.floor(ymin * metadata.height)
|
||||
const width = Math.floor((xmax - xmin) * metadata.width)
|
||||
const height = Math.floor((ymax - ymin) * metadata.height)
|
||||
|
||||
// Ensure we don't go out of bounds
|
||||
const safeLeft = Math.max(0, left);
|
||||
const safeTop = Math.max(0, top);
|
||||
const safeWidth = Math.min(width, metadata.width - safeLeft);
|
||||
const safeHeight = Math.min(height, metadata.height - safeTop);
|
||||
const safeLeft = Math.max(0, left)
|
||||
const safeTop = Math.max(0, top)
|
||||
const safeWidth = Math.min(width, metadata.width - safeLeft)
|
||||
const safeHeight = Math.min(height, metadata.height - safeTop)
|
||||
|
||||
if (safeWidth <= 0 || safeHeight <= 0) continue;
|
||||
if (safeWidth <= 0 || safeHeight <= 0) continue
|
||||
|
||||
const cropBuffer = await image
|
||||
.clone()
|
||||
.extract({ left: safeLeft, top: safeTop, width: safeWidth, height: safeHeight })
|
||||
.toBuffer();
|
||||
.toBuffer()
|
||||
|
||||
croppedAssets.push({
|
||||
name: box.item_name,
|
||||
base64: `data:image/png;base64,${cropBuffer.toString('base64')}`
|
||||
});
|
||||
base64: `data:image/png;base64,${cropBuffer.toString('base64')}`,
|
||||
})
|
||||
}
|
||||
|
||||
return croppedAssets;
|
||||
return croppedAssets
|
||||
}
|
||||
|
||||
/**
|
||||
* Refines a cropped asset (removes background, upscales).
|
||||
*/
|
||||
static async refineAsset(base64: string): Promise<string> {
|
||||
const buffer = Buffer.from(base64.replace(/^data:image\/\w+;base64,/, ''), 'base64');
|
||||
const file = new File([buffer], 'crop.png', { type: 'image/png' });
|
||||
const buffer = Buffer.from(base64.replace(/^data:image\/\w+;base64,/, ''), 'base64')
|
||||
const file = new File([buffer], 'crop.png', { type: 'image/png' })
|
||||
|
||||
const prompt = `
|
||||
Task: Asset Refinement
|
||||
@@ -189,8 +207,8 @@ Instructions:
|
||||
3. Upscale the image and enhance details for high-quality game asset presentation.
|
||||
4. Ensure the object is centered and fully visible.
|
||||
5. Return only the image.
|
||||
`;
|
||||
`
|
||||
// Using generateImage with image input
|
||||
return aiService.generateImage(prompt, [file]);
|
||||
return aiService.generateImage(prompt, [file])
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,23 @@ import { describe, expect, test, mock } from 'bun:test'
|
||||
import { FileService } from './file'
|
||||
|
||||
// Valid 1x1 PNG Base64
|
||||
const VALID_PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
||||
const VALID_PNG_BUFFER = Buffer.from(VALID_PNG_BASE64, 'base64');
|
||||
const VALID_PNG_BASE64 =
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='
|
||||
const VALID_PNG_BUFFER = Buffer.from(VALID_PNG_BASE64, 'base64')
|
||||
|
||||
// Mock sharp
|
||||
mock.module('sharp', () => {
|
||||
return () => ({
|
||||
webp: () => ({
|
||||
toBuffer: () => Promise.resolve(new Uint8Array([1, 2, 3]))
|
||||
})
|
||||
toBuffer: () => Promise.resolve(new Uint8Array([1, 2, 3])),
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
// Mock Bun.write
|
||||
const mockWrite = mock((path: string, content: string | Uint8Array) => Promise.resolve(content.length))
|
||||
const mockWrite = mock((path: string, content: string | Uint8Array) =>
|
||||
Promise.resolve(content.length),
|
||||
)
|
||||
// @ts-ignore
|
||||
Bun.write = mockWrite
|
||||
|
||||
|
||||
+20
-17
@@ -11,7 +11,10 @@ export class FileService {
|
||||
* @param name Optional name for the file (used for extension detection if original name is missing).
|
||||
* @returns The relative path to the saved file.
|
||||
*/
|
||||
static async saveFile(file: File | Blob, name?: string): Promise<{ path: string; filename: string; mimeType: string }> {
|
||||
static async saveFile(
|
||||
file: File | Blob,
|
||||
name?: string,
|
||||
): Promise<{ path: string; filename: string; mimeType: string }> {
|
||||
const id = ulid()
|
||||
let buffer: ArrayBuffer | Buffer
|
||||
let filename: string
|
||||
@@ -32,12 +35,12 @@ export class FileService {
|
||||
mimeType = file.type || 'application/octet-stream'
|
||||
}
|
||||
} else {
|
||||
// Blob case
|
||||
buffer = await file.arrayBuffer()
|
||||
const result = await this.optimizeImage(buffer)
|
||||
buffer = result.buffer
|
||||
filename = `${id}.webp`
|
||||
mimeType = 'image/webp'
|
||||
// Blob case
|
||||
buffer = await file.arrayBuffer()
|
||||
const result = await this.optimizeImage(buffer)
|
||||
buffer = result.buffer
|
||||
filename = `${id}.webp`
|
||||
mimeType = 'image/webp'
|
||||
}
|
||||
|
||||
const path = join('data/files', filename)
|
||||
@@ -62,19 +65,19 @@ export class FileService {
|
||||
* Saves a base64 string as an image file.
|
||||
*/
|
||||
static async saveBase64Image(base64: string): Promise<{ path: string; filename: string }> {
|
||||
// Strip prefix if present
|
||||
const data = base64.replace(/^data:image\/\w+;base64,/, '')
|
||||
const buffer = Buffer.from(data, 'base64')
|
||||
// Strip prefix if present
|
||||
const data = base64.replace(/^data:image\/\w+;base64,/, '')
|
||||
const buffer = Buffer.from(data, 'base64')
|
||||
|
||||
// Optimize
|
||||
const { buffer: optimized } = await this.optimizeImage(buffer)
|
||||
// Optimize
|
||||
const { buffer: optimized } = await this.optimizeImage(buffer)
|
||||
|
||||
const id = ulid()
|
||||
const filename = `${id}.webp`
|
||||
const path = join('data/files', filename)
|
||||
const id = ulid()
|
||||
const filename = `${id}.webp`
|
||||
const path = join('data/files', filename)
|
||||
|
||||
await Bun.write(path, optimized)
|
||||
await Bun.write(path, optimized)
|
||||
|
||||
return { path, filename }
|
||||
return { path, filename }
|
||||
}
|
||||
}
|
||||
+67
-49
@@ -1,25 +1,25 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ExtractionStatus } from './types';
|
||||
import { Extractor } from './components/Extractor';
|
||||
import { Stash } from './components/Stash';
|
||||
import { LoadingOverlay } from './components/LoadingOverlay';
|
||||
import React, { useState } from 'react'
|
||||
import { ExtractionStatus } from './types'
|
||||
import { Extractor } from './components/Extractor'
|
||||
import { Stash } from './components/Stash'
|
||||
import { LoadingOverlay } from './components/LoadingOverlay'
|
||||
|
||||
export default function App() {
|
||||
const [view, setView] = useState<'extractor' | 'stash'>('extractor');
|
||||
const [status, setStatus] = useState<ExtractionStatus>('idle');
|
||||
const [statusMessage, setStatusMessage] = useState<string>('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [view, setView] = useState<'extractor' | 'stash'>('extractor')
|
||||
const [status, setStatus] = useState<ExtractionStatus>('idle')
|
||||
const [statusMessage, setStatusMessage] = useState<string>('')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const handleStatusChange = (newStatus: ExtractionStatus, message?: string) => {
|
||||
setStatus(newStatus);
|
||||
if (message) setStatusMessage(message);
|
||||
};
|
||||
setStatus(newStatus)
|
||||
if (message) setStatusMessage(message)
|
||||
}
|
||||
|
||||
interface NavButtonProps {
|
||||
target: 'extractor' | 'stash';
|
||||
label: string;
|
||||
color: string;
|
||||
icon: string;
|
||||
target: 'extractor' | 'stash'
|
||||
label: string
|
||||
color: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
const NavButton = ({ target, label, color, icon }: NavButtonProps) => (
|
||||
@@ -33,57 +33,75 @@ export default function App() {
|
||||
>
|
||||
<span>{icon}</span> {label}
|
||||
</button>
|
||||
);
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen text-stone-800 font-sans p-4 md:p-8">
|
||||
{/* Global Error */}
|
||||
{error && (
|
||||
<div className="fixed top-8 left-1/2 -translate-x-1/2 z-50 bg-red-100 border-2 border-red-500 text-red-800 px-6 py-4 rounded-xl shadow-xl flex items-center gap-4 animate-fade-in-up">
|
||||
<span className="font-bold">{error}</span>
|
||||
<button onClick={() => setError(null)} className="text-red-500 hover:text-red-900 font-black">X</button>
|
||||
<span className="font-bold">{error}</span>
|
||||
<button
|
||||
onClick={() => setError(null)}
|
||||
className="text-red-500 hover:text-red-900 font-black"
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading Overlay */}
|
||||
{(status !== 'idle' && status !== 'complete' && status !== 'error') && (
|
||||
{status !== 'idle' && status !== 'complete' && status !== 'error' && (
|
||||
<LoadingOverlay status={status} current={50} total={100} />
|
||||
// Note: Progress is illustrative here unless we stream progress numbers from backend
|
||||
)}
|
||||
|
||||
<header className="max-w-7xl mx-auto mb-10 flex flex-col md:flex-row justify-between items-center gap-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-stone-800 rounded-xl shadow-lg flex items-center justify-center text-amber-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-black text-stone-800 uppercase tracking-tighter">Forgery</h1>
|
||||
<p className="text-stone-400 text-xs font-bold tracking-[0.2em] uppercase">Digital Asset Synthesis</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-stone-800 rounded-xl shadow-lg flex items-center justify-center text-amber-500">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-7 w-7"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-black text-stone-800 uppercase tracking-tighter">
|
||||
Forgery
|
||||
</h1>
|
||||
<p className="text-stone-400 text-xs font-bold tracking-[0.2em] uppercase">
|
||||
Digital Asset Synthesis
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 bg-stone-200/50 p-2 rounded-xl backdrop-blur-sm">
|
||||
<NavButton target="extractor" label="Extractor" color="bg-amber-500" icon="⛏" />
|
||||
<NavButton target="stash" label="Stash" color="bg-cyan-600" icon="❖" />
|
||||
</div>
|
||||
<div className="flex items-center gap-4 bg-stone-200/50 p-2 rounded-xl backdrop-blur-sm">
|
||||
<NavButton target="extractor" label="Extractor" color="bg-amber-500" icon="⛏" />
|
||||
<NavButton target="stash" label="Stash" color="bg-cyan-600" icon="❖" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-7xl mx-auto">
|
||||
{view === 'extractor' && (
|
||||
<Extractor
|
||||
onStatusChange={handleStatusChange}
|
||||
onError={setError}
|
||||
onAssetsExtracted={() => {}}
|
||||
status={status}
|
||||
statusMessage={statusMessage}
|
||||
/>
|
||||
)}
|
||||
{view === 'stash' && (
|
||||
<Stash
|
||||
onNavigateToExtractor={() => setView('extractor')}
|
||||
/>
|
||||
)}
|
||||
{view === 'extractor' && (
|
||||
<Extractor
|
||||
onStatusChange={handleStatusChange}
|
||||
onError={setError}
|
||||
onAssetsExtracted={() => {}}
|
||||
status={status}
|
||||
statusMessage={statusMessage}
|
||||
/>
|
||||
)}
|
||||
{view === 'stash' && <Stash onNavigateToExtractor={() => setView('extractor')} />}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
+235
-167
@@ -1,13 +1,13 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ExtractionStatus, ExtractedAsset } from '../types';
|
||||
import { client } from '../client';
|
||||
import React, { useState } from 'react'
|
||||
import { ExtractionStatus, ExtractedAsset } from '../types'
|
||||
import { client } from '../client'
|
||||
|
||||
interface ExtractorProps {
|
||||
onStatusChange: (status: ExtractionStatus, message?: string) => void;
|
||||
onError: (msg: string | null) => void;
|
||||
onAssetsExtracted: (assets: ExtractedAsset[]) => void;
|
||||
status: ExtractionStatus;
|
||||
statusMessage: string;
|
||||
onStatusChange: (status: ExtractionStatus, message?: string) => void
|
||||
onError: (msg: string | null) => void
|
||||
onAssetsExtracted: (assets: ExtractedAsset[]) => void
|
||||
status: ExtractionStatus
|
||||
statusMessage: string
|
||||
}
|
||||
|
||||
export const Extractor: React.FC<ExtractorProps> = ({
|
||||
@@ -15,203 +15,271 @@ export const Extractor: React.FC<ExtractorProps> = ({
|
||||
onError,
|
||||
onAssetsExtracted,
|
||||
status,
|
||||
statusMessage
|
||||
statusMessage,
|
||||
}) => {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [preview, setPreview] = useState<string | null>(null);
|
||||
const [results, setResults] = useState<ExtractedAsset[]>([]);
|
||||
const [file, setFile] = useState<File | null>(null)
|
||||
const [preview, setPreview] = useState<string | null>(null)
|
||||
const [results, setResults] = useState<ExtractedAsset[]>([])
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
const f = e.target.files[0];
|
||||
setFile(f);
|
||||
setPreview(URL.createObjectURL(f));
|
||||
setResults([]);
|
||||
onError(null);
|
||||
onStatusChange('idle');
|
||||
const f = e.target.files[0]
|
||||
setFile(f)
|
||||
setPreview(URL.createObjectURL(f))
|
||||
setResults([])
|
||||
onError(null)
|
||||
onStatusChange('idle')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleExtract = async () => {
|
||||
if (!file) return;
|
||||
if (!file) return
|
||||
|
||||
setResults([]);
|
||||
onError(null);
|
||||
onStatusChange('analyzing', "Starting extraction...");
|
||||
setResults([])
|
||||
onError(null)
|
||||
onStatusChange('analyzing', 'Starting extraction...')
|
||||
|
||||
try {
|
||||
// Hono RPC Client for File Upload
|
||||
const response = await client.extract.$post({
|
||||
form: {
|
||||
image: file
|
||||
}
|
||||
});
|
||||
image: file,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) throw new Error("Server error");
|
||||
if (!response.body) throw new Error("No response body");
|
||||
if (!response.ok) throw new Error('Server error')
|
||||
if (!response.body) throw new Error('No response body')
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n\n'); // SSE events are separated by double newline
|
||||
buffer = lines.pop() || ''; // Keep incomplete line
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
const lines = buffer.split('\n\n') // SSE events are separated by double newline
|
||||
buffer = lines.pop() || '' // Keep incomplete line
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event: ')) {
|
||||
// Basic SSE parsing
|
||||
const eventMatch = line.match(/^event: (.+)$/m);
|
||||
const dataMatch = line.match(/^data: (.+)$/m);
|
||||
// Basic SSE parsing
|
||||
const eventMatch = line.match(/^event: (.+)$/m)
|
||||
const dataMatch = line.match(/^data: (.+)$/m)
|
||||
|
||||
if (eventMatch && dataMatch) {
|
||||
const event = eventMatch[1].trim();
|
||||
const data = JSON.parse(dataMatch[1]);
|
||||
if (eventMatch && dataMatch) {
|
||||
const event = eventMatch[1].trim()
|
||||
const data = JSON.parse(dataMatch[1])
|
||||
|
||||
if (event === 'status') {
|
||||
onStatusChange(data.status, data.message);
|
||||
} else if (event === 'complete') {
|
||||
onStatusChange('complete', "Extraction complete!");
|
||||
setResults(data.assets);
|
||||
onAssetsExtracted(data.assets);
|
||||
} else if (event === 'error') {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
}
|
||||
if (event === 'status') {
|
||||
onStatusChange(data.status, data.message)
|
||||
} else if (event === 'complete') {
|
||||
onStatusChange('complete', 'Extraction complete!')
|
||||
setResults(data.assets)
|
||||
onAssetsExtracted(data.assets)
|
||||
} else if (event === 'error') {
|
||||
throw new Error(data.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: unknown) {
|
||||
console.error(e);
|
||||
const error = e instanceof Error ? e : new Error(String(e));
|
||||
onError(error.message || "Extraction failed");
|
||||
onStatusChange('error');
|
||||
console.error(e)
|
||||
const error = e instanceof Error ? e : new Error(String(e))
|
||||
onError(error.message || 'Extraction failed')
|
||||
onStatusChange('error')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
setFile(null);
|
||||
if (preview) URL.revokeObjectURL(preview);
|
||||
setPreview(null);
|
||||
setResults([]);
|
||||
onStatusChange('idle');
|
||||
};
|
||||
setFile(null)
|
||||
if (preview) URL.revokeObjectURL(preview)
|
||||
setPreview(null)
|
||||
setResults([])
|
||||
onStatusChange('idle')
|
||||
}
|
||||
|
||||
if (results.length > 0) {
|
||||
return (
|
||||
<div className="animate-fade-in-up space-y-8">
|
||||
{/* Source Selection Strip */}
|
||||
<div className="bg-white p-4 rounded-xl border border-stone-200 shadow-sm">
|
||||
<h3 className="text-xs font-bold text-stone-400 uppercase tracking-widest mb-3">Source Reference</h3>
|
||||
<div className="flex gap-3 overflow-x-auto pb-2 custom-scrollbar">
|
||||
<div className="relative w-16 h-20 flex-shrink-0 rounded-lg overflow-hidden border-2 border-amber-500 shadow-md">
|
||||
<img src={preview || ''} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results Grid */}
|
||||
<div className="bg-white p-6 rounded-xl border border-stone-200 shadow-lg">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-black text-stone-800 flex items-center gap-2">
|
||||
<span className="text-amber-500">✦</span> LOOT ACQUIRED
|
||||
</h2>
|
||||
<p className="text-stone-500 text-sm font-medium">{results.length} items found & stashed</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button onClick={handleReset} className="px-5 py-2.5 text-stone-500 font-bold uppercase text-sm hover:bg-stone-100 rounded-lg transition-colors">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
{results.map((item, i) => (
|
||||
<div key={i} className="group relative bg-stone-50 rounded-lg border-2 border-stone-200 hover:border-amber-400 hover:shadow-lg transition-all duration-300">
|
||||
<div className="aspect-square p-4 flex items-center justify-center relative overflow-hidden rounded-t-lg">
|
||||
{/* Checkerboard bg */}
|
||||
<div className="absolute inset-0 opacity-5" style={{backgroundImage: 'radial-gradient(#000 1px, transparent 1px)', backgroundSize: '8px 8px'}}></div>
|
||||
<img src={item.imageUrl} className="max-w-full max-h-full object-contain drop-shadow-md group-hover:scale-110 transition-transform" />
|
||||
|
||||
{/* Download Overlay */}
|
||||
<a
|
||||
href={item.imageUrl}
|
||||
download={`${item.name}.webp`}
|
||||
className="absolute inset-0 bg-stone-900/40 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity backdrop-blur-[1px]"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
|
||||
</a>
|
||||
</div>
|
||||
<div className="p-3 bg-white border-t border-stone-200 rounded-b-lg">
|
||||
<div className="text-xs font-bold text-stone-700 truncate">{item.name}</div>
|
||||
<div className="text-[10px] text-stone-400 truncate">{item.category.name}</div>
|
||||
<div className="w-6 h-1 bg-amber-400 rounded-full mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div className="animate-fade-in-up space-y-8">
|
||||
{/* Source Selection Strip */}
|
||||
<div className="bg-white p-4 rounded-xl border border-stone-200 shadow-sm">
|
||||
<h3 className="text-xs font-bold text-stone-400 uppercase tracking-widest mb-3">
|
||||
Source Reference
|
||||
</h3>
|
||||
<div className="flex gap-3 overflow-x-auto pb-2 custom-scrollbar">
|
||||
<div className="relative w-16 h-20 flex-shrink-0 rounded-lg overflow-hidden border-2 border-amber-500 shadow-md">
|
||||
<img src={preview || ''} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
{/* Results Grid */}
|
||||
<div className="bg-white p-6 rounded-xl border border-stone-200 shadow-lg">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-black text-stone-800 flex items-center gap-2">
|
||||
<span className="text-amber-500">✦</span> LOOT ACQUIRED
|
||||
</h2>
|
||||
<p className="text-stone-500 text-sm font-medium">
|
||||
{results.length} items found & stashed
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="px-5 py-2.5 text-stone-500 font-bold uppercase text-sm hover:bg-stone-100 rounded-lg transition-colors"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
|
||||
{results.map((item, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="group relative bg-stone-50 rounded-lg border-2 border-stone-200 hover:border-amber-400 hover:shadow-lg transition-all duration-300"
|
||||
>
|
||||
<div className="aspect-square p-4 flex items-center justify-center relative overflow-hidden rounded-t-lg">
|
||||
{/* Checkerboard bg */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-5"
|
||||
style={{
|
||||
backgroundImage: 'radial-gradient(#000 1px, transparent 1px)',
|
||||
backgroundSize: '8px 8px',
|
||||
}}
|
||||
></div>
|
||||
<img
|
||||
src={item.imageUrl}
|
||||
className="max-w-full max-h-full object-contain drop-shadow-md group-hover:scale-110 transition-transform"
|
||||
/>
|
||||
|
||||
{/* Download Overlay */}
|
||||
<a
|
||||
href={item.imageUrl}
|
||||
download={`${item.name}.webp`}
|
||||
className="absolute inset-0 bg-stone-900/40 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity backdrop-blur-[1px]"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-8 w-8 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div className="p-3 bg-white border-t border-stone-200 rounded-b-lg">
|
||||
<div className="text-xs font-bold text-stone-700 truncate">{item.name}</div>
|
||||
<div className="text-[10px] text-stone-400 truncate">{item.category.name}</div>
|
||||
<div className="w-6 h-1 bg-amber-400 rounded-full mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Initial State
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto animate-fade-in-up">
|
||||
{!file ? (
|
||||
<div className="border-4 border-dashed border-stone-300 bg-white rounded-3xl p-12 text-center hover:border-cyan-400 hover:bg-cyan-50/30 transition-all group">
|
||||
<div className="w-20 h-20 bg-cyan-100 text-cyan-600 rounded-full flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform shadow-inner">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||
</div>
|
||||
<h2 className="text-2xl font-black text-stone-800 mb-2">UPLOAD SOURCE</h2>
|
||||
<p className="text-stone-500 mb-4 max-w-sm mx-auto">Drop character image to identify and extract equipment sprites.</p>
|
||||
<div className="block">
|
||||
<label className="inline-block cursor-pointer">
|
||||
<input type="file" accept="image/*" onChange={handleFileChange} className="hidden" />
|
||||
<span className="px-8 py-4 bg-stone-900 text-white font-bold uppercase tracking-wider rounded-xl shadow-xl hover:bg-stone-800 transition-colors">Select Image</span>
|
||||
</label>
|
||||
</div>
|
||||
{!file ? (
|
||||
<div className="border-4 border-dashed border-stone-300 bg-white rounded-3xl p-12 text-center hover:border-cyan-400 hover:bg-cyan-50/30 transition-all group">
|
||||
<div className="w-20 h-20 bg-cyan-100 text-cyan-600 rounded-full flex items-center justify-center mx-auto mb-6 group-hover:scale-110 transition-transform shadow-inner">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-10 w-10"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-2xl shadow-xl border border-stone-200 p-6">
|
||||
<div className="flex justify-between items-center mb-6 pb-6 border-b border-stone-100">
|
||||
<div>
|
||||
<h3 className="font-bold text-stone-700 uppercase tracking-wider">Staging Area</h3>
|
||||
</div>
|
||||
<button onClick={handleReset} className="text-red-500 font-bold text-xs uppercase hover:text-red-700">Remove</button>
|
||||
</div>
|
||||
|
||||
<div className="w-full aspect-square rounded-lg overflow-hidden border border-stone-200 mb-8 relative bg-stone-100">
|
||||
<img src={preview || ''} className="w-full h-full object-contain" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleExtract}
|
||||
disabled={status !== 'idle' && status !== 'error'}
|
||||
className={`w-full py-4 text-white font-black uppercase tracking-widest text-lg rounded-xl shadow-lg transition-all flex items-center justify-center gap-3 ${
|
||||
status === 'idle' || status === 'error'
|
||||
? 'bg-gradient-to-r from-amber-500 to-orange-600 hover:shadow-xl hover:-translate-y-1'
|
||||
: 'bg-stone-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{status === 'idle' || status === 'error' ? (
|
||||
<>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
||||
Begin Extraction
|
||||
</>
|
||||
) : (
|
||||
<span>{statusMessage}...</span>
|
||||
)}
|
||||
</button>
|
||||
<p className="text-center text-xs text-stone-400 mt-4 font-mono">POWERED BY GEMINI 2.0 FLASH</p>
|
||||
<h2 className="text-2xl font-black text-stone-800 mb-2">UPLOAD SOURCE</h2>
|
||||
<p className="text-stone-500 mb-4 max-w-sm mx-auto">
|
||||
Drop character image to identify and extract equipment sprites.
|
||||
</p>
|
||||
<div className="block">
|
||||
<label className="inline-block cursor-pointer">
|
||||
<input type="file" accept="image/*" onChange={handleFileChange} className="hidden" />
|
||||
<span className="px-8 py-4 bg-stone-900 text-white font-bold uppercase tracking-wider rounded-xl shadow-xl hover:bg-stone-800 transition-colors">
|
||||
Select Image
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-2xl shadow-xl border border-stone-200 p-6">
|
||||
<div className="flex justify-between items-center mb-6 pb-6 border-b border-stone-100">
|
||||
<div>
|
||||
<h3 className="font-bold text-stone-700 uppercase tracking-wider">Staging Area</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
className="text-red-500 font-bold text-xs uppercase hover:text-red-700"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="w-full aspect-square rounded-lg overflow-hidden border border-stone-200 mb-8 relative bg-stone-100">
|
||||
<img src={preview || ''} className="w-full h-full object-contain" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleExtract}
|
||||
disabled={status !== 'idle' && status !== 'error'}
|
||||
className={`w-full py-4 text-white font-black uppercase tracking-widest text-lg rounded-xl shadow-lg transition-all flex items-center justify-center gap-3 ${
|
||||
status === 'idle' || status === 'error'
|
||||
? 'bg-gradient-to-r from-amber-500 to-orange-600 hover:shadow-xl hover:-translate-y-1'
|
||||
: 'bg-stone-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{status === 'idle' || status === 'error' ? (
|
||||
<>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||
/>
|
||||
</svg>
|
||||
Begin Extraction
|
||||
</>
|
||||
) : (
|
||||
<span>{statusMessage}...</span>
|
||||
)}
|
||||
</button>
|
||||
<p className="text-center text-xs text-stone-400 mt-4 font-mono">
|
||||
POWERED BY GEMINI 2.0 FLASH
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
@@ -1,49 +1,58 @@
|
||||
import React from 'react';
|
||||
import { ExtractionStatus } from '../types';
|
||||
import React from 'react'
|
||||
import { ExtractionStatus } from '../types'
|
||||
|
||||
interface LoadingOverlayProps {
|
||||
status: ExtractionStatus;
|
||||
current?: number;
|
||||
total?: number;
|
||||
status: ExtractionStatus
|
||||
current?: number
|
||||
total?: number
|
||||
}
|
||||
|
||||
export const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ status, current = 0, total = 0 }) => {
|
||||
export const LoadingOverlay: React.FC<LoadingOverlayProps> = ({
|
||||
status,
|
||||
current = 0,
|
||||
total = 0,
|
||||
}) => {
|
||||
const getStatusText = () => {
|
||||
switch (status) {
|
||||
case 'analyzing': return 'Scanning Neural Patterns...';
|
||||
case 'generating': return 'Synthesizing Texture Data...';
|
||||
case 'splitting': return 'Isolating Components...';
|
||||
case 'refining': return 'Upscaling & Polishing...';
|
||||
default: return 'Processing...';
|
||||
case 'analyzing':
|
||||
return 'Scanning Neural Patterns...'
|
||||
case 'generating':
|
||||
return 'Synthesizing Texture Data...'
|
||||
case 'splitting':
|
||||
return 'Isolating Components...'
|
||||
case 'refining':
|
||||
return 'Upscaling & Polishing...'
|
||||
default:
|
||||
return 'Processing...'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const percent = total > 0 ? Math.round((current / total) * 100) : 0;
|
||||
const percent = total > 0 ? Math.round((current / total) * 100) : 0
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-stone-900/90 backdrop-blur-sm flex flex-col items-center justify-center text-white animate-fade-in-up">
|
||||
<div className="w-64 mb-8">
|
||||
<div className="flex justify-between text-xs font-mono uppercase tracking-widest text-stone-400 mb-2">
|
||||
<span>System Status</span>
|
||||
<span>{percent}%</span>
|
||||
</div>
|
||||
<div className="h-2 bg-stone-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-amber-500 transition-all duration-300 ease-out"
|
||||
style={{ width: `${percent}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-64 mb-8">
|
||||
<div className="flex justify-between text-xs font-mono uppercase tracking-widest text-stone-400 mb-2">
|
||||
<span>System Status</span>
|
||||
<span>{percent}%</span>
|
||||
</div>
|
||||
<div className="h-2 bg-stone-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-amber-500 transition-all duration-300 ease-out"
|
||||
style={{ width: `${percent}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="w-16 h-16 border-4 border-stone-700 border-t-amber-500 rounded-full animate-spin"></div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-8 h-8 bg-amber-500/20 rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<div className="w-16 h-16 border-4 border-stone-700 border-t-amber-500 rounded-full animate-spin"></div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="w-8 h-8 bg-amber-500/20 rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="mt-8 text-2xl font-black uppercase tracking-widest">{getStatusText()}</h2>
|
||||
<p className="text-stone-500 font-mono text-sm mt-2">Do not close window</p>
|
||||
<h2 className="mt-8 text-2xl font-black uppercase tracking-widest">{getStatusText()}</h2>
|
||||
<p className="text-stone-500 font-mono text-sm mt-2">Do not close window</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
+110
-57
@@ -1,88 +1,141 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { client } from '../client';
|
||||
import { InferResponseType } from 'hono/client';
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { client } from '../client'
|
||||
import { InferResponseType } from 'hono/client'
|
||||
|
||||
type EquipmentResponse = InferResponseType<typeof client.equipments.$get>;
|
||||
type StashItem = EquipmentResponse['items'][number];
|
||||
type EquipmentResponse = InferResponseType<typeof client.equipments.$get>
|
||||
type StashItem = EquipmentResponse['items'][number]
|
||||
|
||||
interface StashProps {
|
||||
onNavigateToExtractor: () => void;
|
||||
onNavigateToExtractor: () => void
|
||||
}
|
||||
|
||||
export const Stash: React.FC<StashProps> = ({ onNavigateToExtractor }) => {
|
||||
const [items, setItems] = useState<StashItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [items, setItems] = useState<StashItem[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
fetchItems();
|
||||
}, []);
|
||||
fetchItems()
|
||||
}, [])
|
||||
|
||||
const fetchItems = async () => {
|
||||
try {
|
||||
// Using Hono RPC client
|
||||
const res = await client.equipments.$get({
|
||||
query: {
|
||||
limit: '100'
|
||||
}
|
||||
});
|
||||
limit: '100',
|
||||
},
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json() as any;
|
||||
setItems(data.items);
|
||||
const data = (await res.json()) as any
|
||||
setItems(data.items)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to load stash", e);
|
||||
console.error('Failed to load stash', e)
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-center p-12 text-stone-500">Loading Stash...</div>;
|
||||
return <div className="text-center p-12 text-stone-500">Loading Stash...</div>
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className="text-center p-12 animate-fade-in-up">
|
||||
<div className="w-24 h-24 bg-stone-200 rounded-full flex items-center justify-center mx-auto mb-6 text-stone-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-12 w-12" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" /></svg>
|
||||
</div>
|
||||
<h2 className="text-xl font-black text-stone-700 uppercase tracking-wide mb-2">Empty Stash</h2>
|
||||
<p className="text-stone-500 mb-8 max-w-md mx-auto">Your inventory is empty. Use the Extractor to loot equipment from character images.</p>
|
||||
<button onClick={onNavigateToExtractor} className="px-6 py-3 bg-stone-800 text-white font-bold uppercase tracking-wider rounded-lg shadow-lg hover:bg-stone-700 transition-colors">
|
||||
Go to Extractor
|
||||
</button>
|
||||
return (
|
||||
<div className="text-center p-12 animate-fade-in-up">
|
||||
<div className="w-24 h-24 bg-stone-200 rounded-full flex items-center justify-center mx-auto mb-6 text-stone-400">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-12 w-12"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
<h2 className="text-xl font-black text-stone-700 uppercase tracking-wide mb-2">
|
||||
Empty Stash
|
||||
</h2>
|
||||
<p className="text-stone-500 mb-8 max-w-md mx-auto">
|
||||
Your inventory is empty. Use the Extractor to loot equipment from character images.
|
||||
</p>
|
||||
<button
|
||||
onClick={onNavigateToExtractor}
|
||||
className="px-6 py-3 bg-stone-800 text-white font-bold uppercase tracking-wider rounded-lg shadow-lg hover:bg-stone-700 transition-colors"
|
||||
>
|
||||
Go to Extractor
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="animate-fade-in-up">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h2 className="text-3xl font-black text-stone-800 uppercase tracking-tighter">Your Stash</h2>
|
||||
<p className="text-stone-500 text-xs font-bold tracking-[0.2em] uppercase">{items.length} Items Collected</p>
|
||||
</div>
|
||||
<button onClick={fetchItems} className="text-stone-400 hover:text-stone-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h2 className="text-3xl font-black text-stone-800 uppercase tracking-tighter">
|
||||
Your Stash
|
||||
</h2>
|
||||
<p className="text-stone-500 text-xs font-bold tracking-[0.2em] uppercase">
|
||||
{items.length} Items Collected
|
||||
</p>
|
||||
</div>
|
||||
<button onClick={fetchItems} className="text-stone-400 hover:text-stone-600">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="group relative bg-white rounded-xl border border-stone-200 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300 overflow-hidden">
|
||||
<div className="aspect-square bg-stone-50 p-4 flex items-center justify-center relative">
|
||||
<div className="absolute inset-0 opacity-5" style={{backgroundImage: 'radial-gradient(#000 1px, transparent 1px)', backgroundSize: '8px 8px'}}></div>
|
||||
<img src={item.image?.path ? `/files/${item.image.path}` : ''} className="max-w-full max-h-full object-contain drop-shadow-sm group-hover:scale-110 transition-transform" />
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<div className="flex justify-between items-start mb-1">
|
||||
<h3 className="text-sm font-bold text-stone-800 leading-tight truncate pr-2">{item.name}</h3>
|
||||
</div>
|
||||
<p className="text-[10px] text-stone-400 font-mono uppercase tracking-wider truncate">{item.category?.name || 'Unknown'}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4">
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="group relative bg-white rounded-xl border border-stone-200 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300 overflow-hidden"
|
||||
>
|
||||
<div className="aspect-square bg-stone-50 p-4 flex items-center justify-center relative">
|
||||
<div
|
||||
className="absolute inset-0 opacity-5"
|
||||
style={{
|
||||
backgroundImage: 'radial-gradient(#000 1px, transparent 1px)',
|
||||
backgroundSize: '8px 8px',
|
||||
}}
|
||||
></div>
|
||||
<img
|
||||
src={item.image?.path ? `/files/${item.image.path}` : ''}
|
||||
className="max-w-full max-h-full object-contain drop-shadow-sm group-hover:scale-110 transition-transform"
|
||||
/>
|
||||
</div>
|
||||
<div className="p-3">
|
||||
<div className="flex justify-between items-start mb-1">
|
||||
<h3 className="text-sm font-bold text-stone-800 leading-tight truncate pr-2">
|
||||
{item.name}
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-[10px] text-stone-400 font-mono uppercase tracking-wider truncate">
|
||||
{item.category?.name || 'Unknown'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
+13
-6
@@ -1,4 +1,4 @@
|
||||
@import "tailwindcss";
|
||||
@import 'tailwindcss';
|
||||
|
||||
@theme {
|
||||
--color-border: #e5e7eb;
|
||||
@@ -37,10 +37,11 @@ body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background-color: #f3f4f6; /* Gray 100 */
|
||||
background-image:
|
||||
radial-gradient(#e5e7eb 1px, transparent 1px),
|
||||
radial-gradient(#e5e7eb 1px, transparent 1px);
|
||||
radial-gradient(#e5e7eb 1px, transparent 1px), radial-gradient(#e5e7eb 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 10px 10px;
|
||||
background-position:
|
||||
0 0,
|
||||
10px 10px;
|
||||
color: #1f2937; /* Gray 800 */
|
||||
}
|
||||
|
||||
@@ -63,8 +64,14 @@ body {
|
||||
}
|
||||
|
||||
@keyframes fade-in-up {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.animate-fade-in-up {
|
||||
animation: fade-in-up 0.4s ease-out forwards;
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@@ -6,10 +6,10 @@
|
||||
<title>Forgery</title>
|
||||
<!-- Tailwind Plugin will handle this automatically if configured, or we can use the CDN as fallback if plugin fails -->
|
||||
<!-- But we are using bun-plugin-tailwind -->
|
||||
<link rel="stylesheet" href="index.css">
|
||||
<link rel="stylesheet" href="index.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
+1
-1
@@ -1 +1 @@
|
||||
console.log("Hello via Bun!");
|
||||
console.log('Hello via Bun!')
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
const root = createRoot(document.getElementById('root') as HTMLElement);
|
||||
root.render(<App />);
|
||||
const root = createRoot(document.getElementById('root') as HTMLElement)
|
||||
root.render(<App />)
|
||||
+15
-8
@@ -1,14 +1,21 @@
|
||||
export interface Category {
|
||||
id: string;
|
||||
name: string;
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface ExtractedAsset {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: Category;
|
||||
imageUrl: string;
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: Category
|
||||
imageUrl: string
|
||||
}
|
||||
|
||||
export type ExtractionStatus = 'idle' | 'analyzing' | 'generating' | 'splitting' | 'refining' | 'complete' | 'error';
|
||||
export type ExtractionStatus =
|
||||
| 'idle'
|
||||
| 'analyzing'
|
||||
| 'generating'
|
||||
| 'splitting'
|
||||
| 'refining'
|
||||
| 'complete'
|
||||
| 'error'
|
||||
Reference in new issue
Block a user