chatbot debug
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
VITE_TOKEN_NAME = 'dzone_token'
|
VITE_TOKEN_NAME = 'dzone_token'
|
||||||
VITE_REFRESH_TOKEN_NAME = 'dzone_refresh_token'
|
VITE_REFRESH_TOKEN_NAME = 'dzone_refresh_token'
|
||||||
VITE_BASE_URL = 'https://api-dzone.danakcorp.com'
|
# VITE_BASE_URL = 'https://api-dzone.danakcorp.com'
|
||||||
# VITE_BASE_URL = 'http://192.168.82.158:4000'
|
VITE_BASE_URL = 'https://nhrq9m1d-4000.eun1.devtunnels.ms'
|
||||||
|
|
||||||
VITE_SLUG_ID = 'slug_id'
|
VITE_SLUG_ID = 'slug_id'
|
||||||
Generated
+168
-1
@@ -11,6 +11,7 @@
|
|||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
"@material-tailwind/react": "^2.1.10",
|
"@material-tailwind/react": "^2.1.10",
|
||||||
"@tanstack/react-query": "^5.64.1",
|
"@tanstack/react-query": "^5.64.1",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"i18next": "^24.2.1",
|
"i18next": "^24.2.1",
|
||||||
@@ -28,9 +29,12 @@
|
|||||||
"react-otp-input": "^3.1.1",
|
"react-otp-input": "^3.1.1",
|
||||||
"react-router-dom": "^7.1.3",
|
"react-router-dom": "^7.1.3",
|
||||||
"react-spinners": "^0.17.0",
|
"react-spinners": "^0.17.0",
|
||||||
|
"react-string-replace": "^1.1.1",
|
||||||
"react-toastify": "^11.0.3",
|
"react-toastify": "^11.0.3",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"swiper": "^11.2.1",
|
"swiper": "^11.2.1",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
|
"uuid": "^11.1.0",
|
||||||
"yup": "^1.6.1",
|
"yup": "^1.6.1",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
@@ -1732,6 +1736,12 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.15",
|
"version": "0.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||||
@@ -1913,6 +1923,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.20.0",
|
"version": "8.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz",
|
||||||
@@ -2697,6 +2713,45 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/engine.io-client": {
|
||||||
|
"version": "6.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||||
|
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.17.1",
|
||||||
|
"xmlhttprequest-ssl": "~2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.4",
|
"version": "0.25.4",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
||||||
@@ -3834,7 +3889,6 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/mz": {
|
"node_modules/mz": {
|
||||||
@@ -4513,6 +4567,15 @@
|
|||||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-string-replace": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-string-replace/-/react-string-replace-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-toastify": {
|
"node_modules/react-toastify": {
|
||||||
"version": "11.0.3",
|
"version": "11.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.3.tgz",
|
||||||
@@ -4716,6 +4779,68 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/socket.io-client": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io-client": "~6.6.1",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-client/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||||
|
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -5219,6 +5344,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "11.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||||
|
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/esm/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.3.5",
|
"version": "6.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||||
@@ -5452,6 +5590,35 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
"@material-tailwind/react": "^2.1.10",
|
"@material-tailwind/react": "^2.1.10",
|
||||||
"@tanstack/react-query": "^5.64.1",
|
"@tanstack/react-query": "^5.64.1",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"i18next": "^24.2.1",
|
"i18next": "^24.2.1",
|
||||||
@@ -30,9 +31,12 @@
|
|||||||
"react-otp-input": "^3.1.1",
|
"react-otp-input": "^3.1.1",
|
||||||
"react-router-dom": "^7.1.3",
|
"react-router-dom": "^7.1.3",
|
||||||
"react-spinners": "^0.17.0",
|
"react-spinners": "^0.17.0",
|
||||||
|
"react-string-replace": "^1.1.1",
|
||||||
"react-toastify": "^11.0.3",
|
"react-toastify": "^11.0.3",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"swiper": "^11.2.1",
|
"swiper": "^11.2.1",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
|
"uuid": "^11.1.0",
|
||||||
"yup": "^1.6.1",
|
"yup": "^1.6.1",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import FaJson from './langs/fa.json'
|
|||||||
import AuthRouter from './router/Auth';
|
import AuthRouter from './router/Auth';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import ToastContainer from './components/Toast';
|
import ToastContainer from './components/Toast';
|
||||||
|
import { ChatbotButton } from './chatbot';
|
||||||
|
|
||||||
i18next.init({
|
i18next.init({
|
||||||
interpolation: { escapeValue: false },
|
interpolation: { escapeValue: false },
|
||||||
@@ -44,6 +45,7 @@ function App() {
|
|||||||
<Route path="/:slug/*" element={<MainRouter />} />
|
<Route path="/:slug/*" element={<MainRouter />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
<ChatbotButton />
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@@ -0,0 +1,584 @@
|
|||||||
|
import { FC, useState, useRef, useEffect } from 'react';
|
||||||
|
import { Send, CloseCircle, Wifi } from 'iconsax-react';
|
||||||
|
import io, { Socket } from 'socket.io-client';
|
||||||
|
import { SOCKET_EVENTS } from './constants';
|
||||||
|
import { useSocketHandlers } from './hooks/useSocketHandlers';
|
||||||
|
import { useMessages } from './hooks/useMessages';
|
||||||
|
import { Message } from './types';
|
||||||
|
import { getToken } from '../config/func';
|
||||||
|
import './chatbot.css'; // Import CSS for animations
|
||||||
|
|
||||||
|
// Helper function to format text with markdown-like syntax
|
||||||
|
export const formatText = (text: string) => {
|
||||||
|
if (!text) return '';
|
||||||
|
|
||||||
|
// Clean the text first to avoid HTML injection
|
||||||
|
const cleanedText = text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
|
||||||
|
// Format with markdown-like syntax and handle newlines properly
|
||||||
|
const formattedText = cleanedText
|
||||||
|
// Bold text
|
||||||
|
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
||||||
|
// Italic text
|
||||||
|
.replace(/\*(.*?)\*/g, "<em>$1</em>")
|
||||||
|
// Line breaks - ensure they work properly with proper spacing
|
||||||
|
.replace(/\n/g, "<br class='message-line-break' />")
|
||||||
|
// Links (optional)
|
||||||
|
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" class="text-blue-500 underline">$1</a>');
|
||||||
|
|
||||||
|
return formattedText;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ChatbotProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection status types
|
||||||
|
type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
|
||||||
|
|
||||||
|
const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
||||||
|
// State
|
||||||
|
const [inputValue, setInputValue] = useState<string>('');
|
||||||
|
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||||
|
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('disconnected');
|
||||||
|
const [isTyping, setIsTyping] = useState<boolean>(false);
|
||||||
|
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
|
||||||
|
const [socket, setSocket] = useState<Socket | null>(null);
|
||||||
|
const [showChatbot, setShowChatbot] = useState<boolean>(false);
|
||||||
|
const [showConnectionMessage, setShowConnectionMessage] = useState<boolean>(false);
|
||||||
|
const [isStreamingMode, setIsStreamingMode] = useState<boolean>(true);
|
||||||
|
const [reconnectAttempts, setReconnectAttempts] = useState<number>(0);
|
||||||
|
const maxReconnectAttempts = 5;
|
||||||
|
|
||||||
|
// Config
|
||||||
|
const apiBaseUrl = import.meta.env.VITE_BASE_URL;
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const connectionTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
|
// Custom hooks
|
||||||
|
const { messages, addMessage, addSystemMessage, updateMessage, clearMessages } = useMessages();
|
||||||
|
const { setupSocketHandlers, streamingText } = useSocketHandlers({
|
||||||
|
socket,
|
||||||
|
setStatus: (status: string) => {
|
||||||
|
// Map status string to connection status
|
||||||
|
if (status.includes('متصل شد')) {
|
||||||
|
setConnectionStatus('connected');
|
||||||
|
} else if (status.includes('در حال')) {
|
||||||
|
setConnectionStatus('connecting');
|
||||||
|
} else if (status.includes('خطا')) {
|
||||||
|
setConnectionStatus('error');
|
||||||
|
} else if (status.includes('قطع')) {
|
||||||
|
setConnectionStatus('disconnected');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setIsTyping,
|
||||||
|
setIsProcessing,
|
||||||
|
addMessage,
|
||||||
|
updateMessage,
|
||||||
|
addSystemMessage,
|
||||||
|
setCurrentSessionId,
|
||||||
|
isProcessing
|
||||||
|
});
|
||||||
|
|
||||||
|
// Debug streaming text changes
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("Streaming text updated:", streamingText.substring(0, 50) + (streamingText.length > 50 ? "..." : ""));
|
||||||
|
}, [streamingText]);
|
||||||
|
|
||||||
|
// Initialize component
|
||||||
|
const init = () => {
|
||||||
|
console.log("✅ DZone ChatBot initialized!");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run initialization once on mount
|
||||||
|
useEffect(() => {
|
||||||
|
init();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Animation effect for opening/closing
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
// Small delay to trigger animation
|
||||||
|
setTimeout(() => setShowChatbot(true), 50);
|
||||||
|
} else {
|
||||||
|
setShowChatbot(false);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Connect to socket when component mounts
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
connectSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup on unmount or when closed
|
||||||
|
return () => {
|
||||||
|
disconnectSocket();
|
||||||
|
if (connectionTimeoutRef.current) {
|
||||||
|
clearTimeout(connectionTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Handle reconnection attempts
|
||||||
|
useEffect(() => {
|
||||||
|
if (connectionStatus === 'error' && reconnectAttempts < maxReconnectAttempts) {
|
||||||
|
const reconnectTimer = setTimeout(() => {
|
||||||
|
attemptReconnect();
|
||||||
|
}, 2000 * (reconnectAttempts + 1)); // Exponential backoff
|
||||||
|
|
||||||
|
return () => clearTimeout(reconnectTimer);
|
||||||
|
}
|
||||||
|
}, [connectionStatus, reconnectAttempts]);
|
||||||
|
|
||||||
|
// Scroll to bottom when messages change
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
// Set up socket handlers when socket changes
|
||||||
|
useEffect(() => {
|
||||||
|
let cleanup: (() => void) | undefined;
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
console.log("🔄 Setting up socket handlers");
|
||||||
|
cleanup = setupSocketHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (cleanup) {
|
||||||
|
console.log("🔄 Cleaning up socket handlers");
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [socket, setupSocketHandlers]);
|
||||||
|
|
||||||
|
// Show connection message only briefly when status changes
|
||||||
|
useEffect(() => {
|
||||||
|
// Always show connection status, don't hide it
|
||||||
|
setShowConnectionMessage(true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (connectionTimeoutRef.current) {
|
||||||
|
clearTimeout(connectionTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [connectionStatus]);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectSocket = async () => {
|
||||||
|
try {
|
||||||
|
setConnectionStatus('connecting');
|
||||||
|
|
||||||
|
// Get token from environment or localStorage
|
||||||
|
const token = await getToken();
|
||||||
|
|
||||||
|
if (!apiBaseUrl) {
|
||||||
|
console.error("API base URL not defined in environment variables");
|
||||||
|
setConnectionStatus('error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSocket = io(`${apiBaseUrl}/chat`, {
|
||||||
|
auth: {
|
||||||
|
token: token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setSocket(newSocket);
|
||||||
|
setReconnectAttempts(0);
|
||||||
|
|
||||||
|
// Set a timeout for connection
|
||||||
|
connectionTimeoutRef.current = setTimeout(() => {
|
||||||
|
if (connectionStatus === 'connecting') {
|
||||||
|
setConnectionStatus('error');
|
||||||
|
}
|
||||||
|
}, 10000); // 10 seconds timeout
|
||||||
|
|
||||||
|
// Create session after connection
|
||||||
|
setTimeout(() => createSession(newSocket), 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Socket connection error:", error);
|
||||||
|
setConnectionStatus('error');
|
||||||
|
attemptReconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const attemptReconnect = () => {
|
||||||
|
if (reconnectAttempts < maxReconnectAttempts) {
|
||||||
|
const newAttempts = reconnectAttempts + 1;
|
||||||
|
setReconnectAttempts(newAttempts);
|
||||||
|
console.log(`🔄 Reconnecting... Attempt ${newAttempts}/${maxReconnectAttempts}`);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
connectSocket();
|
||||||
|
}, 2000 * newAttempts); // Exponential backoff
|
||||||
|
} else {
|
||||||
|
console.log("❌ Max reconnection attempts reached");
|
||||||
|
addSystemMessage("اتصال قطع شد. لطفاً صفحه را تازه کنید", "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const disconnectSocket = () => {
|
||||||
|
if (socket) {
|
||||||
|
socket.disconnect();
|
||||||
|
setSocket(null);
|
||||||
|
setConnectionStatus('disconnected');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSession = (socketInstance: Socket | null = socket) => {
|
||||||
|
if (!socketInstance || !socketInstance.connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSessionId) {
|
||||||
|
console.log("Session already exists:", currentSessionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socketInstance.emit(SOCKET_EVENTS.CREATE_SESSION, {
|
||||||
|
title: `جلسه ${new Date().toLocaleDateString("fa-IR")}`
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const joinChat = (sessionId: string) => {
|
||||||
|
if (!socket || !socket.connected) {
|
||||||
|
addSystemMessage("اتصال Socket.IO برقرار نیست", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user ID from localStorage or use a default
|
||||||
|
const userId = localStorage.getItem(import.meta.env.VITE_USER_ID_KEY || "user_id") ||
|
||||||
|
localStorage.getItem("userId") ||
|
||||||
|
"anonymous_user";
|
||||||
|
|
||||||
|
console.log(`🔄 Joining chat session: ${sessionId}`);
|
||||||
|
socket.emit(SOCKET_EVENTS.JOIN_CHAT, {
|
||||||
|
sessionId: sessionId,
|
||||||
|
userId: userId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const newSession = () => {
|
||||||
|
setCurrentSessionId(null);
|
||||||
|
clearMessages();
|
||||||
|
createSession();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendMessage = () => {
|
||||||
|
const message = inputValue.trim();
|
||||||
|
|
||||||
|
if (!message || isProcessing) {
|
||||||
|
console.log("❌ Cannot send:", { message: !!message, processing: isProcessing });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Socket.IO connection
|
||||||
|
if (!socket || !socket.connected) {
|
||||||
|
addSystemMessage("اتصال Socket.IO برقرار نیست", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-create session if needed
|
||||||
|
if (!currentSessionId) {
|
||||||
|
console.log("📝 No session, creating one...");
|
||||||
|
createSession();
|
||||||
|
addSystemMessage("لطفاً منتظر ایجاد جلسه باشید", "info");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Disable during processing
|
||||||
|
setIsProcessing(true);
|
||||||
|
updateButtonState();
|
||||||
|
|
||||||
|
// Add user message to UI
|
||||||
|
addMessage({
|
||||||
|
text: message,
|
||||||
|
isUser: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear input immediately to prevent accidental resend
|
||||||
|
setInputValue('');
|
||||||
|
autoResizeTextarea({ target: { value: '', style: { height: 'auto' } } } as React.ChangeEvent<HTMLTextAreaElement>);
|
||||||
|
|
||||||
|
// Get user ID from localStorage or use a default
|
||||||
|
const userId = localStorage.getItem(import.meta.env.VITE_USER_ID_KEY || "user_id") ||
|
||||||
|
localStorage.getItem("userId") ||
|
||||||
|
"anonymous_user";
|
||||||
|
|
||||||
|
// Send message via Socket.IO
|
||||||
|
console.log("📤 Sending message via Socket.IO");
|
||||||
|
socket.emit(SOCKET_EVENTS.SEND_MESSAGE, {
|
||||||
|
sessionId: currentSessionId,
|
||||||
|
content: message,
|
||||||
|
userId: userId
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Send message error:", error);
|
||||||
|
addSystemMessage(`خطا در ارسال پیام`, "error");
|
||||||
|
setIsProcessing(false);
|
||||||
|
updateButtonState();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSendMessage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoResizeTextarea = (e: React.ChangeEvent<HTMLTextAreaElement> | { target: { value: string, style?: { height: string } } }): void => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setInputValue(value);
|
||||||
|
|
||||||
|
// Handle the case when this is called programmatically without a real DOM element
|
||||||
|
if ('style' in e.target) {
|
||||||
|
const textarea = e.target as HTMLTextAreaElement;
|
||||||
|
textarea.style.height = 'auto';
|
||||||
|
textarea.style.height = Math.min(textarea.scrollHeight, 150) + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateButtonState();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateButtonState = () => {
|
||||||
|
const hasText = inputValue.trim().length > 0;
|
||||||
|
const canSend = hasText && !isProcessing && connectionStatus === 'connected';
|
||||||
|
|
||||||
|
console.log("🔄 Button state:", {
|
||||||
|
hasText,
|
||||||
|
isProcessing,
|
||||||
|
currentSessionId,
|
||||||
|
canSend,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Button state is managed via the disabled prop and className
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleStreaming = () => {
|
||||||
|
setIsStreamingMode(!isStreamingMode);
|
||||||
|
addSystemMessage(`حالت ${isStreamingMode ? "عادی" : "استریم"} فعال شد`, "success");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get connection status icon and text
|
||||||
|
const getConnectionStatusInfo = () => {
|
||||||
|
switch (connectionStatus) {
|
||||||
|
case 'connected':
|
||||||
|
return {
|
||||||
|
icon: <Wifi size={16} className="text-green-400" />,
|
||||||
|
text: "متصل",
|
||||||
|
className: "bg-green-100 text-green-800"
|
||||||
|
};
|
||||||
|
case 'connecting':
|
||||||
|
return {
|
||||||
|
icon: <Wifi size={16} className="text-yellow-400 animate-pulse" />,
|
||||||
|
text: "در حال اتصال...",
|
||||||
|
className: "bg-yellow-100 text-yellow-800"
|
||||||
|
};
|
||||||
|
case 'error':
|
||||||
|
return {
|
||||||
|
icon: <Wifi size={16} className="text-red-500" />,
|
||||||
|
text: "خطا در اتصال",
|
||||||
|
className: "bg-red-100 text-red-800"
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
icon: <Wifi size={16} variant="Bold" className="text-gray-500" />,
|
||||||
|
text: "قطع اتصال",
|
||||||
|
className: "bg-gray-100 text-gray-800"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const connectionInfo = getConnectionStatusInfo();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`fixed bottom-20 right-4 w-80 sm:w-96 bg-white rounded-xl shadow-xl flex flex-col z-50 border border-gray-200 transition-all duration-300 ease-in-out ${showChatbot ? 'opacity-100 transform translate-y-0' : 'opacity-0 transform translate-y-10'
|
||||||
|
}`}
|
||||||
|
style={{ height: '500px', direction: 'rtl' }}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-gradient-to-l from-blue-500 to-blue-600 text-white p-3.5 rounded-t-xl flex justify-between items-center shadow-sm">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className={`w-3 h-3 rounded-full ml-2 ${connectionStatus === 'connected' ? 'bg-green-400 animate-pulse' :
|
||||||
|
connectionStatus === 'connecting' ? 'bg-yellow-400 animate-pulse' :
|
||||||
|
'bg-red-400'
|
||||||
|
}`}></div>
|
||||||
|
<h3 className="font-bold text-base">چت بات داناک</h3>
|
||||||
|
{/* Always show a compact connection status */}
|
||||||
|
<div className={`flex items-center text-xs mr-2 ml-2 ${connectionStatus === 'connected' ? 'text-green-200' :
|
||||||
|
connectionStatus === 'connecting' ? 'text-yellow-200' :
|
||||||
|
'text-red-200'
|
||||||
|
}`}>
|
||||||
|
{connectionInfo.icon}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="focus:outline-none hover:bg-blue-700 p-1 rounded-full transition-colors"
|
||||||
|
aria-label="بستن چت"
|
||||||
|
>
|
||||||
|
<CloseCircle size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Control buttons */}
|
||||||
|
<div className="flex justify-between px-3 py-2 bg-gray-50 border-b border-gray-200">
|
||||||
|
<button
|
||||||
|
onClick={() => clearMessages()}
|
||||||
|
className="text-xs text-gray-600 hover:text-blue-600 px-2 py-1 hover:bg-gray-100 rounded transition-colors"
|
||||||
|
>
|
||||||
|
پاک کردن
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={newSession}
|
||||||
|
className="text-xs text-gray-600 hover:text-blue-600 px-2 py-1 hover:bg-gray-100 rounded transition-colors"
|
||||||
|
>
|
||||||
|
جلسه جدید
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={toggleStreaming}
|
||||||
|
className={`text-xs ${isStreamingMode ? 'text-blue-600' : 'text-gray-600'} px-2 py-1 hover:bg-gray-100 rounded transition-colors`}
|
||||||
|
>
|
||||||
|
{isStreamingMode ? '📡 استریم' : '📝 عادی'}
|
||||||
|
</button>
|
||||||
|
{connectionStatus !== 'connected' && (
|
||||||
|
<button
|
||||||
|
onClick={connectSocket}
|
||||||
|
className="text-xs text-blue-600 hover:text-blue-800 px-2 py-1 hover:bg-blue-50 rounded transition-colors"
|
||||||
|
>
|
||||||
|
اتصال مجدد
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Messages */}
|
||||||
|
<div className="flex-1 p-3.5 overflow-y-auto bg-gray-50" style={{ height: '350px' }}>
|
||||||
|
{
|
||||||
|
messages?.map((message: Message) => {
|
||||||
|
// Debug message content
|
||||||
|
console.log(`Rendering message ${message.id}:`, {
|
||||||
|
text: message.text.substring(0, 30) + (message.text.length > 30 ? "..." : ""),
|
||||||
|
isUser: message.isUser,
|
||||||
|
isSystem: message.isSystem,
|
||||||
|
streaming: message.streaming
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={message.id}
|
||||||
|
className={`mb-3 flex ${message.isUser ? 'justify-start' : 'justify-end'} animate-fade-in`}
|
||||||
|
>
|
||||||
|
{message.isUser && (
|
||||||
|
<div className="w-7 h-7 rounded-full bg-blue-600 flex items-center justify-center ml-2 flex-shrink-0">
|
||||||
|
<span className="text-white text-xs">👤</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`relative px-3.5 py-2.5 pb-4 rounded-lg max-w-[85%] ${message.isUser
|
||||||
|
? 'bg-blue-600 text-white rounded-tr-none shadow-sm animate-slide-right'
|
||||||
|
: message.isSystem
|
||||||
|
? message.metadata?.type === 'error'
|
||||||
|
? 'bg-red-100 text-red-800 rounded-tl-none'
|
||||||
|
: message.metadata?.type === 'success'
|
||||||
|
? 'bg-green-100 text-green-800 rounded-tl-none'
|
||||||
|
: 'bg-yellow-100 text-yellow-800 rounded-tl-none'
|
||||||
|
: message.metadata && 'confidence' in message.metadata
|
||||||
|
? message.metadata.confidence === 1
|
||||||
|
? 'bg-green-100 text-green-800 rounded-tl-none'
|
||||||
|
: message.metadata.confidence === 0
|
||||||
|
? 'bg-red-100 text-red-800 rounded-tl-none'
|
||||||
|
: 'bg-yellow-100 text-yellow-800 rounded-tl-none'
|
||||||
|
: 'bg-white text-gray-800 rounded-tl-none shadow-sm animate-slide-left'
|
||||||
|
} ${message.streaming ? 'border-r-4 border-blue-400 streaming-message animate-pulse' : ''}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id={`message-${message.id}`}
|
||||||
|
className={`text-sm leading-6 font-light whitespace-pre-line break-words text-right mb-2 ${message.streaming ? 'streaming-content' : ''}`}
|
||||||
|
dangerouslySetInnerHTML={{ __html: formatText(message.text) }}
|
||||||
|
/>
|
||||||
|
<div className={`text-[8px] opacity-70 absolute bottom-1 ${message.isUser ? 'left-2.5 text-left' : 'right-2.5 text-right'}`}>
|
||||||
|
{message.timestamp.toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</div>
|
||||||
|
{message.metadata?.confidence !== undefined &&
|
||||||
|
message.metadata.confidence !== 0 &&
|
||||||
|
message.metadata.confidence !== 0.5 &&
|
||||||
|
message.metadata.confidence !== 1 && (
|
||||||
|
<div className="text-[9px] mt-0.5 text-right">
|
||||||
|
اعتماد: {Math.round(message.metadata.confidence * 100)}%
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!message.isUser && !message.isSystem && (
|
||||||
|
<div className="w-7 h-7 rounded-full bg-blue-100 flex items-center justify-center mr-2 flex-shrink-0">
|
||||||
|
<span className="text-blue-600 text-xs">🤖</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
{/* Improved typing indicator */}
|
||||||
|
{isTyping && (
|
||||||
|
<div className="flex justify-end mb-3 animate-fade-in">
|
||||||
|
<div className="bg-white text-gray-800 px-3.5 py-2 rounded-lg rounded-tl-none shadow-sm">
|
||||||
|
<div className="typing-indicator">
|
||||||
|
<span>در حال پاسخ...</span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-7 h-7 rounded-full bg-blue-100 flex items-center justify-center mr-2 flex-shrink-0">
|
||||||
|
<span className="text-blue-600 text-xs">🤖</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Input */}
|
||||||
|
<div className="border-t border-gray-200 p-3 bg-white rounded-b-xl">
|
||||||
|
<div className="flex items-center bg-gray-50 rounded-full pl-1 pr-3 overflow-hidden shadow-inner">
|
||||||
|
<textarea
|
||||||
|
value={inputValue}
|
||||||
|
onChange={autoResizeTextarea}
|
||||||
|
onKeyPress={handleKeyPress}
|
||||||
|
placeholder="پیام خود را بنویسید..."
|
||||||
|
disabled={isProcessing || connectionStatus !== 'connected'}
|
||||||
|
className="flex-1 bg-transparent border-none px-2 py-2.5 text-sm focus:outline-none resize-none min-h-[40px] font-light"
|
||||||
|
style={{ maxHeight: '80px' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleSendMessage}
|
||||||
|
disabled={inputValue.trim() === '' || isProcessing || connectionStatus !== 'connected'}
|
||||||
|
className={`ml-2 ${inputValue.trim() === '' || isProcessing || connectionStatus !== 'connected'
|
||||||
|
? 'bg-gray-300'
|
||||||
|
: 'bg-blue-600 hover:bg-blue-700'
|
||||||
|
} text-white rounded-full w-9 h-9 flex items-center justify-center focus:outline-none transition-colors flex-shrink-0`}
|
||||||
|
>
|
||||||
|
<Send size={18} className={`transform rotate-180 ${inputValue.trim() !== '' && !isProcessing && connectionStatus === 'connected' ? 'animate-pulse' : ''}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Chatbot;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { FC, useState } from 'react';
|
||||||
|
import { MessageText1 } from 'iconsax-react';
|
||||||
|
import Chatbot from './Chatbot';
|
||||||
|
|
||||||
|
const ChatbotButton: FC = () => {
|
||||||
|
const [isChatbotOpen, setIsChatbotOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const toggleChatbot = () => {
|
||||||
|
setIsChatbotOpen(!isChatbotOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={toggleChatbot}
|
||||||
|
className="fixed bottom-5 right-5 bg-blue-600 text-white rounded-full w-14 h-14 flex items-center justify-center shadow-lg hover:bg-blue-700 transition-colors z-40"
|
||||||
|
aria-label="چت با پشتیبانی"
|
||||||
|
>
|
||||||
|
<MessageText1 size={24} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Chatbot isOpen={isChatbotOpen} onClose={() => setIsChatbotOpen(false)} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatbotButton;
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
/* Chatbot animations and styling */
|
||||||
|
|
||||||
|
/* Streaming message animation */
|
||||||
|
.streaming-message {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.streaming-message::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 2px;
|
||||||
|
width: 10px;
|
||||||
|
background-color: #3b82f6;
|
||||||
|
animation: typing 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typing {
|
||||||
|
0% {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message animations */
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-right {
|
||||||
|
animation: slideRight 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-left {
|
||||||
|
animation: slideLeft 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(-20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideLeft {
|
||||||
|
from {
|
||||||
|
transform: translateX(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typing indicator */
|
||||||
|
.typing-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:first-child {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:not(:first-child) {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
margin: 0 1px;
|
||||||
|
background-color: #3b82f6;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
animation: typing-dot 1.5s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typing-dot {
|
||||||
|
0%,
|
||||||
|
60%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// WebSocket Event Constants (sync with backend)
|
||||||
|
export const SOCKET_EVENTS = {
|
||||||
|
// Connection events
|
||||||
|
CONNECT: "connect",
|
||||||
|
DISCONNECT: "disconnect",
|
||||||
|
CONNECTION_ERROR: "connect_error",
|
||||||
|
|
||||||
|
// Authentication events
|
||||||
|
AUTHENTICATE: "authenticate",
|
||||||
|
AUTHENTICATED: "authenticated",
|
||||||
|
UNAUTHORIZED: "unauthorized",
|
||||||
|
|
||||||
|
// Session events
|
||||||
|
CREATE_SESSION: "create_session",
|
||||||
|
SESSION_CREATED: "session_created",
|
||||||
|
JOIN_CHAT: "join_chat",
|
||||||
|
CHAT_JOINED: "chat_joined",
|
||||||
|
LEAVE_CHAT: "leave_chat",
|
||||||
|
CHAT_LEFT: "chat_left",
|
||||||
|
|
||||||
|
// Message events
|
||||||
|
SEND_MESSAGE: "send_message",
|
||||||
|
MESSAGE_RECEIVED: "message_received",
|
||||||
|
BOT_RESPONSE_START: "bot_response_start",
|
||||||
|
BOT_RESPONSE_CHUNK: "bot_response_chunk",
|
||||||
|
BOT_RESPONSE_END: "bot_response_end",
|
||||||
|
BOT_RESPONSE: "bot_response",
|
||||||
|
|
||||||
|
// Status events
|
||||||
|
TYPING_START: "typing_start",
|
||||||
|
TYPING_STOP: "typing_stop",
|
||||||
|
USER_JOINED: "user_joined",
|
||||||
|
USER_LEFT: "user_left",
|
||||||
|
|
||||||
|
// Error events
|
||||||
|
ERROR: "error",
|
||||||
|
WEBSOCKET_ERROR: "websocket_error",
|
||||||
|
SESSION_ERROR: "session_error",
|
||||||
|
MESSAGE_ERROR: "message_error",
|
||||||
|
};
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { useState, useCallback } from "react";
|
||||||
|
import { Message } from "../types";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
export const useMessages = () => {
|
||||||
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
|
|
||||||
|
const addMessage = useCallback((message: Partial<Message>) => {
|
||||||
|
const newMessage: Message = {
|
||||||
|
id: uuidv4(),
|
||||||
|
text: message.text || "",
|
||||||
|
isUser: message.isUser || false,
|
||||||
|
timestamp: new Date(),
|
||||||
|
streaming: message.streaming || false,
|
||||||
|
metadata: message.metadata || {},
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages((prevMessages) => [...prevMessages, newMessage]);
|
||||||
|
console.log(
|
||||||
|
`Added message with ID: ${newMessage.id}, streaming: ${newMessage.streaming}`
|
||||||
|
);
|
||||||
|
return newMessage;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const addSystemMessage = useCallback(
|
||||||
|
(text: string, type: string = "info") => {
|
||||||
|
const newMessage: Message = {
|
||||||
|
id: uuidv4(),
|
||||||
|
text,
|
||||||
|
isUser: false,
|
||||||
|
timestamp: new Date(),
|
||||||
|
streaming: false,
|
||||||
|
isSystem: true,
|
||||||
|
metadata: { type },
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages((prevMessages) => [...prevMessages, newMessage]);
|
||||||
|
|
||||||
|
// Auto-remove system messages after a delay
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
setMessages((prevMessages) =>
|
||||||
|
prevMessages.filter((msg) => msg.id !== newMessage.id)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
type === "error" ? 5000 : 3000
|
||||||
|
);
|
||||||
|
|
||||||
|
return newMessage;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateMessage = useCallback((id: string, updates: Partial<Message>) => {
|
||||||
|
console.log(`Updating message ${id} with:`, updates);
|
||||||
|
|
||||||
|
setMessages((prevMessages) =>
|
||||||
|
prevMessages.map((msg) => {
|
||||||
|
if (msg.id === id) {
|
||||||
|
const updated = { ...msg, ...updates };
|
||||||
|
console.log(`Message ${id} updated:`, updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const removeMessage = useCallback((id: string) => {
|
||||||
|
setMessages((prevMessages) => prevMessages.filter((msg) => msg.id !== id));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearMessages = useCallback(() => {
|
||||||
|
setMessages([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
addMessage,
|
||||||
|
addSystemMessage,
|
||||||
|
updateMessage,
|
||||||
|
removeMessage,
|
||||||
|
clearMessages,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,370 @@
|
|||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
import { Socket } from "socket.io-client";
|
||||||
|
import { SOCKET_EVENTS } from "../constants";
|
||||||
|
import { Message } from "../types";
|
||||||
|
|
||||||
|
interface UseSocketHandlersProps {
|
||||||
|
socket: Socket | null;
|
||||||
|
setStatus: (status: string) => void;
|
||||||
|
setIsTyping: (isTyping: boolean) => void;
|
||||||
|
setIsProcessing: (isProcessing: boolean) => void;
|
||||||
|
addMessage: (message: Partial<Message>) => Message;
|
||||||
|
updateMessage: (id: string, updates: Partial<Message>) => void;
|
||||||
|
addSystemMessage: (text: string, type?: string) => void;
|
||||||
|
setCurrentSessionId: (sessionId: string | null) => void;
|
||||||
|
isProcessing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSocketHandlers = ({
|
||||||
|
socket,
|
||||||
|
setStatus,
|
||||||
|
setIsTyping,
|
||||||
|
setIsProcessing,
|
||||||
|
addMessage,
|
||||||
|
updateMessage,
|
||||||
|
addSystemMessage,
|
||||||
|
setCurrentSessionId,
|
||||||
|
isProcessing,
|
||||||
|
}: UseSocketHandlersProps) => {
|
||||||
|
// Keep track of the current streaming message
|
||||||
|
const currentStreamingMessageRef = useRef<{
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
} | null>(null);
|
||||||
|
const [streamingText, setStreamingText] = useState<string>("");
|
||||||
|
|
||||||
|
// Simple function to update streaming message content
|
||||||
|
const updateStreamingMessage = (id: string, text: string) => {
|
||||||
|
console.log(
|
||||||
|
`Updating streaming message ${id} with ${text.length} characters`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update the state for React
|
||||||
|
updateMessage(id, { text });
|
||||||
|
|
||||||
|
// Also try direct DOM update as a fallback
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const element = document.getElementById(`message-${id}`);
|
||||||
|
if (element) {
|
||||||
|
element.innerHTML = formatText(text);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error updating DOM directly:", e);
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupSocketHandlers = useCallback(() => {
|
||||||
|
if (!socket) return () => {};
|
||||||
|
|
||||||
|
// Connection events
|
||||||
|
const onConnect = () => {
|
||||||
|
console.log("🔌 Socket.IO connected!");
|
||||||
|
setStatus("متصل شد ✅");
|
||||||
|
addSystemMessage("اتصال برقرار شد", "success");
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDisconnect = (reason: string) => {
|
||||||
|
console.log("🔌 Socket.IO disconnected:", reason);
|
||||||
|
setStatus("قطع ارتباط");
|
||||||
|
};
|
||||||
|
|
||||||
|
const onConnectionError = (error: any) => {
|
||||||
|
console.error("❌ Socket.IO connection error:", error);
|
||||||
|
addSystemMessage("خطا در اتصال", "error");
|
||||||
|
setStatus("خطا در اتصال");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Authentication events
|
||||||
|
const onAuthenticated = (data: any) => {
|
||||||
|
console.log("✅ Authenticated:", data);
|
||||||
|
setStatus("احراز هویت شد ✅");
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUnauthorized = (data: any) => {
|
||||||
|
console.error("❌ Unauthorized:", data);
|
||||||
|
addSystemMessage("خطا در احراز هویت", "error");
|
||||||
|
setStatus("خطا در احراز هویت");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Session events
|
||||||
|
const onSessionCreated = (data: any) => {
|
||||||
|
console.log("✅ Session created:", data);
|
||||||
|
setCurrentSessionId(data.session.id);
|
||||||
|
addSystemMessage("جلسه ایجاد شد 🎉", "success");
|
||||||
|
setStatus("آماده برای گفتگو");
|
||||||
|
|
||||||
|
// Auto-join the created session
|
||||||
|
if (socket && data.session.id) {
|
||||||
|
const userId =
|
||||||
|
localStorage.getItem(import.meta.env.VITE_USER_ID_KEY || "user_id") ||
|
||||||
|
localStorage.getItem("userId") ||
|
||||||
|
"anonymous_user";
|
||||||
|
|
||||||
|
socket.emit(SOCKET_EVENTS.JOIN_CHAT, {
|
||||||
|
sessionId: data.session.id,
|
||||||
|
userId: userId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChatJoined = (data: any) => {
|
||||||
|
console.log("✅ Joined chat:", data);
|
||||||
|
addSystemMessage("به چت متصل شدید", "success");
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSessionError = (data: any) => {
|
||||||
|
console.error("❌ Session error:", data);
|
||||||
|
addSystemMessage("خطا در ایجاد جلسه", "error");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Message events
|
||||||
|
const onMessageReceived = (data: any) => {
|
||||||
|
console.log("📩 Message received:", data);
|
||||||
|
// Message is already shown in UI when sent
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bot response streaming
|
||||||
|
const onBotResponseStart = (data: any) => {
|
||||||
|
console.log("🤖 Bot response starting:", data);
|
||||||
|
|
||||||
|
// Reset streaming text
|
||||||
|
setStreamingText("");
|
||||||
|
|
||||||
|
// Create a new message and store its ID
|
||||||
|
const newMessage = addMessage({
|
||||||
|
text: "",
|
||||||
|
isUser: false,
|
||||||
|
streaming: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store the message ID and initial text in ref
|
||||||
|
currentStreamingMessageRef.current = {
|
||||||
|
id: newMessage.id,
|
||||||
|
text: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("Created new streaming message with ID:", newMessage.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBotResponseChunk = (data: any) => {
|
||||||
|
console.log(
|
||||||
|
"🤖 Bot response chunk received, data length:",
|
||||||
|
data?.data?.length || 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data.data && currentStreamingMessageRef.current) {
|
||||||
|
try {
|
||||||
|
// Update the text in our ref and state
|
||||||
|
const newText = currentStreamingMessageRef.current.text + data.data;
|
||||||
|
currentStreamingMessageRef.current.text = newText;
|
||||||
|
|
||||||
|
// Update state to trigger re-render
|
||||||
|
setStreamingText(newText);
|
||||||
|
|
||||||
|
// Update the message content
|
||||||
|
updateStreamingMessage(
|
||||||
|
currentStreamingMessageRef.current.id,
|
||||||
|
newText
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing bot response chunk:", error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn("Received chunk but no active streaming message found", {
|
||||||
|
hasData: !!data.data,
|
||||||
|
hasCurrentMessage: !!currentStreamingMessageRef.current,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBotResponseEnd = (data: any) => {
|
||||||
|
console.log("🤖 Bot response ended:", data);
|
||||||
|
|
||||||
|
if (currentStreamingMessageRef.current) {
|
||||||
|
// Make sure we have the final text
|
||||||
|
const finalText = currentStreamingMessageRef.current.text;
|
||||||
|
const messageId = currentStreamingMessageRef.current.id;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Finalizing message ${messageId} with text length: ${finalText.length}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Final update to the message content
|
||||||
|
updateStreamingMessage(messageId, finalText);
|
||||||
|
|
||||||
|
// Finalize the message by removing streaming flag
|
||||||
|
setTimeout(() => {
|
||||||
|
updateMessage(messageId, {
|
||||||
|
streaming: false,
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Clear the ref and state
|
||||||
|
setStreamingText("");
|
||||||
|
currentStreamingMessageRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsTyping(false);
|
||||||
|
setIsProcessing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBotResponse = (data: any) => {
|
||||||
|
console.log("🤖 Bot response (non-streaming):", data);
|
||||||
|
|
||||||
|
if (data.message && data.message.content) {
|
||||||
|
console.log(
|
||||||
|
`Adding non-streaming message: "${data.message.content.substring(
|
||||||
|
0,
|
||||||
|
50
|
||||||
|
)}${data.message.content.length > 50 ? "..." : ""}"`
|
||||||
|
);
|
||||||
|
|
||||||
|
addMessage({
|
||||||
|
text: data.message.content,
|
||||||
|
isUser: false,
|
||||||
|
metadata: data.message.metadata,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn("Received bot_response event with no content", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsTyping(false);
|
||||||
|
setIsProcessing(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Typing events
|
||||||
|
const onTypingStart = (data: any) => {
|
||||||
|
if (data.type === "bot") {
|
||||||
|
setIsTyping(true);
|
||||||
|
setStatus("در حال پاسخ...");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTypingStop = (data: any) => {
|
||||||
|
if (data.type === "bot") {
|
||||||
|
setIsTyping(false);
|
||||||
|
setStatus("آماده برای گفتگو");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// User status events
|
||||||
|
const onUserJoined = (data: any) => {
|
||||||
|
console.log("👤 User joined:", data);
|
||||||
|
addSystemMessage(`کاربر به چت پیوست`, "info");
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUserLeft = (data: any) => {
|
||||||
|
console.log("👤 User left:", data);
|
||||||
|
addSystemMessage(`کاربر چت را ترک کرد`, "info");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Error events
|
||||||
|
const onError = (data: any) => {
|
||||||
|
console.error("❌ Socket.IO error:", data);
|
||||||
|
addSystemMessage(data.message || "خطا در پردازش", "error");
|
||||||
|
setIsProcessing(false);
|
||||||
|
setIsTyping(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMessageError = (data: any) => {
|
||||||
|
console.error("❌ Message error:", data);
|
||||||
|
addSystemMessage(data.message || "خطا در ارسال پیام", "error");
|
||||||
|
setIsProcessing(false);
|
||||||
|
setIsTyping(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register event handlers
|
||||||
|
socket.on(SOCKET_EVENTS.CONNECT, onConnect);
|
||||||
|
socket.on(SOCKET_EVENTS.DISCONNECT, onDisconnect);
|
||||||
|
socket.on(SOCKET_EVENTS.CONNECTION_ERROR, onConnectionError);
|
||||||
|
socket.on(SOCKET_EVENTS.AUTHENTICATED, onAuthenticated);
|
||||||
|
socket.on(SOCKET_EVENTS.UNAUTHORIZED, onUnauthorized);
|
||||||
|
socket.on(SOCKET_EVENTS.SESSION_CREATED, onSessionCreated);
|
||||||
|
socket.on(SOCKET_EVENTS.CHAT_JOINED, onChatJoined);
|
||||||
|
socket.on(SOCKET_EVENTS.SESSION_ERROR, onSessionError);
|
||||||
|
socket.on(SOCKET_EVENTS.MESSAGE_RECEIVED, onMessageReceived);
|
||||||
|
socket.on(SOCKET_EVENTS.BOT_RESPONSE_START, onBotResponseStart);
|
||||||
|
socket.on(SOCKET_EVENTS.BOT_RESPONSE_CHUNK, onBotResponseChunk);
|
||||||
|
socket.on(SOCKET_EVENTS.BOT_RESPONSE_END, onBotResponseEnd);
|
||||||
|
socket.on(SOCKET_EVENTS.BOT_RESPONSE, onBotResponse);
|
||||||
|
socket.on(SOCKET_EVENTS.TYPING_START, onTypingStart);
|
||||||
|
socket.on(SOCKET_EVENTS.TYPING_STOP, onTypingStop);
|
||||||
|
socket.on(SOCKET_EVENTS.USER_JOINED, onUserJoined);
|
||||||
|
socket.on(SOCKET_EVENTS.USER_LEFT, onUserLeft);
|
||||||
|
socket.on(SOCKET_EVENTS.ERROR, onError);
|
||||||
|
socket.on(SOCKET_EVENTS.MESSAGE_ERROR, onMessageError);
|
||||||
|
socket.on(SOCKET_EVENTS.WEBSOCKET_ERROR, onError);
|
||||||
|
|
||||||
|
// Return cleanup function
|
||||||
|
return () => {
|
||||||
|
socket.off(SOCKET_EVENTS.CONNECT, onConnect);
|
||||||
|
socket.off(SOCKET_EVENTS.DISCONNECT, onDisconnect);
|
||||||
|
socket.off(SOCKET_EVENTS.CONNECTION_ERROR, onConnectionError);
|
||||||
|
socket.off(SOCKET_EVENTS.AUTHENTICATED, onAuthenticated);
|
||||||
|
socket.off(SOCKET_EVENTS.UNAUTHORIZED, onUnauthorized);
|
||||||
|
socket.off(SOCKET_EVENTS.SESSION_CREATED, onSessionCreated);
|
||||||
|
socket.off(SOCKET_EVENTS.CHAT_JOINED, onChatJoined);
|
||||||
|
socket.off(SOCKET_EVENTS.SESSION_ERROR, onSessionError);
|
||||||
|
socket.off(SOCKET_EVENTS.MESSAGE_RECEIVED, onMessageReceived);
|
||||||
|
socket.off(SOCKET_EVENTS.BOT_RESPONSE_START, onBotResponseStart);
|
||||||
|
socket.off(SOCKET_EVENTS.BOT_RESPONSE_CHUNK, onBotResponseChunk);
|
||||||
|
socket.off(SOCKET_EVENTS.BOT_RESPONSE_END, onBotResponseEnd);
|
||||||
|
socket.off(SOCKET_EVENTS.BOT_RESPONSE, onBotResponse);
|
||||||
|
socket.off(SOCKET_EVENTS.TYPING_START, onTypingStart);
|
||||||
|
socket.off(SOCKET_EVENTS.TYPING_STOP, onTypingStop);
|
||||||
|
socket.off(SOCKET_EVENTS.USER_JOINED, onUserJoined);
|
||||||
|
socket.off(SOCKET_EVENTS.USER_LEFT, onUserLeft);
|
||||||
|
socket.off(SOCKET_EVENTS.ERROR, onError);
|
||||||
|
socket.off(SOCKET_EVENTS.MESSAGE_ERROR, onMessageError);
|
||||||
|
socket.off(SOCKET_EVENTS.WEBSOCKET_ERROR, onError);
|
||||||
|
|
||||||
|
// Clear streaming message ref on cleanup
|
||||||
|
currentStreamingMessageRef.current = null;
|
||||||
|
setStreamingText("");
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
socket,
|
||||||
|
setStatus,
|
||||||
|
setIsTyping,
|
||||||
|
setIsProcessing,
|
||||||
|
addMessage,
|
||||||
|
updateMessage,
|
||||||
|
addSystemMessage,
|
||||||
|
setCurrentSessionId,
|
||||||
|
streamingText, // Add dependency to ensure handler is updated when streaming text changes
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { setupSocketHandlers, streamingText };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to format text with markdown-like syntax
|
||||||
|
const formatText = (text: string) => {
|
||||||
|
if (!text) return "";
|
||||||
|
|
||||||
|
// Clean the text first to avoid HTML injection
|
||||||
|
const cleanedText = text
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
|
||||||
|
// Format with markdown-like syntax and handle newlines properly
|
||||||
|
const formattedText = cleanedText
|
||||||
|
// Bold text
|
||||||
|
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
||||||
|
// Italic text
|
||||||
|
.replace(/\*(.*?)\*/g, "<em>$1</em>")
|
||||||
|
// Line breaks - ensure they work properly with proper spacing
|
||||||
|
.replace(/\n/g, "<br class='message-line-break' />")
|
||||||
|
// Links (optional)
|
||||||
|
.replace(
|
||||||
|
/\[([^\]]+)\]\(([^)]+)\)/g,
|
||||||
|
'<a href="$2" target="_blank" rel="noopener noreferrer" class="text-blue-500 underline">$1</a>'
|
||||||
|
);
|
||||||
|
|
||||||
|
return formattedText;
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Chatbot } from "./Chatbot";
|
||||||
|
export { default as ChatbotButton } from "./ChatbotButton";
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { Socket } from "socket.io-client";
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
isUser: boolean;
|
||||||
|
timestamp: Date;
|
||||||
|
streaming?: boolean;
|
||||||
|
isSystem?: boolean;
|
||||||
|
metadata?: {
|
||||||
|
confidence?: number;
|
||||||
|
type?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SocketData {
|
||||||
|
message?: {
|
||||||
|
content: string;
|
||||||
|
metadata?: {
|
||||||
|
confidence?: number;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
session?: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
data?: string;
|
||||||
|
type?: string;
|
||||||
|
errorMessage?: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SocketHandlersProps {
|
||||||
|
socket: Socket | null;
|
||||||
|
setStatus: (status: string) => void;
|
||||||
|
setIsTyping: (isTyping: boolean) => void;
|
||||||
|
setIsProcessing: (isProcessing: boolean) => void;
|
||||||
|
addMessage: (message: MessageInput) => Message;
|
||||||
|
addSystemMessage: (text: string, type: "success" | "error" | "info") => void;
|
||||||
|
setCurrentSessionId: (id: string | null) => void;
|
||||||
|
isProcessing?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageInput {
|
||||||
|
text: string;
|
||||||
|
isUser: boolean;
|
||||||
|
metadata?: {
|
||||||
|
confidence?: number;
|
||||||
|
};
|
||||||
|
streaming?: boolean;
|
||||||
|
}
|
||||||
@@ -59,6 +59,12 @@ axiosInstance.interceptors.response.use(
|
|||||||
originalRequest.headers.Authorization = `Bearer ${data.accessToken.token}`;
|
originalRequest.headers.Authorization = `Bearer ${data.accessToken.token}`;
|
||||||
return axiosInstance(originalRequest);
|
return axiosInstance(originalRequest);
|
||||||
} else {
|
} else {
|
||||||
|
await removeRefreshToken();
|
||||||
|
await removeToken();
|
||||||
|
const slug = localStorage.getItem(import.meta.env.VITE_SLUG_ID);
|
||||||
|
const slug_parse = slug ? JSON.parse(slug) : null;
|
||||||
|
window.location.href = `/${slug_parse?.slug}`;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
} catch (refreshError) {
|
} catch (refreshError) {
|
||||||
console.error("Token refresh failed:", refreshError);
|
console.error("Token refresh failed:", refreshError);
|
||||||
|
|||||||
+130
@@ -169,3 +169,133 @@ tbody tr {
|
|||||||
.swiper-rtl .swiper-button-next:after {
|
.swiper-rtl .swiper-button-next:after {
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Chatbot animations */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideLeft {
|
||||||
|
from {
|
||||||
|
transform: translateX(-20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-right {
|
||||||
|
animation: slideRight 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-left {
|
||||||
|
animation: slideLeft 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Improved Chatbot typing indicator */
|
||||||
|
.typing-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:first-child {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:not(:first-child) {
|
||||||
|
height: 5px;
|
||||||
|
width: 5px;
|
||||||
|
float: right;
|
||||||
|
margin: 0 1px;
|
||||||
|
background-color: #9e9ea1;
|
||||||
|
display: block;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.4;
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-of-type(2) {
|
||||||
|
animation: 1s bounce infinite 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-indicator span:nth-of-type(3) {
|
||||||
|
animation: 1s bounce infinite 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0) scale(0.8);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-4px) scale(1);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Message animation */
|
||||||
|
.streaming-message {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.streaming-message::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, transparent, #3b82f6, transparent);
|
||||||
|
animation: loading 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for newlines in messages */
|
||||||
|
.whitespace-pre-line br {
|
||||||
|
display: block;
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-line-break {
|
||||||
|
display: block;
|
||||||
|
height: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user