chatbot debug

This commit is contained in:
hamid zarghami
2025-06-08 16:21:37 +03:30
parent 4ceb91466e
commit 9334e09ac6
14 changed files with 1585 additions and 3 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
VITE_TOKEN_NAME = 'dzone_token'
VITE_REFRESH_TOKEN_NAME = 'dzone_refresh_token'
VITE_BASE_URL = 'https://api-dzone.danakcorp.com'
# VITE_BASE_URL = 'http://192.168.82.158:4000'
# VITE_BASE_URL = 'https://api-dzone.danakcorp.com'
VITE_BASE_URL = 'https://nhrq9m1d-4000.eun1.devtunnels.ms'
VITE_SLUG_ID = 'slug_id'
+168 -1
View File
@@ -11,6 +11,7 @@
"@headlessui/react": "^2.2.0",
"@material-tailwind/react": "^2.1.10",
"@tanstack/react-query": "^5.64.1",
"@types/uuid": "^10.0.0",
"axios": "^1.7.9",
"formik": "^2.4.6",
"i18next": "^24.2.1",
@@ -28,9 +29,12 @@
"react-otp-input": "^3.1.1",
"react-router-dom": "^7.1.3",
"react-spinners": "^0.17.0",
"react-string-replace": "^1.1.1",
"react-toastify": "^11.0.3",
"socket.io-client": "^4.8.1",
"swiper": "^11.2.1",
"tailwind-merge": "^2.6.0",
"uuid": "^11.1.0",
"yup": "^1.6.1",
"zustand": "^5.0.3"
},
@@ -1732,6 +1736,12 @@
"win32"
]
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1913,6 +1923,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz",
@@ -2697,6 +2713,45 @@
"dev": true,
"license": "MIT"
},
"node_modules/engine.io-client": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/esbuild": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
@@ -3834,7 +3889,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/mz": {
@@ -4513,6 +4567,15 @@
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-string-replace": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/react-string-replace/-/react-string-replace-1.1.1.tgz",
"integrity": "sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==",
"license": "MIT",
"engines": {
"node": ">=0.12.0"
}
},
"node_modules/react-toastify": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.3.tgz",
@@ -4716,6 +4779,68 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-client/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -5219,6 +5344,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
@@ -5452,6 +5590,35 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+4
View File
@@ -13,6 +13,7 @@
"@headlessui/react": "^2.2.0",
"@material-tailwind/react": "^2.1.10",
"@tanstack/react-query": "^5.64.1",
"@types/uuid": "^10.0.0",
"axios": "^1.7.9",
"formik": "^2.4.6",
"i18next": "^24.2.1",
@@ -30,9 +31,12 @@
"react-otp-input": "^3.1.1",
"react-router-dom": "^7.1.3",
"react-spinners": "^0.17.0",
"react-string-replace": "^1.1.1",
"react-toastify": "^11.0.3",
"socket.io-client": "^4.8.1",
"swiper": "^11.2.1",
"tailwind-merge": "^2.6.0",
"uuid": "^11.1.0",
"yup": "^1.6.1",
"zustand": "^5.0.3"
},
+2
View File
@@ -7,6 +7,7 @@ import FaJson from './langs/fa.json'
import AuthRouter from './router/Auth';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import ToastContainer from './components/Toast';
import { ChatbotButton } from './chatbot';
i18next.init({
interpolation: { escapeValue: false },
@@ -44,6 +45,7 @@ function App() {
<Route path="/:slug/*" element={<MainRouter />} />
</Routes>
<ToastContainer />
<ChatbotButton />
</I18nextProvider>
</QueryClientProvider>
</BrowserRouter>
+584
View File
@@ -0,0 +1,584 @@
import { FC, useState, useRef, useEffect } from 'react';
import { Send, CloseCircle, Wifi } from 'iconsax-react';
import io, { Socket } from 'socket.io-client';
import { SOCKET_EVENTS } from './constants';
import { useSocketHandlers } from './hooks/useSocketHandlers';
import { useMessages } from './hooks/useMessages';
import { Message } from './types';
import { getToken } from '../config/func';
import './chatbot.css'; // Import CSS for animations
// Helper function to format text with markdown-like syntax
export const formatText = (text: string) => {
if (!text) return '';
// Clean the text first to avoid HTML injection
const cleanedText = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
// 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;
};
interface ChatbotProps {
isOpen: boolean;
onClose: () => void;
}
// Connection status types
type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
const Chatbot: FC<ChatbotProps> = ({ isOpen, onClose }) => {
// State
const [inputValue, setInputValue] = useState<string>('');
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('disconnected');
const [isTyping, setIsTyping] = useState<boolean>(false);
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 [isStreamingMode, setIsStreamingMode] = useState<boolean>(true);
const [reconnectAttempts, setReconnectAttempts] = useState<number>(0);
const maxReconnectAttempts = 5;
// Config
const apiBaseUrl = import.meta.env.VITE_BASE_URL;
const messagesEndRef = useRef<HTMLDivElement>(null);
const connectionTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Custom hooks
const { messages, addMessage, addSystemMessage, updateMessage, clearMessages } = useMessages();
const { setupSocketHandlers, streamingText } = useSocketHandlers({
socket,
setStatus: (status: string) => {
// Map status string to connection status
if (status.includes('متصل شد')) {
setConnectionStatus('connected');
} else if (status.includes('در حال')) {
setConnectionStatus('connecting');
} else if (status.includes('خطا')) {
setConnectionStatus('error');
} else if (status.includes('قطع')) {
setConnectionStatus('disconnected');
}
},
setIsTyping,
setIsProcessing,
addMessage,
updateMessage,
addSystemMessage,
setCurrentSessionId,
isProcessing
});
// Debug streaming text changes
useEffect(() => {
console.log("Streaming text updated:", streamingText.substring(0, 50) + (streamingText.length > 50 ? "..." : ""));
}, [streamingText]);
// Initialize component
const init = () => {
console.log("✅ DZone ChatBot initialized!");
};
// Run initialization once on mount
useEffect(() => {
init();
}, []);
// Animation effect for opening/closing
useEffect(() => {
if (isOpen) {
// Small delay to trigger animation
setTimeout(() => setShowChatbot(true), 50);
} else {
setShowChatbot(false);
}
}, [isOpen]);
// Connect to socket when component mounts
useEffect(() => {
if (isOpen) {
connectSocket();
}
// Cleanup on unmount or when closed
return () => {
disconnectSocket();
if (connectionTimeoutRef.current) {
clearTimeout(connectionTimeoutRef.current);
}
};
}, [isOpen]);
// Handle reconnection attempts
useEffect(() => {
if (connectionStatus === 'error' && reconnectAttempts < maxReconnectAttempts) {
const reconnectTimer = setTimeout(() => {
attemptReconnect();
}, 2000 * (reconnectAttempts + 1)); // Exponential backoff
return () => clearTimeout(reconnectTimer);
}
}, [connectionStatus, reconnectAttempts]);
// Scroll to bottom when messages change
useEffect(() => {
scrollToBottom();
}, [messages]);
// Set up socket handlers when socket changes
useEffect(() => {
let cleanup: (() => void) | undefined;
if (socket) {
console.log("🔄 Setting up socket handlers");
cleanup = setupSocketHandlers();
}
return () => {
if (cleanup) {
console.log("🔄 Cleaning up socket handlers");
cleanup();
}
};
}, [socket, setupSocketHandlers]);
// Show connection message only briefly when status changes
useEffect(() => {
// Always show connection status, don't hide it
setShowConnectionMessage(true);
return () => {
if (connectionTimeoutRef.current) {
clearTimeout(connectionTimeoutRef.current);
}
};
}, [connectionStatus]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
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;
}
const newSocket = io(`${apiBaseUrl}/chat`, {
auth: {
token: token
}
});
setSocket(newSocket);
setReconnectAttempts(0);
// Set a timeout for connection
connectionTimeoutRef.current = setTimeout(() => {
if (connectionStatus === 'connecting') {
setConnectionStatus('error');
}
}, 10000); // 10 seconds timeout
// Create session after connection
setTimeout(() => createSession(newSocket), 1000);
} catch (error) {
console.error("Socket connection error:", error);
setConnectionStatus('error');
attemptReconnect();
}
};
const attemptReconnect = () => {
if (reconnectAttempts < maxReconnectAttempts) {
const newAttempts = reconnectAttempts + 1;
setReconnectAttempts(newAttempts);
console.log(`🔄 Reconnecting... Attempt ${newAttempts}/${maxReconnectAttempts}`);
setTimeout(() => {
connectSocket();
}, 2000 * newAttempts); // Exponential backoff
} else {
console.log("❌ Max reconnection attempts reached");
addSystemMessage("اتصال قطع شد. لطفاً صفحه را تازه کنید", "error");
}
};
const disconnectSocket = () => {
if (socket) {
socket.disconnect();
setSocket(null);
setConnectionStatus('disconnected');
}
};
const createSession = (socketInstance: Socket | null = socket) => {
if (!socketInstance || !socketInstance.connected) {
return;
}
if (currentSessionId) {
console.log("Session already exists:", currentSessionId);
return;
}
socketInstance.emit(SOCKET_EVENTS.CREATE_SESSION, {
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 newSession = () => {
setCurrentSessionId(null);
clearMessages();
createSession();
};
const handleSendMessage = () => {
const message = inputValue.trim();
if (!message || isProcessing) {
console.log("❌ Cannot send:", { message: !!message, processing: isProcessing });
return;
}
// Check Socket.IO connection
if (!socket || !socket.connected) {
addSystemMessage("اتصال Socket.IO برقرار نیست", "error");
return;
}
// Auto-create session if needed
if (!currentSessionId) {
console.log("📝 No session, creating one...");
createSession();
addSystemMessage("لطفاً منتظر ایجاد جلسه باشید", "info");
return;
}
try {
// Disable during processing
setIsProcessing(true);
updateButtonState();
// Add user message to UI
addMessage({
text: message,
isUser: true
});
// Clear input immediately to prevent accidental resend
setInputValue('');
autoResizeTextarea({ target: { value: '', style: { height: 'auto' } } } as React.ChangeEvent<HTMLTextAreaElement>);
// 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";
// Send message via Socket.IO
console.log("📤 Sending message via Socket.IO");
socket.emit(SOCKET_EVENTS.SEND_MESSAGE, {
sessionId: currentSessionId,
content: message,
userId: userId
});
} catch (error) {
console.error("Send message error:", error);
addSystemMessage(`خطا در ارسال پیام`, "error");
setIsProcessing(false);
updateButtonState();
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
};
const autoResizeTextarea = (e: React.ChangeEvent<HTMLTextAreaElement> | { target: { value: string, style?: { height: string } } }): void => {
const value = e.target.value;
setInputValue(value);
// Handle the case when this is called programmatically without a real DOM element
if ('style' in e.target) {
const textarea = e.target as HTMLTextAreaElement;
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 150) + 'px';
}
updateButtonState();
};
const updateButtonState = () => {
const hasText = inputValue.trim().length > 0;
const canSend = hasText && !isProcessing && connectionStatus === 'connected';
console.log("🔄 Button state:", {
hasText,
isProcessing,
currentSessionId,
canSend,
});
// Button state is managed via the disabled prop and className
};
const toggleStreaming = () => {
setIsStreamingMode(!isStreamingMode);
addSystemMessage(`حالت ${isStreamingMode ? "عادی" : "استریم"} فعال شد`, "success");
};
// 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"
};
}
};
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 ? 'opacity-100 transform translate-y-0' : 'opacity-0 transform translate-y-10'
}`}
style={{ height: '500px', direction: 'rtl' }}
>
{/* 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="flex items-center">
<div className={`w-3 h-3 rounded-full ml-2 ${connectionStatus === 'connected' ? 'bg-green-400 animate-pulse' :
connectionStatus === 'connecting' ? 'bg-yellow-400 animate-pulse' :
'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'
}`}>
{connectionInfo.icon}
</div>
</div>
<button
onClick={onClose}
className="focus:outline-none hover:bg-blue-700 p-1 rounded-full transition-colors"
aria-label="بستن چت"
>
<CloseCircle size={20} />
</button>
</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 */}
<div className="flex-1 p-3.5 overflow-y-auto bg-gray-50" style={{ height: '350px' }}>
{
messages?.map((message: Message) => {
// Debug message content
console.log(`Rendering message ${message.id}:`, {
text: message.text.substring(0, 30) + (message.text.length > 30 ? "..." : ""),
isUser: message.isUser,
isSystem: message.isSystem,
streaming: message.streaming
});
return (
<div
key={message.id}
className={`mb-3 flex ${message.isUser ? 'justify-start' : 'justify-end'} animate-fade-in`}
>
{message.isUser && (
<div className="w-7 h-7 rounded-full bg-blue-600 flex items-center justify-center ml-2 flex-shrink-0">
<span className="text-white text-xs">👤</span>
</div>
)}
<div
className={`relative px-3.5 py-2.5 pb-4 rounded-lg max-w-[85%] ${message.isUser
? 'bg-blue-600 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-blue-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-blue-100 flex items-center justify-center mr-2 flex-shrink-0">
<span className="text-blue-600 text-xs">🤖</span>
</div>
)}
</div>
);
})
}
{/* Improved typing indicator */}
{isTyping && (
<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-blue-100 flex items-center justify-center mr-2 flex-shrink-0">
<span className="text-blue-600 text-xs">🤖</span>
</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-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`}
>
<Send size={18} className={`transform rotate-180 ${inputValue.trim() !== '' && !isProcessing && connectionStatus === 'connected' ? 'animate-pulse' : ''}`} />
</button>
</div>
</div>
</div>
);
};
export default Chatbot;
+27
View File
@@ -0,0 +1,27 @@
import { FC, useState } from 'react';
import { MessageText1 } from 'iconsax-react';
import Chatbot from './Chatbot';
const ChatbotButton: FC = () => {
const [isChatbotOpen, setIsChatbotOpen] = useState<boolean>(false);
const toggleChatbot = () => {
setIsChatbotOpen(!isChatbotOpen);
};
return (
<>
<button
onClick={toggleChatbot}
className="fixed bottom-5 right-5 bg-blue-600 text-white rounded-full w-14 h-14 flex items-center justify-center shadow-lg hover:bg-blue-700 transition-colors z-40"
aria-label="چت با پشتیبانی"
>
<MessageText1 size={24} />
</button>
<Chatbot isOpen={isChatbotOpen} onClose={() => setIsChatbotOpen(false)} />
</>
);
};
export default ChatbotButton;
+113
View File
@@ -0,0 +1,113 @@
/* Chatbot animations and styling */
/* Streaming message animation */
.streaming-message {
position: relative;
overflow: hidden;
}
.streaming-message::after {
content: "";
position: absolute;
bottom: 0;
right: 0;
height: 2px;
width: 10px;
background-color: #3b82f6;
animation: typing 1s infinite;
}
@keyframes typing {
0% {
width: 0;
}
50% {
width: 10px;
}
100% {
width: 0;
}
}
/* Message animations */
.animate-fade-in {
animation: fadeIn 0.3s ease-in-out;
}
.animate-slide-right {
animation: slideRight 0.3s ease-in-out;
}
.animate-slide-left {
animation: slideLeft 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideRight {
from {
transform: translateX(-20px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideLeft {
from {
transform: translateX(20px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Typing indicator */
.typing-indicator {
display: flex;
align-items: center;
}
.typing-indicator span:first-child {
margin-right: 5px;
}
.typing-indicator span:not(:first-child) {
width: 4px;
height: 4px;
margin: 0 1px;
background-color: #3b82f6;
border-radius: 50%;
display: inline-block;
animation: typing-dot 1.5s infinite ease-in-out;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing-dot {
0%,
60%,
100% {
transform: translateY(0);
}
30% {
transform: translateY(-4px);
}
}
+40
View File
@@ -0,0 +1,40 @@
// WebSocket Event Constants (sync with backend)
export const SOCKET_EVENTS = {
// Connection events
CONNECT: "connect",
DISCONNECT: "disconnect",
CONNECTION_ERROR: "connect_error",
// Authentication events
AUTHENTICATE: "authenticate",
AUTHENTICATED: "authenticated",
UNAUTHORIZED: "unauthorized",
// Session events
CREATE_SESSION: "create_session",
SESSION_CREATED: "session_created",
JOIN_CHAT: "join_chat",
CHAT_JOINED: "chat_joined",
LEAVE_CHAT: "leave_chat",
CHAT_LEFT: "chat_left",
// Message events
SEND_MESSAGE: "send_message",
MESSAGE_RECEIVED: "message_received",
BOT_RESPONSE_START: "bot_response_start",
BOT_RESPONSE_CHUNK: "bot_response_chunk",
BOT_RESPONSE_END: "bot_response_end",
BOT_RESPONSE: "bot_response",
// Status events
TYPING_START: "typing_start",
TYPING_STOP: "typing_stop",
USER_JOINED: "user_joined",
USER_LEFT: "user_left",
// Error events
ERROR: "error",
WEBSOCKET_ERROR: "websocket_error",
SESSION_ERROR: "session_error",
MESSAGE_ERROR: "message_error",
};
+85
View File
@@ -0,0 +1,85 @@
import { useState, useCallback } from "react";
import { Message } from "../types";
import { v4 as uuidv4 } from "uuid";
export const useMessages = () => {
const [messages, setMessages] = useState<Message[]>([]);
const addMessage = useCallback((message: Partial<Message>) => {
const newMessage: Message = {
id: uuidv4(),
text: message.text || "",
isUser: message.isUser || false,
timestamp: new Date(),
streaming: message.streaming || false,
metadata: message.metadata || {},
};
setMessages((prevMessages) => [...prevMessages, newMessage]);
console.log(
`Added message with ID: ${newMessage.id}, streaming: ${newMessage.streaming}`
);
return newMessage;
}, []);
const addSystemMessage = useCallback(
(text: string, type: string = "info") => {
const newMessage: Message = {
id: uuidv4(),
text,
isUser: false,
timestamp: new Date(),
streaming: false,
isSystem: true,
metadata: { type },
};
setMessages((prevMessages) => [...prevMessages, newMessage]);
// Auto-remove system messages after a delay
setTimeout(
() => {
setMessages((prevMessages) =>
prevMessages.filter((msg) => msg.id !== newMessage.id)
);
},
type === "error" ? 5000 : 3000
);
return newMessage;
},
[]
);
const updateMessage = useCallback((id: string, updates: Partial<Message>) => {
console.log(`Updating message ${id} with:`, updates);
setMessages((prevMessages) =>
prevMessages.map((msg) => {
if (msg.id === id) {
const updated = { ...msg, ...updates };
console.log(`Message ${id} updated:`, updated);
return updated;
}
return msg;
})
);
}, []);
const removeMessage = useCallback((id: string) => {
setMessages((prevMessages) => prevMessages.filter((msg) => msg.id !== id));
}, []);
const clearMessages = useCallback(() => {
setMessages([]);
}, []);
return {
messages,
addMessage,
addSystemMessage,
updateMessage,
removeMessage,
clearMessages,
};
};
+370
View File
@@ -0,0 +1,370 @@
import { useCallback, useRef, useState } 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 streaming message
const currentStreamingMessageRef = useRef<{
id: string;
text: string;
} | 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(() => {
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");
};
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
const onBotResponseStart = (data: any) => {
console.log("🤖 Bot response starting:", data);
// Reset streaming text
setStreamingText("");
// Create a new message and store its ID
const newMessage = addMessage({
text: "",
isUser: false,
streaming: true,
});
// Store the message ID and initial text in ref
currentStreamingMessageRef.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 length:",
data?.data?.length || 0
);
if (data.data && currentStreamingMessageRef.current) {
try {
// Update the text in our ref and state
const newText = currentStreamingMessageRef.current.text + data.data;
currentStreamingMessageRef.current.text = newText;
// Update state to trigger re-render
setStreamingText(newText);
// Update the message content
updateStreamingMessage(
currentStreamingMessageRef.current.id,
newText
);
} catch (error) {
console.error("Error processing bot response chunk:", error);
}
} else {
console.warn("Received chunk but no active streaming message found", {
hasData: !!data.data,
hasCurrentMessage: !!currentStreamingMessageRef.current,
data,
});
}
};
const onBotResponseEnd = (data: any) => {
console.log("🤖 Bot response ended:", data);
if (currentStreamingMessageRef.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
setTimeout(() => {
updateMessage(messageId, {
streaming: false,
});
}, 100);
// Clear the ref and state
setStreamingText("");
currentStreamingMessageRef.current = null;
}
setIsTyping(false);
setIsProcessing(false);
};
const onBotResponse = (data: any) => {
console.log("🤖 Bot response (non-streaming):", data);
if (data.message && data.message.content) {
console.log(
`Adding non-streaming message: "${data.message.content.substring(
0,
50
)}${data.message.content.length > 50 ? "..." : ""}"`
);
addMessage({
text: data.message.content,
isUser: false,
metadata: data.message.metadata,
});
} else {
console.warn("Received bot_response event with no content", data);
}
setIsTyping(false);
setIsProcessing(false);
};
// 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");
setIsProcessing(false);
setIsTyping(false);
};
const onMessageError = (data: any) => {
console.error("❌ Message error:", data);
addSystemMessage(data.message || "خطا در ارسال پیام", "error");
setIsProcessing(false);
setIsTyping(false);
};
// 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 streaming message ref on cleanup
currentStreamingMessageRef.current = null;
setStreamingText("");
};
}, [
socket,
setStatus,
setIsTyping,
setIsProcessing,
addMessage,
updateMessage,
addSystemMessage,
setCurrentSessionId,
streamingText, // Add dependency to ensure handler is updated when streaming text changes
]);
return { setupSocketHandlers, streamingText };
};
// 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
// 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;
};
+2
View File
@@ -0,0 +1,2 @@
export { default as Chatbot } from "./Chatbot";
export { default as ChatbotButton } from "./ChatbotButton";
+52
View File
@@ -0,0 +1,52 @@
import { Socket } from "socket.io-client";
export interface Message {
id: string;
text: string;
isUser: boolean;
timestamp: Date;
streaming?: boolean;
isSystem?: boolean;
metadata?: {
confidence?: number;
type?: string;
[key: string]: any;
};
}
export interface SocketData {
message?: {
content: string;
metadata?: {
confidence?: number;
[key: string]: any;
};
};
session?: {
id: string;
};
data?: string;
type?: string;
errorMessage?: string;
[key: string]: any;
}
export interface SocketHandlersProps {
socket: Socket | null;
setStatus: (status: string) => void;
setIsTyping: (isTyping: boolean) => void;
setIsProcessing: (isProcessing: boolean) => void;
addMessage: (message: MessageInput) => Message;
addSystemMessage: (text: string, type: "success" | "error" | "info") => void;
setCurrentSessionId: (id: string | null) => void;
isProcessing?: boolean;
}
export interface MessageInput {
text: string;
isUser: boolean;
metadata?: {
confidence?: number;
};
streaming?: boolean;
}
+6
View File
@@ -59,6 +59,12 @@ axiosInstance.interceptors.response.use(
originalRequest.headers.Authorization = `Bearer ${data.accessToken.token}`;
return axiosInstance(originalRequest);
} else {
await removeRefreshToken();
await removeToken();
const slug = localStorage.getItem(import.meta.env.VITE_SLUG_ID);
const slug_parse = slug ? JSON.parse(slug) : null;
window.location.href = `/${slug_parse?.slug}`;
return;
}
} catch (refreshError) {
console.error("Token refresh failed:", refreshError);
+130
View File
@@ -169,3 +169,133 @@ tbody tr {
.swiper-rtl .swiper-button-next:after {
color: #fff !important;
}
/* Chatbot animations */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideRight {
from {
transform: translateX(20px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideLeft {
from {
transform: translateX(-20px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.animate-fade-in {
animation: fadeIn 0.3s ease-in-out;
}
.animate-slide-right {
animation: slideRight 0.3s ease-in-out;
}
.animate-slide-left {
animation: slideLeft 0.3s ease-in-out;
}
/* Improved Chatbot typing indicator */
.typing-indicator {
display: flex;
align-items: center;
justify-content: center;
padding: 0 5px;
}
.typing-indicator span:first-child {
font-size: 12px;
color: #666;
margin-left: 5px;
}
.typing-indicator span:not(:first-child) {
height: 5px;
width: 5px;
float: right;
margin: 0 1px;
background-color: #9e9ea1;
display: block;
border-radius: 50%;
opacity: 0.4;
transform: scale(0.8);
}
.typing-indicator span:nth-of-type(2) {
animation: 1s bounce infinite 0.1s;
}
.typing-indicator span:nth-of-type(3) {
animation: 1s bounce infinite 0.2s;
}
@keyframes bounce {
0%,
100% {
transform: translateY(0) scale(0.8);
opacity: 0.4;
}
50% {
transform: translateY(-4px) scale(1);
opacity: 0.9;
}
}
/* Message animation */
.streaming-message {
position: relative;
overflow: hidden;
}
.streaming-message::after {
content: "";
position: absolute;
bottom: 0;
right: 0;
left: 0;
height: 2px;
background: linear-gradient(90deg, transparent, #3b82f6, transparent);
animation: loading 1.5s infinite;
}
@keyframes loading {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(-100%);
}
}
/* Fix for newlines in messages */
.whitespace-pre-line br {
display: block;
margin: 0.75rem 0;
content: "";
}
.message-line-break {
display: block;
height: 1rem;
width: 100%;
margin: 0.25rem 0;
}