list old sessions

This commit is contained in:
hamid zarghami
2025-06-09 11:43:51 +03:30
parent 5153c1e1af
commit 67086b91ff
2 changed files with 403 additions and 235 deletions
+402 -235
View File
@@ -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<ChatbotProps> = ({ isOpen, onClose }) => {
// State
const [inputValue, setInputValue] = useState<string>("");
@@ -55,11 +64,14 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
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 [reconnectAttempts, setReconnectAttempts] = useState<number>(0);
const maxReconnectAttempts = 5;
// Sessions related state
const [sessions, setSessions] = useState<ChatSession[]>([]);
const [isLoadingSessions, setIsLoadingSessions] = useState<boolean>(false);
const [showSessionSelect, setShowSessionSelect] = useState<boolean>(true);
// Config
const apiBaseUrl = import.meta.env.VITE_BASE_URL;
const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -73,6 +85,7 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
addMessage,
addSystemMessage,
updateMessage,
setMessages,
} = useMessages();
const { setupSocketHandlers } = useSocketHandlers({
socket,
@@ -147,10 +160,11 @@ const Chatbot: FC<ChatbotProps> = ({ 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<ChatbotProps> = ({ 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<Socket> => {
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<ChatbotProps> = ({ 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<ChatbotProps> = ({ 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<ChatbotProps> = ({ isOpen, onClose }) => {
});
};
// 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",
// };
// }
// };
// 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 (
<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
@@ -475,18 +594,16 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
: "bg-red-400"
}`}
></div>
{!showSessionSelect && (
<button
onClick={handleReturnToSessions}
className="ml-2 p-1 focus:outline-none hover:bg-gray-700 rounded-full transition-colors"
title="برگشت به لیست جلسات"
>
<ArrowRight size={18} />
</button>
)}
<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}
@@ -497,147 +614,197 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
</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,
});
{showSessionSelect ? (
/* Sessions Selection Screen */
<div className="flex-1 p-3.5 overflow-y-auto bg-gray-50">
<div className="mb-4">
<h3 className="text-lg font-bold text-gray-800 mb-2">جلسات قبلی شما</h3>
<p className="text-sm text-gray-600">یک جلسه را انتخاب کنید یا جلسه جدیدی بسازید.</p>
</div>
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-black flex items-center justify-center ml-2 flex-shrink-0">
<span className="text-white text-xs"><User size={14} color="white" /></span>
{isLoadingSessions ? (
<div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
</div>
) : (
<>
<button
onClick={handleCreateNewSession}
className="w-full bg-black text-white rounded-lg p-3 mb-4 flex items-center justify-center hover:bg-gray-800 transition-colors"
>
<Add size={20} className="ml-2" />
<span>شروع جلسه جدید</span>
</button>
{sessions.length === 0 ? (
<div className="text-center text-gray-500 p-4">
هنوز هیچ جلسهای ندارید
</div>
) : (
<div className="space-y-2">
{sessions.map((session) => (
<div
key={session.id}
onClick={() => loadSession(session.id)}
className="bg-white p-3 rounded-lg shadow-sm cursor-pointer hover:bg-gray-100 transition-colors"
>
<div className="font-medium">{session.title}</div>
<div className="text-xs text-gray-500 mt-1">
{formatDate(session.createdAt)}
</div>
</div>
))}
</div>
)}
<div
className={`relative px-3.5 py-2.5 pb-4 rounded-lg max-w-[85%] ${message.isUser
? "bg-black 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-gray-400 streaming-message animate-pulse"
: ""
}`}
>
</>
)}
</div>
) : (
/* Chat UI */
<>
{/* 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
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"
}`}
key={message.id}
className={`mb-3 flex ${message.isUser ? "justify-start" : "justify-end"
} animate-fade-in`}
>
{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)}%
{message.isUser && (
<div className="w-7 h-7 rounded-full bg-black flex items-center justify-center ml-2 flex-shrink-0">
<span className="text-white text-xs"><User size={14} color="white" /></span>
</div>
)}
</div>
{!message.isUser && !message.isSystem && (
<div
className={`relative px-3.5 py-2.5 pb-4 rounded-lg max-w-[85%] ${message.isUser
? "bg-black 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-gray-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-gray-200 flex items-center justify-center mr-2 flex-shrink-0">
<span className="text-gray-600 text-xs">🤖</span>
</div>
)}
</div>
);
})}
{/* Typing indicator - only show when no streaming messages exist */}
{isTyping && !messages.some((msg) => msg.streaming) && (
<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-gray-200 flex items-center justify-center mr-2 flex-shrink-0">
<span className="text-gray-600 text-xs">🤖</span>
</div>
)}
</div>
);
})}
{/* Typing indicator - only show when no streaming messages exist */}
{isTyping && !messages.some((msg) => msg.streaming) && (
<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-gray-200 flex items-center justify-center mr-2 flex-shrink-0">
<span className="text-gray-600 text-xs">🤖</span>
)}
<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-gray-800 hover:bg-gray-900"
} 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 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-gray-800 hover:bg-gray-900"
} 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;
export default Chatbot;
+1
View File
@@ -81,5 +81,6 @@ export const useMessages = () => {
updateMessage,
removeMessage,
clearMessages,
setMessages,
};
};