diff --git a/.env b/.env index 474dc74..29222af 100644 --- a/.env +++ b/.env @@ -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' \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d63375c..8b6a92b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index bf23606..98dd497 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/App.tsx b/src/App.tsx index c445c85..98a08e3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { } /> + diff --git a/src/chatbot/Chatbot.tsx b/src/chatbot/Chatbot.tsx new file mode 100644 index 0000000..2180774 --- /dev/null +++ b/src/chatbot/Chatbot.tsx @@ -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, '''); + + // Format with markdown-like syntax and handle newlines properly + const formattedText = cleanedText + // Bold text + .replace(/\*\*(.*?)\*\*/g, "$1") + // Italic text + .replace(/\*(.*?)\*/g, "$1") + // Line breaks - ensure they work properly with proper spacing + .replace(/\n/g, "
") + // Links (optional) + .replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); + + return formattedText; +}; + +interface ChatbotProps { + isOpen: boolean; + onClose: () => void; +} + +// Connection status types +type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error'; + +const Chatbot: FC = ({ isOpen, onClose }) => { + // State + const [inputValue, setInputValue] = useState(''); + const [isProcessing, setIsProcessing] = useState(false); + const [connectionStatus, setConnectionStatus] = useState('disconnected'); + const [isTyping, setIsTyping] = useState(false); + const [currentSessionId, setCurrentSessionId] = useState(null); + const [socket, setSocket] = useState(null); + const [showChatbot, setShowChatbot] = useState(false); + const [showConnectionMessage, setShowConnectionMessage] = useState(false); + const [isStreamingMode, setIsStreamingMode] = useState(true); + const [reconnectAttempts, setReconnectAttempts] = useState(0); + const maxReconnectAttempts = 5; + + // Config + const apiBaseUrl = import.meta.env.VITE_BASE_URL; + const messagesEndRef = useRef(null); + const connectionTimeoutRef = useRef | 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); + + // 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 | { 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: , + text: "متصل", + className: "bg-green-100 text-green-800" + }; + case 'connecting': + return { + icon: , + text: "در حال اتصال...", + className: "bg-yellow-100 text-yellow-800" + }; + case 'error': + return { + icon: , + text: "خطا در اتصال", + className: "bg-red-100 text-red-800" + }; + default: + return { + icon: , + text: "قطع اتصال", + className: "bg-gray-100 text-gray-800" + }; + } + }; + + if (!isOpen) return null; + + const connectionInfo = getConnectionStatusInfo(); + + return ( +
+ {/* Header */} +
+
+
+

چت بات داناک

+ {/* Always show a compact connection status */} +
+ {connectionInfo.icon} +
+
+ +
+ + {/* Control buttons */} +
+ + + + {connectionStatus !== 'connected' && ( + + )} +
+ + {/* Messages */} +
+ { + 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 ( +
+ {message.isUser && ( +
+ 👤 +
+ )} +
+
+
+ {message.timestamp.toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' })} +
+ {message.metadata?.confidence !== undefined && + message.metadata.confidence !== 0 && + message.metadata.confidence !== 0.5 && + message.metadata.confidence !== 1 && ( +
+ اعتماد: {Math.round(message.metadata.confidence * 100)}% +
+ )} +
+ {!message.isUser && !message.isSystem && ( +
+ 🤖 +
+ )} +
+ ); + }) + } + {/* Improved typing indicator */} + {isTyping && ( +
+
+
+ در حال پاسخ... + + +
+
+
+ 🤖 +
+
+ )} +
+
+ + {/* Input */} +
+
+