list old sessions
This commit is contained in:
+274
-107
@@ -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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
}, [connectionStatus]);
|
||||
|
||||
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>
|
||||
<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"
|
||||
}`}
|
||||
{!showSessionSelect && (
|
||||
<button
|
||||
onClick={handleReturnToSessions}
|
||||
className="ml-2 p-1 focus:outline-none hover:bg-gray-700 rounded-full transition-colors"
|
||||
title="برگشت به لیست جلسات"
|
||||
>
|
||||
{connectionInfo.icon}
|
||||
</div> */}
|
||||
<ArrowRight size={18} />
|
||||
</button>
|
||||
)}
|
||||
<h3 className="font-bold text-base">چت بات داناک</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
@@ -497,6 +614,54 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
|
||||
{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>
|
||||
) : (
|
||||
/* Chat UI */
|
||||
<>
|
||||
{/* Messages */}
|
||||
<div
|
||||
className="flex-1 p-3.5 overflow-y-auto bg-gray-50"
|
||||
@@ -636,6 +801,8 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -81,5 +81,6 @@ export const useMessages = () => {
|
||||
updateMessage,
|
||||
removeMessage,
|
||||
clearMessages,
|
||||
setMessages,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user