diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..0ff83fa --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +node_modules +.next +next-env.d.ts + diff --git a/.gitignore b/.gitignore index 62190d7..17309cb 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,6 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts -.env \ No newline at end of file +.env + +.cursor/ \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index f920452..810edbf 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -11,7 +11,7 @@ export default [ }, { files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"] }, { - ignores: [".next/", "out/", "node_modules"], + ignores: [".next/", "out/", "node_modules", "next-env.d.ts"], }, { files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } }, { languageOptions: { globals: { ...globals.node } } }, diff --git a/package-lock.json b/package-lock.json index bc05341..ee45585 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "CookCraft", "version": "0.1.0", "dependencies": { + "@google/genai": "^1.30.0", "@supabase/supabase-js": "^2.76.1", "@tanstack/react-query": "5.69.0", "lucide-react": "0.484.0", @@ -251,6 +252,27 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@google/genai": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.30.0.tgz", + "integrity": "sha512-3MRcgczBFbUat1wIlZoLJ0vCCfXgm7Qxjh59cZi2X08RgWLtm9hKOspzp7TOg1TV2e26/MLxR2GR5yD5GmBV2w==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.20.1" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -783,6 +805,23 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@next/env": { "version": "15.5.6", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.6.tgz", @@ -955,6 +994,16 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.0.tgz", @@ -1610,6 +1659,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1627,11 +1685,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1826,14 +1895,41 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -1852,6 +1948,12 @@ "node": ">=8" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -1959,7 +2061,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1972,7 +2073,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/concat-map": { @@ -1986,7 +2086,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2004,6 +2103,15 @@ "dev": true, "license": "MIT" }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -2062,7 +2170,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2170,6 +2277,27 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -2699,6 +2827,12 @@ "node": ">=0.10.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2767,6 +2901,29 @@ "reusify": "^1.0.4" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2847,6 +3004,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/framer-motion": { "version": "12.23.24", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", @@ -2915,6 +3100,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -2972,6 +3186,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3036,6 +3270,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3063,6 +3324,19 @@ "dev": true, "license": "MIT" }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -3157,6 +3431,19 @@ "node": ">= 0.4" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -3386,6 +3673,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", @@ -3614,7 +3910,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -3635,6 +3930,21 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", @@ -3665,6 +3975,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -3702,6 +4021,27 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4001,6 +4341,12 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/lucide-react": { "version": "0.484.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.484.0.tgz", @@ -4048,7 +4394,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -4060,6 +4405,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/motion": { "version": "12.23.24", "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.24.tgz", @@ -4105,7 +4459,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -4213,6 +4566,44 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4389,6 +4780,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4416,7 +4813,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4429,6 +4825,22 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -4769,6 +5181,21 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4813,6 +5240,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -4965,7 +5412,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -4978,7 +5424,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5060,6 +5505,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -5079,6 +5536,65 @@ "node": ">=0.10.0" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -5177,6 +5693,43 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -5678,6 +6231,15 @@ "punycode": "^2.1.0" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -5698,7 +6260,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -5809,6 +6370,94 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", diff --git a/package.json b/package.json index e1698d0..d8282f6 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "prepare": "husky" }, "dependencies": { + "@google/genai": "^1.30.0", "@supabase/supabase-js": "^2.76.1", "@tanstack/react-query": "5.69.0", "lucide-react": "0.484.0", diff --git a/src/app/api/gemini/route.ts b/src/app/api/gemini/route.ts new file mode 100644 index 0000000..005b3d1 --- /dev/null +++ b/src/app/api/gemini/route.ts @@ -0,0 +1,392 @@ +import { GoogleGenAI, Type } from "@google/genai"; +import { NextRequest, NextResponse } from "next/server"; +import { createClient } from "@supabase/supabase-js"; + +const ai = new GoogleGenAI({ + apiKey: process.env.GEMINI_API_KEY, +}); + +function getSystemInstruction( + useDietaryPreferences: boolean, + usePreferredCuisines: boolean, + onlyInventoryIngredients: boolean, +): string { + // using our checkboxes first + let preferencesNote = ""; + if (useDietaryPreferences === false && usePreferredCuisines === false) { + preferencesNote = + "\n- IGNORE dietary preferences and preferred cuisines (user has disabled them)"; + } else if (useDietaryPreferences === false) { + preferencesNote = + "\n- IGNORE dietary preferences (user has disabled them), but respect preferred cuisines"; + } else if (usePreferredCuisines === false) { + preferencesNote = + "\n- IGNORE preferred cuisines (user has disabled them), but respect dietary preferences"; + } else { + preferencesNote = + "\n- Respect both dietary preferences and preferred cuisines from the user's profile"; + } + + let inventoryNote = ""; + if (onlyInventoryIngredients === true) { + inventoryNote = + "\n- CRITICAL: ONLY suggest recipes using ingredients from the user's inventory. DO NOT suggest any ingredients the user doesn't already have. If the user doesn't have enough ingredients for a complete recipe, suggest what they can make with what they have or inform them they need more ingredients."; + } else { + inventoryNote = + "\n- Suggest ANY appropriate recipes based on the user's request, without limiting to inventory ingredients"; + inventoryNote = + inventoryNote + + "\n- ONLY consider inventory limitations if the user EXPLICITLY asks (e.g., 'what can I make with my ingredients', 'use what I have', 'based on my fridge')"; + inventoryNote = + inventoryNote + + "\n- You can still check inventory for context, but do NOT let it restrict your recipe suggestions unless asked"; + } + + const instruction = `You are CookCraft AI, a helpful cooking assistant integrated into the CookCraft recipe management platform. + +Your responsibilities: +1. Help users generate recipe ideas based on their available ingredients +2. Respect user dietary preferences, allergies, and cuisine preferences +3. Create detailed, easy-to-follow recipes +4. Help users add ingredients to their inventory +5. ONLY respond to food, cooking, recipe, and ingredient-related queries + +Important guidelines: +- Always check the user's current ingredients and profile before suggesting recipes${inventoryNote} +- ALWAYS respect allergies listed in the user's profile (CRITICAL - never suggest recipes with allergens)${preferencesNote} +- Consider the difficulty level and time constraints mentioned by the user (e.g., "under 30 minutes", "beginner", "fancy") +- Provide clear, numbered instructions +- Include accurate measurements and cooking times +- If a user asks something completely unrelated to food/cooking/recipes (like math, history, general knowledge), politely decline and remind them you're a cooking assistant + +CRITICAL - When to use functions: +- Call get_user_profile at the start for dietary preferences and allergies +- ONLY call get_user_ingredients if: (1) onlyInventoryIngredients mode is enabled, OR (2) user explicitly asks about their ingredients/what they can make +- DO NOT call create_recipe automatically when suggesting recipes +- ONLY call create_recipe when the user EXPLICITLY says "save this recipe", "add this to my recipes", "create this recipe", or similar confirmation +- Call add_ingredient when the user wants to add items to their inventory + +Response format for recipe suggestions: +- First, present the complete recipe with: + * Recipe name + * Description + * Prep time and cook time + * Difficulty level + * Number of servings + * Full ingredient list with measurements + * Step-by-step numbered instructions + * Cuisine type +- After presenting the recipe, ask: "Would you like me to save this recipe to your collection?" +- Wait for user confirmation before calling create_recipe +- For ingredient additions, confirm what was added`; + + return instruction; +} + +// function declarations from gemini docs +const getUserIngredientsFunctionDeclaration = { + name: "get_user_ingredients", + description: + "Gets the user's current ingredients from their inventory. Use this to see what ingredients are available before generating recipes.", + parameters: { + type: Type.OBJECT, + properties: {}, + }, +}; + +const getUserProfileFunctionDeclaration = { + name: "get_user_profile", + description: + "Gets the user's dietary preferences, allergies, preferred cuisines, and default serving size. Use this to personalize recipe suggestions.", + parameters: { + type: Type.OBJECT, + properties: {}, + }, +}; + +const createRecipeFunctionDeclaration = { + name: "create_recipe", + description: + "Creates a new recipe and adds it to the user's recipe collection. Use this when the user asks you to create or save a recipe.", + parameters: { + type: Type.OBJECT, + properties: { + name: { + type: Type.STRING, + description: "The name of the recipe", + }, + description: { + type: Type.STRING, + description: "A brief description of the recipe", + }, + instructions: { + type: Type.STRING, + description: + "Step-by-step cooking instructions, separated by newlines or numbered", + }, + prep_time: { + type: Type.NUMBER, + description: "Preparation time in minutes", + }, + cook_time: { + type: Type.NUMBER, + description: "Cooking time in minutes", + }, + difficulty: { + type: Type.STRING, + enum: ["easy", "medium", "hard"], + description: "The difficulty level of the recipe", + }, + servings: { + type: Type.NUMBER, + description: "Number of servings this recipe makes", + }, + cuisine_type: { + type: Type.STRING, + description: "The type of cuisine (e.g., Italian, Mexican, Chinese)", + }, + ingredients: { + type: Type.ARRAY, + items: { + type: Type.OBJECT, + properties: { + ingredient_name: { + type: Type.STRING, + description: "Name of the ingredient", + }, + quantity: { + type: Type.NUMBER, + description: "Quantity of the ingredient", + }, + unit: { + type: Type.STRING, + description: + "Unit of measurement (e.g., cups, tbsp, tsp, oz, g, pieces)", + }, + notes: { + type: Type.STRING, + description: "Optional notes about the ingredient", + }, + }, + required: ["ingredient_name", "quantity", "unit"], + }, + description: "List of ingredients needed for the recipe", + }, + }, + required: ["name", "instructions", "ingredients", "difficulty", "servings"], + }, +}; + +const addIngredientFunctionDeclaration = { + name: "add_ingredient", + description: + "Adds a new ingredient to the user's inventory. Use this when the user asks to add ingredients to their pantry/inventory.", + parameters: { + type: Type.OBJECT, + properties: { + name: { + type: Type.STRING, + description: "The name of the ingredient", + }, + quantity: { + type: Type.NUMBER, + description: "The quantity of the ingredient", + }, + unit: { + type: Type.STRING, + description: + "Unit of measurement (e.g., pieces, oz, lb, g, kg, cups, tbsp, tsp, ml, l)", + }, + category: { + type: Type.STRING, + enum: [ + "Dairy", + "Meat", + "Seafood", + "Produce", + "Bakery", + "Grains", + "Spices", + "Condiments", + "Beverages", + "Other", + ], + description: "The category of the ingredient", + }, + notes: { + type: Type.STRING, + description: "Optional notes about the ingredient", + }, + }, + required: ["name", "quantity", "unit"], + }, +}; + +const tools = [ + { + functionDeclarations: [ + getUserIngredientsFunctionDeclaration, + getUserProfileFunctionDeclaration, + createRecipeFunctionDeclaration, + addIngredientFunctionDeclaration, + ], + }, +]; + +export async function POST(request: NextRequest) { + const body = await request.json(); + const message = body.message; + const conversationHistory = body.conversationHistory; + const authToken = body.authToken; + const useDietaryPreferences = body.useDietaryPreferences || true; + const usePreferredCuisines = body.usePreferredCuisines || true; + const onlyInventoryIngredients = body.onlyInventoryIngredients || false; + + if (!authToken) { + return NextResponse.json( + { error: "Authentication required" }, + { status: 401 }, + ); + } + + // setup supabase + const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; + const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!; + const supabase = createClient(supabaseUrl, supabaseKey, { + global: { + headers: { + Authorization: `Bearer ${authToken}`, + }, + }, + }); + + // build convo for ai + const contents = []; + for (let i = 0; i < conversationHistory.length; i++) { + const msg = conversationHistory[i]; + contents.push({ + role: msg.role, + parts: [{ text: msg.text }], + }); + } + contents.push({ + role: "user", + parts: [{ text: message }], + }); + + // get instruction based on user prefs + const systemInstruction = getSystemInstruction( + useDietaryPreferences, + usePreferredCuisines, + onlyInventoryIngredients, + ); + + // call the gemini ai + let response = await ai.models.generateContent({ + model: "gemini-2.5-flash", + contents: contents, + config: { + systemInstruction: systemInstruction, + tools: tools, + temperature: 1.0, + }, + }); + + // check if ai wants to call functions + while (response.functionCalls && response.functionCalls.length > 0) { + const functionResults = []; + + for (let i = 0; i < response.functionCalls.length; i++) { + const functionCall = response.functionCalls[i]; + let result; + + if (functionCall.name === "get_user_ingredients") { + const ingredientsData = await supabase + .from("ingredients") + .select("*") + .order("category", { ascending: true }) + .order("name", { ascending: true }); + + if (ingredientsData.error) { + result = { error: ingredientsData.error.message }; + } else { + result = ingredientsData.data; + } + } else if (functionCall.name === "get_user_profile") { + const userData = await supabase.auth.getUser(); + if (userData.data.user) { + const profileData = await supabase + .from("profiles") + .select("*") + .eq("id", userData.data.user.id) + .single(); + + if (profileData.error) { + result = { error: profileData.error.message }; + } else { + result = profileData.data; + } + } + } else if (functionCall.name === "create_recipe") { + return NextResponse.json({ + text: response.text || "", + functionCalls: response.functionCalls.map((fc) => ({ + name: fc.name, + args: fc.args, + })), + }); + } else if (functionCall.name === "add_ingredient") { + return NextResponse.json({ + text: response.text || "", + functionCalls: response.functionCalls.map((fc) => ({ + name: fc.name, + args: fc.args, + })), + }); + } + + functionResults.push({ + name: functionCall.name, + result: result, + }); + } + + if (response.candidates && response.candidates[0]) { + if (response.candidates[0].content) { + contents.push({ + role: "model", + parts: response.candidates[0].content.parts, + }); + } + } + + const functionResponseParts = []; + for (let i = 0; i < functionResults.length; i++) { + functionResponseParts.push({ + functionResponse: { + name: functionResults[i].name, + response: { result: functionResults[i].result }, + }, + }); + } + contents.push({ + role: "user", + parts: functionResponseParts, + }); + + // calling the ai again with our results + response = await ai.models.generateContent({ + model: "gemini-2.5-flash", + contents: contents, + config: { + systemInstruction: systemInstruction, + tools: tools, + temperature: 1.0, + }, + }); + } + + return NextResponse.json({ + text: response.text, + }); +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index df40af7..d955009 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { supabase } from "@/database/supabase"; import { useAuth } from "@/contexts/AuthContext"; import { useRouter } from "next/navigation"; @@ -15,10 +15,11 @@ const LoginPage = () => { const { user } = useAuth(); const router = useRouter(); - if (user) { - router.push("/profile"); - return null; - } + useEffect(() => { + if (user) { + router.push("/profile"); + } + }, [user, router]); const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index b0eea45..28fda79 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -67,10 +67,10 @@ const ProfilePage = () => { }; await updateProfile(updatedProfileData); - await loadUserProfile(); setIsEditing(false); } catch (error) { console.error(error); + } finally { setIsSaving(false); } }; diff --git a/src/app/recipes/[id]/page.tsx b/src/app/recipes/[id]/page.tsx index f129fc5..76c63b6 100644 --- a/src/app/recipes/[id]/page.tsx +++ b/src/app/recipes/[id]/page.tsx @@ -258,14 +258,6 @@ const RecipeDetailPage = () => {

)} - - {recipeData.source === "ai" && ( -
-

- This recipe was generated by AI! -

-
- )} diff --git a/src/components/home/ChatBot.tsx b/src/components/home/ChatBot.tsx index 8e93e58..f73acb5 100644 --- a/src/components/home/ChatBot.tsx +++ b/src/components/home/ChatBot.tsx @@ -1,11 +1,32 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { motion } from "motion/react"; import Image from "next/image"; import logo from "@/public/home/cookCraftLogo.webp"; +import { sendMessageToAI, type AIMessage } from "@/lib/ai"; +import { createRecipe } from "@/database/api/recipes"; +import { createIngredient } from "@/database/api/ingredients"; + +interface Message { + role: "user" | "assistant" | "system"; + content: string; + loading?: boolean; +} + const ChatBot = () => { const fullText = "Welcome to CookCraft!"; const [displayedText, setDisplayedText] = useState(""); + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [conversationHistory, setConversationHistory] = useState( + [], + ); + const [useDietaryPreferences, setUseDietaryPreferences] = useState(true); + const [usePreferredCuisines, setUsePreferredCuisines] = useState(true); + const [onlyInventoryIngredients, setOnlyInventoryIngredients] = + useState(false); + const messagesEndRef = useRef(null); useEffect(() => { let index = 0; @@ -19,26 +40,253 @@ const ChatBot = () => { return () => clearInterval(interval); }, []); + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + const handleSendMessage = async () => { + if (!input.trim() || isLoading) return; + + const userMessage = input.trim(); + setInput(""); + + setMessages((prev) => [...prev, { role: "user", content: userMessage }]); + setIsLoading(true); + + setMessages((prev) => [ + ...prev, + { role: "assistant", content: "", loading: true }, + ]); + + try { + const aiResponse = await sendMessageToAI( + userMessage, + conversationHistory, + useDietaryPreferences, + usePreferredCuisines, + onlyInventoryIngredients, + ); + + const newHistory: AIMessage[] = [ + ...conversationHistory, + { role: "user", text: userMessage }, + ]; + + if (aiResponse.functionCalls && aiResponse.functionCalls.length > 0) { + for (const functionCall of aiResponse.functionCalls) { + try { + if (functionCall.name === "create_recipe") { + const recipeData = functionCall.args as { + name: string; + description?: string; + instructions: string; + prep_time?: number; + cook_time?: number; + difficulty: "easy" | "medium" | "hard"; + servings: number; + cuisine_type?: string; + ingredients: Array<{ + ingredient_name: string; + quantity: number; + unit: string; + notes?: string; + }>; + }; + + await createRecipe({ + name: recipeData.name, + description: recipeData.description || null, + instructions: recipeData.instructions, + prep_time: recipeData.prep_time || null, + cook_time: recipeData.cook_time || null, + difficulty: recipeData.difficulty, + rating: null, + servings: recipeData.servings, + cuisine_type: recipeData.cuisine_type || null, + source: "ai", + parent_recipe_id: null, + image_url: null, + ingredients: recipeData.ingredients.map((ing) => ({ + ingredient_name: ing.ingredient_name, + quantity: ing.quantity, + unit: ing.unit, + notes: ing.notes || null, + })), + }); + } else if (functionCall.name === "add_ingredient") { + const ingredientData = functionCall.args as { + name: string; + quantity: number; + unit: string; + category?: string; + notes?: string; + }; + + await createIngredient({ + name: ingredientData.name, + quantity: ingredientData.quantity, + unit: ingredientData.unit, + category: ingredientData.category || null, + notes: ingredientData.notes || null, + }); + } + } catch (error) { + console.error(`Error executing ${functionCall.name}:`, error); + } + } + + const finalText = aiResponse.text || "Done!"; + + setMessages((prev) => [ + ...prev.slice(0, -1), + { role: "assistant", content: finalText }, + ]); + + newHistory.push({ role: "model", text: finalText }); + } else { + const responseText = aiResponse.text; + + setMessages((prev) => [ + ...prev.slice(0, -1), + { role: "assistant", content: responseText }, + ]); + + newHistory.push({ role: "model", text: responseText }); + } + + setConversationHistory(newHistory); + } catch (error) { + console.error("Error:", error); + setMessages((prev) => [ + ...prev.slice(0, -1), + { + role: "system", + content: + "Sorry, I encountered an error. Please try again or check your authentication.", + }, + ]); + } finally { + setIsLoading(false); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; + return (
- - {displayedText} - - Cook Craft Logo - + {messages.length === 0 ? ( +
+ + {displayedText} + + Cook Craft Logo +
+ ) : ( +
+
+ Cook Craft Logo +

CookCraft AI

+
+ +
+ {messages.map((message, index) => ( +
+
+ {message.loading ? ( +
+
+
+
+
+ ) : ( +

{message.content}

+ )} +
+
+ ))} +
+
+
+ )} + +
+
+ setInput(e.target.value)} + onKeyPress={handleKeyPress} + disabled={isLoading} + className="border-cookcraft-olive focus:border-cookcraft-red flex-1 rounded-2xl border-3 p-4 text-2xl font-normal focus:outline-none disabled:opacity-50" + /> + +
+
+ + + +
+
); }; diff --git a/src/lib/ai.ts b/src/lib/ai.ts new file mode 100644 index 0000000..cf6dbf1 --- /dev/null +++ b/src/lib/ai.ts @@ -0,0 +1,53 @@ +import { supabase } from "@/database/supabase"; + +export interface AIMessage { + role: "user" | "model"; + text: string; +} + +export interface AIResponse { + text: string; + functionCalls?: Array<{ + name: string; + args: Record; + }>; +} + +export async function sendMessageToAI( + userMessage: string, + conversationHistory: AIMessage[] = [], + useDietaryPreferences: boolean = true, + usePreferredCuisines: boolean = true, + onlyInventoryIngredients: boolean = false, +): Promise { + // getting auth + const session = await supabase.auth.getSession(); + + if (!session.data.session) { + throw new Error("Not authenticated"); + } + + const token = session.data.session.access_token; + + const response = await fetch("/api/gemini", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + message: userMessage, + conversationHistory: conversationHistory, + authToken: token, + useDietaryPreferences: useDietaryPreferences, + usePreferredCuisines: usePreferredCuisines, + onlyInventoryIngredients: onlyInventoryIngredients, + }), + }); + + if (!response.ok) { + throw new Error("Failed to get AI response"); + } + + const data = await response.json(); + return data; +}