From 442ce8bd32df4521d282c8e6a68a51dbff6fe359 Mon Sep 17 00:00:00 2001 From: "nkt.pngd" Date: Sun, 9 Nov 2025 14:30:28 +0200 Subject: [PATCH] Add chatbot --- backend/compose.yaml | 6 +- frontend/package.json | 2 + frontend/pnpm-lock.yaml | 518 ++++++++++++++++++ frontend/src/components/Chatbot/Chatbot.tsx | 511 +++++++++++++++++ frontend/src/components/Chatbot/index.ts | 1 + frontend/src/i18n/locales/en.json | 5 + frontend/src/i18n/locales/ru.json | 5 + frontend/src/i18n/locales/uk.json | 5 + .../pages/GeneralTest/GeneralTestResult.tsx | 155 +++--- frontend/src/plugins/chat-api-plugin.ts | 244 +++++++++ frontend/src/vite-env.d.ts | 2 + frontend/tsconfig.node.json | 2 +- frontend/vite.config.ts | 3 +- 13 files changed, 1391 insertions(+), 68 deletions(-) create mode 100644 frontend/src/components/Chatbot/Chatbot.tsx create mode 100644 frontend/src/components/Chatbot/index.ts create mode 100644 frontend/src/plugins/chat-api-plugin.ts diff --git a/backend/compose.yaml b/backend/compose.yaml index ba9ac67..88a65f2 100644 --- a/backend/compose.yaml +++ b/backend/compose.yaml @@ -4,7 +4,7 @@ image: ptsdetect-api environment: ASPNETCORE_URLS: "http://+:5000" - MongoDbOptions__ConnectionString: "mongodb://root:9BjFZXmE2d@mongo" + MongoDbOptions__ConnectionString: "mongodb+srv://nikitapanagoda16_db_user:wnPJJIIXVWDcRJAK@appdb.uifa92k.mongodb.net/?retryWrites=true&w=majority" MongoDbOptions__AppDatabaseName: "AppDB" JwtOptions__Issuer: "http://localhost:5000" JwtOptions__Audience: "http://localhost:5173" @@ -28,8 +28,8 @@ mongo: image: mongo:7.0.3 environment: - MONGO_INITDB_ROOT_USERNAME: root - MONGO_INITDB_ROOT_PASSWORD: 9BjFZXmE2d + MONGO_INITDB_ROOT_USERNAME: nikitapanagoda16_db_user + MONGO_INITDB_ROOT_PASSWORD: wnPJJIIXVWDcRJAK healthcheck: test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet interval: 10s diff --git a/frontend/package.json b/frontend/package.json index dd738e7..2914f2d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,7 +20,9 @@ "@fontsource-variable/inter": "^5.0.16", "@hookform/resolvers": "^3.3.2", "@mui/joy": "5.0.0-beta.18", + "@mui/material": "^5.14.20", "graphql": "^16.8.1", + "openai": "^4.20.0", "i18next": "^23.11.4", "immer": "^10.0.3", "jwt-decode": "^4.0.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 0388567..5bd2d0c 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: '@mui/joy': specifier: 5.0.0-beta.18 version: 5.0.0-beta.18(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@mui/material': + specifier: ^5.14.20 + version: 5.18.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) graphql: specifier: ^16.8.1 version: 16.8.1 @@ -41,6 +44,9 @@ importers: lucide-react: specifier: ^0.297.0 version: 0.297.0(react@18.2.0) + openai: + specifier: ^4.20.0 + version: 4.104.0(ws@8.15.1)(zod@3.25.76) react: specifier: ^18.2.0 version: 18.2.0 @@ -489,15 +495,24 @@ packages: '@emotion/cache@11.11.0': resolution: {integrity: sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==} + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + '@emotion/hash@0.9.1': resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==} + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + '@emotion/is-prop-valid@1.2.1': resolution: {integrity: sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==} '@emotion/memoize@0.8.1': resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + '@emotion/react@11.11.1': resolution: {integrity: sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==} peerDependencies: @@ -510,9 +525,15 @@ packages: '@emotion/serialize@1.1.2': resolution: {integrity: sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==} + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + '@emotion/sheet@1.2.2': resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==} + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + '@emotion/styled@11.11.0': resolution: {integrity: sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==} peerDependencies: @@ -523,6 +544,9 @@ packages: '@types/react': optional: true + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + '@emotion/unitless@0.8.1': resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} @@ -534,9 +558,15 @@ packages: '@emotion/utils@1.2.1': resolution: {integrity: sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==} + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + '@emotion/weak-memoize@0.3.1': resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + '@esbuild/android-arm64@0.19.9': resolution: {integrity: sha512-q4cR+6ZD0938R19MyEW3jEsMzbb/1rulLXiNAJQADD/XYp7pT+rOS5JGxvpRW8dFDEfjW4wLgC/3FXIw4zYglQ==} engines: {node: '>=12'} @@ -968,6 +998,9 @@ packages: '@mui/core-downloads-tracker@5.15.0': resolution: {integrity: sha512-NpGtlHwuyLfJtdrlERXb8qRqd279O0VnuGaZAor1ehdNhUJOD1bSxHDeXKZkbqNpvi50hasFj7lsbTpluworTQ==} + '@mui/core-downloads-tracker@5.18.0': + resolution: {integrity: sha512-jbhwoQ1AY200PSSOrNXmrFCaSDSJWP7qk6urkTmIirvRXDROkqe+QwcLlUiw/PrREwsIF/vm3/dAXvjlMHF0RA==} + '@mui/joy@5.0.0-beta.18': resolution: {integrity: sha512-TxEo7kqEnbjB5S8cyFrytWjzhxW12UxkEJOT0QM8WpwaBN3Ie1okFuo2bnFW94vYFZperW97/H/08cqqS/2JPA==} engines: {node: '>=12.0.0'} @@ -985,6 +1018,23 @@ packages: '@types/react': optional: true + '@mui/material@5.18.0': + resolution: {integrity: sha512-bbH/HaJZpFtXGvWg3TsBWG4eyt3gah3E7nCNU8GLyRjVoWcA91Vm/T+sjHfUcwgJSw9iLtucfHBoq+qW/T30aA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + '@mui/private-theming@5.15.0': resolution: {integrity: sha512-7WxtIhXxNek0JjtsYy+ut2LtFSLpsUW5JSDehQO+jF7itJ8ehy7Bd9bSt2yIllbwGjCFowLfYpPk2Ykgvqm1tA==} engines: {node: '>=12.0.0'} @@ -995,6 +1045,16 @@ packages: '@types/react': optional: true + '@mui/private-theming@5.17.1': + resolution: {integrity: sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@mui/styled-engine@5.15.0': resolution: {integrity: sha512-6NysIsHkuUS2lF+Lzv1jiK3UjBJk854/vKVcJQVGKlPiqNEVZJNlwaSpsaU5xYXxWEZYfbVFSAomLOS/LV/ovQ==} engines: {node: '>=12.0.0'} @@ -1008,6 +1068,19 @@ packages: '@emotion/styled': optional: true + '@mui/styled-engine@5.18.0': + resolution: {integrity: sha512-BN/vKV/O6uaQh2z5rXV+MBlVrEkwoS/TK75rFQ2mjxA7+NBo8qtTAOA4UaM0XeJfn7kh2wZ+xQw2HAx0u+TiBg==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@mui/system@5.15.0': resolution: {integrity: sha512-8TPjfTlYBNB7/zBJRL4QOD9kImwdZObbiYNh0+hxvhXr2koezGx8USwPXj8y/JynbzGCkIybkUztCdWlMZe6OQ==} engines: {node: '>=12.0.0'} @@ -1024,6 +1097,22 @@ packages: '@types/react': optional: true + '@mui/system@5.18.0': + resolution: {integrity: sha512-ojZGVcRWqWhu557cdO3pWHloIGJdzVtxs3rk0F9L+x55LsUjcMUVkEhiF7E4TMxZoF9MmIHGGs0ZX3FDLAf0Xw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + '@mui/types@7.2.11': resolution: {integrity: sha512-KWe/QTEsFFlFSH+qRYf3zoFEj3z67s+qAuSnMMg+gFwbxG7P96Hm6g300inQL1Wy///gSRb8juX7Wafvp93m3w==} peerDependencies: @@ -1032,6 +1121,14 @@ packages: '@types/react': optional: true + '@mui/types@7.2.24': + resolution: {integrity: sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@mui/utils@5.15.0': resolution: {integrity: sha512-XSmTKStpKYamewxyJ256+srwEnsT3/6eNo6G7+WC1tj2Iq9GfUJ/6yUoB7YXjOD2jTZ3XobToZm4pVz1LBt6GA==} engines: {node: '>=12.0.0'} @@ -1042,6 +1139,16 @@ packages: '@types/react': optional: true + '@mui/utils@5.17.1': + resolution: {integrity: sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1257,6 +1364,12 @@ packages: '@types/json-stable-stringify@1.0.36': resolution: {integrity: sha512-b7bq23s4fgBB76n34m2b3RBf6M369B0Z9uRR8aHTMd8kZISRkmDEpPD8hhpYvDFzr3bJCPES96cm3Q6qRNDbQw==} + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/node@20.10.4': resolution: {integrity: sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==} @@ -1266,9 +1379,17 @@ packages: '@types/prop-types@15.7.11': resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + '@types/react-dom@18.2.17': resolution: {integrity: sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==} + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + '@types/react@18.2.45': resolution: {integrity: sha512-TtAxCNrlrBp8GoeEp1npd5g+d/OejJHFxS3OWmrPBMFaVQMSN0OFySozJio5BHxTuTeug00AVXVAjfDSfk+lUg==} @@ -1389,6 +1510,10 @@ packages: resolution: {integrity: sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==} engines: {node: '>=8'} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1407,6 +1532,10 @@ packages: resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} engines: {node: '>= 14'} + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -1486,6 +1615,9 @@ packages: asynciterator.prototype@1.0.0: resolution: {integrity: sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + auto-bind@4.0.0: resolution: {integrity: sha512-Hdw8qdNiqdJ8LqT0iK0sVzkFbzg6fhnQqqfWhBDxcHZvU75+B+ayzTy8x+k5Ix0Y92XOhOUlx74ps+bA6BeYMQ==} engines: {node: '>=8'} @@ -1560,6 +1692,10 @@ packages: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + call-bind@1.0.5: resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} @@ -1648,6 +1784,10 @@ packages: resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} engines: {node: '>=6'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -1664,6 +1804,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1764,6 +1908,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dependency-graph@0.11.0: resolution: {integrity: sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==} engines: {node: '>= 0.6.0'} @@ -1794,6 +1942,9 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} @@ -1805,6 +1956,10 @@ packages: resolution: {integrity: sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==} engines: {node: '>=4'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + electron-to-chromium@1.4.613: resolution: {integrity: sha512-r4x5+FowKG6q+/Wj0W9nidx7QO31BJwmR2uEo+Qh3YLGQ8SbBAFuDFpTxzly/I2gsbrFwBuIjrMp423L3O5U3w==} @@ -1822,13 +1977,29 @@ packages: resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} engines: {node: '>= 0.4'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-iterator-helpers@1.0.15: resolution: {integrity: sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + es-set-tostringtag@2.0.2: resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + es-shim-unscopables@1.0.2: resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} @@ -1926,6 +2097,10 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -2012,6 +2187,17 @@ packages: for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -2044,6 +2230,14 @@ packages: get-intrinsic@1.2.2: resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -2088,6 +2282,10 @@ packages: gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -2144,14 +2342,26 @@ packages: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + has-tostringtag@1.0.0: resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.0: resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} engines: {node: '>= 0.4'} + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + header-case@2.0.4: resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} @@ -2177,6 +2387,9 @@ packages: resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} engines: {node: '>=14.18.0'} + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + i18next@23.11.4: resolution: {integrity: sha512-CCUjtd5TfaCl+mLUzAA0uPSN+AVn4fP/kWCYt/hocPUwusTpMVczdrRyOBUwk6N05iH40qiKx6q1DoNJtBIwdg==} @@ -2539,6 +2752,10 @@ packages: resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} engines: {node: '>=0.10.0'} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2559,6 +2776,14 @@ packages: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -2594,6 +2819,11 @@ packages: no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -2681,6 +2911,18 @@ packages: resolution: {integrity: sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==} engines: {node: '>=14.16'} + openai@4.104.0: + resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + optimism@0.18.0: resolution: {integrity: sha512-tGn8+REwLRNFnb9WmcY5IfpOqeX2kpaYJ1s6Ae3mn12AeydLkR3j+jSCmVQFoXqU8D41PAJ1RG1rCRNWmNZVmQ==} @@ -2897,6 +3139,9 @@ packages: react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + react-is@19.2.0: + resolution: {integrity: sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==} + react-refresh@0.14.0: resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} engines: {node: '>=0.10.0'} @@ -2920,6 +3165,12 @@ packages: react: '>=16' react-dom: '>=16' + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} @@ -3424,6 +3675,10 @@ packages: resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} engines: {node: '>= 8'} + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + webcrypto-core@1.7.7: resolution: {integrity: sha512-7FjigXNsBfopEj+5DV2nhNpfic2vumtjjgPmeDKk45z+MJwXKKfhPB7118Pfzrmh4jqOMST6Ch37iPAHoImg5g==} @@ -3535,6 +3790,9 @@ packages: zen-observable@0.8.15: resolution: {integrity: sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==} + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zustand@4.4.7: resolution: {integrity: sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==} engines: {node: '>=12.7.0'} @@ -3979,14 +4237,26 @@ snapshots: '@emotion/weak-memoize': 0.3.1 stylis: 4.2.0 + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + '@emotion/hash@0.9.1': {} + '@emotion/hash@0.9.2': {} + '@emotion/is-prop-valid@1.2.1': dependencies: '@emotion/memoize': 0.8.1 '@emotion/memoize@0.8.1': {} + '@emotion/memoize@0.9.0': {} + '@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0)': dependencies: '@babel/runtime': 7.23.6 @@ -4009,8 +4279,18 @@ snapshots: '@emotion/utils': 1.2.1 csstype: 3.1.3 + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.1.3 + '@emotion/sheet@1.2.2': {} + '@emotion/sheet@1.4.0': {} + '@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0)': dependencies: '@babel/runtime': 7.23.6 @@ -4024,6 +4304,8 @@ snapshots: optionalDependencies: '@types/react': 18.2.45 + '@emotion/unitless@0.10.0': {} + '@emotion/unitless@0.8.1': {} '@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.2.0)': @@ -4032,8 +4314,12 @@ snapshots: '@emotion/utils@1.2.1': {} + '@emotion/utils@1.4.2': {} + '@emotion/weak-memoize@0.3.1': {} + '@emotion/weak-memoize@0.4.0': {} + '@esbuild/android-arm64@0.19.9': optional: true @@ -4625,6 +4911,8 @@ snapshots: '@mui/core-downloads-tracker@5.15.0': {} + '@mui/core-downloads-tracker@5.18.0': {} + '@mui/joy@5.0.0-beta.18(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.23.6 @@ -4642,6 +4930,27 @@ snapshots: '@emotion/styled': 11.11.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0) '@types/react': 18.2.45 + '@mui/material@5.18.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.5 + '@mui/core-downloads-tracker': 5.18.0 + '@mui/system': 5.18.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0) + '@mui/types': 7.2.24(@types/react@18.2.45) + '@mui/utils': 5.17.1(@types/react@18.2.45)(react@18.2.0) + '@popperjs/core': 2.11.8 + '@types/react-transition-group': 4.4.12(@types/react@18.2.45) + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 19.2.0 + react-transition-group: 4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + optionalDependencies: + '@emotion/react': 11.11.1(@types/react@18.2.45)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0) + '@types/react': 18.2.45 + '@mui/private-theming@5.15.0(@types/react@18.2.45)(react@18.2.0)': dependencies: '@babel/runtime': 7.23.6 @@ -4651,6 +4960,15 @@ snapshots: optionalDependencies: '@types/react': 18.2.45 + '@mui/private-theming@5.17.1(@types/react@18.2.45)(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.5 + '@mui/utils': 5.17.1(@types/react@18.2.45)(react@18.2.0) + prop-types: 15.8.1 + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.45 + '@mui/styled-engine@5.15.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.23.6 @@ -4662,6 +4980,18 @@ snapshots: '@emotion/react': 11.11.1(@types/react@18.2.45)(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0) + '@mui/styled-engine@5.18.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0))(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + optionalDependencies: + '@emotion/react': 11.11.1(@types/react@18.2.45)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0) + '@mui/system@5.15.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0)': dependencies: '@babel/runtime': 7.23.6 @@ -4678,10 +5008,30 @@ snapshots: '@emotion/styled': 11.11.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0) '@types/react': 18.2.45 + '@mui/system@5.18.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.5 + '@mui/private-theming': 5.17.1(@types/react@18.2.45)(react@18.2.0) + '@mui/styled-engine': 5.18.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0))(react@18.2.0) + '@mui/types': 7.2.24(@types/react@18.2.45) + '@mui/utils': 5.17.1(@types/react@18.2.45)(react@18.2.0) + clsx: 2.1.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + optionalDependencies: + '@emotion/react': 11.11.1(@types/react@18.2.45)(react@18.2.0) + '@emotion/styled': 11.11.0(@emotion/react@11.11.1(@types/react@18.2.45)(react@18.2.0))(@types/react@18.2.45)(react@18.2.0) + '@types/react': 18.2.45 + '@mui/types@7.2.11(@types/react@18.2.45)': optionalDependencies: '@types/react': 18.2.45 + '@mui/types@7.2.24(@types/react@18.2.45)': + optionalDependencies: + '@types/react': 18.2.45 + '@mui/utils@5.15.0(@types/react@18.2.45)(react@18.2.0)': dependencies: '@babel/runtime': 7.23.6 @@ -4692,6 +5042,18 @@ snapshots: optionalDependencies: '@types/react': 18.2.45 + '@mui/utils@5.17.1(@types/react@18.2.45)(react@18.2.0)': + dependencies: + '@babel/runtime': 7.24.5 + '@mui/types': 7.2.24(@types/react@18.2.45) + '@types/prop-types': 15.7.15 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 19.2.0 + optionalDependencies: + '@types/react': 18.2.45 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -4891,6 +5253,15 @@ snapshots: '@types/json-stable-stringify@1.0.36': {} + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 20.10.4 + form-data: 4.0.4 + + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + '@types/node@20.10.4': dependencies: undici-types: 5.26.5 @@ -4899,10 +5270,16 @@ snapshots: '@types/prop-types@15.7.11': {} + '@types/prop-types@15.7.15': {} + '@types/react-dom@18.2.17': dependencies: '@types/react': 18.2.45 + '@types/react-transition-group@4.4.12(@types/react@18.2.45)': + dependencies: + '@types/react': 18.2.45 + '@types/react@18.2.45': dependencies: '@types/prop-types': 15.7.11 @@ -5068,6 +5445,10 @@ snapshots: dependencies: tslib: 2.6.2 + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + acorn-jsx@5.3.2(acorn@8.11.2): dependencies: acorn: 8.11.2 @@ -5082,6 +5463,10 @@ snapshots: transitivePeerDependencies: - supports-color + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + aggregate-error@3.1.0: dependencies: clean-stack: 2.2.0 @@ -5182,6 +5567,8 @@ snapshots: dependencies: has-symbols: 1.0.3 + asynckit@0.4.0: {} + auto-bind@4.0.0: {} autoprefixer@10.4.16(postcss@8.4.32): @@ -5286,6 +5673,11 @@ snapshots: dependencies: streamsearch: 1.1.0 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + call-bind@1.0.5: dependencies: function-bind: 1.1.2 @@ -5399,6 +5791,8 @@ snapshots: clsx@2.0.0: {} + clsx@2.1.1: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -5413,6 +5807,10 @@ snapshots: colorette@2.0.20: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@4.1.1: {} common-tags@1.8.2: {} @@ -5510,6 +5908,8 @@ snapshots: has-property-descriptors: 1.0.1 object-keys: 1.1.1 + delayed-stream@1.0.0: {} + dependency-graph@0.11.0: {} detect-indent@6.1.0: {} @@ -5532,6 +5932,11 @@ snapshots: dependencies: esutils: 2.0.3 + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.24.5 + csstype: 3.1.3 + dot-case@3.0.4: dependencies: no-case: 3.0.4 @@ -5541,6 +5946,12 @@ snapshots: dset@3.1.3: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + electron-to-chromium@1.4.613: {} emoji-regex@8.0.0: {} @@ -5593,6 +6004,10 @@ snapshots: unbox-primitive: 1.0.2 which-typed-array: 1.1.13 + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-iterator-helpers@1.0.15: dependencies: asynciterator.prototype: 1.0.0 @@ -5610,12 +6025,23 @@ snapshots: iterator.prototype: 1.1.2 safe-array-concat: 1.0.1 + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + es-set-tostringtag@2.0.2: dependencies: get-intrinsic: 1.2.2 has-tostringtag: 1.0.0 hasown: 2.0.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + es-shim-unscopables@1.0.2: dependencies: hasown: 2.0.0 @@ -5768,6 +6194,8 @@ snapshots: esutils@2.0.3: {} + event-target-shim@5.0.1: {} + execa@5.1.1: dependencies: cross-spawn: 7.0.3 @@ -5884,6 +6312,21 @@ snapshots: dependencies: is-callable: 1.2.7 + form-data-encoder@1.7.2: {} + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + fraction.js@4.3.7: {} fs.realpath@1.0.0: {} @@ -5913,6 +6356,24 @@ snapshots: has-symbols: 1.0.3 hasown: 2.0.0 + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stream@6.0.1: {} get-symbol-description@1.0.0: @@ -5971,6 +6432,8 @@ snapshots: dependencies: get-intrinsic: 1.2.2 + gopd@1.2.0: {} + graphemer@1.4.0: {} graphql-config@5.0.3(@types/node@20.10.4)(graphql@16.8.1)(typescript@5.3.3): @@ -6027,14 +6490,24 @@ snapshots: has-symbols@1.0.3: {} + has-symbols@1.1.0: {} + has-tostringtag@1.0.0: dependencies: has-symbols: 1.0.3 + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.0.3 + hasown@2.0.0: dependencies: function-bind: 1.1.2 + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + header-case@2.0.4: dependencies: capital-case: 1.0.4 @@ -6066,6 +6539,10 @@ snapshots: human-signals@4.3.1: {} + humanize-ms@1.2.1: + dependencies: + ms: 2.1.2 + i18next@23.11.4: dependencies: '@babel/runtime': 7.23.6 @@ -6409,6 +6886,8 @@ snapshots: map-cache@0.2.2: {} + math-intrinsics@1.1.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -6422,6 +6901,12 @@ snapshots: braces: 3.0.2 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mimic-fn@2.1.0: {} mimic-fn@4.0.0: {} @@ -6453,6 +6938,8 @@ snapshots: lower-case: 2.0.2 tslib: 2.6.2 + node-domexception@1.0.0: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 @@ -6536,6 +7023,21 @@ snapshots: is-inside-container: 1.0.0 is-wsl: 2.2.0 + openai@4.104.0(ws@8.15.1)(zod@3.25.76): + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + optionalDependencies: + ws: 8.15.1 + zod: 3.25.76 + transitivePeerDependencies: + - encoding + optimism@0.18.0: dependencies: '@wry/caches': 1.0.1 @@ -6739,6 +7241,8 @@ snapshots: react-is@18.2.0: {} + react-is@19.2.0: {} + react-refresh@0.14.0: {} react-router-dom@6.21.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): @@ -6759,6 +7263,15 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + react-transition-group@4.4.5(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + dependencies: + '@babel/runtime': 7.24.5 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react@18.2.0: dependencies: loose-envify: 1.4.0 @@ -7288,6 +7801,8 @@ snapshots: web-streams-polyfill@3.2.1: {} + web-streams-polyfill@4.0.0-beta.3: {} + webcrypto-core@1.7.7: dependencies: '@peculiar/asn1-schema': 2.3.8 @@ -7425,6 +7940,9 @@ snapshots: zen-observable@0.8.15: {} + zod@3.25.76: + optional: true + zustand@4.4.7(@types/react@18.2.45)(immer@10.0.3)(react@18.2.0): dependencies: use-sync-external-store: 1.2.0(react@18.2.0) diff --git a/frontend/src/components/Chatbot/Chatbot.tsx b/frontend/src/components/Chatbot/Chatbot.tsx new file mode 100644 index 0000000..8f4d485 --- /dev/null +++ b/frontend/src/components/Chatbot/Chatbot.tsx @@ -0,0 +1,511 @@ +import { Box, CircularProgress, IconButton, Input, Sheet, Typography, useTheme } from '@mui/joy'; +import { ChevronLeft, ChevronRight, MessageCircle, Send, X } from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import useMediaQuery from '@mui/material/useMediaQuery'; + +interface ChatbotProps { + resultData: { + id: string; + completionDate: string; + potentialProblems: string[]; + adviceLists: { problem: string; advices: string[] }[]; + }; +} + +interface Message { + id: string; + role: 'user' | 'assistant'; + content: string; +} + +export const Chatbot = ({ resultData }: ChatbotProps) => { + const { t } = useTranslation(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const isTablet = useMediaQuery(theme.breakpoints.between('md', 'lg')); + const [isOpen, setIsOpen] = useState(!isMobile); + const [messages, setMessages] = useState([ + { + id: 'welcome', + role: 'assistant', + content: t('chatbot.welcome-message'), + }, + ]); + const [input, setInput] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const messagesEndRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + useEffect(() => { + // On mobile, start closed. On tablet/desktop, start open. + setIsOpen(!isMobile); + }, [isMobile]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!input.trim() || isLoading) return; + + const userMessage: Message = { + id: Date.now().toString(), + role: 'user', + content: input, + }; + + setMessages((prev) => [...prev, userMessage]); + setInput(''); + setIsLoading(true); + + try { + const response = await fetch('/api/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + messages: [...messages, userMessage].map((m) => ({ + role: m.role, + content: m.content, + })), + resultContext: { + potentialProblems: resultData.potentialProblems, + completionDate: resultData.completionDate, + adviceLists: resultData.adviceLists, + }, + }), + }); + + if (!response.ok) { + throw new Error('Failed to get response'); + } + + const data = await response.json(); + const assistantMessage: Message = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: data.message, + }; + + setMessages((prev) => [...prev, assistantMessage]); + } catch (error) { + console.error('Chat error:', error); + const errorMessage: Message = { + id: (Date.now() + 1).toString(), + role: 'assistant', + content: 'Sorry, I encountered an error. Please try again.', + }; + setMessages((prev) => [...prev, errorMessage]); + } finally { + setIsLoading(false); + } + }; + + // Mobile: Floating button + Modal + if (isMobile) { + if (!isOpen) { + return ( + + setIsOpen(true)} + sx={{ + width: 64, + height: 64, + borderRadius: '50%', + boxShadow: 'lg', + '&:hover': { + boxShadow: 'xl', + }, + }} + > + + + + ); + } + + return ( + + {/* Header */} + + + + {t('chatbot.title')} + + setIsOpen(false)}> + + + + + {/* Messages */} + + {messages.map((message) => ( + + + + {message.content} + + + + ))} + {isLoading && ( + + + + + + )} +
+ + + {/* Input */} + + setInput(e.target.value)} + placeholder={t('chatbot.input-placeholder')} + disabled={isLoading} + sx={{ flex: 1 }} + /> + + + + + + ); + } + + // Tablet: Collapsible sidebar with toggle button + if (isTablet) { + return ( + + {/* Toggle button when closed */} + {!isOpen && ( + setIsOpen(true)} + sx={{ + position: 'absolute', + top: 0, + right: 0, + zIndex: 10, + boxShadow: 'sm', + }} + > + + + )} + + {/* Collapsible sidebar */} + {isOpen && ( + + {/* Header with close button */} + + + + {t('chatbot.title')} + + setIsOpen(false)}> + + + + + {/* Messages */} + + {messages.map((message) => ( + + + + {message.content} + + + + ))} + {isLoading && ( + + + + + + )} +
+ + + {/* Input */} + + setInput(e.target.value)} + placeholder={t('chatbot.input-placeholder')} + disabled={isLoading} + sx={{ flex: 1 }} + /> + + + + + + )} + + ); + } + + // Desktop: Always visible sidebar (no toggle) + return ( + + {/* Header */} + + + {t('chatbot.title')} + + + {/* Messages */} + + {messages.map((message) => ( + + + + {message.content} + + + + ))} + {isLoading && ( + + + + + + )} +
+ + + {/* Input */} + + setInput(e.target.value)} + placeholder={t('chatbot.input-placeholder')} + disabled={isLoading} + sx={{ flex: 1 }} + /> + + + + + + ); +}; diff --git a/frontend/src/components/Chatbot/index.ts b/frontend/src/components/Chatbot/index.ts new file mode 100644 index 0000000..78dd8de --- /dev/null +++ b/frontend/src/components/Chatbot/index.ts @@ -0,0 +1 @@ +export { Chatbot } from './Chatbot'; diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index ac61d9d..6b8fa28 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -125,5 +125,10 @@ "title": "Completed user evaluations", "label": "Select user", "no-results": "No results" + }, + "chatbot": { + "title": "AI Assistant", + "welcome-message": "Hello! I'm here to help you understand your PTSD evaluation results. Feel free to ask me questions about the problems identified, the advice provided, or any concerns you may have. Remember, I'm here to provide information and support, but I cannot replace professional medical advice.", + "input-placeholder": "Ask me about your results..." } } diff --git a/frontend/src/i18n/locales/ru.json b/frontend/src/i18n/locales/ru.json index fd52f33..d98d994 100644 --- a/frontend/src/i18n/locales/ru.json +++ b/frontend/src/i18n/locales/ru.json @@ -125,5 +125,10 @@ "title": "Завершенные оценки пользователей", "label": "Выберите пользователя", "no-results": "Нет результатов" + }, + "chatbot": { + "title": "ИИ-помощник", + "welcome-message": "Здравствуйте! Я здесь, чтобы помочь вам понять результаты вашей оценки ПТСР. Не стесняйтесь задавать мне вопросы о выявленных проблемах, предоставленных советах или любых ваших опасениях. Помните, я здесь, чтобы предоставить информацию и поддержку, но я не могу заменить профессиональную медицинскую консультацию.", + "input-placeholder": "Спросите меня о ваших результатах..." } } diff --git a/frontend/src/i18n/locales/uk.json b/frontend/src/i18n/locales/uk.json index 46d0c7b..96702b5 100644 --- a/frontend/src/i18n/locales/uk.json +++ b/frontend/src/i18n/locales/uk.json @@ -125,5 +125,10 @@ "title": "Завершені оцінки користувачів", "label": "Виберіть користувача", "no-results": "Немає результатів" + }, + "chatbot": { + "title": "ШІ-помічник", + "welcome-message": "Вітаю! Я тут, щоб допомогти вам зрозуміти результати вашої оцінки ПТСР. Не соромтеся ставити мені питання про виявлені проблеми, надані поради або будь-які ваші занепокоєння. Пам'ятайте, я тут, щоб надати інформацію та підтримку, але я не можу замінити професійну медичну консультацію.", + "input-placeholder": "Запитайте мене про ваші результати..." } } diff --git a/frontend/src/pages/GeneralTest/GeneralTestResult.tsx b/frontend/src/pages/GeneralTest/GeneralTestResult.tsx index d821364..a0de090 100644 --- a/frontend/src/pages/GeneralTest/GeneralTestResult.tsx +++ b/frontend/src/pages/GeneralTest/GeneralTestResult.tsx @@ -18,6 +18,7 @@ import { tabClasses } from '@mui/joy/Tab'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; +import { Chatbot } from '@/components/Chatbot'; interface Result { __typename?: 'GeneralTestResult' | undefined; @@ -74,78 +75,106 @@ export const GeneralTestResult = () => { return ( <> {result && ( -
- {t('general-test-result.title')} + + {/* Main Content */} + +
+ {t('general-test-result.title')} - + - - {t('general-test-result.sub-title')} -
- {result.potentialProblems.map((problem: any) => ( - - {t(`problems.${problem}`)} ({problem}) - - ))} + + {t('general-test-result.sub-title')} +
+ {result.potentialProblems.map((problem: any) => ( + + {t(`problems.${problem}`)} ({problem}) + + ))} +
+
+ + + setIndex(value as number)} sx={{ borderRadius: 'xl' }}> + + {result.adviceLists.map((adviceList, idx) => ( + + {adviceList.problem} + + ))} + + + {result.adviceLists.map((adviceList, idx) => ( + + + {adviceList.advices.map((advice) => ( + {advice} + ))} + + + ))} + + +
-
+ + {/* Chatbot Sidebar */} - setIndex(value as number)} sx={{ borderRadius: 'xl' }}> - - {result.adviceLists.map((adviceList, idx) => ( - - {adviceList.problem} - - ))} - - - {result.adviceLists.map((adviceList, idx) => ( - - - {adviceList.advices.map((advice) => ( - {advice} - ))} - - - ))} - - + -
+
)} ); diff --git a/frontend/src/plugins/chat-api-plugin.ts b/frontend/src/plugins/chat-api-plugin.ts new file mode 100644 index 0000000..ae3c6a1 --- /dev/null +++ b/frontend/src/plugins/chat-api-plugin.ts @@ -0,0 +1,244 @@ +import type { Plugin, ViteDevServer } from 'vite'; +import type { IncomingMessage, ServerResponse } from 'http'; + +/** + * Interface for incoming chat API request payload + */ +interface ChatRequest { + messages: Array<{ + role: 'user' | 'assistant' | 'system'; + content: string; + }>; + resultContext: { + completionDate: string; + potentialProblems: string[]; + adviceLists: Array<{ + problem: string; + advices: string[]; + }>; + }; +} + +/** + * Mapping of problem codes to their full names in English and Ukrainian + */ +const PROBLEM_NAMES: Record = { + SS: 'Suicidal Syndrome (Суїцидальний синдром)', + CTSD: 'Chronic Traumatic Stress Disorder (Хронічний травматичний стресовий розлад)', + PTSD: 'Post-Traumatic Stress Disorder (Посттравматичний стресовий розлад)', + MT: 'Moral Injury/Trauma (Моральна травма)', + PCS: 'Post-Concussion Syndrome (Постконтузійний синдром)', + PTS: 'Post-Traumatic Symptom (Посттравматичний симптом)', + A: 'Anxiety (Тривога)', + D: 'Discomfort (Дискомфорт)', + N: 'Normal (Норма)', +}; + +/** + * OpenAI API configuration constants + */ +const OPENAI_CONFIG = { + MODEL: 'gpt-4-turbo-preview', + MAX_TOKENS: 1000, + TEMPERATURE: 0.7, +} as const; + +/** + * Vite plugin that provides a chat API endpoint for OpenAI integration + * + * This plugin creates a POST endpoint at /api/chat that handles chat requests + * with context from PTSD evaluation results. It integrates with OpenAI's API + * to provide contextual responses based on user assessment data. + * + * @returns {Plugin} Vite plugin configuration + */ +export function chatApiPlugin(): Plugin { + let config: any; + + return { + name: 'chat-api', + + /** + * Store resolved Vite configuration for accessing environment variables + */ + configResolved(resolvedConfig) { + config = resolvedConfig; + }, + + /** + * Configure development server with chat API middleware + */ + configureServer(server: ViteDevServer) { + server.middlewares.use(async (req, res, next) => { + if (req.url === '/api/chat' && req.method === 'POST') { + await handleChatRequest(req, res, config); + } else { + next(); + } + }); + }, + }; +} + +/** + * Handle incoming chat API requests + * + * @param {IncomingMessage} req - HTTP request object + * @param {ServerResponse} res - HTTP response object + * @param {any} config - Vite configuration object + */ +async function handleChatRequest(req: IncomingMessage, res: ServerResponse, config: any): Promise { + try { + const body = await parseRequestBody(req); + const { messages, resultContext } = JSON.parse(body) as ChatRequest; + + // Dynamically import OpenAI to avoid bundling issues + const { default: OpenAI } = await import('openai'); + + // Validate and retrieve API key + const apiKey = getEnvironmentVariable(config, 'OPENAI_API_KEY'); + if (!apiKey) { + return sendErrorResponse( + res, + 500, + 'OpenAI API key not configured. Please add VITE_OPENAI_API_KEY to your .env file.' + ); + } + + // Validate and retrieve Prompt ID + const promptId = getEnvironmentVariable(config, 'OPENAI_PROMPT_ID'); + if (!promptId) { + return sendErrorResponse( + res, + 500, + 'OpenAI Prompt ID not configured. Please add VITE_OPENAI_PROMPT_ID to your .env file.' + ); + } + + // Initialize OpenAI client + const openai = new OpenAI({ apiKey }); + + // Build context message from evaluation results + const contextMessage = buildContextMessage(resultContext); + + // Request completion from OpenAI + const completion = await openai.chat.completions.create({ + model: OPENAI_CONFIG.MODEL, + messages: [ + { + role: 'system', + content: `prompt:${promptId}`, + }, + { + role: 'user', + content: contextMessage, + }, + ...messages, + ], + max_tokens: OPENAI_CONFIG.MAX_TOKENS, + temperature: OPENAI_CONFIG.TEMPERATURE, + }); + + const responseMessage = completion.choices[0]?.message?.content || 'Sorry, I could not generate a response.'; + + sendSuccessResponse(res, { message: responseMessage }); + } catch (error) { + console.error('Chat API error:', error); + const errorMessage = error instanceof Error ? error.message : 'Failed to process request'; + sendErrorResponse(res, 500, errorMessage); + } +} + +/** + * Parse request body from incoming stream + * + * @param {IncomingMessage} req - HTTP request object + * @returns {Promise} Parsed request body + */ +function parseRequestBody(req: IncomingMessage): Promise { + return new Promise((resolve, reject) => { + let body = ''; + + req.on('data', (chunk) => { + body += chunk.toString(); + }); + + req.on('end', () => { + resolve(body); + }); + + req.on('error', (error) => { + reject(error); + }); + }); +} + +/** + * Retrieve environment variable with fallback options + * + * @param {any} config - Vite configuration object + * @param {string} variableName - Base name of the environment variable + * @returns {string | undefined} Environment variable value + */ +function getEnvironmentVariable(config: any, variableName: string): string | undefined { + const viteKey = `VITE_${variableName}`; + return config.env[viteKey] || process.env[viteKey] || process.env[variableName]; +} + +/** + * Build context message from evaluation results + * + * @param {ChatRequest['resultContext']} resultContext - User evaluation results + * @returns {string} Formatted context message for OpenAI + */ +function buildContextMessage(resultContext: ChatRequest['resultContext']): string { + const problemsWithNames = resultContext.potentialProblems + .map((code) => `${code} (${PROBLEM_NAMES[code] || code})`) + .join(', '); + + const problemReference = Object.entries(PROBLEM_NAMES) + .map(([code, name]) => `- ${code}: ${name}`) + .join('\n'); + + const adviceSection = resultContext.adviceLists + .map( + (list) => ` +Problem: ${list.problem} +Advice: +${list.advices.map((advice) => `- ${advice}`).join('\n')}` + ) + .join('\n'); + + return `The user has completed a PTSD evaluation with the following results: +- Completion Date: ${resultContext.completionDate} +- Potential Problems Identified: ${problemsWithNames} + +Problem Code Reference: +${problemReference} + +Advice Lists: +${adviceSection}`; +} + +/** + * Send successful JSON response + * + * @param {ServerResponse} res - HTTP response object + * @param {object} data - Response data to send + */ +function sendSuccessResponse(res: ServerResponse, data: object): void { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(data)); +} + +/** + * Send error JSON response + * + * @param {ServerResponse} res - HTTP response object + * @param {number} statusCode - HTTP status code + * @param {string} errorMessage - Error message + */ +function sendErrorResponse(res: ServerResponse, statusCode: number, errorMessage: string): void { + res.writeHead(statusCode, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: errorMessage })); +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts index 39460a0..f8ed802 100644 --- a/frontend/src/vite-env.d.ts +++ b/frontend/src/vite-env.d.ts @@ -2,6 +2,8 @@ interface ImportMetaEnv { readonly VITE_GRAPHQL_URI: string; + readonly VITE_OPENAI_API_KEY?: string; + readonly OPENAI_API_KEY?: string; } interface ImportMeta { diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json index 42872c5..1f5e509 100644 --- a/frontend/tsconfig.node.json +++ b/frontend/tsconfig.node.json @@ -6,5 +6,5 @@ "moduleResolution": "bundler", "allowSyntheticDefaultImports": true }, - "include": ["vite.config.ts"] + "include": ["vite.config.ts", "src/plugins/**/*.ts"] } diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index b0c58be..c3de2d8 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -2,11 +2,12 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import tsconfigPaths from 'vite-tsconfig-paths'; import svgr from 'vite-plugin-svgr'; +import { chatApiPlugin } from './src/plugins/chat-api-plugin'; // https://vitejs.dev/config/ export default defineConfig({ base: '/', - plugins: [react(), tsconfigPaths(), svgr({ include: '**/*.svg' })], + plugins: [react(), tsconfigPaths(), svgr({ include: '**/*.svg' }), chatApiPlugin()], preview: { port: 5173, strictPort: true,