diff --git a/README.md b/README.md index 34731a9..6d20d34 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ -# Cardex - Pokémon Card Collection Manager +# Cardex - Multi-TCG Card Collection Manager -A modern web application for managing your Pokémon card collection with AI-powered card scanning and identification. +A modern web application for managing your Trading Card Game (TCG) collection with AI-powered card generation and identification. Now supporting **Pokémon, One Piece, Disney Lorcana, Magic: The Gathering, and Dragon Ball Super**! ## Features +- 🎮 **Multi-Game Support**: Generate and collect cards from 5 popular TCG games - 🤖 **AI Card Scanning**: Use Gemini Vision to automatically identify Pokémon cards from photos -- ✨ **AI Card Generation**: Create custom Pokémon cards using Google Imagen4 +- ✨ **AI Card Generation**: Create custom cards for any supported TCG using Google Imagen4 - 🎬 **AI Video Generation**: Bring your cards to life with animated videos using Google Veo 2.0 - 📸 **Photo-to-Card**: Transform your own photos into Pokemon cards using AI image generation - 🔐 **Secure Authentication**: Firebase Authentication with email and Google login @@ -14,6 +15,18 @@ A modern web application for managing your Pokémon card collection with AI-powe - ✏️ **CRUD Operations**: Create, read, update, and delete cards in your collection - 🎯 **Type Safety**: Full TypeScript implementation for better development experience +## Supported Trading Card Games + +| Game | Types/Colors | Unique Stats | +|------|-------------|--------------| +| 🎮 **Pokémon TCG** | 18 types (Fire, Water, etc.) | HP, Attacks, Weakness, Resistance | +| 🏴‍☠️ **One Piece Card Game** | 6 colors (Red, Blue, etc.) | Power, Cost, Counter, Life Points | +| ✨ **Disney Lorcana** | 6 ink colors (Amber, Ruby, etc.) | Ink Cost, Strength, Willpower, Lore | +| 🔮 **Magic: The Gathering** | 5 colors + Colorless | Mana Cost, Power/Toughness, Card Type | +| 🐉 **Dragon Ball Super** | 4 colors (Red, Blue, etc.) | Combat Power, Combo Cost, Era | + +For detailed information about each game's attributes, see [TCG Attributes Reference](docs/TCG_ATTRIBUTES_REFERENCE.md). + ## Technology Stack ```mermaid @@ -135,14 +148,17 @@ npm run genkit:dev ### Card Generation 1. Navigate to the generate page -2. Fill out the card parameters: - - Pokemon name and type - - Card properties (Full Art, Holographic) - - Descriptions for the Pokemon and background - - Language and optional stats -3. Click "Generate Pokemon Card" to create a custom card using AI -4. Preview the generated card -5. Save it to your collection if you like it +2. **Select your TCG game** (Pokémon, One Piece, Lorcana, Magic, or Dragon Ball) +3. Fill out the card parameters: + - Character name and type/color + - Card properties (Special/Full Art, Holographic) + - Descriptions for the character and background + - Language and game-specific stats +4. Click "Generate Card" to create a custom card using AI +5. Preview the generated card +6. Save it to your collection if you like it + +**Note:** Each game has unique stat fields that appear dynamically based on your selection! ### Photo-to-Card Generation 1. Navigate to the "Photo Card" page @@ -182,10 +198,12 @@ Firestore Collection: users/{userId}/pokemon_cards/{cardId} ├── name: string ├── set: string ├── rarity: string +├── game: TCGGame ('pokemon' | 'onepiece' | 'lorcana' | 'magic' | 'dragonball') ├── imageUrl: string (Firebase Storage URL) ├── videoUrl?: string (Firebase Storage URL for animated video) ├── videoGenerationStatus?: 'generating' | 'completed' | 'failed' ├── videoPrompt?: string (AI prompt used for video generation) +├── generationParams?: CardGenerationParams (game-specific stats and settings) ├── userId: string ├── createdAt: timestamp └── updatedAt: timestamp diff --git a/package-lock.json b/package-lock.json index ab8ecd5..a8855c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,6 @@ "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-menubar": "^1.1.6", "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-radio-group": "^1.2.3", @@ -36,7 +35,6 @@ "@tanstack/react-query": "^5.66.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "date-fns": "^3.6.0", "dotenv": "^16.5.0", "firebase": "^11.10.0", "genkit": "^1.24.0", @@ -45,10 +43,8 @@ "openai": "^5.10.1", "patch-package": "^8.0.0", "react": "^18.3.1", - "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", - "recharts": "^2.15.1", "tailwind-merge": "^3.0.1", "tailwindcss-animate": "^1.0.7", "zod": "^3.24.2" @@ -88,15 +84,6 @@ "zod": "^3.20.2" } }, - "node_modules/@babel/runtime": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.4.tgz", - "integrity": "sha512-t3yaEOuGu9NlIZ+hIeGbBjFtZT7j2cb2tg0fuaJKeGotchRjjLfrBA9Kwf8quhpP1EUuxModQg04q/mBwyg8uA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@colors/colors": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", @@ -4766,37 +4753,6 @@ } } }, - "node_modules/@radix-ui/react-menubar": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.6.tgz", - "integrity": "sha512-FHq7+3DlXwh/7FOM4i0G4bC4vPjiq89VEEvNF4VMLchGnaUuUbE5uKXMUCjdKaOghEEMeiKa5XCa2Pk4kteWmg==", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-menu": "2.1.6", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-roving-focus": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-popover": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.6.tgz", @@ -5536,60 +5492,6 @@ "@types/node": "*" } }, - "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" - }, "node_modules/@types/express": { "version": "4.17.25", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", @@ -6863,117 +6765,8 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "engines": { - "node": ">=12" - } + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", @@ -6983,15 +6776,6 @@ "node": ">= 12" } }, - "node_modules/date-fns": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", - "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/kossnocorp" - } - }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -7001,11 +6785,6 @@ "ms": "2.0.0" } }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" - }, "node_modules/deeks": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/deeks/-/deeks-3.1.0.tgz", @@ -7109,15 +6888,6 @@ "node": ">=16" } }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -7388,11 +7158,6 @@ "node": ">=6" } }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" - }, "node_modules/eventid": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/eventid/-/eventid-2.0.1.tgz", @@ -7581,14 +7346,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-equals": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", - "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -8806,14 +8563,6 @@ "node": ">=8" } }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "engines": { - "node": ">=12" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -9320,6 +9069,7 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, "license": "MIT" }, "node_modules/lodash.camelcase": { @@ -10468,21 +10218,6 @@ "node": ">= 0.6.0" } }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, "node_modules/proto3-json-serializer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", @@ -10646,19 +10381,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-day-picker": { - "version": "8.10.1", - "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz", - "integrity": "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==", - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/gpbl" - }, - "peerDependencies": { - "date-fns": "^2.28.0 || ^3.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -10686,11 +10408,6 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" - }, "node_modules/react-remove-scroll": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", @@ -10736,20 +10453,6 @@ } } }, - "node_modules/react-smooth": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", - "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", - "dependencies": { - "fast-equals": "^5.0.1", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -10771,21 +10474,6 @@ } } }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -10820,36 +10508,6 @@ "node": ">=8.10.0" } }, - "node_modules/recharts": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz", - "integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==", - "dependencies": { - "clsx": "^2.0.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.21", - "react-is": "^18.3.1", - "react-smooth": "^4.0.4", - "recharts-scale": "^0.4.4", - "tiny-invariant": "^1.3.1", - "victory-vendor": "^36.6.8" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/recharts-scale": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", - "dependencies": { - "decimal.js-light": "^2.4.1" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -11947,11 +11605,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" - }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -12225,27 +11878,6 @@ "node": ">= 0.8" } }, - "node_modules/victory-vendor": { - "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", - "dependencies": { - "@types/d3-array": "^3.0.3", - "@types/d3-ease": "^3.0.0", - "@types/d3-interpolate": "^3.0.1", - "@types/d3-scale": "^4.0.2", - "@types/d3-shape": "^3.1.0", - "@types/d3-time": "^3.0.0", - "@types/d3-timer": "^3.0.0", - "d3-array": "^3.1.6", - "d3-ease": "^3.0.1", - "d3-interpolate": "^3.0.1", - "d3-scale": "^4.0.2", - "d3-shape": "^3.1.0", - "d3-time": "^3.0.0", - "d3-timer": "^3.0.1" - } - }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/package.json b/package.json index ac23b44..4b5667c 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-menubar": "^1.1.6", "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-radio-group": "^1.2.3", @@ -43,7 +42,6 @@ "@tanstack/react-query": "^5.66.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "date-fns": "^3.6.0", "dotenv": "^16.5.0", "firebase": "^11.10.0", "genkit": "^1.24.0", @@ -52,10 +50,8 @@ "openai": "^5.10.1", "patch-package": "^8.0.0", "react": "^18.3.1", - "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", - "recharts": "^2.15.1", "tailwind-merge": "^3.0.1", "tailwindcss-animate": "^1.0.7", "zod": "^3.24.2" diff --git a/src/ai/dev.ts b/src/ai/dev.ts index 439be0c..b60f531 100644 --- a/src/ai/dev.ts +++ b/src/ai/dev.ts @@ -2,5 +2,3 @@ import { config } from 'dotenv'; config(); import '@/ai/flows/summarize-card-information.ts'; -import '@/ai/flows/scan-pokemon-card.ts'; -import '@/ai/flows/generate-pokemon-card.ts'; diff --git a/src/ai/flows/generate-card-video.ts b/src/ai/flows/generate-card-video.ts index 7e48438..b559245 100644 --- a/src/ai/flows/generate-card-video.ts +++ b/src/ai/flows/generate-card-video.ts @@ -80,7 +80,7 @@ export async function generateCardVideo(input: GenerateCardVideoInput): Promise< // Start video generation operation console.log('Starting Veo video generation...'); let operation = await ai.models.generateVideos({ - model: 'veo-3.0-generate-preview', + model: 'veo-3.1-generate-preview', prompt: animationPrompt, image: { mimeType: mimeType, diff --git a/src/ai/flows/generate-pokemon-card-from-photo.ts b/src/ai/flows/generate-pokemon-card-from-photo.ts deleted file mode 100644 index 27bcc04..0000000 --- a/src/ai/flows/generate-pokemon-card-from-photo.ts +++ /dev/null @@ -1,139 +0,0 @@ -// This file is automatically generated - edits will be lost! -'use server'; - -/** - * @fileOverview Uses Google Imagen4 to generate Pokemon cards based on an uploaded photo reference. - * - * - generatePokemonCardFromPhoto - A function that handles the photo-based card generation process. - * - GenerateFromPhotoInput - The input type for the generatePokemonCardFromPhoto function. - * - GenerateFromPhotoOutput - The return type for the generatePokemonCardFromPhoto function. - */ - -import { z } from 'genkit'; -import OpenAI from 'openai'; -import { getUserApiKeys } from '@/lib/firestore'; - -const GenerateFromPhotoInputSchema = z.object({ - userId: z.string().min(1, "User ID is required"), - photoDataUri: z - .string() - .describe( - "A reference photo as a data URI that must include a MIME type and use Base64 encoding. Expected format: 'data:;base64,'." - ), - pokemonName: z.string().min(1, "Pokemon name is required"), - pokemonType: z.string().min(1, "Pokemon type is required"), - styleDescription: z.string().min(10, "Style description is required - describe how to adapt the photo"), - language: z.enum(['english', 'japanese', 'chinese', 'korean', 'spanish', 'french', 'german', 'italian']).default('english'), - hp: z.number().min(10).max(999).optional().default(130), - attackName1: z.string().optional().default("Quick Attack"), - attackDamage1: z.number().min(0).max(999).optional().default(60), - attackName2: z.string().optional().default("Special Move"), - attackDamage2: z.number().min(0).max(999).optional().default(90), - weakness: z.string().optional().default("Fighting"), - resistance: z.string().optional().default("Psychic"), - retreatCost: z.number().min(0).max(5).optional().default(2), -}); - -export type GenerateFromPhotoInput = z.infer; - -const GenerateFromPhotoOutputSchema = z.object({ - imageBase64: z.string().describe("The generated card image as base64 string").optional(), - prompt: z.string().describe("The prompt used to generate the card").optional(), - error: z.string().describe("Error message if the card could not be generated").optional() -}); - -export type GenerateFromPhotoOutput = z.infer; - -export async function generatePokemonCardFromPhoto(input: GenerateFromPhotoInput): Promise { - try { - // Get user's API keys from Firestore - const userApiKeys = await getUserApiKeys(input.userId); - const apiKey = userApiKeys?.openaiApiKey || process.env.OPENAI_API_KEY; - - if (!apiKey) { - return { - error: 'OpenAI API key is required. Please configure your API key in Settings or set OPENAI_API_KEY environment variable.' - }; - } - - const openai = new OpenAI({ - apiKey: apiKey, - }); - - // Generate the detailed prompt for the Pokemon card based on photo - console.log("Generating Pokemon card using reference photo..."); - - // Generate the enhanced prompt for the Pokemon card - const enhancedPrompt = generatePhotoBasedCardPrompt(input); - console.log("Generated prompt for image generation:", enhancedPrompt); - - // Use OpenAI responses.create with reference image for image generation - const response = await openai.responses.create({ - model: "gpt-4.1", - input: [ - { - role: "user", - content: [ - { type: "input_text", text: enhancedPrompt }, - { - type: "input_image", - image_url: input.photoDataUri, - detail: "high" - }, - ], - }, - ], - tools: [{ type: "image_generation" }], - }); - - const imageData = response.output - .filter((output) => output.type === "image_generation_call") - .map((output) => output.result); - - if (imageData.length > 0) { - const imageBase64 = imageData[0]; - return { - imageBase64: imageBase64 || "", - prompt: enhancedPrompt, - }; - } else { - console.log("No image generated, response output:", response.output); - return { error: 'No image generated from the reference photo.' }; - } - } catch (error: any) { - console.error('Error generating Pokemon card from photo:', error); - return { error: `Failed to generate Pokemon card: ${error?.message || 'Unknown error'}` }; - } -} - -function generatePhotoBasedCardPrompt(params: GenerateFromPhotoInput): string { - const { - pokemonName, - pokemonType, - styleDescription, - language, - hp = 130, - attackName1 = "Quick Attack", - attackDamage1 = 60, - attackName2 = "Special Move", - attackDamage2 = 90, - weakness = "Fighting", - resistance = "Psychic", - retreatCost = 2 - } = params; - - const languageInstruction = language !== 'english' ? `Write all card information in ${language}. The Pokemon name, attacks and description should be translated accordingly.` : ''; - - return `A regular classic Pokémon trading card (not full art), featuring the Pokémon "${pokemonName}". Use the reference image as inspiration and adapt it to create a Pokemon card illustration. Use the base photo, but in Studio Ghibli style, ${styleDescription} - -The card has a standard vertical layout with a detailed illustration in the main area. The Pokemon "${pokemonName}", a ${pokemonType} type, should be depicted as the main subject, incorporating elements and style from the reference photo. - -The card layout includes: The top left corner displays "${pokemonName}" in a stylized font, with "HP ${hp}" next to it in red. Below the Pokémon's name, the ${pokemonType} type symbol is clearly visible. In the lower section, the card has two attacks listed. The first attack is ${attackName1} and should be written in ${language}, deals ${attackDamage1} damage. The second attack is ${attackName2} and should be written in ${language}, deals ${attackDamage2} damage. - -Below the attacks, the Weakness is ${weakness} (x2), Resistance is ${resistance} (-30), and Retreat Cost shows ${retreatCost} energy symbols. The bottom edge of the card features a thin line of text indicating the rarity and copyright information. - -${languageInstruction} - -The overall style should match official Pokémon TCG card design with proper fonts, layout, and professional quality artwork, while incorporating the visual style and elements from the reference photo. -Make sure the information is displayed clearly and the Pokemon illustration feels natural within the adapted photographic style.`; -} diff --git a/src/ai/flows/generate-pokemon-card.ts b/src/ai/flows/generate-pokemon-card.ts deleted file mode 100644 index 8491288..0000000 --- a/src/ai/flows/generate-pokemon-card.ts +++ /dev/null @@ -1,183 +0,0 @@ -// This file is automatically generated - edits will be lost! -'use server'; - -/** - * @fileOverview Uses Google Imagen4 to generate custom Pokemon cards based on user parameters. - * - * - generatePokemonCard - A function that handles the card generation process. - * - GeneratePokemonCardInput - The input type for the generatePokemonCard function. - * - GeneratePokemonCardOutput - The return type for the generatePokemonCard function. - */ - -import { z } from 'genkit'; -import { GoogleGenAI } from '@google/genai'; -import { getUserApiKeys } from '@/lib/firestore'; - -const GeneratePokemonCardInputSchema = z.object({ - userId: z.string().min(1, "User ID is required"), - pokemonName: z.string().min(1, "Pokemon name is required"), - pokemonType: z.string().min(1, "Pokemon type is required"), - isIllustrationRare: z.boolean(), - isHolo: z.boolean(), - backgroundDescription: z.string().min(1, "Background description is required"), - pokemonDescription: z.string().min(1, "Pokemon description is required"), - language: z.enum(['english', 'japanese', 'chinese', 'korean', 'spanish', 'french', 'german', 'italian']), - model: z.enum(['imagen-4.0-ultra-generate-001', 'imagen-4.0-generate-001', 'imagen-4.0-fast-generate-001', 'gemini-2.5-flash-image', 'gemini-3-pro-image-preview']).default('imagen-4.0-ultra-generate-001'), - hp: z.number().optional(), - attackName1: z.string().optional(), - attackDamage1: z.number().optional(), - attackName2: z.string().optional(), - attackDamage2: z.number().optional(), - weakness: z.string().optional(), - resistance: z.string().optional(), - retreatCost: z.number().optional(), -}); - -export type GeneratePokemonCardInput = z.infer; - -const GeneratePokemonCardOutputSchema = z.object({ - imageBase64: z.string().describe("The generated card image as base64 string").optional(), - prompt: z.string().describe("The prompt used to generate the card").optional(), - error: z.string().describe("Error message if the card could not be generated").optional() -}); - -export type GeneratePokemonCardOutput = z.infer; - -export async function generatePokemonCard(input: GeneratePokemonCardInput): Promise { - try { - // Get user's API keys from Firestore - const userApiKeys = await getUserApiKeys(input.userId); - const apiKey = userApiKeys?.geminiApiKey || process.env.GOOGLE_GENAI_API_KEY || process.env.GEMINI_API_KEY; - - if (!apiKey) { - return { - error: 'Gemini API key is required. Please configure your API key in Settings or set GOOGLE_GENAI_API_KEY environment variable.' - }; - } - - const genAI = new GoogleGenAI({ - apiKey: apiKey, - }); - - // Generate the detailed prompt for the Pokemon card - const prompt = generateCardPrompt(input); - console.log("Generated prompt:", prompt); - - // Check if using Gemini model (for generateContentStream) or Imagen model (for generateImages) - const isGeminiModel = input.model.startsWith('gemini-'); - - if (isGeminiModel) { - // Use generateContentStream for Gemini models - const config = { - responseModalities: ['IMAGE', 'TEXT'], - imageConfig: { - aspectRatio: '3:4', - imageSize: '1K', - }, - }; - const contents = [ - { - role: 'user' as const, - parts: [ - { - text: prompt, - }, - ], - }, - ]; - - const response = await genAI.models.generateContentStream({ - model: input.model, - config, - contents, - }); - - let imageBase64: string | undefined; - - for await (const chunk of response) { - if (!chunk.candidates || !chunk.candidates[0].content || !chunk.candidates[0].content.parts) { - continue; - } - if (chunk.candidates?.[0]?.content?.parts?.[0]?.inlineData) { - const inlineData = chunk.candidates[0].content.parts[0].inlineData; - imageBase64 = inlineData.data || ''; - break; // We only need the first image - } - } - - if (!imageBase64) { - return { error: 'No image generated from Gemini model.' }; - } - - return { - imageBase64, - prompt, - }; - } else { - // Use generateImages for Imagen models - const response = await genAI.models.generateImages({ - model: input.model, - prompt: prompt, - config: { - numberOfImages: 1, - outputMimeType: 'image/jpeg', - aspectRatio: '3:4' // Pokemon cards are roughly 3:4 aspect ratio - }, - }); - - if (!response?.generatedImages || response.generatedImages.length === 0) { - return { error: 'No images generated.' }; - } - - const generatedImage = response.generatedImages[0]; - if (!generatedImage?.image?.imageBytes) { - return { error: 'Generated image data is missing.' }; - } - - return { - imageBase64: generatedImage.image.imageBytes, - prompt: prompt, - }; - } - } catch (error: any) { - console.error('Error generating Pokemon card:', error); - return { error: `Failed to generate Pokemon card: ${error?.message || 'Unknown error'}` }; - } -} - -function generateCardPrompt(params: GeneratePokemonCardInput): string { - const { - pokemonName, - pokemonType, - isIllustrationRare, - isHolo, - backgroundDescription, - pokemonDescription, - language, - hp = 130, - attackName1 = "Quick Attack", - attackDamage1 = 60, - attackName2 = "Special Move", - attackDamage2 = 90, - weakness = "Fighting", - resistance = "Psychic", - retreatCost = 2 - } = params; - - const cardType = isIllustrationRare ? "full art (ART)" : "Regular classic and not full art"; - const holoEffect = isHolo ? "The card's surface has a \"holo\" effect, creating a shimmering, rainbow-like reflection that highlights the Pokémon and key elements." : ""; - - const languageInstruction = language !== 'english' ? `Write all card information in ${language}. The Pokemon name, attacks and description should be translated accordingly.` : ''; - - return `A ${cardType} collectible Pokémon trading card, featuring the Pokémon "${pokemonName}". ${isIllustrationRare ? "The illustration covers the entire card" : "The card has a standard vertical layout with a detailed illustration in the main area."} ${pokemonName}, a ${pokemonType} type, is depicted ${pokemonDescription}. The background of the illustration shows ${backgroundDescription}. ${holoEffect} - -The card layout includes: The top left corner displays "${pokemonName}" in a stylized font, with "HP ${hp}" next to it in red. Below the Pokémon's name, the ${pokemonType} type symbol is clearly visible. In the lower section, the card has two attacks listed. The first attack is ${attackName1} and should be written in ${language}, deals ${attackDamage1} damage. The second attack is ${attackName2} and should be written in ${language}, deals ${attackDamage2} damage. - -Below the attacks, the Weakness is ${weakness} (x2), Resistance is ${resistance} (-30), and Retreat Cost shows ${retreatCost} energy symbols. The bottom edge of the card features a thin line of text indicating the rarity and copyright information. - -${languageInstruction} - -The overall style should match official Pokémon TCG card design with proper fonts, layout, and professional quality artwork. -Make sure the information is displayed clearly. -The output should just be the card.`; -} diff --git a/src/ai/flows/generate-tcg-card-from-photo.ts b/src/ai/flows/generate-tcg-card-from-photo.ts new file mode 100644 index 0000000..7ec5ee5 --- /dev/null +++ b/src/ai/flows/generate-tcg-card-from-photo.ts @@ -0,0 +1,216 @@ +// This file is automatically generated - edits will be lost! +'use server'; + +/** + * @fileOverview Uses DALL-E to generate TCG cards based on an uploaded photo reference. + * + * - generateTCGCardFromPhoto - A function that handles the photo-based card generation process. + * - GenerateFromPhotoInput - The input type for the generateTCGCardFromPhoto function. + * - GenerateFromPhotoOutput - The return type for the generateTCGCardFromPhoto function. + */ + +import { z } from 'genkit'; +import OpenAI from 'openai'; +import { getUserApiKeys } from '@/lib/firestore'; +import { getGameConfig } from '@/config/tcg-games'; +import type { TCGGame } from '@/types'; + +const GenerateFromPhotoInputSchema = z.object({ + userId: z.string().min(1, "User ID is required"), + game: z.enum(['pokemon', 'onepiece', 'lorcana', 'magic', 'dragonball']), + photoDataUri: z + .string() + .describe( + "A reference photo as a data URI that must include a MIME type and use Base64 encoding. Expected format: 'data:;base64,'." + ), + characterName: z.string().min(1, "Character name is required"), + characterType: z.string().min(1, "Character type is required"), + styleDescription: z.string().min(10, "Style description is required - describe how to adapt the photo"), + language: z.enum(['english', 'japanese', 'chinese', 'korean', 'spanish', 'french', 'german', 'italian']).default('english'), + + // Game-specific stats (all optional) + hp: z.number().optional(), + attackName1: z.string().optional(), + attackDamage1: z.number().optional(), + attackName2: z.string().optional(), + attackDamage2: z.number().optional(), + weakness: z.string().optional(), + resistance: z.string().optional(), + retreatCost: z.number().optional(), + power: z.number().optional(), + cost: z.number().optional(), + counter: z.number().optional(), + lifePoints: z.number().optional(), + inkCost: z.number().optional(), + strength: z.number().optional(), + willpower: z.number().optional(), + lore: z.number().optional(), + inkable: z.boolean().optional(), + manaCost: z.string().optional(), + cardType: z.string().optional(), + subType: z.string().optional(), + powerToughness: z.string().optional(), + combatPower: z.number().optional(), + comboCost: z.number().optional(), + comboEnergy: z.number().optional(), + era: z.string().optional(), +}); + +export type GenerateFromPhotoInput = z.infer; + +const GenerateFromPhotoOutputSchema = z.object({ + imageBase64: z.string().describe("The generated card image as base64 string").optional(), + prompt: z.string().describe("The prompt used to generate the card").optional(), + error: z.string().describe("Error message if the card could not be generated").optional() +}); + +export type GenerateFromPhotoOutput = z.infer; + +export async function generateTCGCardFromPhoto(input: GenerateFromPhotoInput): Promise { + try { + // Get user's API keys from Firestore + const userApiKeys = await getUserApiKeys(input.userId); + const apiKey = userApiKeys?.openaiApiKey || process.env.OPENAI_API_KEY; + + if (!apiKey) { + return { + error: 'OpenAI API key is required. Please configure your API key in Settings or set OPENAI_API_KEY environment variable.' + }; + } + + const openai = new OpenAI({ + apiKey: apiKey, + }); + + const gameConfig = getGameConfig(input.game as TCGGame); + console.log(`Generating ${gameConfig.name} card using reference photo...`); + + // Generate the enhanced prompt for the card + const enhancedPrompt = generatePhotoBasedCardPrompt(input); + console.log("Generated prompt for image generation:", enhancedPrompt); + + // Use OpenAI responses.create with reference image for image generation + const response = await openai.responses.create({ + model: "gpt-5", + input: [ + { + role: "user", + content: [ + { type: "input_text", text: enhancedPrompt }, + { + type: "input_image", + image_url: input.photoDataUri, + detail: "high" + }, + ], + }, + ], + tools: [{ type: "image_generation" }], + }); + + const imageData = response.output + .filter((output) => output.type === "image_generation_call") + .map((output) => output.result); + + if (imageData.length > 0) { + const imageBase64 = imageData[0]; + return { + imageBase64: imageBase64 || "", + prompt: enhancedPrompt, + }; + } else { + console.log("No image generated, response output:", response.output); + return { error: 'No image generated from the reference photo.' }; + } + } catch (error: any) { + console.error('Error generating card from photo:', error); + return { error: `Failed to generate card: ${error?.message || 'Unknown error'}` }; + } +} + +function generatePhotoBasedCardPrompt(params: GenerateFromPhotoInput): string { + const gameConfig = getGameConfig(params.game as TCGGame); + const gameName = gameConfig.name; + + const { + game, + characterName, + characterType, + styleDescription, + language, + } = params; + + const languageInstruction = language !== 'english' ? `Write all card information in ${language}.` : ''; + + let gameSpecificPrompt = ''; + + switch (game) { + case 'pokemon': + gameSpecificPrompt = generatePokemonPhotoPrompt(params); + break; + case 'onepiece': + gameSpecificPrompt = generateOnePiecePhotoPrompt(params); + break; + case 'lorcana': + gameSpecificPrompt = generateLorcanaPhotoPrompt(params); + break; + case 'magic': + gameSpecificPrompt = generateMagicPhotoPrompt(params); + break; + case 'dragonball': + gameSpecificPrompt = generateDragonBallPhotoPrompt(params); + break; + } + + return `Create a professional ${gameName} trading card featuring "${characterName}". + +IMPORTANT: Use the reference photo as inspiration and adapt it to create a trading card illustration. Use the base photo, but in Studio Ghibli style, ${styleDescription} + +The card should be a ${characterType} type and follow the authentic ${gameName} card design with proper layout, fonts, and formatting. + +${gameSpecificPrompt} + +${languageInstruction} + +The final output must look like an authentic ${gameName} trading card with all text clearly legible and properly formatted. Make sure the information is displayed clearly and the character illustration feels natural within the adapted photographic style.`; +} + +function generatePokemonPhotoPrompt(params: GenerateFromPhotoInput): string { + const { characterName, characterType, hp = 130, attackName1 = "Quick Attack", attackDamage1 = 60, attackName2 = "Special Move", attackDamage2 = 90, weakness = "Fighting", resistance = "Psychic", retreatCost = 2, language = 'english' } = params; + + const languageNote = language !== 'english' ? `All attack names and descriptions should be written in ${language}.` : ''; + + return `A regular classic Pokémon trading card (not full art), featuring the Pokémon "${characterName}". The card has a standard vertical layout with a detailed illustration in the main area. The Pokémon "${characterName}", a ${characterType} type, should be depicted as the main subject. + +The card layout includes: The top left corner displays "${characterName}" in a stylized font, with "HP ${hp}" next to it in red. Below the Pokémon's name, the ${characterType} type symbol is clearly visible. In the lower section, the card has two attacks listed. The first attack is ${attackName1}, deals ${attackDamage1} damage. The second attack is ${attackName2}, deals ${attackDamage2} damage. + +Below the attacks, the Weakness is ${weakness} (x2), Resistance is ${resistance} (-30), and Retreat Cost shows ${retreatCost} energy symbols. The bottom edge of the card features a thin line of text indicating the rarity and copyright information. + +${languageNote} + +The overall style should match official Pokémon TCG card design with proper fonts, layout, and professional quality artwork.`; +} + +function generateOnePiecePhotoPrompt(params: GenerateFromPhotoInput): string { + const { characterName, characterType, power = 5000, cost = 4, counter = 1000 } = params; + + return `Card layout: One Piece Card Game style with "${characterName}" prominently displayed. ${characterType} color indicator. Cost: ${cost}, Power: ${power}, Counter: ${counter}. Include attribute icons and ability text box.`; +} + +function generateLorcanaPhotoPrompt(params: GenerateFromPhotoInput): string { + const { characterName, characterType, inkCost = 3, strength = 2, willpower = 3, lore = 2, inkable = true } = params; + + return `Card layout: Disney Lorcana style with "${characterName}" as title. ${characterType} ink color. Ink cost: ${inkCost}, Strength: ${strength}, Willpower: ${willpower}, Lore: ${lore}. ${inkable ? 'Include inkwell symbol.' : 'No inkwell symbol.'}`; +} + +function generateMagicPhotoPrompt(params: GenerateFromPhotoInput): string { + const { characterName, manaCost = '{2}{G}', cardType = 'Creature', subType = 'Warrior', powerToughness = '3/3' } = params; + + return `Card layout: Magic: The Gathering style. Card name: "${characterName}", Mana cost: ${manaCost} in top right. Type line: "${cardType} — ${subType}". ${cardType.toLowerCase().includes('creature') ? `Power/Toughness: ${powerToughness}` : ''}`; +} + +function generateDragonBallPhotoPrompt(params: GenerateFromPhotoInput): string { + const { characterName, characterType, combatPower = 20000, comboCost = 1, comboEnergy = 5000, era = 'Universe Survival Saga' } = params; + + return `Card layout: Dragon Ball Super Card Game style. Character: "${characterName}", ${characterType} color. Combat Power: ${combatPower}, Combo Cost: ${comboCost}, Combo Energy: ${comboEnergy}, Era: ${era}.`; +} diff --git a/src/ai/flows/generate-tcg-card.ts b/src/ai/flows/generate-tcg-card.ts new file mode 100644 index 0000000..807c691 --- /dev/null +++ b/src/ai/flows/generate-tcg-card.ts @@ -0,0 +1,319 @@ +// This file is automatically generated - edits will be lost! +'use server'; + +/** + * @fileOverview Uses Google Imagen4 to generate custom TCG cards based on user parameters. + * + * - generateTCGCard - A function that handles the card generation process. + * - GenerateTCGCardInput - The input type for the generateTCGCard function. + * - GenerateTCGCardOutput - The return type for the generateTCGCard function. + */ + +import { z } from 'genkit'; +import { GoogleGenAI } from '@google/genai'; +import { getUserApiKeys } from '@/lib/firestore'; +import type { TCGGame } from '@/types'; +import { getGameConfig } from '@/config/tcg-games'; + +const GenerateTCGCardInputSchema = z.object({ + userId: z.string().min(1, "User ID is required"), + game: z.enum(['pokemon', 'onepiece', 'lorcana', 'magic', 'dragonball']), + characterName: z.string().min(1, "Character name is required"), + characterType: z.string().min(1, "Character type is required"), + isSpecialArt: z.boolean(), + isHolo: z.boolean(), + backgroundDescription: z.string().min(1, "Background description is required"), + characterDescription: z.string().min(1, "Character description is required"), + language: z.enum(['english', 'japanese', 'chinese', 'korean', 'spanish', 'french', 'german', 'italian']), + model: z.enum(['imagen-4.0-ultra-generate-001', 'imagen-4.0-generate-001', 'imagen-4.0-fast-generate-001', 'gemini-2.5-flash-image', 'gemini-3-pro-image-preview']).default('imagen-4.0-ultra-generate-001'), + + // Pokemon-specific + hp: z.number().optional(), + attackName1: z.string().optional(), + attackDamage1: z.number().optional(), + attackName2: z.string().optional(), + attackDamage2: z.number().optional(), + weakness: z.string().optional(), + resistance: z.string().optional(), + retreatCost: z.number().optional(), + + // One Piece-specific + power: z.number().optional(), + cost: z.number().optional(), + counter: z.number().optional(), + color: z.string().optional(), + lifePoints: z.number().optional(), + + // Lorcana-specific + inkCost: z.number().optional(), + strength: z.number().optional(), + willpower: z.number().optional(), + lore: z.number().optional(), + inkable: z.boolean().optional(), + + // Magic-specific + manaCost: z.string().optional(), + cardType: z.string().optional(), + subType: z.string().optional(), + powerToughness: z.string().optional(), + + // Dragon Ball-specific + combatPower: z.number().optional(), + comboCost: z.number().optional(), + comboEnergy: z.number().optional(), + era: z.string().optional(), +}); + +export type GenerateTCGCardInput = z.infer; + +const GenerateTCGCardOutputSchema = z.object({ + imageBase64: z.string().describe("The generated card image as base64 string").optional(), + prompt: z.string().describe("The prompt used to generate the card").optional(), + error: z.string().describe("Error message if the card could not be generated").optional() +}); + +export type GenerateTCGCardOutput = z.infer; + +export async function generateTCGCard(input: GenerateTCGCardInput): Promise { + try { + // Get user's API keys from Firestore + const userApiKeys = await getUserApiKeys(input.userId); + const apiKey = userApiKeys?.geminiApiKey || process.env.GOOGLE_GENAI_API_KEY || process.env.GEMINI_API_KEY; + + if (!apiKey) { + return { + error: 'Gemini API key is required. Please configure your API key in Settings or set GOOGLE_GENAI_API_KEY environment variable.' + }; + } + + const genAI = new GoogleGenAI({ + apiKey: apiKey, + }); + + // Generate the detailed prompt for the card + const prompt = generateCardPrompt(input); + console.log("Generated prompt:", prompt); + + // Check if using Gemini model (for generateContentStream) or Imagen model (for generateImages) + const isGeminiModel = input.model.startsWith('gemini-'); + + if (isGeminiModel) { + // Use generateContentStream for Gemini models + const config = { + responseModalities: ['IMAGE', 'TEXT'], + imageConfig: { + aspectRatio: '3:4', + imageSize: '1K', + }, + }; + const contents = [ + { + role: 'user' as const, + parts: [ + { + text: prompt, + }, + ], + }, + ]; + + const response = await genAI.models.generateContentStream({ + model: input.model, + config, + contents, + }); + + let imageBase64: string | undefined; + + for await (const chunk of response) { + if (!chunk.candidates || !chunk.candidates[0].content || !chunk.candidates[0].content.parts) { + continue; + } + if (chunk.candidates?.[0]?.content?.parts?.[0]?.inlineData) { + const inlineData = chunk.candidates[0].content.parts[0].inlineData; + imageBase64 = inlineData.data || ''; + break; // We only need the first image + } + } + + if (!imageBase64) { + return { error: 'No image generated from Gemini model.' }; + } + + return { + imageBase64, + prompt, + }; + } else { + // Use generateImages for Imagen models + const response = await genAI.models.generateImages({ + model: input.model, + prompt: prompt, + config: { + numberOfImages: 1, + outputMimeType: 'image/jpeg', + aspectRatio: '3:4' // TCG cards are roughly 3:4 aspect ratio + }, + }); + + if (!response?.generatedImages || response.generatedImages.length === 0) { + return { error: 'No images generated.' }; + } + + const generatedImage = response.generatedImages[0]; + if (!generatedImage?.image?.imageBytes) { + return { error: 'Generated image data is missing.' }; + } + + return { + imageBase64: generatedImage.image.imageBytes, + prompt: prompt, + }; + } + } catch (error: any) { + console.error('Error generating TCG card:', error); + return { error: `Failed to generate card: ${error?.message || 'Unknown error'}` }; + } +} + +function generateCardPrompt(params: GenerateTCGCardInput): string { + const gameConfig = getGameConfig(params.game as TCGGame); + const gameName = gameConfig.name; + + const { + game, + characterName, + characterType, + isSpecialArt, + isHolo, + backgroundDescription, + characterDescription, + language, + } = params; + + const cardType = isSpecialArt ? "full art special variant" : "standard layout"; + const holoEffect = isHolo ? `The card's surface has a "holo" effect, creating a shimmering, rainbow-like reflection that highlights the character and key elements.` : ""; + const languageInstruction = language !== 'english' ? `Write all card information in ${language}. The character name, abilities, and text should be translated accordingly.` : ''; + + let gameSpecificPrompt = ''; + + switch (game) { + case 'pokemon': + gameSpecificPrompt = generatePokemonPrompt(params); + break; + case 'onepiece': + gameSpecificPrompt = generateOnePiecePrompt(params); + break; + case 'lorcana': + gameSpecificPrompt = generateLorcanaPrompt(params); + break; + case 'magic': + gameSpecificPrompt = generateMagicPrompt(params); + break; + case 'dragonball': + gameSpecificPrompt = generateDragonBallPrompt(params); + break; + } + + return `A ${cardType} collectible ${gameName} trading card, featuring "${characterName}". ${isSpecialArt ? "The illustration covers the entire card in a stunning full-art layout" : "The card has a standard vertical layout with a detailed illustration in the main area."} ${characterName}, a ${characterType} type, is depicted ${characterDescription}. The background of the illustration shows ${backgroundDescription}. ${holoEffect} + +${gameSpecificPrompt} + +${languageInstruction} + +The overall style should match official ${gameName} card design with proper fonts, layout, and professional quality artwork. +Make sure all information is displayed clearly and legibly. +The output should just be the card itself.`; +} + +function generatePokemonPrompt(params: GenerateTCGCardInput): string { + const { + characterName, + characterType, + hp = 130, + attackName1 = "Quick Attack", + attackDamage1 = 60, + attackName2 = "Special Move", + attackDamage2 = 90, + weakness = "Fighting", + resistance = "Psychic", + retreatCost = 2, + } = params; + + return `The card layout includes: The top left corner displays "${characterName}" in a stylized font, with "HP ${hp}" next to it in red. Below the Pokémon's name, the ${characterType} type symbol is clearly visible. In the lower section, the card has two attacks listed. The first attack is ${attackName1}, deals ${attackDamage1} damage. The second attack is ${attackName2}, deals ${attackDamage2} damage. + +Below the attacks, the Weakness is ${weakness} (x2), Resistance is ${resistance} (-30), and Retreat Cost shows ${retreatCost} energy symbols. The bottom edge of the card features a thin line of text indicating the rarity and copyright information.`; +} + +function generateOnePiecePrompt(params: GenerateTCGCardInput): string { + const { + characterName, + characterType, + power = 5000, + cost = 4, + counter = 1000, + lifePoints, + } = params; + + const leaderInfo = lifePoints ? `As a Leader card, it displays ${lifePoints} life points in the top corner.` : ''; + + return `The card layout follows One Piece Card Game design: The character name "${characterName}" appears prominently at the top. The card shows a ${characterType} color indicator. The cost of ${cost} is displayed in the top left corner. The power value of ${power} is shown prominently. The counter value of ${counter} appears at the bottom. ${leaderInfo} + +The card includes attribute icons and a text box for abilities. The bottom features the One Piece Card Game logo and card number.`; +} + +function generateLorcanaPrompt(params: GenerateTCGCardInput): string { + const { + characterName, + characterType, + inkCost = 3, + strength = 2, + willpower = 3, + lore = 2, + inkable = true, + } = params; + + const inkableIndicator = inkable ? 'The card has an inkwell symbol in the bottom left, indicating it is inkable.' : 'No inkwell symbol appears, as this card is not inkable.'; + + return `The card layout follows Disney Lorcana design: "${characterName}" appears as the character name at the top. The ${characterType} ink color is indicated by the card's color scheme and symbol. The ink cost of ${inkCost} is displayed in a prominent circle in the top left corner. + +The card shows strength of ${strength} (shield icon), willpower of ${willpower} (hexagon icon), and lore value of ${lore} (diamond icon) clearly displayed. ${inkableIndicator} + +The card includes a text box for character abilities and flavor text in the Disney Lorcana style. The bottom features the Lorcana logo, card number, and rarity symbol.`; +} + +function generateMagicPrompt(params: GenerateTCGCardInput): string { + const { + characterName, + characterType, + manaCost = '{2}{G}', + cardType = 'Creature', + subType = 'Elf Warrior', + powerToughness = '3/3', + } = params; + + return `The card layout follows Magic: The Gathering design: "${characterName}" appears as the card name at the top. The mana cost ${manaCost} is displayed in the top right corner using standard Magic mana symbols. The ${characterType} color frame is used. + +The type line reads "${cardType} — ${subType}". The main illustration takes up the center portion of the card. Below the illustration is a text box for abilities and flavor text. + +${cardType.toLowerCase().includes('creature') ? `The bottom right corner displays the power/toughness of ${powerToughness}.` : ''} + +The bottom features the expansion symbol, card number, artist credit, and copyright information in standard Magic: The Gathering layout.`; +} + +function generateDragonBallPrompt(params: GenerateTCGCardInput): string { + const { + characterName, + characterType, + combatPower = 20000, + comboCost = 1, + comboEnergy = 5000, + era = 'Universe Survival Saga', + } = params; + + return `The card layout follows Dragon Ball Super Card Game design: The character name "${characterName}" appears at the top in the distinctive Dragon Ball font. The card uses a ${characterType} color border and energy symbols. + +The combat power of ${combatPower} is prominently displayed in the top right corner. The combo cost shows ${comboCost} and combo energy displays ${comboEnergy}. The era "${era}" is indicated on the card. + +The card includes energy cost symbols, special traits, and a text box for card effects. The character illustration is dynamic and action-packed. The bottom features the Dragon Ball Super Card Game logo, card number, and rarity indicator.`; +} diff --git a/src/ai/flows/scan-pokemon-card.ts b/src/ai/flows/scan-pokemon-card.ts deleted file mode 100644 index b4a9c3b..0000000 --- a/src/ai/flows/scan-pokemon-card.ts +++ /dev/null @@ -1,96 +0,0 @@ -// This file is automatically generated - edits will be lost! -'use server'; - -/** - * @fileOverview Uses Gemini Vision to scan and identify a Pokemon card's name, set, and rarity. - * - * - scanPokemonCard - A function that handles the card scanning process. - * - ScanPokemonCardInput - The input type for the scanPokemonCard function. - * - ScanPokemonCardOutput - The return type for the scanPokemonCard function. - */ - -import {ai, createUserAI} from '@/ai/genkit'; -import {z} from 'genkit'; - -const ScanPokemonCardInputSchema = z.object({ - photoDataUri: z - .string() - .describe( - "A photo of a Pokemon card, as a data URI that must include a MIME type and use Base64 encoding. Expected format: 'data:;base64,'." - ), - userId: z.string().describe("The user ID for personalized API key usage.").optional(), -}); -export type ScanPokemonCardInput = z.infer; - -const ScanPokemonCardOutputSchema = z.object({ - cardDetails: z.object({ - name: z.string().describe("The name of the Pokemon card.").optional(), - set: z.string().describe("The first set the Pokemon card belongs to.").optional(), - rarity: z.string().describe("The rarity of the Pokemon card.").optional(), - }).describe("Details about the Pokemon card.").optional(), - error: z.string().describe("Error message if the card could not be identified.").optional() -}); - -export type ScanPokemonCardOutput = z.infer; - -export async function scanPokemonCard(input: ScanPokemonCardInput): Promise { - return scanPokemonCardFlow(input); -} - -async function createScanPromptAndFlow(userAI: any) { - const scanPokemonCardPrompt = userAI.definePrompt({ - name: 'scanPokemonCardPrompt', - input: {schema: ScanPokemonCardInputSchema}, - output: {schema: ScanPokemonCardOutputSchema}, - prompt: `You are an expert Pokemon card appraiser. Use the following image to identify the card's name, first set it was introduced in, and rarity. If you are unable to determine any of these values, leave them blank.\n - Photo: {{media url=photoDataUri}} - \n - Respond using the following format: - { - "cardDetails": { - "name": "Pokemon card name", - "set": "Pokemon first card set it was introduced in", - "rarity": "Pokemon card rarity" - } - } - \nIf you cannot identify the Pokemon card or if the image does not contain a Pokemon card, respond with the following format: - { - "error": "Error message describing why the card could not be identified" - }`, - }); - - const scanPokemonCardFlow = userAI.defineFlow( - { - name: 'scanPokemonCardFlow', - inputSchema: ScanPokemonCardInputSchema, - outputSchema: ScanPokemonCardOutputSchema, - }, - async (input: ScanPokemonCardInput) => { - try { - const {output} = await scanPokemonCardPrompt(input); - return output!; - } catch (error: any) { - console.error('Error during card scanning:', error); - return {error: 'Failed to scan Pokemon card.'}; - } - } - ); - - return scanPokemonCardFlow; -} - -async function scanPokemonCardFlow(input: ScanPokemonCardInput): Promise { - try { - // Get user-specific AI instance if userId provided - const userAI = input.userId ? await createUserAI(input.userId) : ai; - - // Create the flow with the appropriate AI instance - const flow = await createScanPromptAndFlow(userAI); - - // Execute the flow - return await flow(input); - } catch (error: any) { - console.error('Error in scan flow setup:', error); - return {error: 'Failed to scan Pokemon card.'}; - } -} diff --git a/src/ai/flows/scan-tcg-card.ts b/src/ai/flows/scan-tcg-card.ts new file mode 100644 index 0000000..7da9608 --- /dev/null +++ b/src/ai/flows/scan-tcg-card.ts @@ -0,0 +1,128 @@ +// This file is automatically generated - edits will be lost! +'use server'; + +/** + * @fileOverview Uses Gemini Vision to scan and identify a TCG card's name, set, rarity, and game. + * + * - scanTCGCard - A function that handles the card scanning process. + * - ScanTCGCardInput - The input type for the scanTCGCard function. + * - ScanTCGCardOutput - The return type for the scanTCGCard function. + */ + +import {ai, createUserAI} from '@/ai/genkit'; +import {z} from 'genkit'; +import type { TCGGame } from '@/types'; + +const ScanTCGCardInputSchema = z.object({ + photoDataUri: z + .string() + .describe( + "A photo of a trading card, as a data URI that must include a MIME type and use Base64 encoding. Expected format: 'data:;base64,'." + ), + userId: z.string().describe("The user ID for personalized API key usage.").optional(), + game: z.enum(['pokemon', 'onepiece', 'lorcana', 'magic', 'dragonball']).optional().describe("The TCG game to scan for. If not specified, will auto-detect."), +}); +export type ScanTCGCardInput = z.infer; + +const ScanTCGCardOutputSchema = z.object({ + cardDetails: z.object({ + name: z.string().describe("The name of the card.").optional(), + set: z.string().describe("The set the card belongs to.").optional(), + rarity: z.string().describe("The rarity of the card.").optional(), + game: z.enum(['pokemon', 'onepiece', 'lorcana', 'magic', 'dragonball']).describe("The TCG game this card is from.").optional(), + }).describe("Details about the trading card.").optional(), + error: z.string().describe("Error message if the card could not be identified.").optional() +}); + +export type ScanTCGCardOutput = z.infer; + +export async function scanTCGCard(input: ScanTCGCardInput): Promise { + return scanTCGCardFlow(input); +} + +async function createScanPromptAndFlow(userAI: any) { + const gameHint = (game?: string) => { + if (!game) return "First, identify which trading card game this is from (Pokémon, One Piece, Lorcana, Magic: The Gathering, or Dragon Ball)."; + const gameNames = { + pokemon: 'Pokémon TCG', + onepiece: 'One Piece Card Game', + lorcana: 'Disney Lorcana', + magic: 'Magic: The Gathering', + dragonball: 'Dragon Ball Super Card Game' + }; + return `This is a ${gameNames[game as keyof typeof gameNames]} card.`; + }; + + const scanTCGCardPrompt = userAI.definePrompt({ + name: 'scanTCGCardPrompt', + input: {schema: ScanTCGCardInputSchema}, + output: {schema: ScanTCGCardOutputSchema}, + prompt: `You are an expert trading card game appraiser with deep knowledge of Pokémon TCG, One Piece Card Game, Disney Lorcana, Magic: The Gathering, and Dragon Ball Super Card Game. + +{{#if game}} +${gameHint('{{game}}')} +{{else}} +${gameHint()} +{{/if}} + +Use the following image to identify: +1. The card's name +2. The set it belongs to +3. The rarity +4. The game (if not already specified) + +Photo: {{media url=photoDataUri}} + +Respond using the following format: +{ + "cardDetails": { + "name": "Card name", + "set": "Set name or expansion", + "rarity": "Card rarity", + "game": "pokemon" | "onepiece" | "lorcana" | "magic" | "dragonball" + } +} + +If you cannot identify the card or if the image does not contain a trading card, respond with: +{ + "error": "Error message describing why the card could not be identified" +}`, + }); + + const scanTCGCardFlow = userAI.defineFlow( + { + name: 'scanTCGCardFlow', + inputSchema: ScanTCGCardInputSchema, + outputSchema: ScanTCGCardOutputSchema, + }, + async (input: ScanTCGCardInput) => { + try { + const {output} = await scanTCGCardPrompt(input); + return output!; + } catch (error: any) { + console.error('Error during card scanning:', error); + return {error: 'Failed to scan trading card.'}; + } + } + ); + + return scanTCGCardFlow; +} + +const scanTCGCardFlow = ai.defineFlow( + { + name: 'scanTCGCardFlow', + inputSchema: ScanTCGCardInputSchema, + outputSchema: ScanTCGCardOutputSchema, + }, + async (input: ScanTCGCardInput) => { + try { + const userAI = await createUserAI(input.userId || ''); + const flow = await createScanPromptAndFlow(userAI); + return await flow(input); + } catch (error: any) { + console.error('Error during card scanning:', error); + return {error: 'Failed to scan trading card.'}; + } + } +); diff --git a/src/app/api/generate-card-from-photo/route.ts b/src/app/api/generate-card-from-photo/route.ts index 37dd680..434af5c 100644 --- a/src/app/api/generate-card-from-photo/route.ts +++ b/src/app/api/generate-card-from-photo/route.ts @@ -1,12 +1,12 @@ import { NextRequest, NextResponse } from 'next/server'; -import { generatePokemonCardFromPhoto } from '@/ai/flows/generate-pokemon-card-from-photo'; +import { generateTCGCardFromPhoto } from '@/ai/flows/generate-tcg-card-from-photo'; export async function POST(request: NextRequest) { try { const body = await request.json(); // Validate required fields - including userId - const requiredFields = ['userId', 'photoDataUri', 'pokemonName', 'pokemonType', 'styleDescription']; + const requiredFields = ['userId', 'photoDataUri', 'characterName', 'characterType', 'styleDescription', 'game']; const missingFields = requiredFields.filter(field => !body[field]); if (missingFields.length > 0) { @@ -17,7 +17,7 @@ export async function POST(request: NextRequest) { } // Call the AI flow - const result = await generatePokemonCardFromPhoto(body); + const result = await generateTCGCardFromPhoto(body); if (result.error) { return NextResponse.json({ error: result.error }, { status: 500 }); diff --git a/src/app/api/generate-card/route.ts b/src/app/api/generate-card/route.ts index 158e2c2..2766aa3 100644 --- a/src/app/api/generate-card/route.ts +++ b/src/app/api/generate-card/route.ts @@ -1,12 +1,12 @@ import { NextRequest, NextResponse } from 'next/server'; -import { generatePokemonCard } from '@/ai/flows/generate-pokemon-card'; +import { generateTCGCard } from '@/ai/flows/generate-tcg-card'; export async function POST(request: NextRequest) { try { const body = await request.json(); - // Validate required fields - including userId - const requiredFields = ['userId', 'pokemonName', 'pokemonType', 'backgroundDescription', 'pokemonDescription']; + // New multi-game flow + const requiredFields = ['userId', 'game', 'characterName', 'characterType', 'backgroundDescription', 'characterDescription']; const missingFields = requiredFields.filter(field => !body[field]); if (missingFields.length > 0) { @@ -16,14 +16,15 @@ export async function POST(request: NextRequest) { ); } - // Call the AI flow - const result = await generatePokemonCard(body); + // Call the new AI flow + const result = await generateTCGCard(body); if (result.error) { return NextResponse.json({ error: result.error }, { status: 500 }); } return NextResponse.json(result); + } catch (error) { console.error('API route error:', error); return NextResponse.json( @@ -32,3 +33,4 @@ export async function POST(request: NextRequest) { ); } } + diff --git a/src/app/dashboard/generate-from-photo/edit/page.tsx b/src/app/dashboard/generate-from-photo/edit/page.tsx index 09a866d..4b414f4 100644 --- a/src/app/dashboard/generate-from-photo/edit/page.tsx +++ b/src/app/dashboard/generate-from-photo/edit/page.tsx @@ -12,7 +12,9 @@ import { Loader2, Save } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { compressBase64Image, getBase64Size, formatBytes } from '@/utils/imageUtils'; import { parsePhotoGenerationParams } from '@/utils/generationParamsUtils'; +import { getGameConfig } from '@/config/tcg-games'; import type { GenerateFromPhotoInput } from '@/components/cards/PhotoCardGeneratorFormOnly'; +import type { TCGGame } from '@/types'; import Image from 'next/image'; export default function EditGenerateFromPhotoPage() { @@ -25,12 +27,18 @@ export default function EditGenerateFromPhotoPage() { const [isSaving, setIsSaving] = useState(false); const [initialValues, setInitialValues] = useState | undefined>(undefined); const [showComparison, setShowComparison] = useState(true); + const [currentGame, setCurrentGame] = useState('pokemon'); useEffect(() => { // Parse parameters from URL search params const params = parsePhotoGenerationParams(searchParams); setInitialValues(params); + // Set the game from params + if (params.game) { + setCurrentGame(params.game as TCGGame); + } + // Extract original card data const originalCardId = searchParams.get('originalCardId'); const originalCardName = searchParams.get('originalCardName'); @@ -83,13 +91,17 @@ export default function EditGenerateFromPhotoPage() { } } + const game = generatedCard.params?.game || currentGame; + const gameName = getGameConfig(game as TCGGame).name; + // Exclude photoDataUri from params when saving to Firestore (too large for document storage) const { photoDataUri, ...paramsWithoutPhoto } = generatedCard.params; await addCardToCollection(user.uid, { - name: 'Photo-Generated Pokemon Card', + name: `Photo-Generated ${gameName} Card`, set: 'AI Photo Generated', rarity: 'Photo Special', + game: game, imageDataUrl: finalImageDataUrl, isGenerated: true, isPhotoGenerated: true, @@ -99,7 +111,7 @@ export default function EditGenerateFromPhotoPage() { toast({ title: 'Card Saved', - description: 'Your photo-generated Pokemon card has been added to your collection!', + description: `Your photo-generated ${gameName} card has been added to your collection!`, }); // Clear the generated card after saving @@ -124,9 +136,9 @@ export default function EditGenerateFromPhotoPage() { return (
-

Edit Pokemon Card Generator from Photo

+

Edit {getGameConfig(currentGame).name} Card Generator from Photo

- Modify your photo-based Pokemon card parameters and regenerate the card + Modify your photo-based {getGameConfig(currentGame).name} card parameters and regenerate the card

diff --git a/src/app/dashboard/generate-from-photo/page.tsx b/src/app/dashboard/generate-from-photo/page.tsx index 03d17ff..aad1040 100644 --- a/src/app/dashboard/generate-from-photo/page.tsx +++ b/src/app/dashboard/generate-from-photo/page.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import { useAuth } from '@/hooks/useAuth'; import { useToast } from '@/hooks/use-toast'; -import { PhotoCardGenerator } from '@/components/cards/PhotoCardGenerator'; +import { PhotoCardGeneratorFormOnly as PhotoCardGenerator } from '@/components/cards/PhotoCardGeneratorFormOnly'; import { addCardToCollection } from '@/lib/firestore'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; @@ -11,6 +11,8 @@ import { Loader2, Save } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { compressBase64Image, getBase64Size, formatBytes } from '@/utils/imageUtils'; import ApiKeysWarning from '@/components/cards/ApiKeysWarning'; +import { getGameConfig } from '@/config/tcg-games'; +import type { TCGGame } from '@/types'; export default function GenerateFromPhotoPage() { const { user } = useAuth(); @@ -57,13 +59,17 @@ export default function GenerateFromPhotoPage() { } } + const game = generatedCard.params?.game || 'pokemon'; + const gameName = getGameConfig(game as TCGGame).name; + // Exclude photoDataUri from params when saving to Firestore (too large for document storage) const { photoDataUri, ...paramsWithoutPhoto } = generatedCard.params; await addCardToCollection(user.uid, { - name: 'Photo-Generated Pokemon Card', + name: `Photo-Generated ${gameName} Card`, set: 'AI Photo Generated', rarity: 'Photo Special', + game: game, imageDataUrl: finalImageDataUrl, isGenerated: true, isPhotoGenerated: true, @@ -73,7 +79,7 @@ export default function GenerateFromPhotoPage() { toast({ title: 'Card Saved', - description: 'Your photo-generated Pokemon card has been added to your collection!', + description: `Your photo-generated ${gameName} card has been added to your collection!`, }); // Clear the generated card after saving @@ -94,9 +100,12 @@ export default function GenerateFromPhotoPage() { return (
-

Pokemon Card Generator from Photo

+

TCG Card Generator from Photo

- Upload your own photo and transform it into a custom Pokemon card using AI-powered image generation + Upload your own photo and transform it into a custom trading card using AI-powered image generation +

+

+ Supports Pokémon, One Piece, Lorcana, Magic: The Gathering, and Dragon Ball cards

@@ -115,7 +124,7 @@ export default function GenerateFromPhotoPage() { Save Your Card - Your photo-inspired Pokemon card is ready! Save it to your collection. + Your photo-inspired trading card is ready! Save it to your collection. diff --git a/src/app/dashboard/generate/edit/page.tsx b/src/app/dashboard/generate/edit/page.tsx index c2cad60..28b6193 100644 --- a/src/app/dashboard/generate/edit/page.tsx +++ b/src/app/dashboard/generate/edit/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { useSearchParams } from 'next/navigation'; -import { CardGeneratorFormOnly } from '@/components/cards/CardGeneratorFormOnly'; +import { TCGCardGenerator } from '@/components/cards/TCGCardGenerator'; import { Button } from '@/components/ui/button'; import { useAuth } from '@/hooks/useAuth'; import { addCardToCollection } from '@/lib/firestore'; @@ -10,7 +10,8 @@ import { useToast } from '@/hooks/use-toast'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { compressBase64Image, getBase64Size, formatBytes } from '@/utils/imageUtils'; import { parseGenerationParams } from '@/utils/generationParamsUtils'; -import type { GeneratePokemonCardInput } from '@/components/cards/CardGeneratorFormOnly'; +import { getGameConfig } from '@/config/tcg-games'; +import type { TCGGame, CardGenerationParams } from '@/types'; import Image from 'next/image'; export default function EditGenerateCardPage() { @@ -20,14 +21,20 @@ export default function EditGenerateCardPage() { const [generatedCard, setGeneratedCard] = useState<{ imageBase64: string; prompt: string; params?: any } | null>(null); const [originalCard, setOriginalCard] = useState<{ id: string; name: string; imageUrl: string } | null>(null); const [isSaving, setIsSaving] = useState(false); - const [initialValues, setInitialValues] = useState | undefined>(undefined); + const [initialValues, setInitialValues] = useState | undefined>(undefined); const [showComparison, setShowComparison] = useState(true); + const [currentGame, setCurrentGame] = useState('pokemon'); useEffect(() => { // Parse parameters from URL search params const params = parseGenerationParams(searchParams); setInitialValues(params); + // Set the game from params + if (params.game) { + setCurrentGame(params.game as TCGGame); + } + // Extract original card data const originalCardId = searchParams.get('originalCardId'); const originalCardName = searchParams.get('originalCardName'); @@ -82,10 +89,14 @@ export default function EditGenerateCardPage() { } } + const game = generatedCard.params?.game || currentGame; + const gameName = getGameConfig(game as TCGGame).name; + await addCardToCollection(user.uid, { - name: 'Generated Pokemon Card', + name: `Generated ${gameName} Card`, set: 'AI Generated', rarity: 'Special', + game: game, imageDataUrl: imageDataUrl, isGenerated: true, prompt: generatedCard.prompt, @@ -94,7 +105,7 @@ export default function EditGenerateCardPage() { toast({ title: 'Card Saved', - description: 'Your generated Pokemon card has been added to your collection!', + description: `Your generated ${gameName} card has been added to your collection!`, }); // Clear the generated card after saving @@ -118,16 +129,16 @@ export default function EditGenerateCardPage() { return (
-

Edit Pokemon Card Generator

+

Edit {getGameConfig(currentGame).name} Card Generator

- Modify your Pokemon card parameters and regenerate the card + Modify your {getGameConfig(currentGame).name} card parameters and regenerate the card

{/* Form Section - Takes up 2 columns */}
- +
{/* Card Preview Section - Takes up 1 column */} diff --git a/src/app/dashboard/generate/page.tsx b/src/app/dashboard/generate/page.tsx index cb0e293..c6bffcf 100644 --- a/src/app/dashboard/generate/page.tsx +++ b/src/app/dashboard/generate/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState } from 'react'; -import { CardGenerator } from '@/components/cards/CardGenerator'; +import { TCGCardGenerator } from '@/components/cards/TCGCardGenerator'; import { Button } from '@/components/ui/button'; import { useAuth } from '@/hooks/useAuth'; import { addCardToCollection } from '@/lib/firestore'; @@ -9,6 +9,8 @@ import { useToast } from '@/hooks/use-toast'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { compressBase64Image, getBase64Size, formatBytes } from '@/utils/imageUtils'; import ApiKeysWarning from '@/components/cards/ApiKeysWarning'; +import { getGameConfig } from '@/config/tcg-games'; +import type { TCGGame } from '@/types'; export default function GenerateCardPage() { const { user } = useAuth(); @@ -56,10 +58,14 @@ export default function GenerateCardPage() { } } + const game = generatedCard.params?.game || 'pokemon'; + const gameName = getGameConfig(game as TCGGame).name; + await addCardToCollection(user.uid, { - name: 'Generated Pokemon Card', + name: `Generated ${gameName} Card`, set: 'AI Generated', rarity: 'Special', + game: game, imageDataUrl: imageDataUrl, isGenerated: true, prompt: generatedCard.prompt, @@ -68,7 +74,7 @@ export default function GenerateCardPage() { toast({ title: 'Card Saved', - description: 'Your generated Pokemon card has been added to your collection!', + description: `Your generated ${gameName} card has been added to your collection!`, }); // Clear the generated card after saving @@ -88,9 +94,9 @@ export default function GenerateCardPage() { return (
-

Pokemon Card Generator

+

TCG Card Generator

- Create your own custom Pokemon cards using AI-powered image generation + Create your own custom trading cards for Pokémon, One Piece, Lorcana, Magic, and Dragon Ball using AI

@@ -102,7 +108,7 @@ export default function GenerateCardPage() { />
- + {generatedCard && ( diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 2e5859e..9a0d90c 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -15,7 +15,7 @@ export default function DashboardPage() {

Welcome to your Cardex Dashboard

- Manage your Pokémon card collection with ease. + Manage your TCG card collection with ease.

@@ -32,7 +32,7 @@ export default function DashboardPage() { Scan Cards - Use your camera to scan and identify Pokemon cards + Use your camera to scan and identify TCG cards @@ -51,7 +51,7 @@ export default function DashboardPage() { Generate Cards - Create custom Pokemon cards using AI image generation + Create custom TCG cards using AI image generation @@ -70,7 +70,7 @@ export default function DashboardPage() { Photo Cards - Transform your photos into Pokemon cards using AI + Transform your photos into TCG cards using AI @@ -89,7 +89,7 @@ export default function DashboardPage() { My Collection - View and manage your Pokemon card collection + View and manage your TCG card collection diff --git a/src/app/dashboard/scan/page.tsx b/src/app/dashboard/scan/page.tsx index 097a428..c8c76ff 100644 --- a/src/app/dashboard/scan/page.tsx +++ b/src/app/dashboard/scan/page.tsx @@ -5,9 +5,12 @@ export default function ScanCardPage() { return (
-

Scan Pokémon Card

+

Scan Trading Card

- Upload an image of your Pokémon card to automatically identify and add it to your collection + Upload an image of your trading card to automatically identify and add it to your collection +

+

+ Supports Pokémon, One Piece, Lorcana, Magic: The Gathering, and Dragon Ball cards

diff --git a/src/components/cards/CardGenerator.tsx b/src/components/cards/CardGenerator.tsx deleted file mode 100644 index 175e648..0000000 --- a/src/components/cards/CardGenerator.tsx +++ /dev/null @@ -1,506 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { z } from 'zod'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Textarea } from '@/components/ui/textarea'; -import { Switch } from '@/components/ui/switch'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Separator } from '@/components/ui/separator'; -import { Loader2, Download } from 'lucide-react'; -import { downloadCardFromBase64 } from '@/utils/downloadUtils'; -import { useToast } from '@/hooks/use-toast'; -import { useAuth } from '@/hooks/useAuth'; -import { useApiKeys } from '@/hooks/useApiKeys'; - -export interface GeneratePokemonCardInput { - pokemonName: string; - pokemonType: string; - isIllustrationRare: boolean; - isHolo: boolean; - backgroundDescription: string; - pokemonDescription: string; - language: 'english' | 'japanese' | 'chinese' | 'korean' | 'spanish' | 'french' | 'german' | 'italian'; - model: 'imagen-4.0-ultra-generate-001' | 'imagen-4.0-generate-001' | 'imagen-4.0-fast-generate-001' | 'gemini-2.5-flash-image' | 'gemini-3-pro-image-preview'; - hp?: number; - attackName1?: string; - attackDamage1?: number; - attackName2?: string; - attackDamage2?: number; - weakness?: string; - resistance?: string; - retreatCost?: number; -} - -const pokemonTypes = [ - 'Normal', 'Fire', 'Water', 'Electric', 'Grass', 'Ice', 'Fighting', 'Poison', - 'Ground', 'Flying', 'Psychic', 'Bug', 'Rock', 'Ghost', 'Dragon', 'Dark', - 'Steel', 'Fairy' -]; - -const languages = [ - { value: 'english', label: 'English' }, - { value: 'japanese', label: 'Japanese' }, - { value: 'chinese', label: 'Chinese' }, - { value: 'korean', label: 'Korean' }, - { value: 'spanish', label: 'Spanish' }, - { value: 'french', label: 'French' }, - { value: 'german', label: 'German' }, - { value: 'italian', label: 'Italian' }, -] as const; - -const models = [ - { value: 'imagen-4.0-ultra-generate-001', label: 'Imagen 4.0 Ultra' }, - { value: 'imagen-4.0-generate-001', label: 'Imagen 4.0 Standard' }, - { value: 'imagen-4.0-fast-generate-001', label: 'Imagen 4.0 Fast' }, - { value: 'gemini-2.5-flash-image', label: 'Gemini 2.5 Flash (Nano Banana)' }, - { value: 'gemini-3-pro-image-preview', label: 'Gemini 3 Pro (Nano Banana Pro)' }, -] as const; - -const cardGeneratorSchema = z.object({ - pokemonName: z.string().min(1, 'Pokemon name is required'), - pokemonType: z.string().min(1, 'Pokemon type is required'), - isIllustrationRare: z.boolean(), - isHolo: z.boolean(), - backgroundDescription: z.string().min(10, 'Background description must be at least 10 characters'), - pokemonDescription: z.string().min(10, 'Pokemon description must be at least 10 characters'), - language: z.enum(['english', 'japanese', 'chinese', 'korean', 'spanish', 'french', 'german', 'italian']), - model: z.enum(['imagen-4.0-ultra-generate-001', 'imagen-4.0-generate-001', 'imagen-4.0-fast-generate-001', 'gemini-2.5-flash-image', 'gemini-3-pro-image-preview']), - hp: z.number().min(10).max(999).optional(), - attackName1: z.string().optional(), - attackDamage1: z.number().min(0).max(999).optional(), - attackName2: z.string().optional(), - attackDamage2: z.number().min(0).max(999).optional(), - weakness: z.string().optional(), - resistance: z.string().optional(), - retreatCost: z.number().min(0).max(5).optional(), -}); - -type CardGeneratorForm = z.infer; - -interface CardGeneratorProps { - onCardGenerated?: (imageBase64: string, prompt: string, params: GeneratePokemonCardInput) => void; - initialValues?: Partial; -} - -export function CardGenerator({ onCardGenerated, initialValues }: CardGeneratorProps) { - const { user } = useAuth(); - const { hasGeminiKey } = useApiKeys(); - const { toast } = useToast(); - const [isGenerating, setIsGenerating] = useState(false); - const [generatedCard, setGeneratedCard] = useState<{ imageBase64: string; prompt: string } | null>(null); - const [error, setError] = useState(''); - - const { - register, - handleSubmit, - watch, - setValue, - formState: { errors }, - } = useForm({ - resolver: zodResolver(cardGeneratorSchema), - defaultValues: { - pokemonName: initialValues?.pokemonName || '', - pokemonType: initialValues?.pokemonType || '', - isIllustrationRare: initialValues?.isIllustrationRare || false, - isHolo: initialValues?.isHolo || false, - backgroundDescription: initialValues?.backgroundDescription || '', - pokemonDescription: initialValues?.pokemonDescription || '', - language: initialValues?.language || 'english', - model: initialValues?.model || 'imagen-4.0-ultra-generate-001', - hp: initialValues?.hp || 130, - attackName1: initialValues?.attackName1 || 'Quick Attack', - attackDamage1: initialValues?.attackDamage1 || 60, - attackName2: initialValues?.attackName2 || 'Special Move', - attackDamage2: initialValues?.attackDamage2 || 90, - weakness: initialValues?.weakness || 'Fighting', - resistance: initialValues?.resistance || 'Psychic', - retreatCost: initialValues?.retreatCost || 2, - }, - }); - - const handleDownloadCard = () => { - if (!generatedCard) return; - - try { - downloadCardFromBase64(generatedCard.imageBase64, watchedValues.pokemonName || 'pokemon_card'); - toast({ - title: 'Download Started', - description: 'Your Pokemon card is being downloaded.', - }); - } catch (error) { - console.error('Download error:', error); - toast({ - title: 'Download Failed', - description: 'Failed to download the card. Please try again.', - variant: 'destructive', - }); - } - }; - - const watchedValues = watch(); - - const onSubmit = async (data: CardGeneratorForm) => { - if (!user) { - setError('You must be logged in to generate cards'); - return; - } - - if (!hasGeminiKey) { - setError('Gemini API key is required to generate cards. Please configure it in Settings.'); - return; - } - - setIsGenerating(true); - setError(''); - setGeneratedCard(null); - - try { - const response = await fetch('/api/generate-card', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - ...data, - userId: user.uid, - }), - }); - - const result = await response.json(); - - if (!response.ok) { - setError(result.error || 'Failed to generate card'); - return; - } - - if (result.error) { - setError(result.error); - return; - } - - if (result.imageBase64 && result.prompt) { - setGeneratedCard({ - imageBase64: result.imageBase64, - prompt: result.prompt, - }); - onCardGenerated?.(result.imageBase64, result.prompt, data); - } - } catch (err) { - console.error('Error generating card:', err); - setError('Failed to generate card. Please try again.'); - } finally { - setIsGenerating(false); - } - }; - - return ( -
- {/* Form Section */} - - - Create Your Pokemon Card - - Fill in the details to generate a custom Pokemon TCG card using AI - - - -
- {/* Basic Pokemon Info */} -
-

Basic Information

- -
-
- - - {errors.pokemonName && ( -

{errors.pokemonName.message}

- )} -
- -
- - - {errors.pokemonType && ( -

{errors.pokemonType.message}

- )} -
-
- -
- - -
- -
- - - {errors.model && ( -

{errors.model.message}

- )} -
-
- - - - {/* Card Properties */} -
-

Card Properties

- -
-
- -

Horizontal layout with full illustration

-
- setValue('isIllustrationRare', checked)} - /> -
- -
-
- -

Rainbow shimmer effect

-
- setValue('isHolo', checked)} - /> -
-
- - - - {/* Descriptions */} -
-

Descriptions

- -
- -