diff --git a/.env.docker b/.env.docker new file mode 100644 index 0000000..e69de29 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..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'", @@ -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..396e619 --- /dev/null +++ b/src/components/chat/chat.tsx @@ -0,0 +1,193 @@ +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) { + console.log("💬 Chat: Componente renderizando..."); + + const { state, addMessage, setLoading, setError } = useChat(); + const [input, setInput] = useState(""); + 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" }); + }; + + useEffect(scrollToBottom, [state.messages]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + 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(), + content: input.trim(), + role: "user", + 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 = { + id: (Date.now() + 1).toString(), + content: response.message, + 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) => ( +
+
+

{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..7d0e24d --- /dev/null +++ b/src/components/chat/floating-chat.tsx @@ -0,0 +1,74 @@ +import { useState, useEffect } 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); + + useEffect(() => { + 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 && ( + + )} + + {/* 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..004d463 --- /dev/null +++ b/src/routes/chat/+types/index.ts @@ -0,0 +1,3 @@ +export interface ChatPageData { + // Add any data types needed for the chat page +} diff --git a/src/routes/chat/index.tsx b/src/routes/chat/index.tsx new file mode 100644 index 0000000..36ac16f --- /dev/null +++ b/src/routes/chat/index.tsx @@ -0,0 +1,15 @@ +import { Chat } from "@/components/chat/chat"; +import { ChatProvider } from "@/contexts/chat.context"; + +export default function ChatPage() { + return ( +
+
+

Chat de Soporte

+ + + +
+
+ ); +} 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..a43a4ae --- /dev/null +++ b/src/services/chat.service.ts @@ -0,0 +1,315 @@ +import { GoogleGenAI } from "@google/genai"; +import { getGoogleApiKey } from "@/config/google-ai"; +import { getAllCategories } from "./category.service"; +import { getAllProducts } from "./product.service"; + +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; + private categoriesData: any[] = []; + private productsData: any[] = []; + private isInitialized: boolean = false; + private initializationPromise: Promise | null = null; + private static instance: ChatService | null = null; + + constructor() { + console.log("🔧 ChatService: Constructor iniciado"); + + // Check if this is a singleton pattern issue + if (ChatService.instance) { + console.log( + "⚠️ ChatService: Ya existe una instancia, usando la existente" + ); + return ChatService.instance; + } + + const apiKey = getGoogleApiKey(); + console.log( + "🔑 ChatService: API Key obtenida:", + apiKey ? "✅ Presente" : "❌ Faltante" + ); + + try { + this.ai = new GoogleGenAI({ + apiKey: apiKey, + }); + console.log("🤖 ChatService: GoogleGenAI inicializado correctamente"); + } catch (error) { + console.error("❌ ChatService: Error inicializando GoogleGenAI:", error); + throw error; + } + + // Start initialization but don't wait for it in constructor + console.log("🚀 ChatService: Iniciando carga de datos..."); + this.initializationPromise = this.loadDataAndInitialize(); + + ChatService.instance = this; + console.log("🆕 ChatService: Nueva instancia creada y guardada"); + } + + // Add method to check initialization status + public getInitializationStatus(): { + isInitialized: boolean; + hasChat: boolean; + hasAI: boolean; + categoriesCount: number; + productsCount: number; + } { + const status = { + isInitialized: this.isInitialized, + hasChat: !!this.chat, + hasAI: !!this.ai, + categoriesCount: this.categoriesData.length, + productsCount: this.productsData.length, + }; + + console.log("📊 ChatService: Status actual:", status); + return status; + } + + // Add method to force initialization for debugging + public async forceInitialization(): Promise { + console.log("🔄 ChatService: Forzando inicialización..."); + this.isInitialized = false; + this.initializationPromise = this.loadDataAndInitialize(); + await this.initializationPromise; + console.log("✅ ChatService: Inicialización forzada completada"); + } + + private async loadDataAndInitialize() { + console.log("📊 ChatService: Cargando datos de categorías y productos..."); + + try { + // Add timeout to avoid hanging + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error("Timeout loading data")), 10000) + ); + + const dataPromise = Promise.all([getAllCategories(), getAllProducts()]); + + const [categoriesResponse, productsResponse] = (await Promise.race([ + dataPromise, + timeoutPromise, + ])) as any[]; + + console.log( + "📁 ChatService: Categorías cargadas:", + categoriesResponse?.length || 0 + ); + console.log( + "🛍️ ChatService: Productos cargados:", + productsResponse?.length || 0 + ); + + this.categoriesData = categoriesResponse || []; + this.productsData = productsResponse || []; + } catch (error) { + console.error("⚠️ ChatService: Error cargando datos:", error); + // Continue with default data if services fail + this.categoriesData = []; + this.productsData = []; + } + + console.log("🔄 ChatService: Inicializando chat..."); + try { + this.initializeChat(); + console.log("✅ ChatService: Chat inicializado correctamente"); + } catch (error) { + console.error("❌ ChatService: Error inicializando chat:", error); + throw error; + } + + this.isInitialized = true; + console.log("🎉 ChatService: Inicialización completa"); + } + + private initializeChat() { + console.log("🔨 ChatService: Construyendo prompt del sistema..."); + + // Build dynamic product information + const categoriesInfo = + this.categoriesData.length > 0 + ? this.categoriesData + .map( + (cat) => + `- ${cat.name}: ${ + cat.description || "Productos de alta calidad" + }` + ) + .join("\n") + : `- Polos: Ropa personalizada de alta calidad +- Tazas: Tazas personalizadas para diferentes ocasiones +- Stickers: Adhesivos personalizados y creativos`; + + const productsInfo = + this.productsData.length > 0 + ? this.productsData + .slice(0, 10) + .map( + (product) => + `- ${product.name}: $${product.price} - ${ + product.description || "Producto disponible" + }` + ) + .join("\n") + : "Consulta nuestro catálogo completo de productos personalizados."; + + 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 + +CATEGORÍAS DISPONIBLES: +${categoriesInfo} + +PRODUCTOS DESTACADOS: +${productsInfo} + +Mantén un tono amigable y profesional. Si no puedes ayudar con algo específico, deriva al cliente al soporte humano. + +Responde de manera concisa y útil.`; + + console.log( + "📝 ChatService: Prompt generado. Longitud:", + systemPrompt.length + ); + console.log( + "📝 ChatService: Categorías info:", + categoriesInfo.substring(0, 100) + "..." + ); + console.log( + "📝 ChatService: Productos info:", + productsInfo.substring(0, 100) + "..." + ); + + try { + console.log( + "🔗 ChatService: Creando chat con modelo gemini-2.5-flash..." + ); + + // Updated API structure for Google GenAI + 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?", + }, + ], + }, + ], + }); + + console.log("✅ ChatService: Chat creado exitosamente"); + } catch (error) { + console.error("❌ ChatService: Error creando chat:", error); + console.error("❌ ChatService: Detalles del error:", { + error, + }); + throw error; + } + } + + async sendMessage(message: string): Promise { + console.log( + "💬 ChatService: Enviando mensaje:", + message.substring(0, 50) + "..." + ); + console.log( + "🔍 ChatService: Estado antes de enviar:", + this.getInitializationStatus() + ); + + try { + // Wait for initialization to complete + if (!this.isInitialized && this.initializationPromise) { + console.log("⏳ ChatService: Esperando inicialización..."); + await this.initializationPromise; + console.log("✅ ChatService: Inicialización completada"); + } + + if (!this.chat) { + console.error("❌ ChatService: Chat no inicializado"); + console.log( + "🔍 ChatService: Estado final:", + this.getInitializationStatus() + ); + throw new Error("Chat not properly initialized"); + } + + console.log("📤 ChatService: Enviando mensaje a Gemini..."); + + // Add different API call methods to test + let result; + try { + result = await this.chat.sendMessage(message); + } catch (apiError) { + console.error( + "❌ ChatService: Error con sendMessage, intentando sendMessageStream:", + apiError + ); + // Try alternative method + result = await this.chat.sendMessage({ message: message }); + } + + console.log("📥 ChatService: Respuesta recibida de Gemini"); + + const response = await result.response; + const responseText = response.text(); + + console.log("✅ ChatService: Mensaje procesado exitosamente"); + console.log( + "📝 ChatService: Respuesta:", + responseText.substring(0, 100) + "..." + ); + + return { + success: true, + message: responseText, + }; + } catch (error) { + console.error("❌ ChatService: Error enviando mensaje:", error); + console.error("❌ ChatService: Detalles completos:", { + error, + isInitialized: this.isInitialized, + chatExists: !!this.chat, + }); + + return { + success: false, + error: error instanceof Error ? error.message : "Error desconocido", + }; + } + } + + async resetChat() { + console.log("🔄 ChatService: Reiniciando chat..."); + this.isInitialized = false; + this.initializationPromise = this.loadDataAndInitialize(); + await this.initializationPromise; + console.log("✅ ChatService: Chat reiniciado"); + } +}