list old sessions
This commit is contained in:
+402
-235
@@ -1,5 +1,5 @@
|
|||||||
import { FC, useState, useRef, useEffect } from "react";
|
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 io, { Socket } from "socket.io-client";
|
||||||
import { SOCKET_EVENTS } from "./constants";
|
import { SOCKET_EVENTS } from "./constants";
|
||||||
import { useSocketHandlers } from "./hooks/useSocketHandlers";
|
import { useSocketHandlers } from "./hooks/useSocketHandlers";
|
||||||
@@ -7,6 +7,7 @@ import { useMessages } from "./hooks/useMessages";
|
|||||||
import { Message } from "./types";
|
import { Message } from "./types";
|
||||||
import { getToken } from "../config/func";
|
import { getToken } from "../config/func";
|
||||||
import "./chatbot.css"; // Import CSS for animations
|
import "./chatbot.css"; // Import CSS for animations
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
// Helper function to format text with markdown-like syntax
|
// Helper function to format text with markdown-like syntax
|
||||||
export const formatText = (text: string) => {
|
export const formatText = (text: string) => {
|
||||||
@@ -45,6 +46,14 @@ interface ChatbotProps {
|
|||||||
// Connection status types
|
// Connection status types
|
||||||
type ConnectionStatus = "disconnected" | "connecting" | "connected" | "error";
|
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 }) => {
|
const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
||||||
// State
|
// State
|
||||||
const [inputValue, setInputValue] = useState<string>("");
|
const [inputValue, setInputValue] = useState<string>("");
|
||||||
@@ -55,11 +64,14 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
|
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
|
||||||
const [socket, setSocket] = useState<Socket | null>(null);
|
const [socket, setSocket] = useState<Socket | null>(null);
|
||||||
const [showChatbot, setShowChatbot] = useState<boolean>(false);
|
const [showChatbot, setShowChatbot] = useState<boolean>(false);
|
||||||
// const [showConnectionMessage, setShowConnectionMessage] =
|
|
||||||
// useState<boolean>(false);
|
|
||||||
const [reconnectAttempts, setReconnectAttempts] = useState<number>(0);
|
const [reconnectAttempts, setReconnectAttempts] = useState<number>(0);
|
||||||
const maxReconnectAttempts = 5;
|
const maxReconnectAttempts = 5;
|
||||||
|
|
||||||
|
// Sessions related state
|
||||||
|
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||||
|
const [isLoadingSessions, setIsLoadingSessions] = useState<boolean>(false);
|
||||||
|
const [showSessionSelect, setShowSessionSelect] = useState<boolean>(true);
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
const apiBaseUrl = import.meta.env.VITE_BASE_URL;
|
const apiBaseUrl = import.meta.env.VITE_BASE_URL;
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -73,6 +85,7 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
addMessage,
|
addMessage,
|
||||||
addSystemMessage,
|
addSystemMessage,
|
||||||
updateMessage,
|
updateMessage,
|
||||||
|
setMessages,
|
||||||
} = useMessages();
|
} = useMessages();
|
||||||
const { setupSocketHandlers } = useSocketHandlers({
|
const { setupSocketHandlers } = useSocketHandlers({
|
||||||
socket,
|
socket,
|
||||||
@@ -147,10 +160,11 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
// Connect to socket when component mounts
|
// Connect to socket and fetch sessions when component mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
connectSocket();
|
connectSocket();
|
||||||
|
fetchSessions();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup on unmount or when closed
|
// Cleanup on unmount or when closed
|
||||||
@@ -198,17 +212,190 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
};
|
};
|
||||||
}, [socket]); // Only depend on socket, not setupSocketHandlers
|
}, [socket]); // Only depend on socket, not setupSocketHandlers
|
||||||
|
|
||||||
// Show connection message only briefly when status changes
|
// Fetch user's previous chat sessions
|
||||||
useEffect(() => {
|
const fetchSessions = async () => {
|
||||||
// Always show connection status, don't hide it
|
try {
|
||||||
// setShowConnectionMessage(true);
|
setIsLoadingSessions(true);
|
||||||
|
const token = await getToken();
|
||||||
|
const response = await axios.get(`${apiBaseUrl}/chatbot/sessions`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
// Extract sessions from response.data.data as the API returns { statusCode, success, data: [...sessions] }
|
||||||
if (connectionTimeoutRef.current) {
|
const sessionsData = response.data?.data || [];
|
||||||
clearTimeout(connectionTimeoutRef.current);
|
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 = () => {
|
const scrollToBottom = () => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
@@ -216,41 +403,9 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
|
|
||||||
const connectSocket = async () => {
|
const connectSocket = async () => {
|
||||||
try {
|
try {
|
||||||
setConnectionStatus("connecting");
|
|
||||||
|
|
||||||
// Get token from environment or localStorage
|
|
||||||
const token = await getToken();
|
const token = await getToken();
|
||||||
|
await connectSocketPromise(token);
|
||||||
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);
|
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) {
|
} catch (error) {
|
||||||
console.error("Socket connection error:", error);
|
console.error("Socket connection error:", error);
|
||||||
setConnectionStatus("error");
|
setConnectionStatus("error");
|
||||||
@@ -297,29 +452,10 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
|
|
||||||
console.log("🔄 Creating new session via Socket.IO...");
|
console.log("🔄 Creating new session via Socket.IO...");
|
||||||
socketInstance.emit(SOCKET_EVENTS.CREATE_SESSION, {
|
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 handleSendMessage = () => {
|
||||||
const message = inputValue.trim();
|
const message = inputValue.trim();
|
||||||
|
|
||||||
@@ -422,40 +558,23 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get connection status icon and text
|
// Format date for session list
|
||||||
// const getConnectionStatusInfo = () => {
|
const formatDate = (dateString: string) => {
|
||||||
// switch (connectionStatus) {
|
const date = new Date(dateString);
|
||||||
// case "connected":
|
return date.toLocaleDateString("fa-IR", {
|
||||||
// return {
|
year: "numeric",
|
||||||
// icon: <Wifi size={16} className="text-green-400" />,
|
month: "long",
|
||||||
// text: "متصل",
|
day: "numeric",
|
||||||
// className: "bg-green-100 text-green-800",
|
});
|
||||||
// };
|
};
|
||||||
// case "connecting":
|
|
||||||
// return {
|
// Handle return to sessions list
|
||||||
// icon: <Wifi size={16} className="text-yellow-400 animate-pulse" />,
|
const handleReturnToSessions = () => {
|
||||||
// text: "در حال اتصال...",
|
setShowSessionSelect(true);
|
||||||
// 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",
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
// const connectionInfo = getConnectionStatusInfo();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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
|
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"
|
: "bg-red-400"
|
||||||
}`}
|
}`}
|
||||||
></div>
|
></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>
|
<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>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
@@ -497,147 +614,197 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Messages */}
|
{showSessionSelect ? (
|
||||||
<div
|
/* Sessions Selection Screen */
|
||||||
className="flex-1 p-3.5 overflow-y-auto bg-gray-50"
|
<div className="flex-1 p-3.5 overflow-y-auto bg-gray-50">
|
||||||
style={{ height: "350px" }}
|
<div className="mb-4">
|
||||||
>
|
<h3 className="text-lg font-bold text-gray-800 mb-2">جلسات قبلی شما</h3>
|
||||||
{messages?.map((message: Message) => {
|
<p className="text-sm text-gray-600">یک جلسه را انتخاب کنید یا جلسه جدیدی بسازید.</p>
|
||||||
// Debug message content
|
</div>
|
||||||
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 (
|
{isLoadingSessions ? (
|
||||||
<div
|
<div className="flex justify-center items-center h-64">
|
||||||
key={message.id}
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
|
||||||
className={`mb-3 flex ${message.isUser ? "justify-start" : "justify-end"
|
</div>
|
||||||
} animate-fade-in`}
|
) : (
|
||||||
>
|
<>
|
||||||
{message.isUser && (
|
<button
|
||||||
<div className="w-7 h-7 rounded-full bg-black flex items-center justify-center ml-2 flex-shrink-0">
|
onClick={handleCreateNewSession}
|
||||||
<span className="text-white text-xs"><User size={14} color="white" /></span>
|
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>
|
||||||
)}
|
)}
|
||||||
<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"
|
</div>
|
||||||
: message.isSystem
|
) : (
|
||||||
? message.metadata?.type === "error"
|
/* Chat UI */
|
||||||
? "bg-red-100 text-red-800 rounded-tl-none"
|
<>
|
||||||
: message.metadata?.type === "success"
|
{/* Messages */}
|
||||||
? "bg-green-100 text-green-800 rounded-tl-none"
|
<div
|
||||||
: "bg-yellow-100 text-yellow-800 rounded-tl-none"
|
className="flex-1 p-3.5 overflow-y-auto bg-gray-50"
|
||||||
: message.metadata && "confidence" in message.metadata
|
style={{ height: "350px" }}
|
||||||
? message.metadata.confidence === 1
|
>
|
||||||
? "bg-green-100 text-green-800 rounded-tl-none"
|
{messages?.map((message: Message) => {
|
||||||
: message.metadata.confidence === 0
|
// Debug message content
|
||||||
? "bg-red-100 text-red-800 rounded-tl-none"
|
console.log(`Rendering message ${message.id}:`, {
|
||||||
: "bg-yellow-100 text-yellow-800 rounded-tl-none"
|
text:
|
||||||
: "bg-white text-gray-800 rounded-tl-none shadow-sm animate-slide-left"
|
message.text.substring(0, 30) +
|
||||||
} ${message.streaming
|
(message.text.length > 30 ? "..." : ""),
|
||||||
? "border-r-4 border-gray-400 streaming-message animate-pulse"
|
isUser: message.isUser,
|
||||||
: ""
|
isSystem: message.isSystem,
|
||||||
}`}
|
streaming: message.streaming,
|
||||||
>
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
id={`message-${message.id}`}
|
key={message.id}
|
||||||
className={`text-sm leading-6 font-light whitespace-pre-line break-words text-right mb-2 ${message.streaming ? "streaming-content" : ""
|
className={`mb-3 flex ${message.isUser ? "justify-start" : "justify-end"
|
||||||
}`}
|
} animate-fade-in`}
|
||||||
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", {
|
{message.isUser && (
|
||||||
hour: "2-digit",
|
<div className="w-7 h-7 rounded-full bg-black flex items-center justify-center ml-2 flex-shrink-0">
|
||||||
minute: "2-digit",
|
<span className="text-white text-xs"><User size={14} color="white" /></span>
|
||||||
})}
|
|
||||||
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
<div
|
||||||
{!message.isUser && !message.isSystem && (
|
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">
|
<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>
|
<span className="text-gray-600 text-xs">🤖</span>
|
||||||
</div>
|
</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>
|
)}
|
||||||
<div className="w-7 h-7 rounded-full bg-gray-200 flex items-center justify-center mr-2 flex-shrink-0">
|
<div ref={messagesEndRef} />
|
||||||
<span className="text-gray-600 text-xs">🤖</span>
|
</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>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Chatbot;
|
export default Chatbot;
|
||||||
@@ -81,5 +81,6 @@ export const useMessages = () => {
|
|||||||
updateMessage,
|
updateMessage,
|
||||||
removeMessage,
|
removeMessage,
|
||||||
clearMessages,
|
clearMessages,
|
||||||
|
setMessages,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user