chatbot debug
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
VITE_TOKEN_NAME = 'dzone_token'
|
||||
VITE_REFRESH_TOKEN_NAME = 'dzone_refresh_token'
|
||||
VITE_BASE_URL = 'https://api-dzone.danakcorp.com'
|
||||
# VITE_BASE_URL = 'http://192.168.82.158:4000'
|
||||
# VITE_BASE_URL = 'https://api-dzone.danakcorp.com'
|
||||
VITE_BASE_URL = 'https://nhrq9m1d-4000.eun1.devtunnels.ms'
|
||||
|
||||
VITE_SLUG_ID = 'slug_id'
|
||||
Generated
+168
-1
@@ -11,6 +11,7 @@
|
||||
"@headlessui/react": "^2.2.0",
|
||||
"@material-tailwind/react": "^2.1.10",
|
||||
"@tanstack/react-query": "^5.64.1",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"axios": "^1.7.9",
|
||||
"formik": "^2.4.6",
|
||||
"i18next": "^24.2.1",
|
||||
@@ -28,9 +29,12 @@
|
||||
"react-otp-input": "^3.1.1",
|
||||
"react-router-dom": "^7.1.3",
|
||||
"react-spinners": "^0.17.0",
|
||||
"react-string-replace": "^1.1.1",
|
||||
"react-toastify": "^11.0.3",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"swiper": "^11.2.1",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"uuid": "^11.1.0",
|
||||
"yup": "^1.6.1",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
@@ -1732,6 +1736,12 @@
|
||||
"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": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
@@ -1913,6 +1923,12 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz",
|
||||
@@ -2697,6 +2713,45 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.25.4",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
||||
@@ -3834,7 +3889,6 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mz": {
|
||||
@@ -4513,6 +4567,15 @@
|
||||
"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": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.3.tgz",
|
||||
@@ -4716,6 +4779,68 @@
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -5219,6 +5344,19 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
@@ -5452,6 +5590,35 @@
|
||||
"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": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@headlessui/react": "^2.2.0",
|
||||
"@material-tailwind/react": "^2.1.10",
|
||||
"@tanstack/react-query": "^5.64.1",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"axios": "^1.7.9",
|
||||
"formik": "^2.4.6",
|
||||
"i18next": "^24.2.1",
|
||||
@@ -30,9 +31,12 @@
|
||||
"react-otp-input": "^3.1.1",
|
||||
"react-router-dom": "^7.1.3",
|
||||
"react-spinners": "^0.17.0",
|
||||
"react-string-replace": "^1.1.1",
|
||||
"react-toastify": "^11.0.3",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"swiper": "^11.2.1",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"uuid": "^11.1.0",
|
||||
"yup": "^1.6.1",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@ import FaJson from './langs/fa.json'
|
||||
import AuthRouter from './router/Auth';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import ToastContainer from './components/Toast';
|
||||
import { ChatbotButton } from './chatbot';
|
||||
|
||||
i18next.init({
|
||||
interpolation: { escapeValue: false },
|
||||
@@ -44,6 +45,7 @@ function App() {
|
||||
<Route path="/:slug/*" element={<MainRouter />} />
|
||||
</Routes>
|
||||
<ToastContainer />
|
||||
<ChatbotButton />
|
||||
</I18nextProvider>
|
||||
</QueryClientProvider>
|
||||
</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}`;
|
||||
return axiosInstance(originalRequest);
|
||||
} 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) {
|
||||
console.error("Token refresh failed:", refreshError);
|
||||
|
||||
+130
@@ -169,3 +169,133 @@ tbody tr {
|
||||
.swiper-rtl .swiper-button-next:after {
|
||||
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