diff --git a/.env b/.env index 29222af..ae75762 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 = 'https://nhrq9m1d-4000.eun1.devtunnels.ms' +VITE_BASE_URL = 'http://localhost:4000' VITE_SLUG_ID = 'slug_id' \ No newline at end of file diff --git a/src/chatbot/Chatbot.tsx b/src/chatbot/Chatbot.tsx index 2180774..a8fe73c 100644 --- a/src/chatbot/Chatbot.tsx +++ b/src/chatbot/Chatbot.tsx @@ -1,584 +1,654 @@ -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 +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 ''; + if (!text) return ""; - // Clean the text first to avoid HTML injection - const cleanedText = text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); + // 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'); + // 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; + return formattedText; }; interface ChatbotProps { - isOpen: boolean; - onClose: () => void; + isOpen: boolean; + onClose: () => void; } // Connection status types -type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error'; +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; + // 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 [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); + // 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 + // Custom hooks + const { + messages, + addMessage, + addSystemMessage, + updateMessage, + clearMessages, + } = useMessages(); + const { setupSocketHandlers } = useSocketHandlers({ + socket, + setStatus: (status: string) => { + console.log("📊 Status update:", status); + // Map status string to connection status + if ( + status.includes("متصل شد") || + status.includes("آماده") || + 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 connection status changes + useEffect(() => { + console.log("🔄 Connection status changed:", { + connectionStatus, + socketConnected: socket?.connected, + currentSessionId, + isProcessing, }); + }, [connectionStatus, socket?.connected, currentSessionId, isProcessing]); - // Debug streaming text changes - useEffect(() => { - console.log("Streaming text updated:", streamingText.substring(0, 50) + (streamingText.length > 50 ? "..." : "")); - }, [streamingText]); + // Monitor socket connection and session state + useEffect(() => { + if (socket?.connected && currentSessionId) { + console.log( + "🔗 Socket connected and session ready - setting status to connected" + ); + setConnectionStatus("connected"); + } + }, [socket?.connected, currentSessionId]); - // Initialize component - const init = () => { - console.log("✅ DZone ChatBot initialized!"); + // Update button state when processing state changes + useEffect(() => { + updateButtonState(); + }, [isProcessing, connectionStatus, inputValue]); + + // 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]); - // Run initialization once on mount - useEffect(() => { - init(); - }, []); + // Handle reconnection attempts + useEffect(() => { + if ( + connectionStatus === "error" && + reconnectAttempts < maxReconnectAttempts + ) { + const reconnectTimer = setTimeout(() => { + attemptReconnect(); + }, 2000 * (reconnectAttempts + 1)); // Exponential backoff - // Animation effect for opening/closing - useEffect(() => { - if (isOpen) { - // Small delay to trigger animation - setTimeout(() => setShowChatbot(true), 50); - } else { - setShowChatbot(false); + 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]); // Only depend on socket, not 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; + } + + console.log("🔄 Connecting to Socket.IO..."); + const newSocket = io(`${apiBaseUrl}/chat`, { + auth: { + token: token, + }, + }); + + setSocket(newSocket); + setReconnectAttempts(0); + + // Set a timeout for connection + connectionTimeoutRef.current = setTimeout(() => { + if (connectionStatus === "connecting") { + console.log("❌ Connection timeout"); + setConnectionStatus("error"); } - }, [isOpen]); + }, 10000); - // Connect to socket when component mounts - useEffect(() => { - if (isOpen) { - connectSocket(); + // Create session after connection - like in JS version + setTimeout(() => { + if (newSocket.connected) { + createSession(newSocket); } + }, 1000); + } catch (error) { + console.error("Socket connection error:", error); + setConnectionStatus("error"); + attemptReconnect(); + } + }; - // Cleanup on unmount or when closed - return () => { - disconnectSocket(); - if (connectionTimeoutRef.current) { - clearTimeout(connectionTimeoutRef.current); - } + 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) { + console.log("❌ Socket not connected for session creation"); + return; + } + + if (currentSessionId) { + console.log("🚫 Session already exists:", currentSessionId); + addSystemMessage("جلسه از قبل وجود دارد", "info"); + return; + } + + console.log("🔄 Creating new session via Socket.IO..."); + 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 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 updated:", { + hasText, + isProcessing, + connectionStatus, + currentSessionId, + canSend, + }); + }; + + // Get connection status icon and text + const getConnectionStatusInfo = () => { + switch (connectionStatus) { + case "connected": + return { + icon: , + text: "متصل", + className: "bg-green-100 text-green-800", }; - }, [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(); - } + case "connecting": + return { + icon: , + text: "در حال اتصال...", + className: "bg-yellow-100 text-yellow-800", }; - }, [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); - } + case "error": + return { + icon: , + text: "خطا در اتصال", + className: "bg-red-100 text-red-800", }; - }, [connectionStatus]); + default: + return { + icon: , + text: "قطع اتصال", + className: "bg-gray-100 text-gray-800", + }; + } + }; - const scrollToBottom = () => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }; + if (!isOpen) return null; - const connectSocket = async () => { - try { - setConnectionStatus('connecting'); + const connectionInfo = getConnectionStatusInfo(); - // 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 */} -
-
-