From 7e71f3a7e2df4e57768edcdbb8af6a0f6babb6b8 Mon Sep 17 00:00:00 2001 From: Jota Date: Thu, 14 Aug 2025 18:54:14 -0500 Subject: [PATCH 1/4] feat: integrate Google AI chat functionality - Added Google AI configuration to retrieve API key. - Implemented ChatService to handle chat interactions with Google AI. - Created Chat and FloatingChat components for user interaction. - Established chat context for managing chat state. - Updated docker-compose to include environment variables. - Added .env file support for sensitive configurations. - Enhanced package.json and package-lock.json with new dependencies. - Implemented chat page with meta information and user instructions. --- docker-compose.yml | 5 +- gemini.js | 27 +++ package-lock.json | 230 +++++++++++++++++++++++--- package.json | 1 + src/components/chat/chat.tsx | 132 +++++++++++++++ src/components/chat/floating-chat.tsx | 51 ++++++ src/components/chat/index.ts | 2 + src/config/google-ai.ts | 13 ++ src/contexts/chat.context.tsx | 104 ++++++++++++ src/routes/chat/+types/index.ts | 23 +++ src/routes/chat/index.tsx | 31 ++++ src/routes/root/index.tsx | 5 + src/services/chat.service.ts | 95 +++++++++++ 13 files changed, 698 insertions(+), 21 deletions(-) create mode 100644 gemini.js create mode 100644 src/components/chat/chat.tsx create mode 100644 src/components/chat/floating-chat.tsx create mode 100644 src/components/chat/index.ts create mode 100644 src/config/google-ai.ts create mode 100644 src/contexts/chat.context.tsx create mode 100644 src/routes/chat/+types/index.ts create mode 100644 src/routes/chat/index.tsx create mode 100644 src/services/chat.service.ts diff --git a/docker-compose.yml b/docker-compose.yml index 24243a7..c262845 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,8 +5,11 @@ services: dockerfile: Dockerfile ports: - "80:3000" + env_file: + - .env environment: DATABASE_URL: postgresql://postgres:letmein@db:5432/postgres?schema=public + NODE_ENV: production depends_on: db: condition: service_healthy @@ -28,4 +31,4 @@ services: retries: 5 volumes: - fs_data: \ No newline at end of file + fs_data: diff --git a/gemini.js b/gemini.js new file mode 100644 index 0000000..b805467 --- /dev/null +++ b/gemini.js @@ -0,0 +1,27 @@ +import { GoogleGenAI } from "@google/genai"; +import dotenv from "dotenv"; + +dotenv.config(); + +const ai = new GoogleGenAI({ + apiKey: process.env.VITE_GOOGLE_API_KEY || "", +}); + +async function main() { + const chat = ai.chats.create({ + model: "gemini-2.5-flash", + history: [], + }); + + const response1 = await chat.sendMessage({ + message: "I have 2 dogs in my house.", + }); + console.log("Chat response 1:", response1.text); + + const response2 = await chat.sendMessage({ + message: "How many paws are in my house?", + }); + console.log("Chat response 2:", response2.text); +} + +await main(); diff --git a/package-lock.json b/package-lock.json index 5f010ce..98f0eb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "fullstock-frontend", "version": "0.0.0", "dependencies": { + "@google/genai": "^1.13.0", "@hookform/resolvers": "^4.1.3", "@prisma/client": "^6.10.1", "@radix-ui/react-label": "^2.1.1", @@ -2149,7 +2150,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -2162,7 +2163,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -3046,6 +3047,27 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, + "node_modules/@google/genai": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.13.0.tgz", + "integrity": "sha512-BxilXzE8cJ0zt5/lXk6KwuBcIT9P2Lbi2WXhwWMbxf1RNeC68/8DmYQqMrzQP333CieRMdbDXs0eNCphLoScWg==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^9.14.2", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.11.0" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, "node_modules/@grpc/grpc-js": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", @@ -8550,14 +8572,14 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node18": { @@ -8780,7 +8802,7 @@ "version": "22.14.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -9736,7 +9758,7 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "devOptional": true, + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -9759,7 +9781,7 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -9772,7 +9794,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 14" @@ -10891,7 +10912,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -10954,7 +10974,6 @@ "version": "9.3.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "dev": true, "license": "MIT", "engines": { "node": "*" @@ -12618,7 +12637,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -13453,7 +13471,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -13491,7 +13508,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -14326,7 +14342,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true, "license": "MIT" }, "node_modules/fast-deep-equal": { @@ -14880,6 +14895,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -15178,6 +15236,53 @@ "node": ">=0.6.0" } }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library/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/google-auth-library/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/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/google-protobuf": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.6.1.tgz", @@ -15237,6 +15342,40 @@ "dev": true, "license": "MIT" }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/gtoken/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/gtoken/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/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -15510,7 +15649,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -16363,6 +16501,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -16773,7 +16923,6 @@ "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==", - "dev": true, "license": "MIT", "dependencies": { "bignumber.js": "^9.0.0" @@ -17337,7 +17486,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/make-fetch-happen": { @@ -18033,6 +18182,48 @@ "node": ">= 0.6" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-gyp": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.3.1.tgz", @@ -23100,7 +23291,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/unique-filename": { @@ -23309,7 +23500,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/v8-to-istanbul": { @@ -24040,7 +24231,6 @@ "version": "8.18.2", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index 460dd67..391167a 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "seed": "tsx ./prisma/seed.ts" }, "dependencies": { + "@google/genai": "^1.13.0", "@hookform/resolvers": "^4.1.3", "@prisma/client": "^6.10.1", "@radix-ui/react-label": "^2.1.1", diff --git a/src/components/chat/chat.tsx b/src/components/chat/chat.tsx new file mode 100644 index 0000000..162cf7b --- /dev/null +++ b/src/components/chat/chat.tsx @@ -0,0 +1,132 @@ +import { useState, useRef, useEffect } from "react"; +import { useChat } from "@/contexts/chat.context"; +import { ChatService, type ChatMessage } from "@/services/chat.service"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +interface ChatProps { + className?: string; +} + +export function Chat({ className }: ChatProps) { + const { state, addMessage, setLoading, setError } = useChat(); + const [input, setInput] = useState(""); + const [chatService] = useState(() => new ChatService()); + const messagesEndRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + useEffect(scrollToBottom, [state.messages]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!input.trim() || state.isLoading) return; + + const userMessage: ChatMessage = { + id: Date.now().toString(), + content: input.trim(), + role: "user", + timestamp: new Date(), + }; + + addMessage(userMessage); + setInput(""); + setLoading(true); + setError(null); + + try { + const response = await chatService.sendMessage(userMessage.content); + + if (response.success && response.message) { + const assistantMessage: ChatMessage = { + id: (Date.now() + 1).toString(), + content: response.message, + role: "assistant", + timestamp: new Date(), + }; + addMessage(assistantMessage); + } else { + setError(response.error || "Error al enviar el mensaje"); + } + } catch (error) { + setError("Error de conexión"); + } finally { + setLoading(false); + } + }; + + return ( +
+ {/* Header */} +
+

Chat con IA

+
+ + {/* Messages */} +
+ {state.messages.map((message) => ( +
+
+

{message.content}

+

+ {message.timestamp.toLocaleTimeString()} +

+
+
+ ))} + + {state.isLoading && ( +
+
+

Escribiendo...

+
+
+ )} + + {state.error && ( +
+
+

{state.error}

+
+
+ )} + +
+
+ + {/* Input */} +
+
+ setInput(e.target.value)} + placeholder="Escribe tu mensaje..." + className="flex-1 border rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" + disabled={state.isLoading} + /> + +
+
+
+ ); +} diff --git a/src/components/chat/floating-chat.tsx b/src/components/chat/floating-chat.tsx new file mode 100644 index 0000000..c975489 --- /dev/null +++ b/src/components/chat/floating-chat.tsx @@ -0,0 +1,51 @@ +import { useState } from "react"; +import { MessageCircle, X } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Chat } from "./chat"; +import { ChatProvider } from "@/contexts/chat.context"; + +interface FloatingChatProps { + className?: string; +} + +export function FloatingChat({ className }: FloatingChatProps) { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+ {/* Chat Toggle Button */} + {!isOpen && ( + + )} + + {/* Chat Window */} + {isOpen && ( +
+ +
+ {/* Close button */} + + + {/* Chat component */} + +
+
+
+ )} +
+ ); +} diff --git a/src/components/chat/index.ts b/src/components/chat/index.ts new file mode 100644 index 0000000..28fd47b --- /dev/null +++ b/src/components/chat/index.ts @@ -0,0 +1,2 @@ +export { Chat } from "./chat"; +export { FloatingChat } from "./floating-chat"; diff --git a/src/config/google-ai.ts b/src/config/google-ai.ts new file mode 100644 index 0000000..5e59b3b --- /dev/null +++ b/src/config/google-ai.ts @@ -0,0 +1,13 @@ +// Google AI configuration for browser environment +export const getGoogleApiKey = (): string => { + // En el navegador, solo podemos usar import.meta.env con variables VITE_ + const apiKey = + import.meta.env.VITE_GOOGLE_API_KEY || + "AIzaSyDWe2tTi2D6bx9VeWdJlczI99z_ipWP9b4"; // Fallback + + if (!apiKey) { + throw new Error("VITE_GOOGLE_API_KEY not found in environment"); + } + + return apiKey; +}; diff --git a/src/contexts/chat.context.tsx b/src/contexts/chat.context.tsx new file mode 100644 index 0000000..2af040c --- /dev/null +++ b/src/contexts/chat.context.tsx @@ -0,0 +1,104 @@ +import React, { createContext, useContext, useReducer } from "react"; +import type { ChatMessage } from "@/services/chat.service"; + +interface ChatState { + messages: ChatMessage[]; + isLoading: boolean; + error: string | null; +} + +type ChatAction = + | { type: "ADD_MESSAGE"; payload: ChatMessage } + | { type: "SET_LOADING"; payload: boolean } + | { type: "SET_ERROR"; payload: string | null } + | { type: "CLEAR_MESSAGES" }; + +const initialState: ChatState = { + messages: [], + isLoading: false, + error: null, +}; + +function chatReducer(state: ChatState, action: ChatAction): ChatState { + switch (action.type) { + case "ADD_MESSAGE": + return { + ...state, + messages: [...state.messages, action.payload], + }; + case "SET_LOADING": + return { + ...state, + isLoading: action.payload, + }; + case "SET_ERROR": + return { + ...state, + error: action.payload, + }; + case "CLEAR_MESSAGES": + return { + ...state, + messages: [], + error: null, + }; + default: + return state; + } +} + +interface ChatContextType { + state: ChatState; + addMessage: (message: ChatMessage) => void; + setLoading: (loading: boolean) => void; + setError: (error: string | null) => void; + clearMessages: () => void; +} + +const ChatContext = createContext(undefined); + +interface ChatProviderProps { + children: React.ReactNode; +} + +export function ChatProvider({ children }: ChatProviderProps) { + const [state, dispatch] = useReducer(chatReducer, initialState); + + const addMessage = (message: ChatMessage) => { + dispatch({ type: "ADD_MESSAGE", payload: message }); + }; + + const setLoading = (loading: boolean) => { + dispatch({ type: "SET_LOADING", payload: loading }); + }; + + const setError = (error: string | null) => { + dispatch({ type: "SET_ERROR", payload: error }); + }; + + const clearMessages = () => { + dispatch({ type: "CLEAR_MESSAGES" }); + }; + + return ( + + {children} + + ); +} + +export function useChat() { + const context = useContext(ChatContext); + if (context === undefined) { + throw new Error("useChat must be used within a ChatProvider"); + } + return context; +} diff --git a/src/routes/chat/+types/index.ts b/src/routes/chat/+types/index.ts new file mode 100644 index 0000000..eb07b47 --- /dev/null +++ b/src/routes/chat/+types/index.ts @@ -0,0 +1,23 @@ +import type { MetaFunction } from "react-router"; + +// This file is generated by react-router +export interface RouteArgs {} + +export interface LoaderArgs extends RouteArgs { + request: Request; +} + +export interface ActionArgs extends RouteArgs { + request: Request; +} + +export interface MetaArgs extends RouteArgs {} + +export type Meta = MetaFunction; + +// Loader function (placeholder for now) +async function loader(_args: LoaderArgs) { + return null; +} + +export { loader }; diff --git a/src/routes/chat/index.tsx b/src/routes/chat/index.tsx new file mode 100644 index 0000000..a7c5eda --- /dev/null +++ b/src/routes/chat/index.tsx @@ -0,0 +1,31 @@ +import type { Route } from "./+types/index"; +import { Chat } from "@/components/chat/chat"; +import { ChatProvider } from "@/contexts/chat.context"; + +export function meta({}: Route.MetaArgs) { + return [ + { title: "Chat con IA | Fullstock" }, + { + name: "description", + content: "Chatea con nuestra IA para obtener ayuda", + }, + ]; +} + +export default function ChatPage() { + return ( + +
+
+

Chat con IA

+

+ ¡Hola! Soy tu asistente de IA. Puedo ayudarte con preguntas sobre + productos, recomendaciones, o cualquier otra consulta relacionada + con Fullstock. +

+ +
+
+
+ ); +} diff --git a/src/routes/root/index.tsx b/src/routes/root/index.tsx index 99b4610..8cfbcdc 100644 --- a/src/routes/root/index.tsx +++ b/src/routes/root/index.tsx @@ -16,6 +16,7 @@ import { Section, Separator, } from "@/components/ui"; +import { FloatingChat } from "@/components/chat/floating-chat"; import { getCart } from "@/lib/cart"; import type { CartWithItems } from "@/models/cart.model"; import { getCurrentUser } from "@/services/auth.service"; @@ -186,6 +187,10 @@ export default function Root({ loaderData }: Route.ComponentProps) { + + {/* Floating Chat */} + +
); diff --git a/src/services/chat.service.ts b/src/services/chat.service.ts new file mode 100644 index 0000000..23e6ee0 --- /dev/null +++ b/src/services/chat.service.ts @@ -0,0 +1,95 @@ +import { GoogleGenAI } from "@google/genai"; +import { getGoogleApiKey } from "@/config/google-ai"; + +export interface ChatMessage { + id: string; + content: string; + role: "user" | "assistant"; + timestamp: Date; +} + +export interface ChatResponse { + success: boolean; + message?: string; + error?: string; +} + +export class ChatService { + private ai: GoogleGenAI; + private chat: any; + + constructor() { + const apiKey = getGoogleApiKey(); + + this.ai = new GoogleGenAI({ + apiKey: apiKey, + }); + + this.initializeChat(); + } + + private initializeChat() { + const systemPrompt = `Eres un asistente virtual de FullStock, una tienda online que vende productos personalizados como polos, tazas y stickers. + +Tu objetivo es ayudar a los clientes con: +- Información sobre productos (polos, tazas, stickers) +- Proceso de compra y checkout +- Preguntas sobre envíos y devoluciones +- Recomendaciones de productos +- Soporte general + +Mantén un tono amigable y profesional. Si no puedes ayudar con algo específico, deriva al cliente al soporte humano. + +La tienda tiene las siguientes categorías principales: +- Polos: Ropa personalizada de alta calidad +- Tazas: Tazas personalizadas para diferentes ocasiones +- Stickers: Adhesivos personalizados y creativos + +Responde de manera concisa y útil.`; + + this.chat = this.ai.chats.create({ + model: "gemini-2.5-flash", + history: [ + { + role: "user", + parts: [{ text: systemPrompt }], + }, + { + role: "model", + parts: [ + { + text: "¡Hola! Soy el asistente virtual de FullStock. Estoy aquí para ayudarte con cualquier pregunta sobre nuestros productos, proceso de compra, o cualquier otra consulta. ¿En qué puedo ayudarte hoy?", + }, + ], + }, + ], + }); + } + + async sendMessage(message: string): Promise { + try { + if (!this.chat) { + this.initializeChat(); + } + + const response = await this.chat.sendMessage({ + message: message, + }); + + return { + success: true, + message: response.text, + }; + } catch (error) { + console.error("Error sending message to Gemini:", error); + return { + success: false, + error: error instanceof Error ? error.message : "Error desconocido", + }; + } + } + + resetChat() { + this.initializeChat(); + } +} From 017a590f6841cb39860830a07fb9003bacab7511 Mon Sep 17 00:00:00 2001 From: Jota Date: Thu, 14 Aug 2025 19:09:03 -0500 Subject: [PATCH 2/4] refactor: remove chat route and related components --- src/routes/chat/+types/index.ts | 23 ----------------------- src/routes/chat/index.tsx | 31 ------------------------------- 2 files changed, 54 deletions(-) delete mode 100644 src/routes/chat/+types/index.ts delete mode 100644 src/routes/chat/index.tsx diff --git a/src/routes/chat/+types/index.ts b/src/routes/chat/+types/index.ts deleted file mode 100644 index eb07b47..0000000 --- a/src/routes/chat/+types/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { MetaFunction } from "react-router"; - -// This file is generated by react-router -export interface RouteArgs {} - -export interface LoaderArgs extends RouteArgs { - request: Request; -} - -export interface ActionArgs extends RouteArgs { - request: Request; -} - -export interface MetaArgs extends RouteArgs {} - -export type Meta = MetaFunction; - -// Loader function (placeholder for now) -async function loader(_args: LoaderArgs) { - return null; -} - -export { loader }; diff --git a/src/routes/chat/index.tsx b/src/routes/chat/index.tsx deleted file mode 100644 index a7c5eda..0000000 --- a/src/routes/chat/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import type { Route } from "./+types/index"; -import { Chat } from "@/components/chat/chat"; -import { ChatProvider } from "@/contexts/chat.context"; - -export function meta({}: Route.MetaArgs) { - return [ - { title: "Chat con IA | Fullstock" }, - { - name: "description", - content: "Chatea con nuestra IA para obtener ayuda", - }, - ]; -} - -export default function ChatPage() { - return ( - -
-
-

Chat con IA

-

- ¡Hola! Soy tu asistente de IA. Puedo ayudarte con preguntas sobre - productos, recomendaciones, o cualquier otra consulta relacionada - con Fullstock. -

- -
-
-
- ); -} From be8f2e5150e864d7bf13cf52b5cba4ec45c019df Mon Sep 17 00:00:00 2001 From: Jota Date: Thu, 14 Aug 2025 19:36:44 -0500 Subject: [PATCH 3/4] fix: update start script to include dotenv for environment variables --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 391167a..6c1a792 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lint": "eslint .", "preview": "vite preview", "start:local": "dotenv -e .env.test -- react-router-serve ./build/server/index.js", - "start": "react-router-serve ./build/server/index.js", + "start": "dotenv -e .env -- react-router-serve ./build/server/index.js", "type-check": "react-router typegen && tsc", "test": "vitest", "test:load:local": "dotenv -e .env.test -- sh -c 'npx artillery run ./src/tests/test-users.yml --record --key $ARTILLERY_API_KEY'", From 98df3c0b577aa38f729b31983dc80c0d449c3634 Mon Sep 17 00:00:00 2001 From: Jota Date: Fri, 15 Aug 2025 20:16:00 -0500 Subject: [PATCH 4/4] feat: implement chat functionality with logging and initialization checks --- .env.docker | 0 src/components/chat/chat.tsx | 65 +++++- src/components/chat/floating-chat.tsx | 29 ++- src/routes/chat/+types/index.ts | 3 + src/routes/chat/index.tsx | 15 ++ src/services/chat.service.ts | 290 ++++++++++++++++++++++---- 6 files changed, 362 insertions(+), 40 deletions(-) create mode 100644 .env.docker create mode 100644 src/routes/chat/+types/index.ts create mode 100644 src/routes/chat/index.tsx diff --git a/.env.docker b/.env.docker new file mode 100644 index 0000000..e69de29 diff --git a/src/components/chat/chat.tsx b/src/components/chat/chat.tsx index 162cf7b..396e619 100644 --- a/src/components/chat/chat.tsx +++ b/src/components/chat/chat.tsx @@ -9,11 +9,30 @@ interface ChatProps { } export function Chat({ className }: ChatProps) { + console.log("💬 Chat: Componente renderizando..."); + const { state, addMessage, setLoading, setError } = useChat(); const [input, setInput] = useState(""); - const [chatService] = useState(() => new ChatService()); + const [chatService] = useState(() => { + console.log("💬 Chat: Creando nueva instancia de ChatService..."); + return new ChatService(); + }); const messagesEndRef = useRef(null); + useEffect(() => { + console.log("💬 Chat: Componente montado"); + + // Check service status after mount + setTimeout(() => { + const status = chatService.getInitializationStatus(); + console.log("💬 Chat: Estado del servicio después del montaje:", status); + }, 1000); + + return () => { + console.log("💬 Chat: Componente desmontado"); + }; + }, [chatService]); + const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; @@ -22,7 +41,12 @@ export function Chat({ className }: ChatProps) { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - if (!input.trim() || state.isLoading) return; + console.log("💬 Chat: Enviando mensaje:", input.trim()); + + if (!input.trim() || state.isLoading) { + console.log("💬 Chat: Mensaje vacío o cargando, cancelando envío"); + return; + } const userMessage: ChatMessage = { id: Date.now().toString(), @@ -31,13 +55,29 @@ export function Chat({ className }: ChatProps) { timestamp: new Date(), }; + console.log("💬 Chat: Mensaje del usuario creado:", userMessage); addMessage(userMessage); setInput(""); setLoading(true); setError(null); try { + console.log( + "💬 Chat: Verificando estado del servicio antes de enviar..." + ); + const status = chatService.getInitializationStatus(); + console.log("💬 Chat: Estado del servicio:", status); + + if (!status.isInitialized) { + console.log( + "💬 Chat: Servicio no inicializado, forzando inicialización..." + ); + await chatService.forceInitialization(); + } + + console.log("💬 Chat: Enviando mensaje al servicio..."); const response = await chatService.sendMessage(userMessage.content); + console.log("💬 Chat: Respuesta recibida:", response); if (response.success && response.message) { const assistantMessage: ChatMessage = { @@ -46,26 +86,47 @@ export function Chat({ className }: ChatProps) { role: "assistant", timestamp: new Date(), }; + console.log("💬 Chat: Mensaje del asistente creado:", assistantMessage); addMessage(assistantMessage); } else { + console.error("💬 Chat: Error en la respuesta:", response.error); setError(response.error || "Error al enviar el mensaje"); } } catch (error) { + console.error("💬 Chat: Error de conexión:", error); setError("Error de conexión"); } finally { setLoading(false); + console.log("💬 Chat: Proceso de envío completado"); } }; + console.log("💬 Chat: Estado actual:", { + messagesCount: state.messages.length, + isLoading: state.isLoading, + error: state.error, + }); + return (
{/* Header */}

Chat con IA

+

+ Debug: {state.messages.length} mensajes +

{/* Messages */}
+ {state.messages.length === 0 && !state.isLoading && ( +
+

+ ¡Hola! Escribe un mensaje para comenzar +

+
+ )} + {state.messages.map((message) => (
{ + console.log("🎈 FloatingChat: Componente montado"); + return () => { + console.log("🎈 FloatingChat: Componente desmontado"); + }; + }, []); + + useEffect(() => { + console.log("🎈 FloatingChat: Estado isOpen cambió a:", isOpen); + }, [isOpen]); + + const handleToggleOpen = () => { + console.log("🎈 FloatingChat: Botón clickeado, abriendo chat..."); + setIsOpen(true); + }; + + const handleClose = () => { + console.log("🎈 FloatingChat: Cerrando chat..."); + setIsOpen(false); + }; + + console.log("🎈 FloatingChat: Renderizando, isOpen:", isOpen); + return (
{/* Chat Toggle Button */} {!isOpen && (