From b1cf465b5798ad082c16ecf407aacb47ce6090c8 Mon Sep 17 00:00:00 2001 From: xavidop Date: Sun, 4 Jan 2026 18:33:10 +0100 Subject: [PATCH] feat: add grading feature --- next.config.ts | 5 + package-lock.json | 249 ++++++++----- src/ai/flows/grade-tcg-card.ts | 269 ++++++++++++++ src/ai/genkit.ts | 4 +- src/app/api/grade-card/route.ts | 18 + src/app/dashboard/grade/page.tsx | 28 ++ src/app/dashboard/page.tsx | 23 +- src/components/cards/CardGrader.tsx | 547 ++++++++++++++++++++++++++++ src/components/layout/Navbar.tsx | 7 +- src/types/index.ts | 41 +++ 10 files changed, 1086 insertions(+), 105 deletions(-) create mode 100644 src/ai/flows/grade-tcg-card.ts create mode 100644 src/app/api/grade-card/route.ts create mode 100644 src/app/dashboard/grade/page.tsx create mode 100644 src/components/cards/CardGrader.tsx diff --git a/next.config.ts b/next.config.ts index 0b57d5e..1a78019 100644 --- a/next.config.ts +++ b/next.config.ts @@ -8,6 +8,11 @@ const nextConfig: NextConfig = { eslint: { ignoreDuringBuilds: true, }, + experimental: { + serverActions: { + bodySizeLimit: '10mb', // Increased for card image uploads + }, + }, images: { remotePatterns: [ { diff --git a/package-lock.json b/package-lock.json index 3c89ece..52f1336 100644 --- a/package-lock.json +++ b/package-lock.json @@ -563,7 +563,8 @@ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.2.0.tgz", "integrity": "sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@firebase/ai": { "version": "1.4.1", @@ -628,7 +629,6 @@ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.13.2.tgz", "integrity": "sha512-jwtMmJa1BXXDCiDx1vC6SFN/+HfYG53UkfJa6qeN5ogvOunzbFDO3wISZy5n9xgYFUrEP6M7e8EG++riHNTv9w==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/component": "0.6.18", "@firebase/logger": "0.4.4", @@ -695,7 +695,6 @@ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.4.2.tgz", "integrity": "sha512-LssbyKHlwLeiV8GBATyOyjmHcMpX/tFjzRUCS1jnwGAew1VsBB4fJowyS5Ud5LdFbYpJeS+IQoC+RQxpK7eH3Q==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/app": "0.13.2", "@firebase/component": "0.6.18", @@ -711,8 +710,7 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@firebase/auth-compat": { "version": "0.5.28", @@ -1176,7 +1174,6 @@ "integrity": "sha512-zGlBn/9Dnya5ta9bX/fgEoNC3Cp8s6h+uYPYaDieZsFOAdHP/ExzQ/eaDgxD3GOROdPkLKpvKY0iIzr9adle0w==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" }, @@ -1454,7 +1451,6 @@ "integrity": "sha512-qsM3/WHpawF07SRVvEJJVRwhYzM7o9qtuksyuqnrMig6fxIrwWnsezECWsG/D5TyYru51Fv5c/RTqNDQ2yU+4w==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api": "^1.3.0", "fast-deep-equal": "^3.1.1", @@ -1654,6 +1650,7 @@ "integrity": "sha512-gOnCAbFgAYKRozywLsxagdevTF7Gm+2Ncz5u5CQAuOv/2VCa0rdGJWvJFDOftPx1tc+q8TXiC2pEJfFKu+yeMQ==", "license": "Apache-2.0", "optional": true, + "peer": true, "dependencies": { "@google-cloud/paginator": "^5.0.0", "@google-cloud/projectify": "^4.0.0", @@ -1681,6 +1678,7 @@ "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "license": "MIT", "optional": true, + "peer": true, "bin": { "mime": "cli.js" }, @@ -1694,6 +1692,7 @@ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "license": "MIT", "optional": true, + "peer": true, "bin": { "uuid": "dist/bin/uuid" } @@ -2432,7 +2431,6 @@ "integrity": "sha512-D8h5KXY2vHFW8zTuxn2vuZGN0HGrQ5No6LkHwlEA9trVgNdPL3TF1dSqKA7Dny6BbBYKSW/rOBDXdC8KJAjUCg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", @@ -2728,22 +2726,6 @@ "node": ">= 0.6" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "devOptional": true, - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", @@ -2830,7 +2812,6 @@ "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "devOptional": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -3015,7 +2996,6 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -3111,7 +3091,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "1.25.1" }, @@ -4301,7 +4280,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.25.1.tgz", "integrity": "sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "1.25.1", "@opentelemetry/semantic-conventions": "1.25.1" @@ -4335,7 +4313,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.25.1.tgz", "integrity": "sha512-9Mb7q5ioFL4E4dDrc4wC/A3NTHDat44v4I3p2pLPSxRvqUbDIQyMVr9uK+EU69+HWhlET1VaSrRzwdckWqY15Q==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "1.25.1", "@opentelemetry/resources": "1.25.1", @@ -4380,7 +4357,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz", "integrity": "sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "1.25.1", "@opentelemetry/resources": "1.25.1", @@ -5602,7 +5578,6 @@ "version": "5.67.1", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.67.1.tgz", "integrity": "sha512-fH5u4JLwB6A+wLFdi8wWBWAYoJV5deYif2OveJ26ktAWjU499uvVFS1wPWnyEyq5LvZX1MZInvv9QRaIZANRaQ==", - "peer": true, "dependencies": { "@tanstack/query-core": "5.67.1" }, @@ -5647,6 +5622,7 @@ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -5685,6 +5661,7 @@ "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -5698,6 +5675,7 @@ "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -5710,7 +5688,8 @@ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@types/json-schema": { "version": "7.0.15", @@ -5724,6 +5703,7 @@ "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@types/ms": "*", "@types/node": "*" @@ -5751,14 +5731,16 @@ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@types/mysql": { "version": "2.15.22", @@ -5811,21 +5793,22 @@ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@types/react": { "version": "18.3.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", "devOptional": true, - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -5836,7 +5819,6 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", "devOptional": true, - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -5878,6 +5860,7 @@ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@types/node": "*" } @@ -5888,6 +5871,7 @@ "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@types/http-errors": "*", "@types/node": "*", @@ -5900,6 +5884,7 @@ "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -5981,7 +5966,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6173,6 +6157,7 @@ "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "retry": "0.13.1" } @@ -6252,29 +6237,58 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -7407,21 +7421,6 @@ "express": ">= 4.11" } }, - "node_modules/express/node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -7476,6 +7475,7 @@ "integrity": "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=18.0.0" } @@ -7540,6 +7540,7 @@ ], "license": "MIT", "optional": true, + "peer": true, "dependencies": { "strnum": "^1.1.1" }, @@ -7663,7 +7664,6 @@ "resolved": "https://registry.npmjs.org/firebase/-/firebase-11.10.0.tgz", "integrity": "sha512-nKBXoDzF0DrXTBQJlZa+sbC5By99ysYU1D6PkMRYknm0nCW7rJly47q492Ht7Ndz5MeYSBuboKuhS1e6mFC03w==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@firebase/ai": "1.4.1", "@firebase/analytics": "0.10.17", @@ -7701,6 +7701,7 @@ "integrity": "sha512-GdPA/t0+Cq8p1JnjFRBmxRxAGvF/kl2yfdhALl38PrRp325YxyQ5aNaHui0XmaKcKiGRFIJ/EgBNWFoDP0onjw==", "license": "Apache-2.0", "optional": true, + "peer": true, "dependencies": { "@fastify/busboy": "^3.0.0", "@firebase/database-compat": "^2.0.0", @@ -7728,6 +7729,7 @@ "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -7737,7 +7739,8 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/firebase-admin/node_modules/uuid": { "version": "11.1.0", @@ -7749,6 +7752,7 @@ ], "license": "MIT", "optional": true, + "peer": true, "bin": { "uuid": "dist/esm/bin/uuid" } @@ -7979,7 +7983,6 @@ "resolved": "https://registry.npmjs.org/genkit/-/genkit-1.24.0.tgz", "integrity": "sha512-9BjPrLULfWdzauibKkbZcCf2LVu0mW2EW6EXHK/cTber6QdiP/guA5mXahaaDWBo+qdIKpInXAYDEzcRUmCVSA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@genkit-ai/ai": "1.24.0", "@genkit-ai/core": "1.24.0", @@ -8905,6 +8908,7 @@ "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", "license": "MIT", "optional": true, + "peer": true, "funding": { "url": "https://github.com/sponsors/panva" } @@ -9017,6 +9021,7 @@ "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", @@ -9040,6 +9045,7 @@ "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", @@ -9052,6 +9058,7 @@ "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "jwa": "^1.4.2", "safe-buffer": "^5.0.1" @@ -9062,7 +9069,8 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/jwa": { "version": "2.0.1", @@ -9081,6 +9089,7 @@ "integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@types/express": "^4.17.20", "@types/jsonwebtoken": "^9.0.4", @@ -9099,6 +9108,7 @@ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "ms": "^2.1.3" }, @@ -9116,7 +9126,8 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/jws": { "version": "4.0.1", @@ -9158,7 +9169,8 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", - "optional": true + "optional": true, + "peer": true }, "node_modules/lines-and-columns": { "version": "1.2.4", @@ -9199,49 +9211,56 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/lodash.isnumber": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/lodash.mapvalues": { "version": "4.6.0", @@ -9261,7 +9280,8 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/log-symbols": { "version": "4.1.0", @@ -9332,6 +9352,7 @@ "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "lodash.clonedeep": "^4.5.0", "lru-cache": "6.0.0" @@ -9343,6 +9364,7 @@ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "license": "ISC", "optional": true, + "peer": true, "dependencies": { "yallist": "^4.0.0" }, @@ -9584,7 +9606,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.5.9.tgz", "integrity": "sha512-agNLK89seZEtC5zUHwtut0+tNrc0Xw4FT/Dg+B/VLEo9pAcS9rtTKpek3V6kVcVwsB2YlqMaHdfZL4eLEVYuCg==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "15.5.9", "@swc/helpers": "0.5.15", @@ -9700,6 +9721,7 @@ "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==", "license": "(BSD-3-Clause OR GPL-2.0)", "optional": true, + "peer": true, "engines": { "node": ">= 6.13.0" } @@ -9906,6 +9928,7 @@ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -10147,7 +10170,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -10412,12 +10434,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -10455,25 +10477,53 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -10485,7 +10535,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -10498,7 +10547,6 @@ "version": "7.54.2", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.2.tgz", "integrity": "sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -10720,6 +10768,7 @@ "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 4" } @@ -11346,7 +11395,8 @@ } ], "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/stubs": { "version": "3.0.0", @@ -11433,7 +11483,6 @@ "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -11993,7 +12042,6 @@ "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", @@ -12201,7 +12249,8 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC", - "optional": true + "optional": true, + "peer": true }, "node_modules/yaml": { "version": "2.8.0", @@ -12293,6 +12342,7 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=10" }, @@ -12304,7 +12354,6 @@ "version": "3.24.2", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/ai/flows/grade-tcg-card.ts b/src/ai/flows/grade-tcg-card.ts new file mode 100644 index 0000000..6165a7f --- /dev/null +++ b/src/ai/flows/grade-tcg-card.ts @@ -0,0 +1,269 @@ +// This file is automatically generated - edits will be lost! +'use server'; + +/** + * @fileOverview Uses Gemini Vision to grade a TCG card's condition based on PSA/BGS/CGC standards. + * + * - gradeTCGCard - A function that handles the card grading process. + * - GradeTCGCardInput - The input type for the gradeTCGCard function. + * - GradeTCGCardOutput - The return type for the gradeTCGCard function. + */ + +import {ai, createUserAI} from '@/ai/genkit'; +import {z} from 'genkit'; +import type { TCGGame } from '@/types'; +import { tcgGameSchema } from '@/constants'; + +const GradeTCGCardInputSchema = z.object({ + frontPhotoDataUri: z + .string() + .describe( + "A photo of the front of a trading card, as a data URI that must include a MIME type and use Base64 encoding. Expected format: 'data:;base64,'." + ), + backPhotoDataUri: z + .string() + .optional() + .describe( + "Optional photo of the back of the trading card, as a data URI. If provided, will be used for more accurate centering and surface grading." + ), + userId: z.string().describe("The user ID for personalized API key usage.").optional(), + cardName: z.string().describe("The name of the card being graded.").optional(), + cardSet: z.string().describe("The set the card belongs to.").optional(), + game: tcgGameSchema.optional().describe("The TCG game this card is from."), + gradingScale: z.enum(['PSA', 'BGS', 'CGC']).optional().describe("The grading scale to use. Default is PSA."), +}); +export type GradeTCGCardInput = z.infer; + +const GradeTCGCardOutputSchema = z.object({ + gradingResult: z.object({ + overallGrade: z.number().min(1).max(10).describe("Overall grade on a 1-10 scale."), + centering: z.object({ + score: z.number().min(1).max(10).describe("Centering score 1-10."), + frontCentering: z.string().describe("Front centering ratio (e.g., '55/45')."), + backCentering: z.string().describe("Back centering ratio (e.g., '60/40')."), + notes: z.string().describe("Notes about centering condition."), + }).describe("Centering analysis."), + corners: z.object({ + score: z.number().min(1).max(10).describe("Corners score 1-10."), + condition: z.string().describe("Corners condition description."), + notes: z.string().describe("Notes about corner condition."), + }).describe("Corners analysis."), + edges: z.object({ + score: z.number().min(1).max(10).describe("Edges score 1-10."), + condition: z.string().describe("Edges condition description."), + notes: z.string().describe("Notes about edge condition."), + }).describe("Edges analysis."), + surface: z.object({ + score: z.number().min(1).max(10).describe("Surface score 1-10."), + condition: z.string().describe("Surface condition description."), + notes: z.string().describe("Notes about surface condition."), + }).describe("Surface analysis."), + gradingScale: z.enum(['PSA', 'BGS', 'CGC']).describe("The grading scale used."), + gradeName: z.string().describe("Grade name (e.g., 'GEM MINT', 'MINT', 'NEAR MINT')."), + estimatedValue: z.string().optional().describe("Estimated market value if card info provided."), + recommendations: z.array(z.string()).describe("Recommendations for card protection/preservation."), + detailedAnalysis: z.string().describe("Overall detailed assessment of the card's condition."), + }).describe("Comprehensive grading result.").optional(), + error: z.string().describe("Error message if the card could not be graded.").optional() +}); + +export type GradeTCGCardOutput = z.infer; + +export async function gradeTCGCard(input: GradeTCGCardInput): Promise { + return gradeTCGCardFlow(input); +} + +async function createGradePromptAndFlow(userAI: any) { + const gradeScale = (scale: string = 'PSA') => { + const scales = { + PSA: `PSA Grading Scale: +- 10 (GEM MINT): Perfect card with no visible flaws +- 9 (MINT): Minor imperfections allowed +- 8 (NM-MT): Near Mint to Mint condition +- 7 (NM): Near Mint condition +- 6 (EX-MT): Excellent to Mint +- 5 (EX): Excellent condition +- 4 (VG-EX): Very Good to Excellent +- 3 (VG): Very Good condition +- 2 (GOOD): Good condition with visible wear +- 1 (POOR): Poor condition with heavy wear`, + BGS: `Beckett Grading Scale (BGS): +- 10 (PRISTINE): Perfect in every way +- 9.5 (GEM MINT): Near perfect card +- 9 (MINT): Minor imperfections +- 8.5 (NM-MT+): Near Mint to Mint+ +- 8 (NM-MT): Near Mint to Mint +- 7.5 (NM+): Near Mint+ +- 7 (NM): Near Mint +- 6.5 (EX-NM+): Excellent to Near Mint+ +- 6 (EX-NM): Excellent to Near Mint +- 5.5 (EX+): Excellent+ +- 5 (EX): Excellent +- Lower grades for worse condition`, + CGC: `CGC Grading Scale: +- 10 (PRISTINE): Perfect card +- 9.5 (GEM MINT): Near perfect +- 9 (MINT): Mint condition +- 8.5 (NM/MINT+): Near Mint to Mint+ +- 8 (NM/MINT): Near Mint to Mint +- 7.5 (NM+): Near Mint+ +- 7 (NM): Near Mint +- 6.5 (EX/NM+): Excellent to Near Mint+ +- 6 (EX/NM): Excellent to Near Mint +- 5.5 (EX+): Excellent+ +- 5 (EX): Excellent condition +- Lower grades for worse condition` + }; + return scales[scale as keyof typeof scales] || scales.PSA; + }; + + const gradeTCGCardPrompt = userAI.definePrompt({ + name: 'gradeTCGCardPrompt', + input: {schema: GradeTCGCardInputSchema}, + output: {schema: GradeTCGCardOutputSchema}, + prompt: `You are an expert trading card grading professional with extensive knowledge of PSA, BGS (Beckett), and CGC grading standards. You specialize in evaluating TCG cards including Pokémon, One Piece, Lorcana, Magic: The Gathering, and Dragon Ball cards. + +{{#if gradingScale}} +Use the ${gradeScale('{{gradingScale}}')} grading scale. +{{else}} +Use the ${gradeScale('PSA')} grading scale (default). +{{/if}} + +Analyze the following card image(s) and provide a comprehensive grading assessment: + +Front Photo: {{media url=frontPhotoDataUri}} +{{#if backPhotoDataUri}} +Back Photo: {{media url=backPhotoDataUri}} +{{/if}} + +{{#if cardName}} +Card Name: {{cardName}} +{{/if}} +{{#if cardSet}} +Card Set: {{cardSet}} +{{/if}} +{{#if game}} +Game: {{game}} +{{/if}} + +{{#if backPhotoDataUri}} +Note: Both front and back images are provided. Use both images to provide a more accurate assessment of centering, surface quality, and overall condition. +{{else}} +Note: Only front image provided. Estimate back centering and condition based on typical manufacturing tolerances. +{{/if}} + +GRADING CRITERIA TO EVALUATE: + +1. **CENTERING** (Front and Back): + - Measure the borders on all four sides + - Front centering should be within specific tolerances (e.g., 60/40 for PSA 9, 55/45 for PSA 10) + - Back centering has slightly more tolerance + - Provide specific ratios (e.g., "55/45" for left-right, top-bottom) + +2. **CORNERS**: + - Examine all four corners for wear, whitening, or rounding + - Sharp corners = higher grade + - Any fraying, bending, or whitening reduces the grade + - Note which corners (if any) show issues + +3. **EDGES**: + - Check all four edges for wear, chipping, or roughness + - Look for whitening along edges + - Clean, sharp edges = higher grade + - Note any specific edge issues + +4. **SURFACE**: + - Look for scratches, print lines, print dots + - Check for indentations, creases, or wrinkles + - Evaluate holographic surface quality (if applicable) + - Look for any staining or discoloration + - Note any surface imperfections + +IMPORTANT GRADING RULES: +- A card can only be as good as its worst attribute +- Centering issues often limit the maximum grade +- Surface defects can significantly drop the grade +- Be realistic and conservative in grading +- Corner and edge wear are critical factors + +Provide your assessment in the following JSON format: +{ + "gradingResult": { + "overallGrade": 8.5, + "centering": { + "score": 9, + "frontCentering": "55/45", + "backCentering": "60/40", + "notes": "Front centering is excellent, back slightly off-center left to right" + }, + "corners": { + "score": 8, + "condition": "Mostly sharp with minor wear", + "notes": "Top right corner shows slight whitening, other corners are sharp" + }, + "edges": { + "score": 9, + "condition": "Clean with minimal wear", + "notes": "Edges are clean and sharp with no visible whitening" + }, + "surface": { + "score": 8, + "condition": "Minor imperfections visible", + "notes": "Small print line visible near top edge, holographic surface shows minor wear" + }, + "gradingScale": "PSA", + "gradeName": "NM-MT", + "estimatedValue": "$50-75 (based on condition)", + "recommendations": [ + "Store in a penny sleeve and top loader", + "Keep away from direct sunlight", + "Consider professional grading if valuable", + "Handle with clean hands or gloves" + ], + "detailedAnalysis": "This card presents in Near Mint to Mint condition with excellent centering and clean edges. The primary factors preventing a higher grade are the slight corner wear on the top right corner and a minor print line on the surface. Overall, this is a well-preserved card that would likely grade PSA 8-8.5. With proper storage, it should maintain its condition." + } +} + +If the image is unclear, not a trading card, or cannot be properly graded, respond with: +{ + "error": "Unable to grade card. [Specific reason: image too blurry, not a trading card, etc.]" +}`, + }); + + const gradeTCGCardFlow = userAI.defineFlow( + { + name: 'gradeTCGCardFlow', + inputSchema: GradeTCGCardInputSchema, + outputSchema: GradeTCGCardOutputSchema, + }, + async (input: GradeTCGCardInput) => { + try { + const {output} = await gradeTCGCardPrompt(input); + return output!; + } catch (error: any) { + console.error('Error during card grading:', error); + return {error: 'Failed to grade trading card.'}; + } + } + ); + + return gradeTCGCardFlow; +} + +const gradeTCGCardFlow = ai.defineFlow( + { + name: 'gradeTCGCardFlow', + inputSchema: GradeTCGCardInputSchema, + outputSchema: GradeTCGCardOutputSchema, + }, + async (input: GradeTCGCardInput) => { + try { + const userAI = await createUserAI(input.userId || ''); + const flow = await createGradePromptAndFlow(userAI); + return await flow(input); + } catch (error: any) { + console.error('Error during card grading:', error); + return {error: 'Failed to grade trading card.'}; + } + } +); diff --git a/src/ai/genkit.ts b/src/ai/genkit.ts index db6614a..82d8585 100644 --- a/src/ai/genkit.ts +++ b/src/ai/genkit.ts @@ -9,7 +9,7 @@ export const ai = genkit({ plugins: [ googleAI({apiKey: defaultApiKey}) ], - model: 'googleai/gemini-2.5-pro-preview-05-06', + model: 'googleai/gemini-3-pro-preview', }); // Function to create genkit instance with user API key @@ -23,7 +23,7 @@ export async function createUserAI(userId: string) { plugins: [ googleAI({apiKey: userApiKey}) ], - model: 'googleai/gemini-2.5-pro-preview-05-06', + model: 'googleai/gemini-3-pro-preview', }); } } catch (error) { diff --git a/src/app/api/grade-card/route.ts b/src/app/api/grade-card/route.ts new file mode 100644 index 0000000..395b046 --- /dev/null +++ b/src/app/api/grade-card/route.ts @@ -0,0 +1,18 @@ +import { NextRequest } from 'next/server'; +import { gradeTCGCard } from '@/ai/flows/grade-tcg-card'; +import { handleApiRequest } from '@/lib/api-handler'; + +/** + * Required fields for card grading + */ +const REQUIRED_FIELDS = [ + 'frontPhotoDataUri' +] as const; + +/** + * POST /api/grade-card + * Grades a TCG card's condition using AI based on professional grading standards + */ +export async function POST(request: NextRequest) { + return handleApiRequest(request, REQUIRED_FIELDS, gradeTCGCard); +} diff --git a/src/app/dashboard/grade/page.tsx b/src/app/dashboard/grade/page.tsx new file mode 100644 index 0000000..b03ba36 --- /dev/null +++ b/src/app/dashboard/grade/page.tsx @@ -0,0 +1,28 @@ +import CardGrader from '@/components/cards/CardGrader'; +import ApiKeysWarning from '@/components/cards/ApiKeysWarning'; + +export default function GradeCardPage() { + return ( +
+
+

Pre-Grade Your Card

+

+ Get a professional grading assessment before sending to PSA, BGS, or CGC +

+

+ Upload a clear photo of your card to receive detailed condition analysis and grading score +

+
+ + {/* API Keys Warning - only show Gemini key warning for grading */} +
+ +
+ + +
+ ); +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 9a0d90c..bcaac10 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -4,7 +4,7 @@ import { useRouter } from 'next/navigation'; import Link from 'next/link'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; -import { ScanLine, Sparkles, BookOpen, Camera } from 'lucide-react'; +import { ScanLine, Sparkles, BookOpen, Camera, Award } from 'lucide-react'; import ApiKeysWarning from '@/components/cards/ApiKeysWarning'; export default function DashboardPage() { @@ -24,7 +24,7 @@ export default function DashboardPage() { -
+
@@ -44,6 +44,25 @@ export default function DashboardPage() { + + + + + Grade Cards + + + Pre-grade cards before sending to PSA, BGS, or CGC + + + + + + + diff --git a/src/components/cards/CardGrader.tsx b/src/components/cards/CardGrader.tsx new file mode 100644 index 0000000..4273b9e --- /dev/null +++ b/src/components/cards/CardGrader.tsx @@ -0,0 +1,547 @@ +'use client'; + +import { useState, type ChangeEvent, useRef } from 'react'; +import { gradeTCGCard, type GradeTCGCardOutput } from '@/ai/flows/grade-tcg-card'; +import { Button } from '@/components/ui/button'; +import { useToast } from '@/hooks/use-toast'; +import { Loader2, Upload, Camera, Award, TrendingUp, Shield } from 'lucide-react'; +import Image from 'next/image'; +import { useAuth } from '@/hooks/useAuth'; +import { useApiKeys } from '@/hooks/useApiKeys'; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '../ui/card'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import type { TCGGame } from '@/types'; +import { Badge } from '@/components/ui/badge'; +import { Progress } from '@/components/ui/progress'; +import { Separator } from '@/components/ui/separator'; + +export default function CardGrader() { + const { hasGeminiKey } = useApiKeys(); + const [isGrading, setIsGrading] = useState(false); + const [gradingResult, setGradingResult] = useState(null); + const [error, setError] = useState(null); + + // Front image (required) + const [frontImagePreview, setFrontImagePreview] = useState(null); + const [frontImageDataUrl, setFrontImageDataUrl] = useState(null); + + // Back image (optional) + const [backImagePreview, setBackImagePreview] = useState(null); + const [backImageDataUrl, setBackImageDataUrl] = useState(null); + + // Optional metadata for better grading + const [cardName, setCardName] = useState(''); + const [cardSet, setCardSet] = useState(''); + const [game, setGame] = useState(''); + const [gradingScale, setGradingScale] = useState<'PSA' | 'BGS' | 'CGC'>('PSA'); + + const { toast } = useToast(); + const { user } = useAuth(); + const frontFileInputRef = useRef(null); + const backFileInputRef = useRef(null); + + const handleFrontImageChange = (event: ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + // Reset previous grading state + setGradingResult(null); + setError(null); + setFrontImageDataUrl(null); + + const reader = new FileReader(); + reader.onloadend = () => { + const dataUrl = reader.result as string; + setFrontImagePreview(dataUrl); + setFrontImageDataUrl(dataUrl); + }; + reader.readAsDataURL(file); + } + }; + + const handleBackImageChange = (event: ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + // Reset previous grading state + setGradingResult(null); + setError(null); + setBackImageDataUrl(null); + + const reader = new FileReader(); + reader.onloadend = () => { + const dataUrl = reader.result as string; + setBackImagePreview(dataUrl); + setBackImageDataUrl(dataUrl); + }; + reader.readAsDataURL(file); + } + }; + + const handleGrade = async () => { + if (!frontImageDataUrl) { + toast({ + title: 'No front image selected', + description: 'Please select the front image of the card to grade.', + variant: 'destructive' + }); + return; + } + + if (!hasGeminiKey) { + setError('Gemini API key is required to grade trading cards. Please configure it in Settings.'); + toast({ + title: 'API Key Required', + description: 'Gemini API key is required. Please configure it in Settings.', + variant: 'destructive' + }); + return; + } + + setIsGrading(true); + setError(null); + setGradingResult(null); + + try { + const result = await gradeTCGCard({ + frontPhotoDataUri: frontImageDataUrl, + backPhotoDataUri: backImageDataUrl || undefined, + userId: user?.uid || '', + cardName: cardName || undefined, + cardSet: cardSet || undefined, + game: game || undefined, + gradingScale + }); + + if (result.error) { + setError(result.error); + toast({ + title: 'Grading Failed', + description: result.error, + variant: 'destructive' + }); + } else if (result.gradingResult) { + setGradingResult(result); + toast({ + title: 'Grading Complete', + description: `Card graded: ${result.gradingResult.gradeName}`, + variant: 'default' + }); + } else { + setError('Unexpected response from grading service.'); + toast({ + title: 'Grading Error', + description: 'Unexpected response from grading service.', + variant: 'destructive' + }); + } + } catch (e) { + console.error('Grading error:', e); + const errorMessage = (e instanceof Error) ? e.message : 'An unknown error occurred during grading.'; + setError(errorMessage); + toast({ + title: 'Grading Error', + description: errorMessage, + variant: 'destructive' + }); + } finally { + setIsGrading(false); + } + }; + + const getGradeColor = (grade: number) => { + if (grade >= 9.5) return 'text-green-600 dark:text-green-400'; + if (grade >= 8) return 'text-blue-600 dark:text-blue-400'; + if (grade >= 6) return 'text-yellow-600 dark:text-yellow-400'; + return 'text-orange-600 dark:text-orange-400'; + }; + + const getScorePercentage = (score: number) => (score / 10) * 100; + + return ( +
+ {/* Show results in optimized layout when grading is complete */} + {gradingResult?.gradingResult ? ( +
+ {/* Top Section - Overall Grade & Images */} + + +
+ {/* Images - Compact on the left */} +
+ {frontImagePreview && ( +
+ +
+ Card front +
+
+ )} + {backImagePreview && ( +
+ +
+ Card back +
+
+ )} +
+ + {/* Overall Grade - Center */} +
+
+ + {gradingResult.gradingResult.gradingScale} + + +
+
+

Overall Grade

+
+ {gradingResult.gradingResult.overallGrade} +
+

{gradingResult.gradingResult.gradeName}

+
+ {gradingResult.gradingResult.estimatedValue && ( +
+ +

+ Est. Value: {gradingResult.gradingResult.estimatedValue} +

+
+ )} + + {/* AI Disclaimer */} +
+

+ ⚠️ This grade is generated by Google Gemini AI and is for reference only. + Final professional grading results may vary. Always consult with PSA, BGS, or CGC for official grading. +

+
+
+
+
+
+ + {/* Detailed Scores Grid */} +
+ {/* Centering */} + + + + Centering + {gradingResult.gradingResult.centering.score}/10 + + + + +
+

Front: {gradingResult.gradingResult.centering.frontCentering}

+

Back: {gradingResult.gradingResult.centering.backCentering}

+

{gradingResult.gradingResult.centering.notes}

+
+
+
+ + {/* Corners */} + + + + Corners + {gradingResult.gradingResult.corners.score}/10 + + + + +
+

{gradingResult.gradingResult.corners.condition}

+

{gradingResult.gradingResult.corners.notes}

+
+
+
+ + {/* Edges */} + + + + Edges + {gradingResult.gradingResult.edges.score}/10 + + + + +
+

{gradingResult.gradingResult.edges.condition}

+

{gradingResult.gradingResult.edges.notes}

+
+
+
+ + {/* Surface */} + + + + Surface + {gradingResult.gradingResult.surface.score}/10 + + + + +
+

{gradingResult.gradingResult.surface.condition}

+

{gradingResult.gradingResult.surface.notes}

+
+
+
+
+ + {/* Analysis & Recommendations */} +
+ + + Analysis + + +

+ {gradingResult.gradingResult.detailedAnalysis} +

+
+
+ + + + Recommendations + + +
    + {gradingResult.gradingResult.recommendations.map((rec, index) => ( +
  • + + {rec} +
  • + ))} +
+
+
+
+
+ ) : ( + /* Upload Form - Show when no results */ + + + + + Card Grading Tool + + + Upload a photo of your card to receive a professional-grade assessment + + + + {/* Image Upload Section */} +
+

Card Images

+ +
+ {/* Front Image */} +
+ +
+ {frontImagePreview ? ( +
+ Card front +
+ ) : ( +
+ +
+

Front of Card

+

+ Show all edges & corners +

+
+
+ )} +
+ + +
+ + {/* Back Image */} +
+ +
+ {backImagePreview ? ( +
+ Card back +
+ ) : ( +
+ +
+

Back of Card

+

+ For accurate centering +

+
+
+ )} +
+ + +
+
+
+ + {/* Optional Card Metadata */} +
+

Optional Card Information

+

+ Providing card details helps with value estimation +

+ +
+
+ + setCardName(e.target.value)} + /> +
+ +
+ + setCardSet(e.target.value)} + /> +
+ +
+ + +
+ +
+ + +
+
+
+ + {/* Grade Button */} + + + {/* Error Display */} + {error && ( +
+

{error}

+
+ )} +
+
+ )} +
+ ); +} diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx index b91278d..5105903 100644 --- a/src/components/layout/Navbar.tsx +++ b/src/components/layout/Navbar.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; import { useRouter } from 'next/navigation'; -import { LogIn, LogOut, UserCircle, ScanLine, BookOpen, Sparkles, Camera, Menu, Settings } from 'lucide-react'; +import { LogIn, LogOut, UserCircle, ScanLine, BookOpen, Sparkles, Camera, Menu, Settings, Award } from 'lucide-react'; import { useAuth } from '@/hooks/useAuth'; import { Button } from '@/components/ui/button'; import { signOut } from 'firebase/auth'; @@ -57,6 +57,11 @@ export default function Navbar() { Scan Card +