diff --git a/src/chatbot/Chatbot.tsx b/src/chatbot/Chatbot.tsx index 47a54e7..61b2df4 100644 --- a/src/chatbot/Chatbot.tsx +++ b/src/chatbot/Chatbot.tsx @@ -1,5 +1,5 @@ import { FC, useState, useRef, useEffect } from "react"; -import { Send, CloseCircle, User } from "iconsax-react"; +import { Send, CloseCircle, User, Add, ArrowRight } from "iconsax-react"; import io, { Socket } from "socket.io-client"; import { SOCKET_EVENTS } from "./constants"; import { useSocketHandlers } from "./hooks/useSocketHandlers"; @@ -7,6 +7,7 @@ import { useMessages } from "./hooks/useMessages"; import { Message } from "./types"; import { getToken } from "../config/func"; import "./chatbot.css"; // Import CSS for animations +import axios from "axios"; // Helper function to format text with markdown-like syntax export const formatText = (text: string) => { @@ -45,6 +46,14 @@ interface ChatbotProps { // Connection status types type ConnectionStatus = "disconnected" | "connecting" | "connected" | "error"; +// Session type definition +interface ChatSession { + id: string; + title: string; + createdAt: string; + updatedAt: string; +} + const Chatbot: FC = ({ isOpen, onClose }) => { // State const [inputValue, setInputValue] = useState(""); @@ -55,11 +64,14 @@ const Chatbot: FC = ({ isOpen, onClose }) => { 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; + // Sessions related state + const [sessions, setSessions] = useState([]); + const [isLoadingSessions, setIsLoadingSessions] = useState(false); + const [showSessionSelect, setShowSessionSelect] = useState(true); + // Config const apiBaseUrl = import.meta.env.VITE_BASE_URL; const messagesEndRef = useRef(null); @@ -73,6 +85,7 @@ const Chatbot: FC = ({ isOpen, onClose }) => { addMessage, addSystemMessage, updateMessage, + setMessages, } = useMessages(); const { setupSocketHandlers } = useSocketHandlers({ socket, @@ -147,10 +160,11 @@ const Chatbot: FC = ({ isOpen, onClose }) => { } }, [isOpen]); - // Connect to socket when component mounts + // Connect to socket and fetch sessions when component mounts useEffect(() => { if (isOpen) { connectSocket(); + fetchSessions(); } // Cleanup on unmount or when closed @@ -198,17 +212,190 @@ const Chatbot: FC = ({ isOpen, onClose }) => { }; }, [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); + // Fetch user's previous chat sessions + const fetchSessions = async () => { + try { + setIsLoadingSessions(true); + const token = await getToken(); + const response = await axios.get(`${apiBaseUrl}/chatbot/sessions`, { + headers: { Authorization: `Bearer ${token}` } + }); - return () => { - if (connectionTimeoutRef.current) { - clearTimeout(connectionTimeoutRef.current); + // Extract sessions from response.data.data as the API returns { statusCode, success, data: [...sessions] } + const sessionsData = response.data?.data || []; + setSessions(sessionsData); + console.log("📝 Fetched sessions:", sessionsData); + } catch (error) { + console.error("Error fetching sessions:", error); + addSystemMessage("خطا در دریافت جلسات قبلی", "error"); + } finally { + setIsLoadingSessions(false); + } + }; + + // Join an existing chat session + 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, + }); + }; + + // Load a specific session with its messages + const loadSession = async (sessionId: string) => { + try { + setIsProcessing(true); + const token = await getToken(); + + // First ensure we have a socket connection + if (!socket || !socket.connected) { + console.log("Socket not connected, connecting first..."); + await connectSocketPromise(token); } - }; - }, [connectionStatus]); + + const response = await axios.get(`${apiBaseUrl}/chatbot/sessions/${sessionId}`, { + headers: { Authorization: `Bearer ${token}` } + }); + + // Extract session data from response.data.data + const sessionData = response.data?.data || {}; + + // Set current session ID + setCurrentSessionId(sessionId); + + // Convert API messages to our message format and load them + if (sessionData && sessionData.messages) { + const formattedMessages = sessionData.messages.map((msg: any) => ({ + id: msg.id || Date.now().toString() + Math.random().toString(), + text: msg.content || msg.text || "", + isUser: msg.type === "user", + isSystem: msg.type === "system", + timestamp: new Date(msg.createdAt || msg.timestamp || Date.now()), + metadata: msg.metadata || {} + })); + + setMessages(formattedMessages); + } + + // Join the chat session + if (socket && socket.connected) { + joinChat(sessionId); + } else { + console.error("Still couldn't connect socket after retrying"); + addSystemMessage("خطا در اتصال به سرور چت", "error"); + } + + // Switch to chat view + setShowSessionSelect(false); + } catch (error) { + console.error("Error loading session:", error); + addSystemMessage("خطا در بارگذاری جلسه", "error"); + } finally { + setIsProcessing(false); + } + }; + + // Promise-based socket connection + const connectSocketPromise = (token: string | undefined): Promise => { + return new Promise((resolve, reject) => { + try { + setConnectionStatus("connecting"); + + if (!apiBaseUrl) { + console.error("API base URL not defined"); + setConnectionStatus("error"); + reject(new Error("API base URL not defined")); + return; + } + + if (!token) { + console.error("No authentication token available"); + setConnectionStatus("error"); + reject(new Error("No authentication token available")); + return; + } + + console.log("🔄 Connecting to Socket.IO..."); + const newSocket = io(`${apiBaseUrl}/chat`, { + auth: { token } + }); + + // Set up connection timeout + const timeout = setTimeout(() => { + if (newSocket.connected) { + clearTimeout(timeout); + return; + } + + console.log("❌ Connection timeout"); + setConnectionStatus("error"); + reject(new Error("Connection timeout")); + }, 10000); + + // Handle successful connection + newSocket.on("connect", () => { + console.log("✅ Socket connected successfully"); + setConnectionStatus("connected"); + setSocket(newSocket); + clearTimeout(timeout); + resolve(newSocket); + }); + + // Handle connection error + newSocket.on("connect_error", (err) => { + console.error("Socket connection error:", err); + setConnectionStatus("error"); + clearTimeout(timeout); + reject(err); + }); + } catch (error) { + console.error("Socket connection promise error:", error); + setConnectionStatus("error"); + reject(error); + } + }); + }; + + // Create a new chat session + const handleCreateNewSession = async () => { + try { + setIsProcessing(true); + setShowSessionSelect(false); + setCurrentSessionId(null); + setMessages([]); + + // Ensure socket is connected + if (!socket || !socket.connected) { + const token = await getToken(); + await connectSocketPromise(token); + } + + // Create a new session + if (socket && socket.connected) { + createSession(socket); + } else { + console.error("Failed to connect socket for new session"); + addSystemMessage("خطا در اتصال به سرور چت", "error"); + } + } catch (error) { + console.error("Error creating new session:", error); + addSystemMessage("خطا در ایجاد جلسه جدید", "error"); + } finally { + setIsProcessing(false); + } + }; const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); @@ -216,41 +403,9 @@ const Chatbot: FC = ({ isOpen, onClose }) => { 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); + await connectSocketPromise(token); setReconnectAttempts(0); - - // Set a timeout for connection - connectionTimeoutRef.current = setTimeout(() => { - if (connectionStatus === "connecting") { - console.log("❌ Connection timeout"); - setConnectionStatus("error"); - } - }, 10000); - - // 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"); @@ -297,29 +452,10 @@ const Chatbot: FC = ({ isOpen, onClose }) => { console.log("🔄 Creating new session via Socket.IO..."); socketInstance.emit(SOCKET_EVENTS.CREATE_SESSION, { - title: `جلسه ${new Date().toLocaleDateString("fa-IR")}`, + 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(); @@ -422,40 +558,23 @@ const Chatbot: FC = ({ isOpen, onClose }) => { }); }; - // 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", - // }; - // } - // }; + // Format date for session list + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleDateString("fa-IR", { + year: "numeric", + month: "long", + day: "numeric", + }); + }; + + // Handle return to sessions list + const handleReturnToSessions = () => { + setShowSessionSelect(true); + }; if (!isOpen) return null; - // const connectionInfo = getConnectionStatusInfo(); - return (
= ({ isOpen, onClose }) => { : "bg-red-400" }`} >
+ {!showSessionSelect && ( + + )}

چت بات داناک

- {/* Always show a compact connection status */} - {/*
- {connectionInfo.icon} -
*/} - {/* 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, - }); + {showSessionSelect ? ( + /* Sessions Selection Screen */ +
+
+

جلسات قبلی شما

+

یک جلسه را انتخاب کنید یا جلسه جدیدی بسازید.

+
- return ( -
- {message.isUser && ( -
- + {isLoadingSessions ? ( +
+
+
+ ) : ( + <> + + + {sessions.length === 0 ? ( +
+ هنوز هیچ جلسه‌ای ندارید +
+ ) : ( +
+ {sessions.map((session) => ( +
loadSession(session.id)} + className="bg-white p-3 rounded-lg shadow-sm cursor-pointer hover:bg-gray-100 transition-colors" + > +
{session.title}
+
+ {formatDate(session.createdAt)} +
+
+ ))}
)} -
+ + )} +
+ ) : ( + /* Chat UI */ + <> + {/* 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.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.isUser && !message.isSystem && ( +
+
+
+ {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 && ( +
+ 🤖 +
+ )} +
+ ); + })} + {/* Typing indicator - only show when no streaming messages exist */} + {isTyping && !messages.some((msg) => msg.streaming) && ( +
+
+
+ در حال پاسخ... + + +
+
🤖
- )} -
- ); - })} - {/* Typing indicator - only show when no streaming messages exist */} - {isTyping && !messages.some((msg) => msg.streaming) && ( -
-
-
- در حال پاسخ... - -
-
-
- 🤖 + )} +
+
+ + {/* Input */} +
+
+