feat: Integrate Extraction UI Demo with Hono Backend
- Migrated frontend demo to `src/ui` as a Bun-native React app. - Implemented `ExtractionService` with 5-step AI extraction flow (Analyze, Generate, Detect, Crop, Refine). - Implemented `FileService` for image processing and storage using `sharp`. - Implemented `EquipmentService` and `CategoryService` for managing game assets. - Set up Hono RPC (`hc`) for type-safe frontend-backend communication. - Configured `Bun.serve` to handle both API routes and frontend static files/SPA routing. - Added `zod` validation for API endpoints. - Updated `AGENTS.md` with new architecture details.
This commit is contained in:
1 parent
4615a075b1
commit
7194663eb5
55 files changed
+3499
-5735
No files matched your search
@@ -8,6 +8,7 @@ The backend is built with:
|
||||
- **Framework**: Hono
|
||||
- **ORM**: Prisma (with SQLite)
|
||||
- **Database**: SQLite (local file)
|
||||
- **AI**: Google Generative AI (Gemini)
|
||||
|
||||
### Layered Architecture
|
||||
|
||||
@@ -17,6 +18,32 @@ We follow a strict layered architecture:
|
||||
2. **Service Layer (`src/service`)**: Contains business logic and interacts with the Database via Prisma.
|
||||
3. **Data Layer (`src/db.ts`)**: Exports the Prisma Client instance.
|
||||
|
||||
## Frontend Architecture
|
||||
|
||||
The frontend is built with:
|
||||
|
||||
- **Runtime**: Bun (Bundler & Runner)
|
||||
- **Framework**: React
|
||||
- **Styling**: Tailwind CSS + Shadcn UI (Components)
|
||||
- **State**: React Hooks (useState, useEffect)
|
||||
|
||||
### Project Structure
|
||||
|
||||
- `ui/`: Root of the frontend project.
|
||||
- `ui/index.html`: Entry point for the frontend.
|
||||
- `ui/App.tsx`: Main application component.
|
||||
- `ui/components/`: Reusable UI components.
|
||||
- `ui/index.css`: Global styles (Tailwind imports).
|
||||
|
||||
### 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)
|
||||
|
||||
### Build
|
||||
|
||||
- **Build Frontend**: `bun build ui/index.html --outdir=ui/dist`
|
||||
|
||||
## Testing
|
||||
|
||||
We use `bun:test` for testing.
|
||||
@@ -55,7 +82,7 @@ We use `bun:test` for testing.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `.env`: Development environment variables (`DATABASE_URL="file:./dev.db"`).
|
||||
- `.env`: Development environment variables (`DATABASE_URL="file:./dev.db"`, `API_KEY="..."`).
|
||||
- `.env.test`: Test environment variables (`DATABASE_URL="file:./test.db"`).
|
||||
|
||||
## Known Issues
|
||||
|
||||
@@ -4,11 +4,27 @@
|
||||
"": {
|
||||
"name": "forgery",
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.40.0",
|
||||
"@hono/zod-validator": "^0.7.6",
|
||||
"@libsql/client": "^0.17.0",
|
||||
"@prisma/adapter-libsql": "^7.3.0",
|
||||
"@prisma/client": "^7.3.0",
|
||||
"@types/react": "^19.2.13",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"bun-plugin-tailwind": "^0.1.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.33.0",
|
||||
"hono": "^4.11.8",
|
||||
"lucide-react": "^0.563.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"sharp": "^0.34.5",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"ulidx": "^2.4.1",
|
||||
"zod": "^4.3.6",
|
||||
"zod-to-json-schema": "^3.25.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
@@ -39,6 +55,8 @@
|
||||
|
||||
"@electric-sql/pglite-tools": ["@electric-sql/pglite-tools@0.2.20", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.15" } }, "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
@@ -55,8 +73,12 @@
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.0", "", { "dependencies": { "@eslint/core": "^1.1.0", "levn": "^0.4.1" } }, "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ=="],
|
||||
|
||||
"@google/genai": ["@google/genai@1.40.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-fhIww8smT0QYRX78qWOiz/nIQhHMF5wXOrlXvj33HBrz3vKDBb+wibLcEmTA+L9dmPD4KmfNr7UF3LDQVTXNjA=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
|
||||
|
||||
"@hono/zod-validator": ["@hono/zod-validator@0.7.6", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Io1B6d011Gj1KknV4rXYz4le5+5EubcWEU/speUjuw9XMMIaP3n78yXLhjd2A3PXaXaUwEAluOiAyLqhBEJgsw=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
|
||||
@@ -65,10 +87,62 @@
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||
|
||||
"@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
|
||||
|
||||
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
|
||||
|
||||
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
|
||||
|
||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
|
||||
|
||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
|
||||
|
||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
|
||||
|
||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
||||
|
||||
"@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="],
|
||||
|
||||
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.1", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ=="],
|
||||
|
||||
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||
|
||||
"@libsql/client": ["@libsql/client@0.17.0", "", { "dependencies": { "@libsql/core": "^0.17.0", "@libsql/hrana-client": "^0.9.0", "js-base64": "^3.7.5", "libsql": "^0.5.22", "promise-limit": "^2.7.0" } }, "sha512-TLjSU9Otdpq0SpKHl1tD1Nc9MKhrsZbCFGot3EbCxRa8m1E5R1mMwoOjKMMM31IyF7fr+hPNHLpYfwbMKNusmg=="],
|
||||
|
||||
"@libsql/core": ["@libsql/core@0.17.0", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-hnZRnJHiS+nrhHKLGYPoJbc78FE903MSDrFJTbftxo+e52X+E0Y0fHOCVYsKWcg6XgB7BbJYUrz/xEkVTSaipw=="],
|
||||
@@ -101,6 +175,30 @@
|
||||
|
||||
"@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="],
|
||||
|
||||
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-hPERz4IgXCM6Y6GdEEsJAFceyJMt29f3HlFzsvE/k+TQjChRhar6S+JggL35b9VmFfsdxyCOOTPqgnSrdV0etA=="],
|
||||
|
||||
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-SaWIxsRQYiT/eA60bqA4l8iNO7cJ6YD8ie82RerRp9voceBxPIZiwX4y20cTKy5qNaSGr9LxfYq7vDywTipiog=="],
|
||||
|
||||
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-ArHVWpCRZI3vGLoN2/8ud8Kzqlgn1Gv+fNw+pMB9x18IzgAEhKxFxsWffnoaH21amam4tAOhpeewRIgdNtB0Cw=="],
|
||||
|
||||
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-rq0nNckobtS+ONoB95/Frfqr8jCtmSjjjEZlN4oyUx0KEBV11Vj4v3cDVaWzuI34ryL8FCog3HaqjfKn8R82Tw=="],
|
||||
|
||||
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-HvJmhrfipL7GtuqFz6xNpmf27NGcCOMwCalPjNR6fvkLpe8A7Z1+QbxKKjOglelmlmZc3Vi2TgDUtxSqfqOToQ=="],
|
||||
|
||||
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-YDgqVx1MI8E0oDbCEUSkAMBKKGnUKfaRtMdLh9Bjhu7JQacQ/ZCpxwi4HPf5Q0O1TbWRrdxGw2tA2Ytxkn7s1Q=="],
|
||||
|
||||
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-3IkS3TuVOzMqPW6Gg9/8FEoKF/rpKZ9DZUfNy9GQ54+k4PGcXpptU3+dy8D4iDFCt4qe6bvoiAOdM44OOsZ+Wg=="],
|
||||
|
||||
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-o7Jm5zL4aw9UBs3BcZLVbgGm2V4F10MzAQAV+ziKzoEfYmYtvDqRVxgKEq7BzUOVy4LgfrfwzEXw5gAQGRrhQQ=="],
|
||||
|
||||
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-5g8XJwHhcTh8SGoKO7pR54ILYDbuFkGo+68DOMTiVB5eLxuLET+Or/camHgk4QWp3nUS5kNjip4G8BE8i0rHVQ=="],
|
||||
|
||||
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.8", "", { "os": "win32", "cpu": "x64" }, "sha512-UDI3rowMm/tI6DIynpE4XqrOhr+1Ztk1NG707Wxv2nygup+anTswgCwjfjgmIe78LdoRNFrux2GpeolhQGW6vQ=="],
|
||||
|
||||
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.8", "", { "os": "win32", "cpu": "x64" }, "sha512-K6qBUKAZLXsjAwFxGTG87dsWlDjyDl2fqjJr7+x7lmv2m+aSEzmLOK+Z5pSvGkpjBp3LXV35UUgj8G0UTd0pPg=="],
|
||||
|
||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||
|
||||
"@prisma/adapter-libsql": ["@prisma/adapter-libsql@7.3.0", "", { "dependencies": { "@libsql/client": "^0.8.1", "@prisma/driver-adapter-utils": "7.3.0", "async-mutex": "0.5.0" } }, "sha512-PKsnG/fmwP/lOuO7kb+oZDbGtmfGFxEiE0OXU9lk4V117H8fQ7Wy6qzFtGmojWEgSQIRN/4+XwhRe+KvY/beZw=="],
|
||||
|
||||
"@prisma/client": ["@prisma/client@7.3.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.3.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-FXBIxirqQfdC6b6HnNgxGmU7ydCPEPk7maHMOduJJfnTP+MuOGa15X4omjR/zpPUUpm8ef/mEFQjJudOGkXFcQ=="],
|
||||
@@ -127,6 +225,26 @@
|
||||
|
||||
"@prisma/studio-core": ["@prisma/studio-core@0.13.1", "", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg=="],
|
||||
|
||||
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||
|
||||
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||
|
||||
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
|
||||
|
||||
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
|
||||
|
||||
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
|
||||
|
||||
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
|
||||
|
||||
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
|
||||
|
||||
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
|
||||
|
||||
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
|
||||
|
||||
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
||||
@@ -141,6 +259,8 @@
|
||||
|
||||
"@types/react": ["@types/react@19.2.13", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.54.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/type-utils": "8.54.0", "@typescript-eslint/utils": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.54.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ=="],
|
||||
@@ -167,16 +287,32 @@
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||
|
||||
"async-mutex": ["async-mutex@0.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA=="],
|
||||
|
||||
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
"bun": ["bun@1.3.8", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.8", "@oven/bun-darwin-x64": "1.3.8", "@oven/bun-darwin-x64-baseline": "1.3.8", "@oven/bun-linux-aarch64": "1.3.8", "@oven/bun-linux-aarch64-musl": "1.3.8", "@oven/bun-linux-x64": "1.3.8", "@oven/bun-linux-x64-baseline": "1.3.8", "@oven/bun-linux-x64-musl": "1.3.8", "@oven/bun-linux-x64-musl-baseline": "1.3.8", "@oven/bun-windows-x64": "1.3.8", "@oven/bun-windows-x64-baseline": "1.3.8" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-TmmtMGQSXY3o0enSMMkvR5g7Dw/2n2oylAUprLVdqA8pDPBoKbzgOmmgHOtvnfRznFppl+Gu2cid3vgAjcNoSg=="],
|
||||
|
||||
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
|
||||
|
||||
"c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="],
|
||||
@@ -187,6 +323,14 @@
|
||||
|
||||
"citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="],
|
||||
|
||||
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
|
||||
|
||||
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||
@@ -211,12 +355,18 @@
|
||||
|
||||
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
|
||||
|
||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
@@ -241,6 +391,8 @@
|
||||
|
||||
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
@@ -265,24 +417,40 @@
|
||||
|
||||
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
|
||||
|
||||
"framer-motion": ["framer-motion@12.33.0", "", { "dependencies": { "motion-dom": "^12.33.0", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-ca8d+rRPcDP5iIF+MoT3WNc0KHJMjIyFAbtVLvM9eA7joGSpeqDfiNH/kCs1t4CHi04njYvWyj0jS4QlEK/rJQ=="],
|
||||
|
||||
"gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="],
|
||||
|
||||
"gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
|
||||
|
||||
"generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
|
||||
|
||||
"get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="],
|
||||
|
||||
"giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="],
|
||||
|
||||
"glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="],
|
||||
|
||||
"google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"grammex": ["grammex@3.1.12", "", {}, "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ=="],
|
||||
|
||||
"graphmatch": ["graphmatch@1.1.0", "", {}, "sha512-0E62MaTW5rPZVRLyIJZG/YejmdA/Xr1QydHEw3Vt+qOKkMIOE8WDLc9ZX2bmAjtJFZcId4lEdrdmASsEy7D1QA=="],
|
||||
|
||||
"gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="],
|
||||
|
||||
"hono": ["hono@4.11.8", "", {}, "sha512-eVkB/CYCCei7K2WElZW9yYQFWssG0DhaDhVvr7wy5jJ22K+ck8fWW0EsLpB0sITUTvPnc97+rrbQqIr5iqiy9Q=="],
|
||||
|
||||
"http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="],
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
|
||||
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
@@ -291,22 +459,32 @@
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||
|
||||
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="],
|
||||
|
||||
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
|
||||
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
||||
|
||||
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"layerr": ["layerr@3.0.0", "", {}, "sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA=="],
|
||||
@@ -323,10 +501,20 @@
|
||||
|
||||
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||
|
||||
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="],
|
||||
|
||||
"minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="],
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"motion-dom": ["motion-dom@12.33.0", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-XRPebVypsl0UM+7v0Hr8o9UAj0S2djsQWRdHBd5iVouVpMrQqAI0C/rDAT3QaYnXnHuC5hMcwDHCboNeyYjPoQ=="],
|
||||
|
||||
"motion-utils": ["motion-utils@12.29.2", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mysql2": ["mysql2@3.15.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg=="],
|
||||
@@ -351,10 +539,14 @@
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
||||
@@ -375,6 +567,8 @@
|
||||
|
||||
"proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
|
||||
|
||||
"protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
|
||||
@@ -393,6 +587,10 @@
|
||||
|
||||
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||
|
||||
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
@@ -401,6 +599,8 @@
|
||||
|
||||
"seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="],
|
||||
|
||||
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
@@ -411,6 +611,18 @@
|
||||
|
||||
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
||||
|
||||
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
|
||||
|
||||
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
|
||||
|
||||
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
@@ -445,12 +657,20 @@
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
|
||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="],
|
||||
|
||||
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@prisma/adapter-libsql/@libsql/client": ["@libsql/client@0.8.1", "", { "dependencies": { "@libsql/core": "^0.8.1", "@libsql/hrana-client": "^0.6.2", "js-base64": "^3.7.5", "libsql": "^0.3.10", "promise-limit": "^2.7.0" } }, "sha512-xGg0F4iTDFpeBZ0r4pA6icGsYa5rG6RAG+i/iLDnpCAnSuTqEWMDdPlVseiq4Z/91lWI9jvvKKiKpovqJ1kZWA=="],
|
||||
@@ -471,16 +691,38 @@
|
||||
|
||||
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],
|
||||
|
||||
"nypm/citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="],
|
||||
|
||||
"proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"@prisma/adapter-libsql/@libsql/client/@libsql/core": ["@libsql/core@0.8.1", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-u6nrj6HZMTPsgJ9EBhLzO2uhqhlHQJQmVHV+0yFLvfGf3oSP8w7TjZCNUgu1G8jHISx6KFi7bmcrdXW9lRt++A=="],
|
||||
|
||||
"@prisma/adapter-libsql/@libsql/client/@libsql/hrana-client": ["@libsql/hrana-client@0.6.2", "", { "dependencies": { "@libsql/isomorphic-fetch": "^0.2.1", "@libsql/isomorphic-ws": "^0.1.5", "js-base64": "^3.7.5", "node-fetch": "^3.3.2" } }, "sha512-MWxgD7mXLNf9FXXiM0bc90wCjZSpErWKr5mGza7ERy2FJNNMXd7JIOv+DepBA1FQTIfI8TFO4/QDYgaQC0goNw=="],
|
||||
|
||||
"@prisma/adapter-libsql/@libsql/client/libsql": ["libsql@0.3.19", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2", "libsql": "^0.3.15" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.3.19", "@libsql/darwin-x64": "0.3.19", "@libsql/linux-arm64-gnu": "0.3.19", "@libsql/linux-arm64-musl": "0.3.19", "@libsql/linux-x64-gnu": "0.3.19", "@libsql/linux-x64-musl": "0.3.19", "@libsql/win32-x64-msvc": "0.3.19" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-Aj5cQ5uk/6fHdmeW0TiXK42FqUlwx7ytmMLPSaUQPin5HKKKuUPD62MAbN4OEweGBBI7q1BekoEN4gPUEL6MZA=="],
|
||||
|
||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"@prisma/adapter-libsql/@libsql/client/libsql/@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.3.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rmOqsLcDI65zzxlUOoEiPJLhqmbFsZF6p4UJQ2kMqB+Kc0Rt5/A1OAdOZ/Wo8fQfJWjR1IbkbpEINFioyKf+nQ=="],
|
||||
|
||||
"@prisma/adapter-libsql/@libsql/client/libsql/@libsql/darwin-x64": ["@libsql/darwin-x64@0.3.19", "", { "os": "darwin", "cpu": "x64" }, "sha512-q9O55B646zU+644SMmOQL3FIfpmEvdWpRpzubwFc2trsa+zoBlSkHuzU9v/C+UNoPHQVRMP7KQctJ455I/h/xw=="],
|
||||
@@ -494,5 +736,7 @@
|
||||
"@prisma/adapter-libsql/@libsql/client/libsql/@libsql/linux-x64-musl": ["@libsql/linux-x64-musl@0.3.19", "", { "os": "linux", "cpu": "x64" }, "sha512-BLsXyJaL8gZD8+3W2LU08lDEd9MIgGds0yPy5iNPp8tfhXx3pV/Fge2GErN0FC+nzt4DYQtjL+A9GUMglQefXQ=="],
|
||||
|
||||
"@prisma/adapter-libsql/@libsql/client/libsql/@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.3.19", "", { "os": "win32", "cpu": "x64" }, "sha512-ay1X9AobE4BpzG0XPw1gplyLZPGHIgJOovvW23gUrukRegiUP62uzhpRbKNogLlUOynyXeq//prHgPXiebUfWg=="],
|
||||
|
||||
"@prisma/adapter-libsql/@libsql/client/libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
[serve.static]
|
||||
plugins = ["bun-plugin-tailwind"]
|
||||
|
||||
# Optional: Environment variables to expose to client
|
||||
# env = "PUBLIC_*"
|
||||
+17
-1
@@ -10,11 +10,27 @@
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.40.0",
|
||||
"@hono/zod-validator": "^0.7.6",
|
||||
"@libsql/client": "^0.17.0",
|
||||
"@prisma/adapter-libsql": "^7.3.0",
|
||||
"@prisma/client": "^7.3.0",
|
||||
"@types/react": "^19.2.13",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"bun-plugin-tailwind": "^0.1.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.33.0",
|
||||
"hono": "^4.11.8",
|
||||
"ulidx": "^2.4.1"
|
||||
"lucide-react": "^0.563.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"sharp": "^0.34.5",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"ulidx": "^2.4.1",
|
||||
"zod": "^4.3.6",
|
||||
"zod-to-json-schema": "^3.25.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
|
||||
+13
-6
@@ -1,18 +1,25 @@
|
||||
import { Hono } from 'hono'
|
||||
import { zValidator } from '@hono/zod-validator'
|
||||
import { z } from 'zod'
|
||||
import { EquipmentService } from '../service/equipment'
|
||||
|
||||
const equipment = new Hono()
|
||||
|
||||
equipment.get('/', async (c) => {
|
||||
const page = parseInt(c.req.query('page') || '1')
|
||||
const limit = parseInt(c.req.query('limit') || '10')
|
||||
const categoryId = c.req.query('categoryId')
|
||||
const subCategoryId = c.req.query('subCategoryId')
|
||||
const listSchema = z.object({
|
||||
page: z.coerce.number().optional().default(1),
|
||||
limit: z.coerce.number().optional().default(20),
|
||||
categoryId: z.string().optional(),
|
||||
subCategoryId: z.string().optional(),
|
||||
})
|
||||
|
||||
equipment.get('/', zValidator('query', listSchema), async (c) => {
|
||||
const { page, limit, categoryId, subCategoryId } = c.req.valid('query')
|
||||
|
||||
const result = await EquipmentService.listEquipments(
|
||||
{ page, limit },
|
||||
{ categoryId, subCategoryId },
|
||||
{ categoryId, subCategoryId }
|
||||
)
|
||||
|
||||
return c.json(result)
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { Hono } from 'hono'
|
||||
import { streamSSE } from 'hono/streaming'
|
||||
import { zValidator } from '@hono/zod-validator'
|
||||
import { z } from 'zod'
|
||||
import { ExtractionService } from '../service/extraction'
|
||||
import { FileService } from '../service/file'
|
||||
import { EquipmentService } from '../service/equipment'
|
||||
import { CategoryService } from '../service/category'
|
||||
import { AssetService } from '../service/asset'
|
||||
|
||||
const extract = new Hono()
|
||||
|
||||
const extractSchema = z.object({
|
||||
image: z.instanceof(File),
|
||||
})
|
||||
|
||||
extract.post('/', zValidator('form', extractSchema), async (c) => {
|
||||
const { image } = c.req.valid('form')
|
||||
const file = image as File
|
||||
|
||||
return streamSSE(c, async (stream) => {
|
||||
try {
|
||||
// 1. Analyze
|
||||
await stream.writeSSE({
|
||||
event: 'status',
|
||||
data: JSON.stringify({ status: 'analyzing', message: 'Analyzing character assets...' })
|
||||
})
|
||||
|
||||
let analysis;
|
||||
try {
|
||||
analysis = await ExtractionService.analyzeImage(file)
|
||||
} catch (err: unknown) {
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
throw new Error("Analysis failed: " + error.message, { cause: error });
|
||||
}
|
||||
|
||||
if (!analysis.assets || analysis.assets.length === 0) {
|
||||
await stream.writeSSE({
|
||||
event: 'complete',
|
||||
data: JSON.stringify({ assets: [] })
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Generate Texture Sheet
|
||||
await stream.writeSSE({
|
||||
event: 'status',
|
||||
data: JSON.stringify({ status: 'generating', message: 'Generating texture sheet...' })
|
||||
})
|
||||
|
||||
let sheetBase64: string;
|
||||
try {
|
||||
sheetBase64 = await ExtractionService.generateTextureSheet(file, analysis.assets)
|
||||
} catch (err: unknown) {
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
throw new Error("Generation failed: " + error.message, { cause: error });
|
||||
}
|
||||
|
||||
// 3. Detect Bounding Boxes
|
||||
await stream.writeSSE({
|
||||
event: 'status',
|
||||
data: JSON.stringify({ status: 'splitting', message: 'Detecting asset boundaries...' })
|
||||
})
|
||||
|
||||
let boxes;
|
||||
try {
|
||||
boxes = await ExtractionService.detectBoundingBoxes(sheetBase64)
|
||||
} catch (err: unknown) {
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
throw new Error("Detection failed: " + error.message, { cause: error });
|
||||
}
|
||||
|
||||
// 4. Crop Assets
|
||||
const crops = await ExtractionService.cropAssets(sheetBase64, boxes)
|
||||
|
||||
// 5. Refine & Save
|
||||
await stream.writeSSE({
|
||||
event: 'status',
|
||||
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"
|
||||
}
|
||||
|
||||
// Refine image
|
||||
const refinedBase64 = await ExtractionService.refineAsset(crop.base64)
|
||||
|
||||
// Save to file system
|
||||
const savedFile = await FileService.saveBase64Image(refinedBase64)
|
||||
|
||||
// Create Asset record (AssetService now has createAssetRecord)
|
||||
const asset = await AssetService.createAssetRecord({
|
||||
name: originalMeta.item_name,
|
||||
type: 'image/webp',
|
||||
path: savedFile.filename
|
||||
})
|
||||
|
||||
// Find or create category
|
||||
const categoryId = await CategoryService.findOrCreate(originalMeta.category)
|
||||
|
||||
// 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
|
||||
})
|
||||
|
||||
refinedAssets.push({
|
||||
...equipment,
|
||||
imageUrl: `/files/${savedFile.filename}`, // Frontend needs URL
|
||||
})
|
||||
}
|
||||
|
||||
await stream.writeSSE({
|
||||
event: 'complete',
|
||||
data: JSON.stringify({ assets: refinedAssets })
|
||||
})
|
||||
|
||||
} catch (e: unknown) {
|
||||
console.error(e)
|
||||
const error = e instanceof Error ? e : new Error(String(e));
|
||||
await stream.writeSSE({
|
||||
event: 'error',
|
||||
data: JSON.stringify({ message: error.message || 'Extraction failed' })
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export default extract
|
||||
@@ -1,5 +1,6 @@
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
@@ -12,10 +13,10 @@
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import * as Prisma from './internal/prismaNamespaceBrowser.js'
|
||||
import * as Prisma from './internal/prismaNamespaceBrowser.ts'
|
||||
export { Prisma }
|
||||
export * as $Enums from './enums.js'
|
||||
export * from './enums.js'
|
||||
export * as $Enums from './enums.ts'
|
||||
export * from './enums.ts';
|
||||
/**
|
||||
* Model Asset
|
||||
*
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
@@ -14,13 +15,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.js'
|
||||
import * as $Class from './internal/class.js'
|
||||
import * as Prisma from './internal/prismaNamespace.js'
|
||||
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.js'
|
||||
export * from './enums.js'
|
||||
export * as $Enums from './enums.ts'
|
||||
export * from "./enums.ts"
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
@@ -35,11 +36,7 @@ export * from './enums.js'
|
||||
* 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,3 +1,4 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
@@ -8,9 +9,10 @@
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import type * as runtime from '@prisma/client/runtime/client'
|
||||
import * as $Enums from './enums.js'
|
||||
import type * as Prisma from './internal/prismaNamespace.js'
|
||||
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,12 +1,15 @@
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
|
||||
/* !!! 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,3 +1,4 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
@@ -10,26 +11,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.js'
|
||||
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')
|
||||
@@ -38,26 +39,23 @@ 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
|
||||
@@ -74,13 +72,9 @@ 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>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,27 +94,23 @@ 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
|
||||
* ```
|
||||
@@ -129,10 +119,7 @@ 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.
|
||||
@@ -144,7 +131,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.
|
||||
@@ -155,10 +142,7 @@ 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.
|
||||
@@ -170,7 +154,8 @@ 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.
|
||||
@@ -185,91 +170,73 @@ 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,3 +1,4 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
@@ -14,11 +15,11 @@
|
||||
* model files in the `model` directory!
|
||||
*/
|
||||
|
||||
import * as runtime from '@prisma/client/runtime/client'
|
||||
import type * as Prisma from '../models.js'
|
||||
import { type PrismaClient } from './class.js'
|
||||
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.js'
|
||||
export type * from '../models.ts'
|
||||
|
||||
export type DMMF = typeof runtime.DMMF
|
||||
|
||||
@@ -53,6 +54,8 @@ export const raw = runtime.raw
|
||||
export const Sql = runtime.Sql
|
||||
export type Sql = runtime.Sql
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Decimal.js
|
||||
*/
|
||||
@@ -62,8 +65,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>
|
||||
@@ -81,8 +84,8 @@ export type PrismaVersion = {
|
||||
* Query Engine version: 9d6ad21cbbceab97458517b147a6a09ff43aa735
|
||||
*/
|
||||
export const prismaVersion: PrismaVersion = {
|
||||
client: '7.3.0',
|
||||
engine: '9d6ad21cbbceab97458517b147a6a09ff43aa735',
|
||||
client: "7.3.0",
|
||||
engine: "9d6ad21cbbceab97458517b147a6a09ff43aa735"
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,10 +100,11 @@ 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)
|
||||
@@ -123,6 +127,7 @@ export const JsonNull = runtime.JsonNull
|
||||
*/
|
||||
export const AnyNull = runtime.AnyNull
|
||||
|
||||
|
||||
type SelectAndInclude = {
|
||||
select: any
|
||||
include: any
|
||||
@@ -137,18 +142,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
|
||||
@@ -157,11 +162,12 @@ 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
|
||||
@@ -169,35 +175,37 @@ 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
|
||||
@@ -218,14 +226,20 @@ 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
|
||||
|
||||
@@ -234,67 +248,56 @@ 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
|
||||
|
||||
@@ -310,10 +313,12 @@ 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: {
|
||||
@@ -328,17 +333,22 @@ 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
|
||||
@@ -346,8 +356,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]
|
||||
|
||||
/**
|
||||
@@ -360,53 +370,42 @@ 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: {
|
||||
@@ -850,9 +849,7 @@ export type TypeMap<
|
||||
}
|
||||
count: {
|
||||
args: Prisma.GenerationEquipmentCountArgs<ExtArgs>
|
||||
result:
|
||||
| runtime.Types.Utils.Optional<Prisma.GenerationEquipmentCountAggregateOutputType>
|
||||
| number
|
||||
result: runtime.Types.Utils.Optional<Prisma.GenerationEquipmentCountAggregateOutputType> | number
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -862,19 +859,19 @@ export type TypeMap<
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -886,11 +883,11 @@ export type TypeMap<
|
||||
*/
|
||||
|
||||
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',
|
||||
@@ -898,33 +895,34 @@ 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',
|
||||
@@ -934,64 +932,73 @@ 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
|
||||
*/
|
||||
@@ -999,30 +1006,22 @@ 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"
|
||||
*/
|
||||
@@ -1110,12 +1109,15 @@ 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
|
||||
@@ -1132,6 +1134,7 @@ export type LogEvent = {
|
||||
}
|
||||
/* End Types for Logging */
|
||||
|
||||
|
||||
export type PrismaAction =
|
||||
| 'findUnique'
|
||||
| 'findUniqueOrThrow'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
@@ -14,17 +15,18 @@
|
||||
* 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.js'
|
||||
export type * from './prismaNamespace.js'
|
||||
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)
|
||||
@@ -47,13 +49,14 @@ 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]
|
||||
@@ -63,11 +66,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',
|
||||
@@ -75,33 +78,34 @@ 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',
|
||||
@@ -111,41 +115,42 @@ 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,5 +1,6 @@
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
@@ -7,10 +8,10 @@
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
export type * from './models/Asset.js'
|
||||
export type * from './models/Character.js'
|
||||
export type * from './models/Category.js'
|
||||
export type * from './models/Equipment.js'
|
||||
export type * from './models/Generation.js'
|
||||
export type * from './models/GenerationEquipment.js'
|
||||
export type * from './commonInputTypes.js'
|
||||
export type * from './models/Asset.ts'
|
||||
export type * from './models/Character.ts'
|
||||
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'
|
||||
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
+41
-10
@@ -1,22 +1,53 @@
|
||||
import { Hono } from 'hono'
|
||||
// @ts-ignore
|
||||
import index from "./ui/index.html";
|
||||
import { Hono } from "hono";
|
||||
import { serveStatic } from 'hono/bun'
|
||||
import { cors } from 'hono/cors'
|
||||
import asset from './api/asset'
|
||||
import character from './api/character'
|
||||
import equipment from './api/equipment'
|
||||
import generation from './api/generation'
|
||||
import extract from './api/extract'
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.get('/', (c) => {
|
||||
return c.text('Hello Hono!')
|
||||
})
|
||||
// Enable CORS
|
||||
app.use('/*', cors())
|
||||
|
||||
// Serve static files from data/files
|
||||
// API Sub-App
|
||||
const api = new Hono()
|
||||
const route = api.route('/assets', asset)
|
||||
.route('/characters', character)
|
||||
.route('/equipments', equipment)
|
||||
.route('/generations', generation)
|
||||
.route('/extract', extract)
|
||||
|
||||
// Mount API
|
||||
app.route('/api', api)
|
||||
|
||||
// Serve static files from data/files under /files path
|
||||
app.use('/files/*', serveStatic({ root: './data' }))
|
||||
|
||||
app.route('/assets', asset)
|
||||
app.route('/characters', character)
|
||||
app.route('/equipments', equipment)
|
||||
app.route('/generations', generation)
|
||||
// Export type for RPC
|
||||
export type AppType = typeof route
|
||||
|
||||
export default app
|
||||
// Serve the frontend
|
||||
const server = Bun.serve({
|
||||
port: 3000,
|
||||
routes: {
|
||||
// Serve index.html for root
|
||||
"/": index,
|
||||
|
||||
// Proxy API requests to Hono
|
||||
"/api/*": app.fetch,
|
||||
"/files/*": app.fetch,
|
||||
|
||||
// Catch-all for SPA routing (fallback to index.html)
|
||||
"/*": index,
|
||||
},
|
||||
development: process.env.NODE_ENV !== "production",
|
||||
});
|
||||
|
||||
console.log(`🚀 Server running at ${server.url}`);
|
||||
|
||||
export default app;
|
||||
@@ -0,0 +1,107 @@
|
||||
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>;
|
||||
}
|
||||
|
||||
export class GeminiService implements AIService {
|
||||
private genAI: GoogleGenAI;
|
||||
|
||||
constructor() {
|
||||
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 parts: any[] = [];
|
||||
|
||||
// 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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
parts.push({ text: prompt });
|
||||
|
||||
const config: any = {
|
||||
responseMimeType: "application/json",
|
||||
};
|
||||
|
||||
if (schema) {
|
||||
config.responseSchema = zodToJsonSchema(schema);
|
||||
}
|
||||
|
||||
const result = await this.genAI.models.generateContent({
|
||||
model: model,
|
||||
contents: { parts },
|
||||
config: config
|
||||
});
|
||||
|
||||
const text = result.response.text();
|
||||
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);
|
||||
|
||||
if (schema) {
|
||||
return schema.parse(json);
|
||||
}
|
||||
return json as T;
|
||||
} catch (e: unknown) {
|
||||
console.error("Failed to parse JSON", text);
|
||||
const error = e instanceof Error ? e : new Error(String(e));
|
||||
throw new Error("Invalid JSON response from AI", { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
// Generates an image based on prompt and optional reference images
|
||||
async generateImage(prompt: string, referenceImages: File[] = []): Promise<string> {
|
||||
const parts: any[] = [];
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
parts.push({ text: prompt });
|
||||
|
||||
const result = await this.genAI.models.generateContent({
|
||||
model: 'gemini-2.0-flash-exp', // Using a known capable model
|
||||
contents: { parts },
|
||||
});
|
||||
|
||||
// Check for image in response
|
||||
const candidate = result.response.candidates?.[0];
|
||||
if (candidate?.content?.parts) {
|
||||
for (const part of candidate.content.parts) {
|
||||
if (part.inlineData && part.inlineData.mimeType.startsWith('image/')) {
|
||||
return `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("No image generated in response");
|
||||
}
|
||||
}
|
||||
|
||||
export const aiService = new GeminiService();
|
||||
+17
-3
@@ -1,7 +1,7 @@
|
||||
import { ulid } from 'ulidx'
|
||||
import { prisma } from '../db'
|
||||
import { join, extname } from 'path'
|
||||
import { writeFile } from '../lib/fileSystem'
|
||||
// import { writeFile } from '../lib/fileSystem' // Using Bun.write
|
||||
|
||||
export class AssetService {
|
||||
static async createAsset(file: File, meta: { name: string; type: string }) {
|
||||
@@ -10,20 +10,34 @@ export class AssetService {
|
||||
const filename = `${id}${ext}`
|
||||
const path = join('data/files', filename)
|
||||
|
||||
await writeFile(path, file)
|
||||
// await writeFile(path, file)
|
||||
await Bun.write(path, await file.arrayBuffer())
|
||||
|
||||
const asset = await prisma.asset.create({
|
||||
data: {
|
||||
id,
|
||||
name: meta.name,
|
||||
type: meta.type,
|
||||
path,
|
||||
path: filename, // Save only filename or relative path
|
||||
},
|
||||
})
|
||||
|
||||
return asset
|
||||
}
|
||||
|
||||
// New method to handle already saved files
|
||||
static async createAssetRecord(params: { path: string; name: string; type: string }) {
|
||||
const id = ulid()
|
||||
return prisma.asset.create({
|
||||
data: {
|
||||
id,
|
||||
name: params.name,
|
||||
type: params.type,
|
||||
path: params.path,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
static async getAsset(id: string) {
|
||||
return prisma.asset.findUnique({
|
||||
where: { id },
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { prisma } from '../db'
|
||||
import { ulid } from 'ulidx'
|
||||
|
||||
export class CategoryService {
|
||||
/**
|
||||
* Finds a category by name (case-insensitive) or creates it if it doesn't exist.
|
||||
* If a parentId is provided, it links the category as a sub-category.
|
||||
*/
|
||||
static async findOrCreate(name: string, parentId?: string): Promise<string> {
|
||||
const normalizedName = name.trim()
|
||||
|
||||
// Check if category exists
|
||||
const existing = await prisma.category.findFirst({
|
||||
where: {
|
||||
name: {
|
||||
equals: normalizedName,
|
||||
// mode: 'insensitive' // SQLite doesn't support 'mode: insensitive' directly like Postgres, usually need manual lower() or collate NOCASE
|
||||
},
|
||||
parentId: parentId || null
|
||||
}
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
return existing.id
|
||||
}
|
||||
|
||||
// Create new category
|
||||
const id = ulid()
|
||||
const newCategory = await prisma.category.create({
|
||||
data: {
|
||||
id,
|
||||
name: normalizedName,
|
||||
parentId: parentId || null
|
||||
}
|
||||
})
|
||||
|
||||
return newCategory.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all main categories with their sub-categories.
|
||||
*/
|
||||
static async listAll() {
|
||||
return prisma.category.findMany({
|
||||
where: {
|
||||
parentId: null
|
||||
},
|
||||
include: {
|
||||
subCategories: true
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
import { prisma } from '../db'
|
||||
import { Prisma } from '../generated/prisma/client'
|
||||
import { ulid } from 'ulidx'
|
||||
|
||||
export class EquipmentService {
|
||||
/**
|
||||
* Lists equipment with pagination and optional filtering by category/subcategory.
|
||||
*/
|
||||
static async listEquipments(
|
||||
pagination: { page: number; limit: number },
|
||||
filters?: { categoryId?: string; subCategoryId?: string },
|
||||
@@ -36,4 +40,32 @@ export class EquipmentService {
|
||||
|
||||
return { total, items, page, limit, totalPages: Math.ceil(total / limit) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a new equipment item, linking it to an asset (the refined image) and a category.
|
||||
*/
|
||||
static async createEquipment(data: {
|
||||
name: string
|
||||
description: string
|
||||
imageId: string
|
||||
categoryId: string
|
||||
subCategoryId?: string
|
||||
}) {
|
||||
const id = ulid()
|
||||
|
||||
return prisma.equipment.create({
|
||||
data: {
|
||||
id,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
imageId: data.imageId,
|
||||
categoryId: data.categoryId,
|
||||
subCategoryId: data.subCategoryId,
|
||||
},
|
||||
include: {
|
||||
image: true,
|
||||
category: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, test, mock } from 'bun:test'
|
||||
import { ExtractionService } from './extraction'
|
||||
|
||||
// Mock dependencies
|
||||
mock.module('./ai', () => ({
|
||||
aiService: {
|
||||
generateText: mock(async () => ({
|
||||
assets: [{
|
||||
item_name: 'Helmet',
|
||||
description: 'A rusty helmet',
|
||||
background_color: 'Red',
|
||||
category: 'Head'
|
||||
}]
|
||||
})),
|
||||
generateImage: mock(async () => 'data:image/png;base64,mockImage'),
|
||||
}
|
||||
}))
|
||||
|
||||
mock.module('./file', () => ({
|
||||
FileService: {
|
||||
saveBase64Image: mock(async () => ({
|
||||
path: 'data/files/123.webp',
|
||||
filename: '123.webp',
|
||||
mimeType: 'image/webp'
|
||||
}))
|
||||
}
|
||||
}))
|
||||
|
||||
mock.module('./category', () => ({
|
||||
CategoryService: {
|
||||
findOrCreate: mock(async () => 'cat_123')
|
||||
}
|
||||
}))
|
||||
|
||||
mock.module('./asset', () => ({
|
||||
AssetService: {
|
||||
createAssetRecord: mock(async () => ({ id: 'asset_123' }))
|
||||
}
|
||||
}))
|
||||
|
||||
mock.module('./equipment', () => ({
|
||||
EquipmentService: {
|
||||
createEquipment: mock(async () => ({ id: 'eq_123', name: 'Helmet' }))
|
||||
}
|
||||
}))
|
||||
|
||||
mock.module('sharp', () => {
|
||||
return () => ({
|
||||
metadata: () => Promise.resolve({ width: 100, height: 100 }),
|
||||
clone: () => ({
|
||||
extract: () => ({
|
||||
toBuffer: () => Promise.resolve(Buffer.from('mockCrop'))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('ExtractionService', () => {
|
||||
test('analyzeImage calls AI service', async () => {
|
||||
const file = new File(['dummy'], 'test.png', { type: 'image/png' })
|
||||
const result = await ExtractionService.analyzeImage(file)
|
||||
expect(result.assets.length).toBe(1)
|
||||
expect(result.assets[0].item_name).toBe('Helmet')
|
||||
})
|
||||
|
||||
test('generateTextureSheet calls AI service', async () => {
|
||||
const file = new File(['dummy'], 'test.png', { type: 'image/png' })
|
||||
const result = await ExtractionService.generateTextureSheet(file, [])
|
||||
expect(result).toBe('data:image/png;base64,mockImage')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,196 @@
|
||||
import { aiService } from './ai'
|
||||
import sharp from 'sharp'
|
||||
import { z } from 'zod'
|
||||
|
||||
// Types for the extraction process
|
||||
export interface ExtractedAsset {
|
||||
item_name: string
|
||||
description: string
|
||||
background_color: string
|
||||
category: string // Added category
|
||||
}
|
||||
|
||||
export interface AnalysisResponse {
|
||||
assets: ExtractedAsset[]
|
||||
}
|
||||
|
||||
export interface BoundingBoxResponse {
|
||||
grid_dimensions: {
|
||||
rows: number
|
||||
cols: number
|
||||
}
|
||||
assets: {
|
||||
item_name: string
|
||||
coordinates: [number, number, number, number] // ymin, xmin, ymax, xmax
|
||||
bg_color_detected: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export class ExtractionService {
|
||||
/**
|
||||
* Analyzes an image to identify extractable assets.
|
||||
*/
|
||||
static async analyzeImage(file: File): Promise<AnalysisResponse> {
|
||||
const prompt = `
|
||||
Task: Character Asset Identification for Extraction
|
||||
|
||||
Instructions:
|
||||
1. Analyze the character in the image and identify all distinct, extractable equipment and clothing items.
|
||||
2. Filter for items that are clearly visible: tops, bottoms, headwear, footwear, specialized armor (arm guards, leg guards), and accessories (scarves, belts, capes).
|
||||
3. For each identified item, assign one of the following high-contrast background colors for future segmentation: Red, Yellow, Green, White, Blue, Black, Magenta, Cyan, Orange, Gray. Avoid repeating colors if possible.
|
||||
4. Limit to a maximum of 9 most prominent assets.
|
||||
5. Assign a general category to each item (e.g., Head, Body, Legs, Feet, Weapon, Accessory).
|
||||
|
||||
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"),
|
||||
})),
|
||||
});
|
||||
|
||||
return aiService.generateText<AnalysisResponse>(prompt, [file], schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a texture sheet with isolated assets on solid backgrounds.
|
||||
*/
|
||||
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'
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
Instructions:
|
||||
Analyze the character in the provided image and extract the following ${assets.length} specific assets into a new, single image organized in a grid layout (e.g., 2x3, 3x3 depending on count).
|
||||
|
||||
Core Constraints:
|
||||
1. Isolated Assets: Each item must be placed within its own distinct square tile. No overlapping.
|
||||
2. Ghost Mannequin Style: Extract ONLY the items. Remove the character's body entirely. Clothing and armor must appear as empty, 3D shells as if worn by an invisible person.
|
||||
3. Solid Chroma Key Backgrounds: Each tile must use the specific solid background color assigned below. Ensure zero shadows, highlights, or gradients on the background to facilitate clean digital segmentation.
|
||||
4. Output Format: Generate a square texture sheet. Ignore the original image aspect ratio for the final output canvas.
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 prompt = `
|
||||
Task: Spatial Asset Detection & Normalized Grid Mapping
|
||||
|
||||
Instructions:
|
||||
1. Analyze the provided image consisting of character assets arranged in a grid.
|
||||
2. Identify the bounding box for each distinct asset container (the colored square background tiles).
|
||||
3. Return the coordinates for each box using a normalized scale of 0.0 to 1.0.
|
||||
- [ymin, xmin, ymax, xmax]
|
||||
- (0, 0) is the top-left corner; (1.0, 1.0) is the bottom-right corner.
|
||||
4. Ensure the box coordinates strictly encompass the outer boundary of each colored tile.
|
||||
|
||||
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(),
|
||||
})),
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
if (!metadata.width || !metadata.height) throw new Error("Could not determine image dimensions");
|
||||
|
||||
const croppedAssets = [];
|
||||
|
||||
for (const box of boxes.assets) {
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
if (safeWidth <= 0 || safeHeight <= 0) continue;
|
||||
|
||||
const cropBuffer = await image
|
||||
.clone()
|
||||
.extract({ left: safeLeft, top: safeTop, width: safeWidth, height: safeHeight })
|
||||
.toBuffer();
|
||||
|
||||
croppedAssets.push({
|
||||
name: box.item_name,
|
||||
base64: `data:image/png;base64,${cropBuffer.toString('base64')}`
|
||||
});
|
||||
}
|
||||
|
||||
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 prompt = `
|
||||
Task: Asset Refinement
|
||||
|
||||
Instructions:
|
||||
1. Process the provided asset image.
|
||||
2. Remove the existing colored background and replace it with a pure white background (#FFFFFF) (or transparent if possible, but white is safer for now).
|
||||
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]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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');
|
||||
|
||||
// Mock sharp
|
||||
mock.module('sharp', () => {
|
||||
return () => ({
|
||||
webp: () => ({
|
||||
toBuffer: () => Promise.resolve(new Uint8Array([1, 2, 3]))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Mock Bun.write
|
||||
const mockWrite = mock((path: string, content: any) => Promise.resolve(content.length))
|
||||
// @ts-ignore
|
||||
Bun.write = mockWrite
|
||||
|
||||
describe('FileService', () => {
|
||||
test('saveFile saves a file and converts to webp if image', async () => {
|
||||
// Pass valid PNG buffer
|
||||
const file = new File([VALID_PNG_BUFFER], 'test.png', { type: 'image/png' })
|
||||
const result = await FileService.saveFile(file)
|
||||
|
||||
expect(result.filename.endsWith('.webp')).toBe(true)
|
||||
expect(result.mimeType).toBe('image/webp')
|
||||
expect(mockWrite).toHaveBeenCalled()
|
||||
expect(mockWrite.mock.calls[0][0]).toContain('data/files')
|
||||
})
|
||||
|
||||
test('saveBase64Image saves a base64 string as webp', async () => {
|
||||
// Pass valid base64
|
||||
const result = await FileService.saveBase64Image(VALID_PNG_BASE64)
|
||||
|
||||
expect(result.filename.endsWith('.webp')).toBe(true)
|
||||
expect(mockWrite).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
import { join } from 'path'
|
||||
import { ulid } from 'ulidx'
|
||||
import sharp from 'sharp'
|
||||
// import { writeFile as bunWriteFile } from 'bun'
|
||||
|
||||
export class FileService {
|
||||
/**
|
||||
* Saves a file to the data/files directory.
|
||||
* If the file is an image, it converts it to WebP format.
|
||||
* @param file The file to save.
|
||||
* @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 }> {
|
||||
const id = ulid()
|
||||
let buffer: ArrayBuffer
|
||||
let filename: string
|
||||
let mimeType: string
|
||||
|
||||
if (file instanceof File) {
|
||||
buffer = await file.arrayBuffer()
|
||||
// Detect if it's an image based on type or extension
|
||||
if (file.type.startsWith('image/')) {
|
||||
const result = await this.optimizeImage(buffer)
|
||||
buffer = result.buffer
|
||||
filename = `${id}.webp`
|
||||
mimeType = 'image/webp'
|
||||
} else {
|
||||
// Fallback for non-images (though we mostly deal with images)
|
||||
const ext = name ? name.split('.').pop() : 'bin'
|
||||
filename = `${id}.${ext}`
|
||||
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'
|
||||
}
|
||||
|
||||
const path = join('data/files', filename)
|
||||
// Bun.write is global
|
||||
await Bun.write(path, buffer)
|
||||
|
||||
return { path, filename, mimeType }
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimizes an image buffer by converting it to WebP using sharp.
|
||||
*/
|
||||
private static async optimizeImage(buffer: ArrayBuffer): Promise<{ buffer: ArrayBuffer }> {
|
||||
const optimizedBuffer = await sharp(buffer)
|
||||
.webp({ quality: 80 }) // Good balance between size and quality
|
||||
.toBuffer()
|
||||
|
||||
return { buffer: optimizedBuffer }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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')
|
||||
|
||||
// Optimize
|
||||
const { buffer: optimized } = await this.optimizeImage(buffer)
|
||||
|
||||
const id = ulid()
|
||||
const filename = `${id}.webp`
|
||||
const path = join('data/files', filename)
|
||||
|
||||
await Bun.write(path, optimized)
|
||||
|
||||
return { path, filename }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
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 handleStatusChange = (newStatus: ExtractionStatus, message?: string) => {
|
||||
setStatus(newStatus);
|
||||
if (message) setStatusMessage(message);
|
||||
};
|
||||
|
||||
const NavButton = ({ target, label, color, icon }: any) => (
|
||||
<button
|
||||
onClick={() => setView(target)}
|
||||
className={`px-4 py-2 rounded-lg font-bold text-sm uppercase tracking-wider flex items-center gap-2 transition-all ${
|
||||
view === target
|
||||
? `${color} text-white shadow-md transform -translate-y-0.5`
|
||||
: 'bg-white text-stone-500 hover:bg-stone-50'
|
||||
}`}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading Overlay */}
|
||||
{(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 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')}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { hc } from 'hono/client'
|
||||
import type { AppType } from '../../index'
|
||||
|
||||
// Create a typed client
|
||||
// Since we are serving frontend and backend on the same origin (port 3000),
|
||||
// we can use '/' as the base URL, or explicit localhost.
|
||||
// However, 'hc' needs the base URL to construct requests.
|
||||
// If running in browser, '/' works.
|
||||
// Note: We need to point to '/api' because our routes are mounted under '/api'.
|
||||
// But wait, AppType is exported from 'route' variable which is the API sub-app.
|
||||
// The routes in AppType are like '/assets', '/extract', etc.
|
||||
// But they are mounted at '/api'.
|
||||
// So if we use hc<AppType>('/api'), it should work.
|
||||
|
||||
export const client = hc<AppType>('/api')
|
||||
@@ -0,0 +1,217 @@
|
||||
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;
|
||||
}
|
||||
|
||||
export const Extractor: React.FC<ExtractorProps> = ({
|
||||
onStatusChange,
|
||||
onError,
|
||||
onAssetsExtracted,
|
||||
status,
|
||||
statusMessage
|
||||
}) => {
|
||||
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 handleExtract = async () => {
|
||||
if (!file) return;
|
||||
|
||||
setResults([]);
|
||||
onError(null);
|
||||
onStatusChange('analyzing', "Starting extraction...");
|
||||
|
||||
try {
|
||||
// Hono RPC Client for File Upload
|
||||
const response = await client.extract.$post({
|
||||
form: {
|
||||
image: file
|
||||
}
|
||||
});
|
||||
|
||||
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 = '';
|
||||
|
||||
while (true) {
|
||||
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
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event: ')) {
|
||||
// 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 (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');
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
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.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.item_name}</div>
|
||||
<div className="text-[10px] text-stone-400 truncate">{item.category}</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>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import { ExtractionStatus } from '../types';
|
||||
|
||||
interface LoadingOverlayProps {
|
||||
status: ExtractionStatus;
|
||||
current?: number;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
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...';
|
||||
}
|
||||
};
|
||||
|
||||
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="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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { StashItem } from '../types';
|
||||
import { client } from '../client';
|
||||
|
||||
interface StashProps {
|
||||
onNavigateToExtractor: () => void;
|
||||
}
|
||||
|
||||
export const Stash: React.FC<StashProps> = ({ onNavigateToExtractor }) => {
|
||||
const [items, setItems] = useState<StashItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchItems();
|
||||
}, []);
|
||||
|
||||
const fetchItems = async () => {
|
||||
try {
|
||||
// Using Hono RPC client
|
||||
const res = await client.equipments.$get({
|
||||
query: {
|
||||
limit: '100'
|
||||
}
|
||||
});
|
||||
const data = await res.json();
|
||||
setItems(data.items);
|
||||
} catch (e) {
|
||||
console.error("Failed to load stash", e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
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>
|
||||
</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="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 as any).image?.path ? `/files/${(item as any).image.path}` : item.imageUrl} 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.item_name}</h3>
|
||||
</div>
|
||||
<p className="text-[10px] text-stone-400 font-mono uppercase tracking-wider truncate">{(item as any).category?.name || 'Unknown'}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-border: #e5e7eb;
|
||||
--color-input: #e5e7eb;
|
||||
--color-ring: #0a0a0a;
|
||||
--color-background: #ffffff;
|
||||
--color-foreground: #0a0a0a;
|
||||
|
||||
--color-primary: #171717;
|
||||
--color-primary-foreground: #fafafa;
|
||||
|
||||
--color-secondary: #f5f5f5;
|
||||
--color-secondary-foreground: #171717;
|
||||
|
||||
--color-destructive: #ef4444;
|
||||
--color-destructive-foreground: #fafafa;
|
||||
|
||||
--color-muted: #f5f5f5;
|
||||
--color-muted-foreground: #737373;
|
||||
|
||||
--color-accent: #f5f5f5;
|
||||
--color-accent-foreground: #171717;
|
||||
|
||||
--color-popover: #ffffff;
|
||||
--color-popover-foreground: #0a0a0a;
|
||||
|
||||
--color-card: #ffffff;
|
||||
--color-card-foreground: #0a0a0a;
|
||||
|
||||
--radius-lg: 0.5rem;
|
||||
--radius-md: calc(0.5rem - 2px);
|
||||
--radius-sm: calc(0.5rem - 4px);
|
||||
}
|
||||
|
||||
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);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 10px 10px;
|
||||
color: #1f2937; /* Gray 800 */
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: #e5e7eb;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #9ca3af;
|
||||
border-radius: 4px;
|
||||
border: 2px solid #e5e7eb;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
@keyframes fade-in-up {
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
console.log("Hello via Bun!");
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
const root = createRoot(document.getElementById('root') as HTMLElement);
|
||||
root.render(<App />);
|
||||
@@ -0,0 +1,28 @@
|
||||
export interface Asset {
|
||||
item_name: string;
|
||||
background_color: string;
|
||||
description: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
export interface ExtractedAsset extends Asset {
|
||||
imageUrl: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface StashItem {
|
||||
id: string;
|
||||
item_name: string;
|
||||
imageUrl: string;
|
||||
description: string;
|
||||
category?: Category;
|
||||
subCategory?: Category;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
export type ExtractionStatus = 'idle' | 'analyzing' | 'generating' | 'splitting' | 'refining' | 'complete' | 'error';
|
||||
@@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -1,173 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ExtractionStatus, StashItem, CroppedAsset } from './types';
|
||||
import { LoadingOverlay } from './components/LoadingOverlay';
|
||||
import { Extractor } from './components/Extractor';
|
||||
import { Stash } from './components/Stash';
|
||||
import { Modifier } from './components/Modifier';
|
||||
|
||||
export default function App() {
|
||||
const [view, setView] = useState<'extractor' | 'stash' | 'modifier'>('extractor');
|
||||
const [status, setStatus] = useState<ExtractionStatus>('idle');
|
||||
const [loadingCurrent, setLoadingCurrent] = useState(0);
|
||||
const [loadingTotal, setLoadingTotal] = useState(0);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [stash, setStash] = useState<StashItem[]>([]);
|
||||
const [apiKeySelected, setApiKeySelected] = useState<boolean>(false);
|
||||
const [notification, setNotification] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
checkApiKey();
|
||||
loadStash();
|
||||
}, []);
|
||||
|
||||
const checkApiKey = async () => {
|
||||
const aistudio = (window as any).aistudio;
|
||||
if (aistudio?.hasSelectedApiKey) {
|
||||
const hasKey = await aistudio.hasSelectedApiKey();
|
||||
setApiKeySelected(hasKey);
|
||||
} else {
|
||||
setApiKeySelected(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectKey = async () => {
|
||||
const aistudio = (window as any).aistudio;
|
||||
if (aistudio?.openSelectKey) {
|
||||
await aistudio.openSelectKey();
|
||||
setApiKeySelected(true);
|
||||
}
|
||||
};
|
||||
|
||||
const loadStash = () => {
|
||||
try {
|
||||
// Changed key to reflect new app name
|
||||
const saved = localStorage.getItem('forgery_stash');
|
||||
if (saved) setStash(JSON.parse(saved));
|
||||
} catch (e) {
|
||||
console.error("Failed to load stash", e);
|
||||
}
|
||||
};
|
||||
|
||||
const saveStash = (items: StashItem[]) => {
|
||||
setStash(items);
|
||||
localStorage.setItem('forgery_stash', JSON.stringify(items));
|
||||
};
|
||||
|
||||
const handleAddToStash = (assets: CroppedAsset[]) => {
|
||||
const newItems: StashItem[] = assets.map(asset => ({
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
item_name: asset.item_name,
|
||||
imageUrl: asset.refinedImageUrl || asset.imageUrl,
|
||||
createdAt: Date.now()
|
||||
}));
|
||||
saveStash([...newItems, ...stash]);
|
||||
showNotification("Loot successfully stashed!");
|
||||
};
|
||||
|
||||
const showNotification = (msg: string) => {
|
||||
setNotification(msg);
|
||||
setTimeout(() => setNotification(null), 3000);
|
||||
};
|
||||
|
||||
const handleStatusChange = (newStatus: ExtractionStatus, current?: number, total?: number) => {
|
||||
setStatus(newStatus);
|
||||
if (current !== undefined) setLoadingCurrent(current);
|
||||
if (total !== undefined) setLoadingTotal(total);
|
||||
};
|
||||
|
||||
const NavButton = ({ target, label, color, icon }: any) => (
|
||||
<button
|
||||
onClick={() => setView(target)}
|
||||
className={`px-4 py-2 rounded-lg font-bold text-sm uppercase tracking-wider flex items-center gap-2 transition-all ${
|
||||
view === target
|
||||
? `${color} text-white shadow-md transform -translate-y-0.5`
|
||||
: 'bg-white text-stone-500 hover:bg-stone-50'
|
||||
}`}
|
||||
>
|
||||
<span>{icon}</span> {label}
|
||||
{target === 'stash' && stash.length > 0 && (
|
||||
<span className="bg-stone-800 text-white text-[10px] px-1.5 py-0.5 rounded-full ml-1">{stash.length}</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen text-stone-800 font-sans p-4 md:p-8">
|
||||
{/* Notifications */}
|
||||
{notification && (
|
||||
<div className="fixed bottom-8 right-8 z-50 bg-stone-800 text-white px-6 py-4 rounded-xl shadow-2xl flex items-center gap-3 animate-fade-in-up border border-stone-700">
|
||||
<div className="bg-green-500 rounded-full p-1"><svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" /></svg></div>
|
||||
<span className="font-bold tracking-wide">{notification}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading */}
|
||||
{(status !== 'idle' && status !== 'complete' && status !== 'error') && (
|
||||
<LoadingOverlay status={status} current={loadingCurrent} total={loadingTotal} />
|
||||
)}
|
||||
|
||||
<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 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="❖" />
|
||||
<NavButton target="modifier" label="Smithy" color="bg-purple-600" icon="⚒" />
|
||||
|
||||
{!apiKeySelected && (window as any).aistudio && (
|
||||
<button onClick={handleSelectKey} className="ml-2 px-3 py-1 bg-red-500 text-white text-xs font-bold rounded hover:bg-red-600 uppercase tracking-wide">
|
||||
Key Required
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-7xl mx-auto">
|
||||
{view === 'extractor' && (
|
||||
<Extractor
|
||||
onStatusChange={handleStatusChange}
|
||||
onError={setError}
|
||||
onAssetsExtracted={() => {}}
|
||||
apiKeyValid={apiKeySelected}
|
||||
onConnectKey={handleSelectKey}
|
||||
onSaveToStash={handleAddToStash}
|
||||
/>
|
||||
)}
|
||||
{view === 'stash' && (
|
||||
<Stash
|
||||
items={stash}
|
||||
onRemove={(id) => saveStash(stash.filter(i => i.id !== id))}
|
||||
onClear={() => saveStash([])}
|
||||
onNavigateToExtractor={() => setView('extractor')}
|
||||
/>
|
||||
)}
|
||||
{view === 'modifier' && (
|
||||
<Modifier
|
||||
stash={stash}
|
||||
onStatusChange={handleStatusChange}
|
||||
onError={setError}
|
||||
apiKeyValid={apiKeySelected}
|
||||
onConnectKey={handleSelectKey}
|
||||
onNavigateToExtractor={() => setView('extractor')}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
<div align="center">
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
|
||||
</div>
|
||||
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
View your app in AI Studio: https://ai.studio/apps/drive/19Xb6Drl-US_jUxV3S8ruwCrcP6C8ZUHh
|
||||
|
||||
## Run Locally
|
||||
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
@@ -1,89 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Asset, HEX_COLORS } from '../types';
|
||||
|
||||
interface AssetListProps {
|
||||
assets: Asset[];
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
selectedModel: string;
|
||||
onModelChange: (model: string) => void;
|
||||
}
|
||||
|
||||
export const AssetList: React.FC<AssetListProps> = ({ assets, onConfirm, onCancel, selectedModel, onModelChange }) => {
|
||||
return (
|
||||
<div className="bg-slate-800 rounded-xl p-6 shadow-2xl border border-slate-700 max-w-4xl mx-auto w-full animate-fade-in-up">
|
||||
<div className="flex justify-between items-end mb-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">Identified Assets</h2>
|
||||
<p className="text-slate-400 mt-1">Review the items detected for extraction.</p>
|
||||
</div>
|
||||
<span className="bg-cyan-900/50 text-cyan-300 px-3 py-1 rounded-full text-sm font-medium border border-cyan-700/50">
|
||||
{assets.length} Items Found
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8 max-h-[400px] overflow-y-auto pr-2 custom-scrollbar">
|
||||
{assets.map((asset, idx) => (
|
||||
<div key={idx} className="bg-slate-900/50 p-4 rounded-lg border border-slate-700 hover:border-slate-600 transition-colors flex gap-4">
|
||||
<div
|
||||
className="w-12 h-12 rounded-full flex-shrink-0 shadow-inner"
|
||||
style={{ backgroundColor: HEX_COLORS[asset.background_color] || asset.background_color }}
|
||||
title={`Background: ${asset.background_color}`}
|
||||
/>
|
||||
<div>
|
||||
<h3 className="font-semibold text-slate-100">{asset.item_name}</h3>
|
||||
<p className="text-sm text-slate-400 mt-1 leading-relaxed">{asset.description}</p>
|
||||
<div className="mt-2 text-xs font-mono text-slate-500">
|
||||
Target Key: {asset.background_color}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Model Selection */}
|
||||
<div className="mb-6 bg-slate-900/30 p-4 rounded-lg border border-slate-700/50">
|
||||
<h3 className="text-sm font-semibold text-slate-300 mb-3 uppercase tracking-wider">Extraction Model</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label className={`relative p-3 rounded-lg border cursor-pointer transition-all ${selectedModel === 'gemini-3-pro-image-preview' ? 'bg-cyan-900/20 border-cyan-500/70 shadow-[0_0_15px_-3px_rgba(6,182,212,0.3)]' : 'bg-slate-800 border-slate-700 hover:border-slate-600'}`}>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<div className={`w-4 h-4 rounded-full border flex items-center justify-center ${selectedModel === 'gemini-3-pro-image-preview' ? 'border-cyan-500' : 'border-slate-500'}`}>
|
||||
{selectedModel === 'gemini-3-pro-image-preview' && <div className="w-2 h-2 rounded-full bg-cyan-500" />}
|
||||
</div>
|
||||
<input type="radio" name="model" value="gemini-3-pro-image-preview" checked={selectedModel === 'gemini-3-pro-image-preview'} onChange={(e) => onModelChange(e.target.value)} className="hidden" />
|
||||
<span className={`font-bold ${selectedModel === 'gemini-3-pro-image-preview' ? 'text-cyan-400' : 'text-slate-300'}`}>Gemini 3 Pro</span>
|
||||
<span className="text-[10px] font-bold bg-cyan-500/20 text-cyan-300 px-1.5 py-0.5 rounded ml-auto">RECOMMENDED</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 pl-7">Best quality, 2K resolution support, precise geometry.</p>
|
||||
</label>
|
||||
|
||||
<label className={`relative p-3 rounded-lg border cursor-pointer transition-all ${selectedModel === 'gemini-2.5-flash-image' ? 'bg-amber-900/20 border-amber-500/70 shadow-[0_0_15px_-3px_rgba(245,158,11,0.3)]' : 'bg-slate-800 border-slate-700 hover:border-slate-600'}`}>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<div className={`w-4 h-4 rounded-full border flex items-center justify-center ${selectedModel === 'gemini-2.5-flash-image' ? 'border-amber-500' : 'border-slate-500'}`}>
|
||||
{selectedModel === 'gemini-2.5-flash-image' && <div className="w-2 h-2 rounded-full bg-amber-500" />}
|
||||
</div>
|
||||
<input type="radio" name="model" value="gemini-2.5-flash-image" checked={selectedModel === 'gemini-2.5-flash-image'} onChange={(e) => onModelChange(e.target.value)} className="hidden" />
|
||||
<span className={`font-bold ${selectedModel === 'gemini-2.5-flash-image' ? 'text-amber-400' : 'text-slate-300'}`}>Gemini 2.5 Flash</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 pl-7">Fast generation, standard resolution, experimental for extraction.</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 pt-4 border-t border-slate-700">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="px-6 py-2 rounded-lg text-slate-300 hover:bg-slate-700 hover:text-white transition-colors font-medium"
|
||||
>
|
||||
Discard
|
||||
</button>
|
||||
<button
|
||||
onClick={onConfirm}
|
||||
className="px-6 py-2 rounded-lg bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-500 hover:to-blue-500 text-white font-bold shadow-lg shadow-cyan-900/20 transform hover:-translate-y-0.5 transition-all"
|
||||
>
|
||||
Generate Texture Sheet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,277 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { analyzeImageForAssets, generateTextureSheet, detectAssetBoundingBoxes, cropAssetsFromSheet, refineAssetImage } from '../services/geminiService';
|
||||
import { AnalysisResponse, CroppedAsset, ExtractionStatus } from '../types';
|
||||
|
||||
interface ExtractorProps {
|
||||
onStatusChange: (status: ExtractionStatus, index?: number, total?: number) => void;
|
||||
onError: (msg: string | null) => void;
|
||||
onAssetsExtracted: (assets: CroppedAsset[]) => void;
|
||||
apiKeyValid: boolean;
|
||||
onConnectKey: () => void;
|
||||
onSaveToStash: (assets: CroppedAsset[]) => void;
|
||||
}
|
||||
|
||||
export const Extractor: React.FC<ExtractorProps> = ({
|
||||
onStatusChange,
|
||||
onError,
|
||||
onAssetsExtracted,
|
||||
apiKeyValid,
|
||||
onConnectKey,
|
||||
onSaveToStash
|
||||
}) => {
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [previews, setPreviews] = useState<string[]>([]);
|
||||
const [results, setResults] = useState<CroppedAsset[]>([]);
|
||||
const [selectedSourceIdx, setSelectedSourceIdx] = useState<number | null>(null);
|
||||
|
||||
// Handle Paste Events
|
||||
useEffect(() => {
|
||||
const handlePaste = (e: ClipboardEvent) => {
|
||||
if (e.clipboardData && e.clipboardData.items) {
|
||||
const items = e.clipboardData.items;
|
||||
const newFiles: File[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].type.indexOf("image") !== -1) {
|
||||
const blob = items[i].getAsFile();
|
||||
if (blob) {
|
||||
newFiles.push(blob);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newFiles.length > 0) {
|
||||
addFiles(newFiles);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('paste', handlePaste);
|
||||
return () => {
|
||||
window.removeEventListener('paste', handlePaste);
|
||||
};
|
||||
}, []); // Empty dependency array means this runs once on mount and cleans up on unmount
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) addFiles(Array.from(e.target.files));
|
||||
};
|
||||
|
||||
const addFiles = (newFiles: File[]) => {
|
||||
setFiles(prev => [...prev, ...newFiles]);
|
||||
setPreviews(prev => [...prev, ...newFiles.map(f => URL.createObjectURL(f))]);
|
||||
setResults([]); // Clear results on new upload
|
||||
onError(null);
|
||||
};
|
||||
|
||||
const removeFile = (idx: number) => {
|
||||
const newFiles = [...files];
|
||||
const newPreviews = [...previews];
|
||||
URL.revokeObjectURL(newPreviews[idx]);
|
||||
newFiles.splice(idx, 1);
|
||||
newPreviews.splice(idx, 1);
|
||||
setFiles(newFiles);
|
||||
setPreviews(newPreviews);
|
||||
};
|
||||
|
||||
const handleExtract = async () => {
|
||||
if (files.length === 0) return;
|
||||
if (!apiKeyValid) {
|
||||
onConnectKey();
|
||||
return;
|
||||
}
|
||||
|
||||
setResults([]);
|
||||
onError(null);
|
||||
const allAssets: CroppedAsset[] = [];
|
||||
|
||||
try {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
onStatusChange('analyzing', i + 1, files.length);
|
||||
|
||||
// 1. Analyze
|
||||
let analysis: AnalysisResponse;
|
||||
try {
|
||||
analysis = await analyzeImageForAssets(files[i]);
|
||||
} catch (e) {
|
||||
console.error(`Analysis failed for ${files[i].name}`, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (analysis.assets.length === 0) continue;
|
||||
|
||||
// 2. Generate Sheet
|
||||
onStatusChange('generating', i + 1, files.length);
|
||||
const sheetUrl = await generateTextureSheet(files[i], analysis.assets);
|
||||
|
||||
// 3. Slice
|
||||
onStatusChange('splitting', i + 1, files.length);
|
||||
const base64 = sheetUrl.split(',')[1];
|
||||
const boxes = await detectAssetBoundingBoxes(base64);
|
||||
const cropped = await cropAssetsFromSheet(sheetUrl, boxes.assets);
|
||||
|
||||
// 4. Refine
|
||||
onStatusChange('refining', i + 1, files.length);
|
||||
const refined = await Promise.all(cropped.map(async (a) => {
|
||||
try {
|
||||
const refinedUrl = await refineAssetImage(a.imageUrl);
|
||||
return { ...a, refinedImageUrl: refinedUrl, sourceIndex: i };
|
||||
} catch {
|
||||
return { ...a, sourceIndex: i };
|
||||
}
|
||||
}));
|
||||
|
||||
allAssets.push(...refined);
|
||||
}
|
||||
|
||||
setResults(allAssets);
|
||||
onAssetsExtracted(allAssets);
|
||||
onStatusChange('complete');
|
||||
if (allAssets.length > 0) setSelectedSourceIdx(0);
|
||||
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
onError(e.message || "Extraction failed");
|
||||
onStatusChange('error');
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setFiles([]);
|
||||
previews.forEach(p => URL.revokeObjectURL(p));
|
||||
setPreviews([]);
|
||||
setResults([]);
|
||||
onStatusChange('idle');
|
||||
};
|
||||
|
||||
if (results.length > 0) {
|
||||
const filtered = selectedSourceIdx !== null
|
||||
? results.filter(r => r.sourceIndex === selectedSourceIdx)
|
||||
: results;
|
||||
|
||||
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 References</h3>
|
||||
<div className="flex gap-3 overflow-x-auto pb-2 custom-scrollbar">
|
||||
{previews.map((src, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => setSelectedSourceIdx(idx === selectedSourceIdx ? null : idx)}
|
||||
className={`relative w-16 h-20 flex-shrink-0 rounded-lg overflow-hidden border-2 transition-all ${idx === selectedSourceIdx ? 'border-amber-500 shadow-md scale-105' : 'border-stone-200 opacity-60 hover:opacity-100'}`}
|
||||
>
|
||||
<img src={src} className="w-full h-full object-cover" />
|
||||
{idx === selectedSourceIdx && <div className="absolute inset-0 bg-amber-500/10" />}
|
||||
</button>
|
||||
))}
|
||||
</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">{filtered.length} items found</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => onSaveToStash(results)}
|
||||
className="px-5 py-2.5 bg-cyan-600 hover:bg-cyan-500 text-white text-sm font-bold uppercase tracking-wider rounded-lg shadow-lg hover:-translate-y-0.5 transition-all flex items-center gap-2"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" /></svg>
|
||||
Stash All
|
||||
</button>
|
||||
<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">
|
||||
{filtered.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.refinedImageUrl || 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.refinedImageUrl || item.imageUrl}
|
||||
download={`${item.item_name}.png`}
|
||||
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.item_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">
|
||||
{files.length === 0 ? (
|
||||
<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 SOURCES</h2>
|
||||
<p className="text-stone-500 mb-4 max-w-sm mx-auto">Drop character images to identify and extract equipment sprites.</p>
|
||||
<div className="inline-block bg-cyan-50 border border-cyan-100 text-cyan-700 text-xs font-mono py-1 px-3 rounded mb-8">
|
||||
Paste (Ctrl+V) Supported
|
||||
</div>
|
||||
<div className="block">
|
||||
<label className="inline-block cursor-pointer">
|
||||
<input type="file" multiple 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 Images</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 ({files.length})</h3>
|
||||
<span className="text-[10px] text-stone-400 font-mono">Drag, Drop, or Paste more</span>
|
||||
</div>
|
||||
<label className="text-cyan-600 font-bold text-sm cursor-pointer hover:text-cyan-700 uppercase tracking-wide">
|
||||
+ Add More
|
||||
<input type="file" multiple accept="image/*" onChange={handleFileChange} className="hidden" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4 mb-8">
|
||||
{previews.map((src, i) => (
|
||||
<div key={i} className="relative group aspect-square rounded-lg overflow-hidden border border-stone-200">
|
||||
<img src={src} className="w-full h-full object-cover" />
|
||||
<button onClick={() => removeFile(i)} className="absolute top-1 right-1 bg-red-500 text-white p-1 rounded-md opacity-0 group-hover:opacity-100 transition-opacity shadow-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleExtract}
|
||||
className="w-full py-4 bg-gradient-to-r from-amber-500 to-orange-600 text-white font-black uppercase tracking-widest text-lg rounded-xl shadow-lg hover:shadow-xl hover:-translate-y-1 transition-all flex items-center justify-center gap-3"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 animate-pulse" 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
|
||||
</button>
|
||||
<p className="text-center text-xs text-stone-400 mt-4 font-mono">POWERED BY GEMINI 2.5 FLASH</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
interface LoadingOverlayProps {
|
||||
status: string;
|
||||
current?: number;
|
||||
total?: number;
|
||||
}
|
||||
|
||||
export const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ status, current, total }) => {
|
||||
let title = 'PROCESSING';
|
||||
let description = 'Please wait...';
|
||||
|
||||
switch (status) {
|
||||
case 'analyzing':
|
||||
title = 'SCANNING ASSETS';
|
||||
description = 'Identifying equipment geometry...';
|
||||
break;
|
||||
case 'generating':
|
||||
title = 'FORGING TEXTURES';
|
||||
description = 'Creating ghost mannequin sprites...';
|
||||
break;
|
||||
case 'splitting':
|
||||
title = 'CUTTING SPRITES';
|
||||
description = 'Slicing texture sheet boundaries...';
|
||||
break;
|
||||
case 'refining':
|
||||
title = 'POLISHING';
|
||||
description = 'Refining edges and normalizing...';
|
||||
break;
|
||||
case 'modifying':
|
||||
title = 'SYNTHESIZING';
|
||||
description = 'Equipping character with new gear...';
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-stone-900/60 backdrop-blur-md flex flex-col items-center justify-center z-50">
|
||||
<div className="bg-white p-8 rounded-2xl shadow-2xl border-4 border-stone-200 max-w-sm w-full text-center relative overflow-hidden">
|
||||
{/* Top Decoration */}
|
||||
<div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-amber-400 via-orange-500 to-amber-400"></div>
|
||||
|
||||
<div className="relative w-20 h-20 mx-auto mb-6">
|
||||
<div className="absolute inset-0 border-4 border-t-amber-500 border-r-stone-200 border-b-stone-200 border-l-stone-200 rounded-full animate-spin"></div>
|
||||
<div className="absolute inset-3 border-4 border-t-cyan-500 border-r-transparent border-b-transparent border-l-transparent rounded-full animate-spin-reverse"></div>
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl font-black text-stone-800 mb-2 uppercase tracking-widest font-mono">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-stone-500 text-sm font-medium">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
{total && total > 1 && (
|
||||
<div className="mt-6">
|
||||
<div className="flex justify-between text-xs font-bold text-stone-400 mb-1 uppercase">
|
||||
<span>Progress</span>
|
||||
<span>{current} / {total}</span>
|
||||
</div>
|
||||
<div className="h-2 bg-stone-100 rounded-full overflow-hidden border border-stone-200">
|
||||
<div
|
||||
className="h-full bg-cyan-500 transition-all duration-300"
|
||||
style={{ width: `${(current! / total) * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,197 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { generateModifiedCharacter } from '../services/geminiService';
|
||||
import { StashItem, ExtractionStatus } from '../types';
|
||||
|
||||
interface ModifierProps {
|
||||
stash: StashItem[];
|
||||
onStatusChange: (status: ExtractionStatus) => void;
|
||||
onError: (msg: string | null) => void;
|
||||
apiKeyValid: boolean;
|
||||
onConnectKey: () => void;
|
||||
onNavigateToExtractor: () => void;
|
||||
}
|
||||
|
||||
export const Modifier: React.FC<ModifierProps> = ({
|
||||
stash,
|
||||
onStatusChange,
|
||||
onError,
|
||||
apiKeyValid,
|
||||
onConnectKey,
|
||||
onNavigateToExtractor
|
||||
}) => {
|
||||
const [baseFile, setBaseFile] = useState<File | null>(null);
|
||||
const [basePreview, setBasePreview] = useState<string | null>(null);
|
||||
const [selectedItemIds, setSelectedItemIds] = useState<Set<string>>(new Set());
|
||||
const [resultUrl, setResultUrl] = useState<string | null>(null);
|
||||
const [selectedModel, setSelectedModel] = useState<string>('gemini-3-pro-image-preview');
|
||||
|
||||
const handleBaseChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
const f = e.target.files[0];
|
||||
setBaseFile(f);
|
||||
setBasePreview(URL.createObjectURL(f));
|
||||
setResultUrl(null);
|
||||
onError(null);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleItem = (id: string) => {
|
||||
const next = new Set(selectedItemIds);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
setSelectedItemIds(next);
|
||||
};
|
||||
|
||||
const handleForge = async () => {
|
||||
if (!baseFile || selectedItemIds.size === 0) return;
|
||||
if (!apiKeyValid) {
|
||||
onConnectKey();
|
||||
return;
|
||||
}
|
||||
|
||||
onStatusChange('modifying');
|
||||
setResultUrl(null);
|
||||
onError(null);
|
||||
|
||||
try {
|
||||
const items = stash.filter(i => selectedItemIds.has(i.id));
|
||||
const url = await generateModifiedCharacter(baseFile, items, selectedModel);
|
||||
setResultUrl(url);
|
||||
onStatusChange('idle');
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
onError(e.message || "Modification failed");
|
||||
onStatusChange('error');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="animate-fade-in-up pb-20">
|
||||
<div className="mb-8">
|
||||
<h2 className="text-3xl font-black text-stone-800 flex items-center gap-3">
|
||||
<span className="text-purple-600">⚒</span> SMITHY
|
||||
</h2>
|
||||
<p className="text-stone-500 font-medium">Equip your characters with extracted assets.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* LEFT: Base Character */}
|
||||
<div className="bg-white rounded-xl border border-stone-200 shadow-lg p-5 flex flex-col h-fit">
|
||||
<h3 className="text-xs font-bold text-stone-400 uppercase tracking-widest mb-4">Base Character</h3>
|
||||
|
||||
<div className="aspect-[3/4] bg-stone-100 rounded-lg border-2 border-dashed border-stone-300 overflow-hidden relative flex flex-col items-center justify-center hover:border-purple-400 transition-colors">
|
||||
{basePreview ? (
|
||||
<>
|
||||
<img src={basePreview} className="w-full h-full object-contain" />
|
||||
<button
|
||||
onClick={() => { setBaseFile(null); setBasePreview(null); }}
|
||||
className="absolute top-2 right-2 bg-white/90 text-stone-600 hover:text-red-500 p-1 rounded-full shadow-sm"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" /></svg>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<label className="cursor-pointer flex flex-col items-center p-6 text-center w-full h-full justify-center">
|
||||
<input type="file" accept="image/*" onChange={handleBaseChange} className="hidden" />
|
||||
<div className="w-12 h-12 bg-purple-100 text-purple-600 rounded-full flex items-center justify-center mb-3">
|
||||
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /></svg>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-stone-600 uppercase">Upload Character</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Model Selector */}
|
||||
<div className="mt-6 bg-stone-50 p-4 rounded-lg border border-stone-200">
|
||||
<h3 className="text-xs font-bold text-stone-400 uppercase tracking-widest mb-3">Forge Model</h3>
|
||||
<div className="space-y-2">
|
||||
<label className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-all ${selectedModel === 'gemini-3-pro-image-preview' ? 'bg-white border-purple-500 shadow-sm' : 'border-stone-200 hover:bg-white'}`}>
|
||||
<input type="radio" name="model" value="gemini-3-pro-image-preview" checked={selectedModel === 'gemini-3-pro-image-preview'} onChange={(e) => setSelectedModel(e.target.value)} className="w-4 h-4 text-purple-600 accent-purple-600" />
|
||||
<div>
|
||||
<div className="font-bold text-sm text-stone-800 flex items-center gap-2">
|
||||
Banana Pro
|
||||
<span className="text-[10px] bg-purple-100 text-purple-600 px-1.5 rounded">High Quality</span>
|
||||
</div>
|
||||
<div className="text-[10px] text-stone-500">2K Resolution, Better coherence</div>
|
||||
</div>
|
||||
</label>
|
||||
<label className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-all ${selectedModel === 'gemini-2.5-flash-image' ? 'bg-white border-amber-500 shadow-sm' : 'border-stone-200 hover:bg-white'}`}>
|
||||
<input type="radio" name="model" value="gemini-2.5-flash-image" checked={selectedModel === 'gemini-2.5-flash-image'} onChange={(e) => setSelectedModel(e.target.value)} className="w-4 h-4 text-amber-600 accent-amber-600" />
|
||||
<div>
|
||||
<div className="font-bold text-sm text-stone-800">Banana Flash</div>
|
||||
<div className="text-[10px] text-stone-500">Faster, Standard Resolution</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CENTER: Stash Selector */}
|
||||
<div className="lg:col-span-2 flex flex-col h-[650px] bg-white rounded-xl border border-stone-200 shadow-lg overflow-hidden">
|
||||
<div className="p-4 border-b border-stone-100 flex justify-between items-center bg-stone-50/50">
|
||||
<h3 className="text-xs font-bold text-stone-400 uppercase tracking-widest">Available Equipment</h3>
|
||||
<span className="text-xs font-bold bg-stone-200 text-stone-600 px-2 py-0.5 rounded-full">{selectedItemIds.size} Selected</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 custom-scrollbar bg-stone-50/30">
|
||||
{stash.length === 0 ? (
|
||||
<div className="h-full flex flex-col items-center justify-center text-stone-400">
|
||||
<p>No equipment in stash.</p>
|
||||
<button onClick={onNavigateToExtractor} className="text-cyan-600 text-sm font-bold uppercase mt-2 hover:underline">Go to Extractor</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 gap-3">
|
||||
{stash.map(item => {
|
||||
const isSelected = selectedItemIds.has(item.id);
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => toggleItem(item.id)}
|
||||
className={`cursor-pointer rounded-lg border-2 p-2 transition-all hover:shadow-md ${isSelected ? 'border-purple-500 bg-purple-50' : 'border-stone-200 bg-white hover:border-stone-300'}`}
|
||||
>
|
||||
<div className="aspect-square bg-stone-100 rounded overflow-hidden mb-2">
|
||||
<img src={item.imageUrl} className="w-full h-full object-contain" />
|
||||
</div>
|
||||
<p className={`text-[10px] font-bold truncate text-center ${isSelected ? 'text-purple-700' : 'text-stone-500'}`}>{item.item_name}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-stone-200 bg-white flex justify-end">
|
||||
<button
|
||||
onClick={handleForge}
|
||||
disabled={!baseFile || selectedItemIds.size === 0}
|
||||
className={`px-8 py-3 rounded-lg font-bold uppercase tracking-wider shadow-lg flex items-center gap-2 transition-all ${
|
||||
!baseFile || selectedItemIds.size === 0
|
||||
? 'bg-stone-200 text-stone-400 cursor-not-allowed'
|
||||
: 'bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white hover:-translate-y-1'
|
||||
}`}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fillRule="evenodd" d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" clipRule="evenodd" /></svg>
|
||||
Forge
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* RESULT */}
|
||||
{resultUrl && (
|
||||
<div className="mt-12 text-center animate-fade-in-up">
|
||||
<h2 className="text-2xl font-black text-stone-800 mb-6 uppercase">Masterpiece Created</h2>
|
||||
<div className="inline-block p-4 bg-white rounded-2xl shadow-2xl border-2 border-purple-200 relative group">
|
||||
<img src={resultUrl} className="max-w-md w-full rounded-lg" />
|
||||
<div className="absolute inset-0 bg-stone-900/40 opacity-0 group-hover:opacity-100 transition-opacity rounded-2xl flex items-center justify-center">
|
||||
<a href={resultUrl} download="forged_character.png" className="bg-purple-600 text-white px-6 py-3 rounded-lg font-bold uppercase tracking-wider shadow-xl hover:scale-105 transition-transform flex items-center gap-2">
|
||||
<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 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
import React from 'react';
|
||||
import { StashItem } from '../types';
|
||||
|
||||
interface StashProps {
|
||||
items: StashItem[];
|
||||
onRemove: (id: string) => void;
|
||||
onClear: () => void;
|
||||
onNavigateToExtractor: () => void;
|
||||
}
|
||||
|
||||
export const Stash: React.FC<StashProps> = ({ items, onRemove, onClear, onNavigateToExtractor }) => {
|
||||
return (
|
||||
<div className="animate-fade-in-up">
|
||||
<div className="flex justify-between items-end mb-8">
|
||||
<div>
|
||||
<h2 className="text-3xl font-black text-stone-800 flex items-center gap-3">
|
||||
<span className="text-cyan-500">❖</span> INVENTORY
|
||||
</h2>
|
||||
<p className="text-stone-500 font-medium">Manage your collection of extracted assets.</p>
|
||||
</div>
|
||||
{items.length > 0 && (
|
||||
<button
|
||||
onClick={onClear}
|
||||
className="text-red-500 hover:text-red-700 font-bold uppercase text-xs tracking-wider border border-red-200 px-3 py-1.5 rounded bg-red-50 hover:bg-red-100 transition-colors"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="bg-white rounded-2xl p-16 text-center border-4 border-dashed border-stone-200">
|
||||
<div className="w-16 h-16 bg-stone-100 rounded-full flex items-center justify-center mx-auto mb-4 text-stone-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-8 w-8" 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>
|
||||
<h3 className="text-xl font-bold text-stone-700">Inventory Empty</h3>
|
||||
<p className="text-stone-400 mt-2 mb-6">Extract assets to populate your stash.</p>
|
||||
<button onClick={onNavigateToExtractor} className="text-cyan-600 font-bold uppercase hover:underline text-sm">Go to Extractor</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="group relative bg-white rounded-lg border-2 border-stone-200 shadow-sm hover:border-cyan-400 hover:shadow-lg transition-all duration-300">
|
||||
<div className="aspect-square p-3 flex items-center justify-center relative bg-stone-50 rounded-t-md overflow-hidden">
|
||||
<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 group-hover:scale-110 transition-transform" />
|
||||
|
||||
<div className="absolute inset-0 bg-stone-900/60 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col gap-2 items-center justify-center p-2 backdrop-blur-[1px]">
|
||||
<a
|
||||
href={item.imageUrl}
|
||||
download={`${item.item_name}.png`}
|
||||
className="w-full py-1.5 bg-cyan-600 hover:bg-cyan-500 text-white rounded text-[10px] font-bold uppercase tracking-widest text-center shadow-md"
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
<button
|
||||
onClick={() => onRemove(item.id)}
|
||||
className="w-full py-1.5 bg-red-500 hover:bg-red-400 text-white rounded text-[10px] font-bold uppercase tracking-widest text-center shadow-md"
|
||||
>
|
||||
Discard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 border-t border-stone-100">
|
||||
<h3 className="text-xs font-bold text-stone-700 truncate" title={item.item_name}>{item.item_name}</h3>
|
||||
<p className="text-[10px] text-stone-400 mt-1">{new Date(item.createdAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,64 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Forgery</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;700&display=swap');
|
||||
|
||||
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);
|
||||
background-size: 20px 20px;
|
||||
background-position: 0 0, 10px 10px;
|
||||
color: #1f2937; /* Gray 800 */
|
||||
}
|
||||
|
||||
/* Custom Scrollbar for 'Game UI' feel */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: #e5e7eb;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #9ca3af;
|
||||
border-radius: 4px;
|
||||
border: 2px solid #e5e7eb;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: #6b7280;
|
||||
}
|
||||
|
||||
@keyframes fade-in-up {
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"react/": "https://esm.sh/react@^19.2.4/",
|
||||
"react": "https://esm.sh/react@^19.2.4",
|
||||
"@google/genai": "https://esm.sh/@google/genai@^1.40.0",
|
||||
"react-dom/": "https://esm.sh/react-dom@^19.2.4/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="/index.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (!rootElement) {
|
||||
throw new Error("Could not find root element to mount to");
|
||||
}
|
||||
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "Forgery",
|
||||
"description": "Extract character assets and forge new characters using Gemini models.",
|
||||
"requestFramePermissions": []
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "forgery",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.4",
|
||||
"@google/genai": "^1.40.0",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1,398 +0,0 @@
|
||||
import { GoogleGenAI, Type } from "@google/genai";
|
||||
import { AnalysisResponse, Asset, BoundingBoxResponse, CroppedAsset, DetectedAssetBox, HEX_COLORS, StashItem } from "../types";
|
||||
|
||||
// Helper to convert File to Base64
|
||||
const fileToGenericBase64 = async (file: File): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const result = reader.result as string;
|
||||
// Remove data URL prefix (e.g., "data:image/png;base64,")
|
||||
const base64 = result.split(',')[1];
|
||||
resolve(base64);
|
||||
};
|
||||
reader.onerror = (error) => reject(error);
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
};
|
||||
|
||||
export const analyzeImageForAssets = async (file: File): Promise<AnalysisResponse> => {
|
||||
// Use a smart model for analysis (Gemini 3 Pro or Flash) to get accurate JSON
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
||||
const base64Data = await fileToGenericBase64(file);
|
||||
|
||||
const prompt = `
|
||||
Task: Character Asset Identification for Extraction
|
||||
|
||||
Instructions:
|
||||
1. Analyze the character in the image and identify all distinct, extractable equipment and clothing items.
|
||||
2. Filter for items that are clearly visible: tops, bottoms, headwear, footwear, specialized armor (arm guards, leg guards), and accessories (scarves, belts, capes).
|
||||
3. For each identified item, assign one of the following high-contrast background colors for future segmentation: Red, Yellow, Green, White, Blue, Black, Magenta, Cyan, Orange, Gray. Avoid repeating colors if possible.
|
||||
4. Limit to a maximum of 9 most prominent assets.
|
||||
|
||||
Output Format (Strict JSON):
|
||||
Return ONLY a JSON object with a list of assets.
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-pro-preview',
|
||||
contents: {
|
||||
parts: [
|
||||
{ inlineData: { mimeType: file.type, data: base64Data } },
|
||||
{ text: prompt }
|
||||
]
|
||||
},
|
||||
config: {
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
assets: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
item_name: { type: Type.STRING },
|
||||
background_color: { type: Type.STRING },
|
||||
description: { type: Type.STRING },
|
||||
},
|
||||
required: ['item_name', 'background_color', 'description'],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ['assets']
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const text = response.text;
|
||||
if (!text) throw new Error("No response from analysis model");
|
||||
|
||||
let cleanText = text.trim();
|
||||
if (cleanText.startsWith('```')) {
|
||||
cleanText = cleanText.replace(/^```(json)?/, '').replace(/```$/, '').trim();
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(cleanText);
|
||||
if (!parsed.assets) parsed.assets = [];
|
||||
|
||||
return parsed as AnalysisResponse;
|
||||
};
|
||||
|
||||
export const generateTextureSheet = async (originalFile: File, assets: Asset[]): Promise<string> => {
|
||||
// Use Gemini 2.5 Flash Image ("Banana") for generation as requested
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
||||
const base64Data = await fileToGenericBase64(originalFile);
|
||||
|
||||
// Construct the mapping string dynamically
|
||||
const mappingString = assets.map(asset => {
|
||||
const colorName = asset.background_color;
|
||||
// Fallback to black if hex not found
|
||||
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
|
||||
|
||||
Instructions:
|
||||
Analyze the character in the provided image and extract the following ${assets.length} specific assets into a new, single image organized in a grid layout (e.g., 2x3, 3x3 depending on count).
|
||||
|
||||
Core Constraints:
|
||||
1. Isolated Assets: Each item must be placed within its own distinct square tile. No overlapping.
|
||||
2. Ghost Mannequin Style: Extract ONLY the items. Remove the character's body entirely. Clothing and armor must appear as empty, 3D shells as if worn by an invisible person.
|
||||
3. Solid Chroma Key Backgrounds: Each tile must use the specific solid background color assigned below. Ensure zero shadows, highlights, or gradients on the background to facilitate clean digital segmentation.
|
||||
4. Output Format: Generate a square texture sheet. Ignore the original image aspect ratio for the final output canvas.
|
||||
|
||||
Item & Background Mapping:
|
||||
${mappingString}
|
||||
|
||||
Output Requirement: High-resolution texture sheet, flat lay presentation, sharp edges, and uniform lighting across all assets.
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-2.5-flash-image',
|
||||
contents: {
|
||||
parts: [
|
||||
{ inlineData: { mimeType: originalFile.type, data: base64Data } },
|
||||
{ text: prompt }
|
||||
]
|
||||
},
|
||||
config: {
|
||||
imageConfig: {
|
||||
aspectRatio: '1:1',
|
||||
// imageSize not supported for gemini-2.5-flash-image
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Extract image
|
||||
let imageUrl = '';
|
||||
if (response.candidates && response.candidates[0].content.parts) {
|
||||
for (const part of response.candidates[0].content.parts) {
|
||||
if (part.inlineData) {
|
||||
imageUrl = `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!imageUrl) {
|
||||
throw new Error("No image generated.");
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
};
|
||||
|
||||
export const detectAssetBoundingBoxes = async (textureSheetBase64: string): Promise<BoundingBoxResponse> => {
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
||||
|
||||
const prompt = `
|
||||
Task: Spatial Asset Detection & Normalized Grid Mapping
|
||||
|
||||
Instructions:
|
||||
1. Analyze the provided image consisting of character assets arranged in a grid.
|
||||
2. Identify the bounding box for each distinct asset container (the colored square background tiles).
|
||||
3. Return the coordinates for each box using a normalized scale of 0.0 to 1.0.
|
||||
- [ymin, xmin, ymax, xmax]
|
||||
- (0, 0) is the top-left corner; (1.0, 1.0) is the bottom-right corner.
|
||||
4. Ensure the box coordinates strictly encompass the outer boundary of each colored tile.
|
||||
|
||||
Output Format (Strict JSON):
|
||||
Return ONLY a JSON object. Provide the grid dimensions and a list of detected assets with their coordinates.
|
||||
`;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-3-pro-preview',
|
||||
contents: {
|
||||
parts: [
|
||||
{ inlineData: { mimeType: 'image/png', data: textureSheetBase64 } },
|
||||
{ text: prompt }
|
||||
]
|
||||
},
|
||||
config: {
|
||||
responseMimeType: "application/json",
|
||||
responseSchema: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
grid_dimensions: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
rows: { type: Type.INTEGER },
|
||||
cols: { type: Type.INTEGER }
|
||||
}
|
||||
},
|
||||
assets: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
item_name: { type: Type.STRING },
|
||||
coordinates: {
|
||||
type: Type.ARRAY,
|
||||
items: { type: Type.NUMBER },
|
||||
description: "[ymin, xmin, ymax, xmax]"
|
||||
},
|
||||
bg_color_detected: { type: Type.STRING }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
required: ['assets', 'grid_dimensions']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const text = response.text;
|
||||
if (!text) throw new Error("No bounding box response from model");
|
||||
|
||||
let cleanText = text.trim();
|
||||
if (cleanText.startsWith('```')) {
|
||||
cleanText = cleanText.replace(/^```(json)?/, '').replace(/```$/, '').trim();
|
||||
}
|
||||
|
||||
let parsed: any;
|
||||
try {
|
||||
parsed = JSON.parse(cleanText);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse JSON:", cleanText);
|
||||
throw new Error("Invalid JSON response from detection model");
|
||||
}
|
||||
|
||||
// Ensure assets array exists
|
||||
if (!parsed.assets || !Array.isArray(parsed.assets)) {
|
||||
parsed.assets = [];
|
||||
}
|
||||
|
||||
return parsed as BoundingBoxResponse;
|
||||
};
|
||||
|
||||
export const cropAssetsFromSheet = async (sheetDataUrl: string, boxes: DetectedAssetBox[]): Promise<Omit<CroppedAsset, 'sourceIndex'>[]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
img.onload = () => {
|
||||
try {
|
||||
const cropped: Omit<CroppedAsset, 'sourceIndex'>[] = [];
|
||||
|
||||
if (boxes && Array.isArray(boxes)) {
|
||||
boxes.forEach(box => {
|
||||
if (!box.coordinates || box.coordinates.length !== 4) return;
|
||||
|
||||
const [ymin, xmin, ymax, xmax] = box.coordinates;
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) return;
|
||||
|
||||
// Convert normalized coords to pixels
|
||||
const srcX = xmin * img.width;
|
||||
const srcY = ymin * img.height;
|
||||
const srcW = (xmax - xmin) * img.width;
|
||||
const srcH = (ymax - ymin) * img.height;
|
||||
|
||||
canvas.width = srcW;
|
||||
canvas.height = srcH;
|
||||
|
||||
ctx.drawImage(img, srcX, srcY, srcW, srcH, 0, 0, srcW, srcH);
|
||||
|
||||
cropped.push({
|
||||
item_name: box.item_name || "Unknown Asset",
|
||||
imageUrl: canvas.toDataURL('image/png')
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
resolve(cropped);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
img.onerror = (e) => reject(e);
|
||||
img.src = sheetDataUrl;
|
||||
});
|
||||
};
|
||||
|
||||
export const refineAssetImage = async (base64Data: string): Promise<string> => {
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
||||
|
||||
const prompt = `
|
||||
Task: Asset Refinement
|
||||
|
||||
Instructions:
|
||||
1. Process the provided asset image.
|
||||
2. Remove the existing colored background and replace it with a pure white background (#FFFFFF).
|
||||
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.
|
||||
`;
|
||||
|
||||
// Remove header if present
|
||||
const cleanBase64 = base64Data.includes(',') ? base64Data.split(',')[1] : base64Data;
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-2.5-flash-image',
|
||||
contents: {
|
||||
parts: [
|
||||
{ inlineData: { mimeType: 'image/png', data: cleanBase64 } },
|
||||
{ text: prompt }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Extract image
|
||||
let imageUrl = '';
|
||||
if (response.candidates && response.candidates[0].content.parts) {
|
||||
for (const part of response.candidates[0].content.parts) {
|
||||
if (part.inlineData) {
|
||||
imageUrl = `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!imageUrl) {
|
||||
throw new Error("No refined image generated.");
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
};
|
||||
|
||||
export const generateModifiedCharacter = async (
|
||||
characterFile: File,
|
||||
assets: StashItem[],
|
||||
model: string = 'gemini-3-pro-image-preview'
|
||||
): Promise<string> => {
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
|
||||
|
||||
// Base Character
|
||||
const charBase64 = await fileToGenericBase64(characterFile);
|
||||
|
||||
const parts: any[] = [];
|
||||
|
||||
parts.push({
|
||||
inlineData: { mimeType: characterFile.type, data: charBase64 }
|
||||
});
|
||||
parts.push({ text: "Base Character Reference" });
|
||||
|
||||
// Assets
|
||||
for (const asset of assets) {
|
||||
// imageUrl is likely a data URL "data:image/png;base64,....."
|
||||
const matches = asset.imageUrl.match(/^data:(.+);base64,(.+)$/);
|
||||
if (matches && matches.length === 3) {
|
||||
const mimeType = matches[1];
|
||||
const data = matches[2];
|
||||
parts.push({
|
||||
inlineData: { mimeType, data }
|
||||
});
|
||||
parts.push({ text: `Equipment to equip: ${asset.item_name}` });
|
||||
}
|
||||
}
|
||||
|
||||
const prompt = `
|
||||
Task: Character Synthesis and Equipment Modification.
|
||||
|
||||
1. Reference the 'Base Character' for pose, body type, facial features, and general art style.
|
||||
2. Reference the 'Equipment to equip' images.
|
||||
3. Generate a new image of the Base Character wearing the provided items.
|
||||
4. The items should replace existing clothing/armor in that slot (e.g. new helmet replaces old hat, armor replaces shirt).
|
||||
5. Seamlessly blend the items into the character's style.
|
||||
6. Maintain the exact pose and angle of the Base Character.
|
||||
7. High quality, detailed, game art style.
|
||||
`;
|
||||
|
||||
parts.push({ text: prompt });
|
||||
|
||||
const imageConfig: any = {
|
||||
aspectRatio: '1:1',
|
||||
};
|
||||
|
||||
if (model === 'gemini-3-pro-image-preview') {
|
||||
imageConfig.imageSize = '2K';
|
||||
}
|
||||
|
||||
const response = await ai.models.generateContent({
|
||||
model: model,
|
||||
contents: { parts },
|
||||
config: {
|
||||
imageConfig: imageConfig
|
||||
}
|
||||
});
|
||||
|
||||
// Extract image
|
||||
let imageUrl = '';
|
||||
if (response.candidates && response.candidates[0].content.parts) {
|
||||
for (const part of response.candidates[0].content.parts) {
|
||||
if (part.inlineData) {
|
||||
imageUrl = `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!imageUrl) {
|
||||
throw new Error("No image generated.");
|
||||
}
|
||||
|
||||
return imageUrl;
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
export interface Asset {
|
||||
item_name: string;
|
||||
background_color: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface AnalysisResponse {
|
||||
assets: Asset[];
|
||||
}
|
||||
|
||||
export type ExtractionStatus = 'idle' | 'analyzing' | 'reviewing' | 'generating' | 'sheet_ready' | 'splitting' | 'refining' | 'complete' | 'error' | 'modifying';
|
||||
|
||||
export interface ColorMap {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export const HEX_COLORS: ColorMap = {
|
||||
Red: '#FF0000',
|
||||
Yellow: '#FFFF00',
|
||||
Green: '#00FF00',
|
||||
White: '#FFFFFF',
|
||||
Blue: '#0000FF',
|
||||
Black: '#000000',
|
||||
Magenta: '#FF00FF',
|
||||
Cyan: '#00FFFF',
|
||||
Orange: '#FFA500',
|
||||
Gray: '#808080',
|
||||
};
|
||||
|
||||
export interface DetectedAssetBox {
|
||||
item_name: string;
|
||||
coordinates: [number, number, number, number]; // ymin, xmin, ymax, xmax
|
||||
bg_color_detected: string;
|
||||
}
|
||||
|
||||
export interface BoundingBoxResponse {
|
||||
grid_dimensions: { rows: number; cols: number };
|
||||
assets: DetectedAssetBox[];
|
||||
}
|
||||
|
||||
export interface CroppedAsset {
|
||||
item_name: string;
|
||||
imageUrl: string;
|
||||
refinedImageUrl?: string;
|
||||
sourceIndex: number;
|
||||
}
|
||||
|
||||
export interface StashItem {
|
||||
id: string;
|
||||
item_name: string;
|
||||
imageUrl: string;
|
||||
createdAt: number;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import path from 'path';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
return {
|
||||
server: {
|
||||
port: 3000,
|
||||
host: '0.0.0.0',
|
||||
},
|
||||
plugins: [react()],
|
||||
define: {
|
||||
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
Reference in new issue
Block a user