chat bot
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
VITE_TOKEN_NAME = 'dzone_token'
|
VITE_TOKEN_NAME = 'dzone_token'
|
||||||
VITE_REFRESH_TOKEN_NAME = 'dzone_refresh_token'
|
VITE_REFRESH_TOKEN_NAME = 'dzone_refresh_token'
|
||||||
# VITE_BASE_URL = 'https://api-dzone.danakcorp.com'
|
# VITE_BASE_URL = 'https://api-dzone.danakcorp.com'
|
||||||
VITE_BASE_URL = 'https://nhrq9m1d-4000.eun1.devtunnels.ms'
|
VITE_BASE_URL = 'http://localhost:4000'
|
||||||
|
|
||||||
VITE_SLUG_ID = 'slug_id'
|
VITE_SLUG_ID = 'slug_id'
|
||||||
+232
-162
@@ -1,24 +1,24 @@
|
|||||||
import { FC, useState, useRef, useEffect } from 'react';
|
import { FC, useState, useRef, useEffect } from "react";
|
||||||
import { Send, CloseCircle, Wifi } from 'iconsax-react';
|
import { Send, CloseCircle, Wifi } 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";
|
||||||
import { useMessages } from './hooks/useMessages';
|
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
|
||||||
|
|
||||||
// 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) => {
|
||||||
if (!text) return '';
|
if (!text) return "";
|
||||||
|
|
||||||
// Clean the text first to avoid HTML injection
|
// Clean the text first to avoid HTML injection
|
||||||
const cleanedText = text
|
const cleanedText = text
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, "&")
|
||||||
.replace(/</g, '<')
|
.replace(/</g, "<")
|
||||||
.replace(/>/g, '>')
|
.replace(/>/g, ">")
|
||||||
.replace(/"/g, '"')
|
.replace(/"/g, """)
|
||||||
.replace(/'/g, ''');
|
.replace(/'/g, "'");
|
||||||
|
|
||||||
// Format with markdown-like syntax and handle newlines properly
|
// Format with markdown-like syntax and handle newlines properly
|
||||||
const formattedText = cleanedText
|
const formattedText = cleanedText
|
||||||
@@ -29,7 +29,10 @@ export const formatText = (text: string) => {
|
|||||||
// Line breaks - ensure they work properly with proper spacing
|
// Line breaks - ensure they work properly with proper spacing
|
||||||
.replace(/\n/g, "<br class='message-line-break' />")
|
.replace(/\n/g, "<br class='message-line-break' />")
|
||||||
// Links (optional)
|
// Links (optional)
|
||||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" class="text-blue-500 underline">$1</a>');
|
.replace(
|
||||||
|
/\[([^\]]+)\]\(([^)]+)\)/g,
|
||||||
|
'<a href="$2" target="_blank" rel="noopener noreferrer" class="text-blue-500 underline">$1</a>'
|
||||||
|
);
|
||||||
|
|
||||||
return formattedText;
|
return formattedText;
|
||||||
};
|
};
|
||||||
@@ -40,41 +43,55 @@ interface ChatbotProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Connection status types
|
// Connection status types
|
||||||
type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
|
type ConnectionStatus = "disconnected" | "connecting" | "connected" | "error";
|
||||||
|
|
||||||
const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
||||||
// State
|
// State
|
||||||
const [inputValue, setInputValue] = useState<string>('');
|
const [inputValue, setInputValue] = useState<string>("");
|
||||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||||
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('disconnected');
|
const [connectionStatus, setConnectionStatus] =
|
||||||
|
useState<ConnectionStatus>("disconnected");
|
||||||
const [isTyping, setIsTyping] = useState<boolean>(false);
|
const [isTyping, setIsTyping] = useState<boolean>(false);
|
||||||
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 [showConnectionMessage, setShowConnectionMessage] =
|
||||||
const [isStreamingMode, setIsStreamingMode] = useState<boolean>(true);
|
useState<boolean>(false);
|
||||||
const [reconnectAttempts, setReconnectAttempts] = useState<number>(0);
|
const [reconnectAttempts, setReconnectAttempts] = useState<number>(0);
|
||||||
const maxReconnectAttempts = 5;
|
const maxReconnectAttempts = 5;
|
||||||
|
|
||||||
// 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);
|
||||||
const connectionTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const connectionTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
// Custom hooks
|
// Custom hooks
|
||||||
const { messages, addMessage, addSystemMessage, updateMessage, clearMessages } = useMessages();
|
const {
|
||||||
const { setupSocketHandlers, streamingText } = useSocketHandlers({
|
messages,
|
||||||
|
addMessage,
|
||||||
|
addSystemMessage,
|
||||||
|
updateMessage,
|
||||||
|
clearMessages,
|
||||||
|
} = useMessages();
|
||||||
|
const { setupSocketHandlers } = useSocketHandlers({
|
||||||
socket,
|
socket,
|
||||||
setStatus: (status: string) => {
|
setStatus: (status: string) => {
|
||||||
|
console.log("📊 Status update:", status);
|
||||||
// Map status string to connection status
|
// Map status string to connection status
|
||||||
if (status.includes('متصل شد')) {
|
if (
|
||||||
setConnectionStatus('connected');
|
status.includes("متصل شد") ||
|
||||||
} else if (status.includes('در حال')) {
|
status.includes("آماده") ||
|
||||||
setConnectionStatus('connecting');
|
status.includes("متصل و آماده")
|
||||||
} else if (status.includes('خطا')) {
|
) {
|
||||||
setConnectionStatus('error');
|
setConnectionStatus("connected");
|
||||||
} else if (status.includes('قطع')) {
|
} else if (status.includes("در حال")) {
|
||||||
setConnectionStatus('disconnected');
|
setConnectionStatus("connecting");
|
||||||
|
} else if (status.includes("خطا")) {
|
||||||
|
setConnectionStatus("error");
|
||||||
|
} else if (status.includes("قطع")) {
|
||||||
|
setConnectionStatus("disconnected");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setIsTyping,
|
setIsTyping,
|
||||||
@@ -83,13 +100,33 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
updateMessage,
|
updateMessage,
|
||||||
addSystemMessage,
|
addSystemMessage,
|
||||||
setCurrentSessionId,
|
setCurrentSessionId,
|
||||||
isProcessing
|
isProcessing,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Debug streaming text changes
|
// Debug connection status changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("Streaming text updated:", streamingText.substring(0, 50) + (streamingText.length > 50 ? "..." : ""));
|
console.log("🔄 Connection status changed:", {
|
||||||
}, [streamingText]);
|
connectionStatus,
|
||||||
|
socketConnected: socket?.connected,
|
||||||
|
currentSessionId,
|
||||||
|
isProcessing,
|
||||||
|
});
|
||||||
|
}, [connectionStatus, socket?.connected, currentSessionId, isProcessing]);
|
||||||
|
|
||||||
|
// Monitor socket connection and session state
|
||||||
|
useEffect(() => {
|
||||||
|
if (socket?.connected && currentSessionId) {
|
||||||
|
console.log(
|
||||||
|
"🔗 Socket connected and session ready - setting status to connected"
|
||||||
|
);
|
||||||
|
setConnectionStatus("connected");
|
||||||
|
}
|
||||||
|
}, [socket?.connected, currentSessionId]);
|
||||||
|
|
||||||
|
// Update button state when processing state changes
|
||||||
|
useEffect(() => {
|
||||||
|
updateButtonState();
|
||||||
|
}, [isProcessing, connectionStatus, inputValue]);
|
||||||
|
|
||||||
// Initialize component
|
// Initialize component
|
||||||
const init = () => {
|
const init = () => {
|
||||||
@@ -128,7 +165,10 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
|
|
||||||
// Handle reconnection attempts
|
// Handle reconnection attempts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (connectionStatus === 'error' && reconnectAttempts < maxReconnectAttempts) {
|
if (
|
||||||
|
connectionStatus === "error" &&
|
||||||
|
reconnectAttempts < maxReconnectAttempts
|
||||||
|
) {
|
||||||
const reconnectTimer = setTimeout(() => {
|
const reconnectTimer = setTimeout(() => {
|
||||||
attemptReconnect();
|
attemptReconnect();
|
||||||
}, 2000 * (reconnectAttempts + 1)); // Exponential backoff
|
}, 2000 * (reconnectAttempts + 1)); // Exponential backoff
|
||||||
@@ -157,7 +197,7 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [socket, setupSocketHandlers]);
|
}, [socket]); // Only depend on socket, not setupSocketHandlers
|
||||||
|
|
||||||
// Show connection message only briefly when status changes
|
// Show connection message only briefly when status changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -172,26 +212,27 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
}, [connectionStatus]);
|
}, [connectionStatus]);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const connectSocket = async () => {
|
const connectSocket = async () => {
|
||||||
try {
|
try {
|
||||||
setConnectionStatus('connecting');
|
setConnectionStatus("connecting");
|
||||||
|
|
||||||
// Get token from environment or localStorage
|
// Get token from environment or localStorage
|
||||||
const token = await getToken();
|
const token = await getToken();
|
||||||
|
|
||||||
if (!apiBaseUrl) {
|
if (!apiBaseUrl) {
|
||||||
console.error("API base URL not defined in environment variables");
|
console.error("API base URL not defined in environment variables");
|
||||||
setConnectionStatus('error');
|
setConnectionStatus("error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("🔄 Connecting to Socket.IO...");
|
||||||
const newSocket = io(`${apiBaseUrl}/chat`, {
|
const newSocket = io(`${apiBaseUrl}/chat`, {
|
||||||
auth: {
|
auth: {
|
||||||
token: token
|
token: token,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
setSocket(newSocket);
|
setSocket(newSocket);
|
||||||
@@ -199,16 +240,21 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
|
|
||||||
// Set a timeout for connection
|
// Set a timeout for connection
|
||||||
connectionTimeoutRef.current = setTimeout(() => {
|
connectionTimeoutRef.current = setTimeout(() => {
|
||||||
if (connectionStatus === 'connecting') {
|
if (connectionStatus === "connecting") {
|
||||||
setConnectionStatus('error');
|
console.log("❌ Connection timeout");
|
||||||
|
setConnectionStatus("error");
|
||||||
}
|
}
|
||||||
}, 10000); // 10 seconds timeout
|
}, 10000);
|
||||||
|
|
||||||
// Create session after connection
|
// Create session after connection - like in JS version
|
||||||
setTimeout(() => createSession(newSocket), 1000);
|
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");
|
||||||
attemptReconnect();
|
attemptReconnect();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -217,7 +263,9 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
if (reconnectAttempts < maxReconnectAttempts) {
|
if (reconnectAttempts < maxReconnectAttempts) {
|
||||||
const newAttempts = reconnectAttempts + 1;
|
const newAttempts = reconnectAttempts + 1;
|
||||||
setReconnectAttempts(newAttempts);
|
setReconnectAttempts(newAttempts);
|
||||||
console.log(`🔄 Reconnecting... Attempt ${newAttempts}/${maxReconnectAttempts}`);
|
console.log(
|
||||||
|
`🔄 Reconnecting... Attempt ${newAttempts}/${maxReconnectAttempts}`
|
||||||
|
);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
connectSocket();
|
connectSocket();
|
||||||
@@ -232,22 +280,25 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
if (socket) {
|
if (socket) {
|
||||||
socket.disconnect();
|
socket.disconnect();
|
||||||
setSocket(null);
|
setSocket(null);
|
||||||
setConnectionStatus('disconnected');
|
setConnectionStatus("disconnected");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const createSession = (socketInstance: Socket | null = socket) => {
|
const createSession = (socketInstance: Socket | null = socket) => {
|
||||||
if (!socketInstance || !socketInstance.connected) {
|
if (!socketInstance || !socketInstance.connected) {
|
||||||
|
console.log("❌ Socket not connected for session creation");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentSessionId) {
|
if (currentSessionId) {
|
||||||
console.log("Session already exists:", currentSessionId);
|
console.log("🚫 Session already exists:", currentSessionId);
|
||||||
|
addSystemMessage("جلسه از قبل وجود دارد", "info");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")}`,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -258,28 +309,26 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get user ID from localStorage or use a default
|
// Get user ID from localStorage or use a default
|
||||||
const userId = localStorage.getItem(import.meta.env.VITE_USER_ID_KEY || "user_id") ||
|
const userId =
|
||||||
|
localStorage.getItem(import.meta.env.VITE_USER_ID_KEY || "user_id") ||
|
||||||
localStorage.getItem("userId") ||
|
localStorage.getItem("userId") ||
|
||||||
"anonymous_user";
|
"anonymous_user";
|
||||||
|
|
||||||
console.log(`🔄 Joining chat session: ${sessionId}`);
|
console.log(`🔄 Joining chat session: ${sessionId}`);
|
||||||
socket.emit(SOCKET_EVENTS.JOIN_CHAT, {
|
socket.emit(SOCKET_EVENTS.JOIN_CHAT, {
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
userId: userId
|
userId: userId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const newSession = () => {
|
|
||||||
setCurrentSessionId(null);
|
|
||||||
clearMessages();
|
|
||||||
createSession();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSendMessage = () => {
|
const handleSendMessage = () => {
|
||||||
const message = inputValue.trim();
|
const message = inputValue.trim();
|
||||||
|
|
||||||
if (!message || isProcessing) {
|
if (!message || isProcessing) {
|
||||||
console.log("❌ Cannot send:", { message: !!message, processing: isProcessing });
|
console.log("❌ Cannot send:", {
|
||||||
|
message: !!message,
|
||||||
|
processing: isProcessing,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,15 +354,18 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
// Add user message to UI
|
// Add user message to UI
|
||||||
addMessage({
|
addMessage({
|
||||||
text: message,
|
text: message,
|
||||||
isUser: true
|
isUser: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear input immediately to prevent accidental resend
|
// Clear input immediately to prevent accidental resend
|
||||||
setInputValue('');
|
setInputValue("");
|
||||||
autoResizeTextarea({ target: { value: '', style: { height: 'auto' } } } as React.ChangeEvent<HTMLTextAreaElement>);
|
autoResizeTextarea({
|
||||||
|
target: { value: "", style: { height: "auto" } },
|
||||||
|
} as React.ChangeEvent<HTMLTextAreaElement>);
|
||||||
|
|
||||||
// Get user ID from localStorage or use a default
|
// Get user ID from localStorage or use a default
|
||||||
const userId = localStorage.getItem(import.meta.env.VITE_USER_ID_KEY || "user_id") ||
|
const userId =
|
||||||
|
localStorage.getItem(import.meta.env.VITE_USER_ID_KEY || "user_id") ||
|
||||||
localStorage.getItem("userId") ||
|
localStorage.getItem("userId") ||
|
||||||
"anonymous_user";
|
"anonymous_user";
|
||||||
|
|
||||||
@@ -322,9 +374,8 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
socket.emit(SOCKET_EVENTS.SEND_MESSAGE, {
|
socket.emit(SOCKET_EVENTS.SEND_MESSAGE, {
|
||||||
sessionId: currentSessionId,
|
sessionId: currentSessionId,
|
||||||
content: message,
|
content: message,
|
||||||
userId: userId
|
userId: userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Send message error:", error);
|
console.error("Send message error:", error);
|
||||||
addSystemMessage(`خطا در ارسال پیام`, "error");
|
addSystemMessage(`خطا در ارسال پیام`, "error");
|
||||||
@@ -334,21 +385,25 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSendMessage();
|
handleSendMessage();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const autoResizeTextarea = (e: React.ChangeEvent<HTMLTextAreaElement> | { target: { value: string, style?: { height: string } } }): void => {
|
const autoResizeTextarea = (
|
||||||
|
e:
|
||||||
|
| React.ChangeEvent<HTMLTextAreaElement>
|
||||||
|
| { target: { value: string; style?: { height: string } } }
|
||||||
|
): void => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
setInputValue(value);
|
setInputValue(value);
|
||||||
|
|
||||||
// Handle the case when this is called programmatically without a real DOM element
|
// Handle the case when this is called programmatically without a real DOM element
|
||||||
if ('style' in e.target) {
|
if ("style" in e.target) {
|
||||||
const textarea = e.target as HTMLTextAreaElement;
|
const textarea = e.target as HTMLTextAreaElement;
|
||||||
textarea.style.height = 'auto';
|
textarea.style.height = "auto";
|
||||||
textarea.style.height = Math.min(textarea.scrollHeight, 150) + 'px';
|
textarea.style.height = Math.min(textarea.scrollHeight, 150) + "px";
|
||||||
}
|
}
|
||||||
|
|
||||||
updateButtonState();
|
updateButtonState();
|
||||||
@@ -356,49 +411,44 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
|
|
||||||
const updateButtonState = () => {
|
const updateButtonState = () => {
|
||||||
const hasText = inputValue.trim().length > 0;
|
const hasText = inputValue.trim().length > 0;
|
||||||
const canSend = hasText && !isProcessing && connectionStatus === 'connected';
|
const canSend =
|
||||||
|
hasText && !isProcessing && connectionStatus === "connected";
|
||||||
|
|
||||||
console.log("🔄 Button state:", {
|
console.log("🔄 Button state updated:", {
|
||||||
hasText,
|
hasText,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
|
connectionStatus,
|
||||||
currentSessionId,
|
currentSessionId,
|
||||||
canSend,
|
canSend,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Button state is managed via the disabled prop and className
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleStreaming = () => {
|
|
||||||
setIsStreamingMode(!isStreamingMode);
|
|
||||||
addSystemMessage(`حالت ${isStreamingMode ? "عادی" : "استریم"} فعال شد`, "success");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get connection status icon and text
|
// Get connection status icon and text
|
||||||
const getConnectionStatusInfo = () => {
|
const getConnectionStatusInfo = () => {
|
||||||
switch (connectionStatus) {
|
switch (connectionStatus) {
|
||||||
case 'connected':
|
case "connected":
|
||||||
return {
|
return {
|
||||||
icon: <Wifi size={16} className="text-green-400" />,
|
icon: <Wifi size={16} className="text-green-400" />,
|
||||||
text: "متصل",
|
text: "متصل",
|
||||||
className: "bg-green-100 text-green-800"
|
className: "bg-green-100 text-green-800",
|
||||||
};
|
};
|
||||||
case 'connecting':
|
case "connecting":
|
||||||
return {
|
return {
|
||||||
icon: <Wifi size={16} className="text-yellow-400 animate-pulse" />,
|
icon: <Wifi size={16} className="text-yellow-400 animate-pulse" />,
|
||||||
text: "در حال اتصال...",
|
text: "در حال اتصال...",
|
||||||
className: "bg-yellow-100 text-yellow-800"
|
className: "bg-yellow-100 text-yellow-800",
|
||||||
};
|
};
|
||||||
case 'error':
|
case "error":
|
||||||
return {
|
return {
|
||||||
icon: <Wifi size={16} className="text-red-500" />,
|
icon: <Wifi size={16} className="text-red-500" />,
|
||||||
text: "خطا در اتصال",
|
text: "خطا در اتصال",
|
||||||
className: "bg-red-100 text-red-800"
|
className: "bg-red-100 text-red-800",
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
icon: <Wifi size={16} variant="Bold" className="text-gray-500" />,
|
icon: <Wifi size={16} variant="Bold" className="text-gray-500" />,
|
||||||
text: "قطع اتصال",
|
text: "قطع اتصال",
|
||||||
className: "bg-gray-100 text-gray-800"
|
className: "bg-gray-100 text-gray-800",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -409,23 +459,36 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
|
|
||||||
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 ? 'opacity-100 transform translate-y-0' : 'opacity-0 transform translate-y-10'
|
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
|
||||||
|
? "opacity-100 transform translate-y-0"
|
||||||
|
: "opacity-0 transform translate-y-10"
|
||||||
}`}
|
}`}
|
||||||
style={{ height: '500px', direction: 'rtl' }}
|
style={{ height: "500px", direction: "rtl" }}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-gradient-to-l from-blue-500 to-blue-600 text-white p-3.5 rounded-t-xl flex justify-between items-center shadow-sm">
|
<div className="bg-gradient-to-l from-blue-500 to-blue-600 text-white p-3.5 rounded-t-xl flex justify-between items-center shadow-sm">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<div className={`w-3 h-3 rounded-full ml-2 ${connectionStatus === 'connected' ? 'bg-green-400 animate-pulse' :
|
<div
|
||||||
connectionStatus === 'connecting' ? 'bg-yellow-400 animate-pulse' :
|
className={`w-3 h-3 rounded-full ml-2 ${
|
||||||
'bg-red-400'
|
connectionStatus === "connected"
|
||||||
}`}></div>
|
? "bg-green-400 animate-pulse"
|
||||||
|
: connectionStatus === "connecting"
|
||||||
|
? "bg-yellow-400 animate-pulse"
|
||||||
|
: "bg-red-400"
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
<h3 className="font-bold text-base">چت بات داناک</h3>
|
<h3 className="font-bold text-base">چت بات داناک</h3>
|
||||||
{/* Always show a compact connection status */}
|
{/* Always show a compact connection status */}
|
||||||
<div className={`flex items-center text-xs mr-2 ml-2 ${connectionStatus === 'connected' ? 'text-green-200' :
|
<div
|
||||||
connectionStatus === 'connecting' ? 'text-yellow-200' :
|
className={`flex items-center text-xs mr-2 ml-2 ${
|
||||||
'text-red-200'
|
connectionStatus === "connected"
|
||||||
}`}>
|
? "text-green-200"
|
||||||
|
: connectionStatus === "connecting"
|
||||||
|
? "text-yellow-200"
|
||||||
|
: "text-red-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{connectionInfo.icon}
|
{connectionInfo.icon}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -438,52 +501,28 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Control buttons */}
|
|
||||||
<div className="flex justify-between px-3 py-2 bg-gray-50 border-b border-gray-200">
|
|
||||||
<button
|
|
||||||
onClick={() => clearMessages()}
|
|
||||||
className="text-xs text-gray-600 hover:text-blue-600 px-2 py-1 hover:bg-gray-100 rounded transition-colors"
|
|
||||||
>
|
|
||||||
پاک کردن
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={newSession}
|
|
||||||
className="text-xs text-gray-600 hover:text-blue-600 px-2 py-1 hover:bg-gray-100 rounded transition-colors"
|
|
||||||
>
|
|
||||||
جلسه جدید
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={toggleStreaming}
|
|
||||||
className={`text-xs ${isStreamingMode ? 'text-blue-600' : 'text-gray-600'} px-2 py-1 hover:bg-gray-100 rounded transition-colors`}
|
|
||||||
>
|
|
||||||
{isStreamingMode ? '📡 استریم' : '📝 عادی'}
|
|
||||||
</button>
|
|
||||||
{connectionStatus !== 'connected' && (
|
|
||||||
<button
|
|
||||||
onClick={connectSocket}
|
|
||||||
className="text-xs text-blue-600 hover:text-blue-800 px-2 py-1 hover:bg-blue-50 rounded transition-colors"
|
|
||||||
>
|
|
||||||
اتصال مجدد
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
<div className="flex-1 p-3.5 overflow-y-auto bg-gray-50" style={{ height: '350px' }}>
|
<div
|
||||||
{
|
className="flex-1 p-3.5 overflow-y-auto bg-gray-50"
|
||||||
messages?.map((message: Message) => {
|
style={{ height: "350px" }}
|
||||||
|
>
|
||||||
|
{messages?.map((message: Message) => {
|
||||||
// Debug message content
|
// Debug message content
|
||||||
console.log(`Rendering message ${message.id}:`, {
|
console.log(`Rendering message ${message.id}:`, {
|
||||||
text: message.text.substring(0, 30) + (message.text.length > 30 ? "..." : ""),
|
text:
|
||||||
|
message.text.substring(0, 30) +
|
||||||
|
(message.text.length > 30 ? "..." : ""),
|
||||||
isUser: message.isUser,
|
isUser: message.isUser,
|
||||||
isSystem: message.isSystem,
|
isSystem: message.isSystem,
|
||||||
streaming: message.streaming
|
streaming: message.streaming,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={message.id}
|
key={message.id}
|
||||||
className={`mb-3 flex ${message.isUser ? 'justify-start' : 'justify-end'} animate-fade-in`}
|
className={`mb-3 flex ${
|
||||||
|
message.isUser ? "justify-start" : "justify-end"
|
||||||
|
} animate-fade-in`}
|
||||||
>
|
>
|
||||||
{message.isUser && (
|
{message.isUser && (
|
||||||
<div className="w-7 h-7 rounded-full bg-blue-600 flex items-center justify-center ml-2 flex-shrink-0">
|
<div className="w-7 h-7 rounded-full bg-blue-600 flex items-center justify-center ml-2 flex-shrink-0">
|
||||||
@@ -491,30 +530,46 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={`relative px-3.5 py-2.5 pb-4 rounded-lg max-w-[85%] ${message.isUser
|
className={`relative px-3.5 py-2.5 pb-4 rounded-lg max-w-[85%] ${
|
||||||
? 'bg-blue-600 text-white rounded-tr-none shadow-sm animate-slide-right'
|
message.isUser
|
||||||
|
? "bg-blue-600 text-white rounded-tr-none shadow-sm animate-slide-right"
|
||||||
: message.isSystem
|
: message.isSystem
|
||||||
? message.metadata?.type === 'error'
|
? message.metadata?.type === "error"
|
||||||
? 'bg-red-100 text-red-800 rounded-tl-none'
|
? "bg-red-100 text-red-800 rounded-tl-none"
|
||||||
: message.metadata?.type === 'success'
|
: message.metadata?.type === "success"
|
||||||
? 'bg-green-100 text-green-800 rounded-tl-none'
|
? "bg-green-100 text-green-800 rounded-tl-none"
|
||||||
: 'bg-yellow-100 text-yellow-800 rounded-tl-none'
|
: "bg-yellow-100 text-yellow-800 rounded-tl-none"
|
||||||
: message.metadata && 'confidence' in message.metadata
|
: message.metadata && "confidence" in message.metadata
|
||||||
? message.metadata.confidence === 1
|
? message.metadata.confidence === 1
|
||||||
? 'bg-green-100 text-green-800 rounded-tl-none'
|
? "bg-green-100 text-green-800 rounded-tl-none"
|
||||||
: message.metadata.confidence === 0
|
: message.metadata.confidence === 0
|
||||||
? 'bg-red-100 text-red-800 rounded-tl-none'
|
? "bg-red-100 text-red-800 rounded-tl-none"
|
||||||
: 'bg-yellow-100 text-yellow-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'
|
: "bg-white text-gray-800 rounded-tl-none shadow-sm animate-slide-left"
|
||||||
} ${message.streaming ? 'border-r-4 border-blue-400 streaming-message animate-pulse' : ''}`}
|
} ${
|
||||||
|
message.streaming
|
||||||
|
? "border-r-4 border-blue-400 streaming-message animate-pulse"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id={`message-${message.id}`}
|
id={`message-${message.id}`}
|
||||||
className={`text-sm leading-6 font-light whitespace-pre-line break-words text-right mb-2 ${message.streaming ? 'streaming-content' : ''}`}
|
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) }}
|
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'}`}>
|
<div
|
||||||
{message.timestamp.toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' })}
|
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>
|
</div>
|
||||||
{message.metadata?.confidence !== undefined &&
|
{message.metadata?.confidence !== undefined &&
|
||||||
message.metadata.confidence !== 0 &&
|
message.metadata.confidence !== 0 &&
|
||||||
@@ -532,10 +587,9 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})
|
})}
|
||||||
}
|
{/* Typing indicator - only show when no streaming messages exist */}
|
||||||
{/* Improved typing indicator */}
|
{isTyping && !messages.some((msg) => msg.streaming) && (
|
||||||
{isTyping && (
|
|
||||||
<div className="flex justify-end mb-3 animate-fade-in">
|
<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="bg-white text-gray-800 px-3.5 py-2 rounded-lg rounded-tl-none shadow-sm">
|
||||||
<div className="typing-indicator">
|
<div className="typing-indicator">
|
||||||
@@ -560,20 +614,36 @@ const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
|
|||||||
onChange={autoResizeTextarea}
|
onChange={autoResizeTextarea}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
placeholder="پیام خود را بنویسید..."
|
placeholder="پیام خود را بنویسید..."
|
||||||
disabled={isProcessing || connectionStatus !== 'connected'}
|
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"
|
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' }}
|
style={{ maxHeight: "80px" }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleSendMessage}
|
onClick={handleSendMessage}
|
||||||
disabled={inputValue.trim() === '' || isProcessing || connectionStatus !== 'connected'}
|
disabled={
|
||||||
className={`ml-2 ${inputValue.trim() === '' || isProcessing || connectionStatus !== 'connected'
|
inputValue.trim() === "" ||
|
||||||
? 'bg-gray-300'
|
isProcessing ||
|
||||||
: 'bg-blue-600 hover:bg-blue-700'
|
connectionStatus !== "connected"
|
||||||
|
}
|
||||||
|
className={`ml-2 ${
|
||||||
|
inputValue.trim() === "" ||
|
||||||
|
isProcessing ||
|
||||||
|
connectionStatus !== "connected"
|
||||||
|
? "bg-gray-300"
|
||||||
|
: "bg-blue-600 hover:bg-blue-700"
|
||||||
} text-white rounded-full w-9 h-9 flex items-center justify-center focus:outline-none transition-colors flex-shrink-0`}
|
} 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' : ''}`} />
|
<Send
|
||||||
|
size={18}
|
||||||
|
className={`transform rotate-180 ${
|
||||||
|
inputValue.trim() !== "" &&
|
||||||
|
!isProcessing &&
|
||||||
|
connectionStatus === "connected"
|
||||||
|
? "animate-pulse"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -111,3 +111,31 @@
|
|||||||
transform: translateY(-4px);
|
transform: translateY(-4px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Typing dots for streaming messages */
|
||||||
|
.typing-dots {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-dots span {
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
margin: 0 1px;
|
||||||
|
background-color: #3b82f6;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
animation: typing-dot 1.2s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-dots span:nth-child(1) {
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-dots span:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typing-dots span:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
import { Socket } from "socket.io-client";
|
import { Socket } from "socket.io-client";
|
||||||
import { SOCKET_EVENTS } from "../constants";
|
import { SOCKET_EVENTS } from "../constants";
|
||||||
import { Message } from "../types";
|
import { Message } from "../types";
|
||||||
@@ -26,34 +26,11 @@ export const useSocketHandlers = ({
|
|||||||
setCurrentSessionId,
|
setCurrentSessionId,
|
||||||
isProcessing,
|
isProcessing,
|
||||||
}: UseSocketHandlersProps) => {
|
}: UseSocketHandlersProps) => {
|
||||||
// Keep track of the current streaming message
|
// Keep track of the current bot message like in the JS version
|
||||||
const currentStreamingMessageRef = useRef<{
|
const botMessageRef = useRef<{
|
||||||
id: string;
|
id: string;
|
||||||
text: string;
|
text: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
const [streamingText, setStreamingText] = useState<string>("");
|
|
||||||
|
|
||||||
// Simple function to update streaming message content
|
|
||||||
const updateStreamingMessage = (id: string, text: string) => {
|
|
||||||
console.log(
|
|
||||||
`Updating streaming message ${id} with ${text.length} characters`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update the state for React
|
|
||||||
updateMessage(id, { text });
|
|
||||||
|
|
||||||
// Also try direct DOM update as a fallback
|
|
||||||
setTimeout(() => {
|
|
||||||
try {
|
|
||||||
const element = document.getElementById(`message-${id}`);
|
|
||||||
if (element) {
|
|
||||||
element.innerHTML = formatText(text);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error updating DOM directly:", e);
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setupSocketHandlers = useCallback(() => {
|
const setupSocketHandlers = useCallback(() => {
|
||||||
if (!socket) return () => {};
|
if (!socket) return () => {};
|
||||||
@@ -112,6 +89,7 @@ export const useSocketHandlers = ({
|
|||||||
const onChatJoined = (data: any) => {
|
const onChatJoined = (data: any) => {
|
||||||
console.log("✅ Joined chat:", data);
|
console.log("✅ Joined chat:", data);
|
||||||
addSystemMessage("به چت متصل شدید", "success");
|
addSystemMessage("به چت متصل شدید", "success");
|
||||||
|
setStatus("متصل و آماده ✅");
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSessionError = (data: any) => {
|
const onSessionError = (data: any) => {
|
||||||
@@ -125,22 +103,28 @@ export const useSocketHandlers = ({
|
|||||||
// Message is already shown in UI when sent
|
// Message is already shown in UI when sent
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bot response streaming
|
// Bot response streaming - following the JS implementation pattern
|
||||||
const onBotResponseStart = (data: any) => {
|
const onBotResponseStart = (data: any) => {
|
||||||
console.log("🤖 Bot response starting:", data);
|
console.log("🤖 Bot response starting:", data);
|
||||||
|
|
||||||
// Reset streaming text
|
// Turn off typing indicator since we're starting streaming
|
||||||
setStreamingText("");
|
setIsTyping(false);
|
||||||
|
|
||||||
// Create a new message and store its ID
|
// 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({
|
const newMessage = addMessage({
|
||||||
text: "",
|
text: "",
|
||||||
isUser: false,
|
isUser: false,
|
||||||
streaming: true,
|
streaming: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store the message ID and initial text in ref
|
// Store reference like in JS version
|
||||||
currentStreamingMessageRef.current = {
|
botMessageRef.current = {
|
||||||
id: newMessage.id,
|
id: newMessage.id,
|
||||||
text: "",
|
text: "",
|
||||||
};
|
};
|
||||||
@@ -149,32 +133,43 @@ export const useSocketHandlers = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onBotResponseChunk = (data: any) => {
|
const onBotResponseChunk = (data: any) => {
|
||||||
console.log(
|
console.log("🤖 Bot response chunk received:", data);
|
||||||
"🤖 Bot response chunk received, data length:",
|
|
||||||
data?.data?.length || 0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (data.data && currentStreamingMessageRef.current) {
|
// If no bot message exists, create one (in case bot_response_start was missed)
|
||||||
try {
|
if (!botMessageRef.current && data?.data) {
|
||||||
// Update the text in our ref and state
|
console.log("⚠️ Creating bot message from chunk (missing start event)");
|
||||||
const newText = currentStreamingMessageRef.current.text + data.data;
|
|
||||||
currentStreamingMessageRef.current.text = newText;
|
|
||||||
|
|
||||||
// Update state to trigger re-render
|
// Turn off typing indicator since we're starting streaming
|
||||||
setStreamingText(newText);
|
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
|
// Update the message content
|
||||||
updateStreamingMessage(
|
updateMessage(botMessageRef.current.id, {
|
||||||
currentStreamingMessageRef.current.id,
|
text: botMessageRef.current.text,
|
||||||
newText
|
});
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Updated message to length: ${botMessageRef.current.text.length}`
|
||||||
);
|
);
|
||||||
} catch (error) {
|
|
||||||
console.error("Error processing bot response chunk:", error);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.warn("Received chunk but no active streaming message found", {
|
console.warn("Received chunk but no active bot message found", {
|
||||||
hasData: !!data.data,
|
hasBotMessage: !!botMessageRef.current,
|
||||||
hasCurrentMessage: !!currentStreamingMessageRef.current,
|
hasData: !!data?.data,
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -183,45 +178,28 @@ export const useSocketHandlers = ({
|
|||||||
const onBotResponseEnd = (data: any) => {
|
const onBotResponseEnd = (data: any) => {
|
||||||
console.log("🤖 Bot response ended:", data);
|
console.log("🤖 Bot response ended:", data);
|
||||||
|
|
||||||
if (currentStreamingMessageRef.current) {
|
if (botMessageRef.current) {
|
||||||
// Make sure we have the final text
|
|
||||||
const finalText = currentStreamingMessageRef.current.text;
|
|
||||||
const messageId = currentStreamingMessageRef.current.id;
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Finalizing message ${messageId} with text length: ${finalText.length}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Final update to the message content
|
|
||||||
updateStreamingMessage(messageId, finalText);
|
|
||||||
|
|
||||||
// Finalize the message by removing streaming flag
|
// Finalize the message by removing streaming flag
|
||||||
setTimeout(() => {
|
updateMessage(botMessageRef.current.id, {
|
||||||
updateMessage(messageId, {
|
|
||||||
streaming: false,
|
streaming: false,
|
||||||
});
|
});
|
||||||
}, 100);
|
|
||||||
|
|
||||||
// Clear the ref and state
|
console.log(`Finalized message ${botMessageRef.current.id}`);
|
||||||
setStreamingText("");
|
|
||||||
currentStreamingMessageRef.current = null;
|
// Clear the reference
|
||||||
|
botMessageRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset all states to allow user to send messages again
|
||||||
setIsTyping(false);
|
setIsTyping(false);
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
|
console.log("✅ Bot response ended - user can send messages again");
|
||||||
};
|
};
|
||||||
|
|
||||||
const onBotResponse = (data: any) => {
|
const onBotResponse = (data: any) => {
|
||||||
console.log("🤖 Bot response (non-streaming):", data);
|
console.log("🤖 Bot response (non-streaming):", data);
|
||||||
|
|
||||||
if (data.message && data.message.content) {
|
if (data.message && data.message.content) {
|
||||||
console.log(
|
|
||||||
`Adding non-streaming message: "${data.message.content.substring(
|
|
||||||
0,
|
|
||||||
50
|
|
||||||
)}${data.message.content.length > 50 ? "..." : ""}"`
|
|
||||||
);
|
|
||||||
|
|
||||||
addMessage({
|
addMessage({
|
||||||
text: data.message.content,
|
text: data.message.content,
|
||||||
isUser: false,
|
isUser: false,
|
||||||
@@ -231,8 +209,10 @@ export const useSocketHandlers = ({
|
|||||||
console.warn("Received bot_response event with no content", data);
|
console.warn("Received bot_response event with no content", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset all states to allow user to send messages again
|
||||||
setIsTyping(false);
|
setIsTyping(false);
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
|
console.log("✅ Bot response completed - user can send messages again");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Typing events
|
// Typing events
|
||||||
@@ -265,15 +245,21 @@ export const useSocketHandlers = ({
|
|||||||
const onError = (data: any) => {
|
const onError = (data: any) => {
|
||||||
console.error("❌ Socket.IO error:", data);
|
console.error("❌ Socket.IO error:", data);
|
||||||
addSystemMessage(data.message || "خطا در پردازش", "error");
|
addSystemMessage(data.message || "خطا در پردازش", "error");
|
||||||
|
|
||||||
|
// Reset states on error to allow user to try again
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
setIsTyping(false);
|
setIsTyping(false);
|
||||||
|
console.log("❌ Error occurred - reset processing state");
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMessageError = (data: any) => {
|
const onMessageError = (data: any) => {
|
||||||
console.error("❌ Message error:", data);
|
console.error("❌ Message error:", data);
|
||||||
addSystemMessage(data.message || "خطا در ارسال پیام", "error");
|
addSystemMessage(data.message || "خطا در ارسال پیام", "error");
|
||||||
|
|
||||||
|
// Reset states on error to allow user to try again
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
setIsTyping(false);
|
setIsTyping(false);
|
||||||
|
console.log("❌ Message error - reset processing state");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Register event handlers
|
// Register event handlers
|
||||||
@@ -321,9 +307,8 @@ export const useSocketHandlers = ({
|
|||||||
socket.off(SOCKET_EVENTS.MESSAGE_ERROR, onMessageError);
|
socket.off(SOCKET_EVENTS.MESSAGE_ERROR, onMessageError);
|
||||||
socket.off(SOCKET_EVENTS.WEBSOCKET_ERROR, onError);
|
socket.off(SOCKET_EVENTS.WEBSOCKET_ERROR, onError);
|
||||||
|
|
||||||
// Clear streaming message ref on cleanup
|
// Clear bot message reference on cleanup
|
||||||
currentStreamingMessageRef.current = null;
|
botMessageRef.current = null;
|
||||||
setStreamingText("");
|
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
socket,
|
socket,
|
||||||
@@ -334,10 +319,9 @@ export const useSocketHandlers = ({
|
|||||||
updateMessage,
|
updateMessage,
|
||||||
addSystemMessage,
|
addSystemMessage,
|
||||||
setCurrentSessionId,
|
setCurrentSessionId,
|
||||||
streamingText, // Add dependency to ensure handler is updated when streaming text changes
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { setupSocketHandlers, streamingText };
|
return { setupSocketHandlers };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to format text with markdown-like syntax
|
// Helper function to format text with markdown-like syntax
|
||||||
|
|||||||
Reference in New Issue
Block a user