355 lines
12 KiB
TypeScript
355 lines
12 KiB
TypeScript
import { useCallback, useRef } from "react";
|
|
import { Socket } from "socket.io-client";
|
|
import { SOCKET_EVENTS } from "../constants";
|
|
import { Message } from "../types";
|
|
|
|
interface UseSocketHandlersProps {
|
|
socket: Socket | null;
|
|
setStatus: (status: string) => void;
|
|
setIsTyping: (isTyping: boolean) => void;
|
|
setIsProcessing: (isProcessing: boolean) => void;
|
|
addMessage: (message: Partial<Message>) => Message;
|
|
updateMessage: (id: string, updates: Partial<Message>) => void;
|
|
addSystemMessage: (text: string, type?: string) => void;
|
|
setCurrentSessionId: (sessionId: string | null) => void;
|
|
isProcessing: boolean;
|
|
}
|
|
|
|
export const useSocketHandlers = ({
|
|
socket,
|
|
setStatus,
|
|
setIsTyping,
|
|
setIsProcessing,
|
|
addMessage,
|
|
updateMessage,
|
|
addSystemMessage,
|
|
setCurrentSessionId,
|
|
isProcessing,
|
|
}: UseSocketHandlersProps) => {
|
|
// Keep track of the current bot message like in the JS version
|
|
const botMessageRef = useRef<{
|
|
id: string;
|
|
text: string;
|
|
} | null>(null);
|
|
|
|
const setupSocketHandlers = useCallback(() => {
|
|
if (!socket) return () => {};
|
|
|
|
// Connection events
|
|
const onConnect = () => {
|
|
console.log("🔌 Socket.IO connected!");
|
|
setStatus("متصل شد ✅");
|
|
addSystemMessage("اتصال برقرار شد", "success");
|
|
};
|
|
|
|
const onDisconnect = (reason: string) => {
|
|
console.log("🔌 Socket.IO disconnected:", reason);
|
|
setStatus("قطع ارتباط");
|
|
};
|
|
|
|
const onConnectionError = (error: any) => {
|
|
console.error("❌ Socket.IO connection error:", error);
|
|
addSystemMessage("خطا در اتصال", "error");
|
|
setStatus("خطا در اتصال");
|
|
};
|
|
|
|
// Authentication events
|
|
const onAuthenticated = (data: any) => {
|
|
console.log("✅ Authenticated:", data);
|
|
setStatus("احراز هویت شد ✅");
|
|
};
|
|
|
|
const onUnauthorized = (data: any) => {
|
|
console.error("❌ Unauthorized:", data);
|
|
addSystemMessage("خطا در احراز هویت", "error");
|
|
setStatus("خطا در احراز هویت");
|
|
};
|
|
|
|
// Session events
|
|
const onSessionCreated = (data: any) => {
|
|
console.log("✅ Session created:", data);
|
|
setCurrentSessionId(data.session.id);
|
|
addSystemMessage("جلسه ایجاد شد 🎉", "success");
|
|
setStatus("آماده برای گفتگو");
|
|
|
|
// Auto-join the created session
|
|
if (socket && data.session.id) {
|
|
const userId =
|
|
localStorage.getItem(import.meta.env.VITE_USER_ID_KEY || "user_id") ||
|
|
localStorage.getItem("userId") ||
|
|
"anonymous_user";
|
|
|
|
socket.emit(SOCKET_EVENTS.JOIN_CHAT, {
|
|
sessionId: data.session.id,
|
|
userId: userId,
|
|
});
|
|
}
|
|
};
|
|
|
|
const onChatJoined = (data: any) => {
|
|
console.log("✅ Joined chat:", data);
|
|
addSystemMessage("به چت متصل شدید", "success");
|
|
setStatus("متصل و آماده ✅");
|
|
};
|
|
|
|
const onSessionError = (data: any) => {
|
|
console.error("❌ Session error:", data);
|
|
addSystemMessage("خطا در ایجاد جلسه", "error");
|
|
};
|
|
|
|
// Message events
|
|
const onMessageReceived = (data: any) => {
|
|
console.log("📩 Message received:", data);
|
|
// Message is already shown in UI when sent
|
|
};
|
|
|
|
// Bot response streaming - following the JS implementation pattern
|
|
const onBotResponseStart = (data: any) => {
|
|
console.log("🤖 Bot response starting:", data);
|
|
|
|
// Turn off typing indicator since we're starting streaming
|
|
setIsTyping(false);
|
|
|
|
// Clear any existing bot message first
|
|
if (botMessageRef.current) {
|
|
console.log("⚠️ Clearing existing bot message before starting new one");
|
|
botMessageRef.current = null;
|
|
}
|
|
|
|
// Create new bot message with streaming flag
|
|
const newMessage = addMessage({
|
|
text: "",
|
|
isUser: false,
|
|
streaming: true,
|
|
});
|
|
|
|
// Store reference like in JS version
|
|
botMessageRef.current = {
|
|
id: newMessage.id,
|
|
text: "",
|
|
};
|
|
|
|
console.log("Created new streaming message with ID:", newMessage.id);
|
|
};
|
|
|
|
const onBotResponseChunk = (data: any) => {
|
|
console.log("🤖 Bot response chunk received:", data);
|
|
|
|
// If no bot message exists, create one (in case bot_response_start was missed)
|
|
if (!botMessageRef.current && data?.data) {
|
|
console.log("⚠️ Creating bot message from chunk (missing start event)");
|
|
|
|
// Turn off typing indicator since we're starting streaming
|
|
setIsTyping(false);
|
|
|
|
const newMessage = addMessage({
|
|
text: "",
|
|
isUser: false,
|
|
streaming: true,
|
|
});
|
|
|
|
botMessageRef.current = {
|
|
id: newMessage.id,
|
|
text: "",
|
|
};
|
|
}
|
|
|
|
if (botMessageRef.current && data?.data) {
|
|
// Accumulate text like in JS version
|
|
botMessageRef.current.text += data.data;
|
|
|
|
// Update the message content
|
|
updateMessage(botMessageRef.current.id, {
|
|
text: botMessageRef.current.text,
|
|
});
|
|
|
|
console.log(
|
|
`Updated message to length: ${botMessageRef.current.text.length}`
|
|
);
|
|
} else {
|
|
console.warn("Received chunk but no active bot message found", {
|
|
hasBotMessage: !!botMessageRef.current,
|
|
hasData: !!data?.data,
|
|
data,
|
|
});
|
|
}
|
|
};
|
|
|
|
const onBotResponseEnd = (data: any) => {
|
|
console.log("🤖 Bot response ended:", data);
|
|
|
|
if (botMessageRef.current) {
|
|
// Finalize the message by removing streaming flag
|
|
updateMessage(botMessageRef.current.id, {
|
|
streaming: false,
|
|
});
|
|
|
|
console.log(`Finalized message ${botMessageRef.current.id}`);
|
|
|
|
// Clear the reference
|
|
botMessageRef.current = null;
|
|
}
|
|
|
|
// Reset all states to allow user to send messages again
|
|
setIsTyping(false);
|
|
setIsProcessing(false);
|
|
console.log("✅ Bot response ended - user can send messages again");
|
|
};
|
|
|
|
const onBotResponse = (data: any) => {
|
|
console.log("🤖 Bot response (non-streaming):", data);
|
|
|
|
if (data.message && data.message.content) {
|
|
addMessage({
|
|
text: data.message.content,
|
|
isUser: false,
|
|
metadata: data.message.metadata,
|
|
});
|
|
} else {
|
|
console.warn("Received bot_response event with no content", data);
|
|
}
|
|
|
|
// Reset all states to allow user to send messages again
|
|
setIsTyping(false);
|
|
setIsProcessing(false);
|
|
console.log("✅ Bot response completed - user can send messages again");
|
|
};
|
|
|
|
// Typing events
|
|
const onTypingStart = (data: any) => {
|
|
if (data.type === "bot") {
|
|
setIsTyping(true);
|
|
setStatus("در حال پاسخ...");
|
|
}
|
|
};
|
|
|
|
const onTypingStop = (data: any) => {
|
|
if (data.type === "bot") {
|
|
setIsTyping(false);
|
|
setStatus("آماده برای گفتگو");
|
|
}
|
|
};
|
|
|
|
// User status events
|
|
const onUserJoined = (data: any) => {
|
|
console.log("👤 User joined:", data);
|
|
addSystemMessage(`کاربر به چت پیوست`, "info");
|
|
};
|
|
|
|
const onUserLeft = (data: any) => {
|
|
console.log("👤 User left:", data);
|
|
addSystemMessage(`کاربر چت را ترک کرد`, "info");
|
|
};
|
|
|
|
// Error events
|
|
const onError = (data: any) => {
|
|
console.error("❌ Socket.IO error:", data);
|
|
addSystemMessage(data.message || "خطا در پردازش", "error");
|
|
|
|
// Reset states on error to allow user to try again
|
|
setIsProcessing(false);
|
|
setIsTyping(false);
|
|
console.log("❌ Error occurred - reset processing state");
|
|
};
|
|
|
|
const onMessageError = (data: any) => {
|
|
console.error("❌ Message error:", data);
|
|
addSystemMessage(data.message || "خطا در ارسال پیام", "error");
|
|
|
|
// Reset states on error to allow user to try again
|
|
setIsProcessing(false);
|
|
setIsTyping(false);
|
|
console.log("❌ Message error - reset processing state");
|
|
};
|
|
|
|
// Register event handlers
|
|
socket.on(SOCKET_EVENTS.CONNECT, onConnect);
|
|
socket.on(SOCKET_EVENTS.DISCONNECT, onDisconnect);
|
|
socket.on(SOCKET_EVENTS.CONNECTION_ERROR, onConnectionError);
|
|
socket.on(SOCKET_EVENTS.AUTHENTICATED, onAuthenticated);
|
|
socket.on(SOCKET_EVENTS.UNAUTHORIZED, onUnauthorized);
|
|
socket.on(SOCKET_EVENTS.SESSION_CREATED, onSessionCreated);
|
|
socket.on(SOCKET_EVENTS.CHAT_JOINED, onChatJoined);
|
|
socket.on(SOCKET_EVENTS.SESSION_ERROR, onSessionError);
|
|
socket.on(SOCKET_EVENTS.MESSAGE_RECEIVED, onMessageReceived);
|
|
socket.on(SOCKET_EVENTS.BOT_RESPONSE_START, onBotResponseStart);
|
|
socket.on(SOCKET_EVENTS.BOT_RESPONSE_CHUNK, onBotResponseChunk);
|
|
socket.on(SOCKET_EVENTS.BOT_RESPONSE_END, onBotResponseEnd);
|
|
socket.on(SOCKET_EVENTS.BOT_RESPONSE, onBotResponse);
|
|
socket.on(SOCKET_EVENTS.TYPING_START, onTypingStart);
|
|
socket.on(SOCKET_EVENTS.TYPING_STOP, onTypingStop);
|
|
socket.on(SOCKET_EVENTS.USER_JOINED, onUserJoined);
|
|
socket.on(SOCKET_EVENTS.USER_LEFT, onUserLeft);
|
|
socket.on(SOCKET_EVENTS.ERROR, onError);
|
|
socket.on(SOCKET_EVENTS.MESSAGE_ERROR, onMessageError);
|
|
socket.on(SOCKET_EVENTS.WEBSOCKET_ERROR, onError);
|
|
|
|
// Return cleanup function
|
|
return () => {
|
|
socket.off(SOCKET_EVENTS.CONNECT, onConnect);
|
|
socket.off(SOCKET_EVENTS.DISCONNECT, onDisconnect);
|
|
socket.off(SOCKET_EVENTS.CONNECTION_ERROR, onConnectionError);
|
|
socket.off(SOCKET_EVENTS.AUTHENTICATED, onAuthenticated);
|
|
socket.off(SOCKET_EVENTS.UNAUTHORIZED, onUnauthorized);
|
|
socket.off(SOCKET_EVENTS.SESSION_CREATED, onSessionCreated);
|
|
socket.off(SOCKET_EVENTS.CHAT_JOINED, onChatJoined);
|
|
socket.off(SOCKET_EVENTS.SESSION_ERROR, onSessionError);
|
|
socket.off(SOCKET_EVENTS.MESSAGE_RECEIVED, onMessageReceived);
|
|
socket.off(SOCKET_EVENTS.BOT_RESPONSE_START, onBotResponseStart);
|
|
socket.off(SOCKET_EVENTS.BOT_RESPONSE_CHUNK, onBotResponseChunk);
|
|
socket.off(SOCKET_EVENTS.BOT_RESPONSE_END, onBotResponseEnd);
|
|
socket.off(SOCKET_EVENTS.BOT_RESPONSE, onBotResponse);
|
|
socket.off(SOCKET_EVENTS.TYPING_START, onTypingStart);
|
|
socket.off(SOCKET_EVENTS.TYPING_STOP, onTypingStop);
|
|
socket.off(SOCKET_EVENTS.USER_JOINED, onUserJoined);
|
|
socket.off(SOCKET_EVENTS.USER_LEFT, onUserLeft);
|
|
socket.off(SOCKET_EVENTS.ERROR, onError);
|
|
socket.off(SOCKET_EVENTS.MESSAGE_ERROR, onMessageError);
|
|
socket.off(SOCKET_EVENTS.WEBSOCKET_ERROR, onError);
|
|
|
|
// Clear bot message reference on cleanup
|
|
botMessageRef.current = null;
|
|
};
|
|
}, [
|
|
socket,
|
|
setStatus,
|
|
setIsTyping,
|
|
setIsProcessing,
|
|
addMessage,
|
|
updateMessage,
|
|
addSystemMessage,
|
|
setCurrentSessionId,
|
|
]);
|
|
|
|
return { setupSocketHandlers };
|
|
};
|
|
|
|
// Helper function to format text with markdown-like syntax
|
|
const formatText = (text: string) => {
|
|
if (!text) return "";
|
|
|
|
// Clean the text first to avoid HTML injection
|
|
const cleanedText = text
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
|
|
// Format with markdown-like syntax and handle newlines properly
|
|
const formattedText = cleanedText
|
|
// Bold text
|
|
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
|
// Italic text
|
|
.replace(/\*(.*?)\*/g, "<em>$1</em>")
|
|
// Line breaks - ensure they work properly with proper spacing
|
|
.replace(/\n/g, "<br class='message-line-break' />")
|
|
// Links (optional)
|
|
.replace(
|
|
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
'<a href="$2" target="_blank" rel="noopener noreferrer" class="text-blue-500 underline">$1</a>'
|
|
);
|
|
|
|
return formattedText;
|
|
};
|