import React, { useState, useRef, useEffect, useCallback } from 'react'; import { Message, Role, ChatbotSettings, Citation } from '../types'; import MessageComponent from './Message'; import { CloseIcon, SendIcon, UserSupportIcon } from './icons'; // --- Start of inlined services --- const getApiParams = () => { if (!window.jaguar_chatbot_params) { throw new Error("Jaguar Chatbot params not found."); } return window.jaguar_chatbot_params; }; type ChatPayload = { history: Message[]; message: string; conversationId: string; userName?: string; userEmail?: string; phoneNumber?: string; }; const getChatResponse = async (payload: ChatPayload): Promise<{ text: string; suggestions?: string[], citations?: Citation[] }> => { const params = getApiParams(); if (!params.rest_url) { throw new Error("REST API URL is not configured. Cannot send chat message."); } const body = { ...payload }; const response = await fetch(`${params.rest_url}/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); if (!response.ok) { try { const jsonError = await response.json(); const errorMessage = (jsonError.data && jsonError.data.message) ? jsonError.data.message : (jsonError.message || 'An unknown server error occurred.'); if (window.jaguar_chatbot_params?.is_admin) { const details = (jsonError.data && jsonError.data.details) ? ` Details: ${JSON.stringify(jsonError.data.details)}` : ''; throw new Error(errorMessage + details); } else { if (errorMessage.includes('API key')) { throw new Error('خطای پیکربندی: کلید API معتبر نیست. لطفاً با مدیر سایت تماس بگیرید.'); } throw new Error('متاسفانه در ارتباط با سرویس هوش مصنوعی خطایی رخ داد. لطفاً لحظاتی دیگر دوباره تلاش کنید.'); } } catch (e: any) { if (e instanceof Error && e.message) { throw e; } const errorText = await response.text().catch(() => 'Could not retrieve error details.'); console.error("Jaguar Chatbot: Server returned a non-OK status.", { status: response.status, statusText: response.statusText, body: errorText, }); if (window.jaguar_chatbot_params?.is_admin) { throw new Error(`خطای سرور: ${response.status} ${response.statusText}. به نظر می‌رسد فایروال پاسخ خطا را مسدود کرده است. پاسخ کامل در کنسول مرورگر ثبت شد.`); } else { throw new Error('متاسفانه در ارتباط با سرور خطایی رخ داد. این مشکل معمولا به دلیل فایروال امنیتی سرور است. لطفاً با مدیر سایت تماس بگیرید.'); } } } const jsonResponse = await response.json(); if (!jsonResponse.success) { const errorData = jsonResponse.data || {}; const finalMessage = errorData.message || 'Failed to get chat response from application'; if (window.jaguar_chatbot_params?.is_admin) { throw new Error(finalMessage); } else { if (finalMessage.includes('API key')) { throw new Error('خطای پیکربندی: کلید API معتبر نیست. لطفاً با مدیر سایت تماس بگیرید.'); } throw new Error('متاسفانه در ارتباط با سرویس هوش مصنوعی خطایی رخ داد. لطفاً لحظاتی دیگر دوباره تلاش کنید.'); } } return jsonResponse.data; }; const requestHumanSupport = async (conversationId: string, userName: string, userEmail: string, phoneNumber: string): Promise => { const params = getApiParams(); const response = await fetch(`${params.rest_url}/conversations/request-support`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': params.nonce, }, body: JSON.stringify({ conversationId, userName, userEmail, phoneNumber }), }); if (!response.ok) { throw new Error('Failed to request human support'); } }; const getConversationHistory = async (conversationId: string): Promise => { const params = getApiParams(); const response = await fetch(`${params.rest_url}/conversations/${conversationId}/poll`, { method: 'GET', headers: { // Polling should be public and not require a nonce that might expire or cause issues. // 'X-WP-Nonce': params.nonce, }, }); if (!response.ok) { throw new Error('Failed to fetch conversation history'); } return response.json(); }; // --- End of inlined services --- interface ChatWindowProps { isOpen: boolean; onClose: () => void; settings: ChatbotSettings | null; isInline?: boolean; } const ChatWindow: React.FC = ({ isOpen, onClose, settings, isInline = false }) => { const [messages, setMessages] = useState([]); const [userInput, setUserInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const [isPreChat, setIsPreChat] = useState(true); const [userName, setUserName] = useState(''); const [userEmail, setUserEmail] = useState(''); const [userPhone, setUserPhone] = useState(''); const [nameError, setNameError] = useState(''); const [emailError, setEmailError] = useState(''); const [phoneError, setPhoneError] = useState(''); const [conversationId, setConversationId] = useState(''); const [supportRequested, setSupportRequested] = useState(false); const [suggestedActions, setSuggestedActions] = useState([]); const messagesEndRef = useRef(null); const inputRef = useRef(null); const pollIntervalRef = useRef(null); const suggestionsRef = useRef(null); const dragInfo = useRef({ isDragging: false, startX: 0, scrollLeft: 0, hasDragged: false }).current; const handleMouseDown = (e: React.MouseEvent) => { if (!suggestionsRef.current) return; dragInfo.isDragging = true; dragInfo.hasDragged = false; dragInfo.startX = e.pageX - suggestionsRef.current.offsetLeft; dragInfo.scrollLeft = suggestionsRef.current.scrollLeft; suggestionsRef.current.style.cursor = 'grabbing'; suggestionsRef.current.style.userSelect = 'none'; }; const handleMouseLeaveOrUp = () => { if (!suggestionsRef.current) return; dragInfo.isDragging = false; suggestionsRef.current.style.cursor = 'grab'; suggestionsRef.current.style.removeProperty('user-select'); }; const handleMouseMove = (e: React.MouseEvent) => { if (!dragInfo.isDragging || !suggestionsRef.current) return; e.preventDefault(); dragInfo.hasDragged = true; const x = e.pageX - suggestionsRef.current.offsetLeft; const walk = (x - dragInfo.startX) * 2; // Adjust scroll speed suggestionsRef.current.scrollLeft = dragInfo.scrollLeft - walk; }; const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { scrollToBottom(); }, [messages, isLoading]); useEffect(() => { if (isOpen) { inputRef.current?.focus(); } }, [isOpen]); // Initialize conversation and add initial greeting useEffect(() => { if (settings && !conversationId) { const newConversationId = `web-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; setConversationId(newConversationId); const greetingMessage: Message = { id: 'initial-greeting', role: Role.AI, text: settings.initialGreeting, }; setMessages([greetingMessage]); // Reset state if window is re-opened (unless inline) if (!isInline) { setIsPreChat(settings.saveConversations); setSupportRequested(false); setUserInput(''); } else { // For inline mode, always skip pre-chat setIsPreChat(false); } } }, [settings, conversationId, isInline, isOpen]); // Rerun if reopened const pollMessages = useCallback(async () => { if (document.hidden || !conversationId) return; try { const history = await getConversationHistory(conversationId); setMessages(currentMessages => { // Only update if the history from the server is different to avoid unnecessary re-renders if (JSON.stringify(history) !== JSON.stringify(currentMessages)) { return history; } return currentMessages; }); } catch (error) { console.error("Polling error:", error); // Stop polling on error to avoid spamming a broken endpoint if (pollIntervalRef.current) clearInterval(pollIntervalRef.current); } }, [conversationId]); useEffect(() => { if (isOpen && conversationId && settings?.saveConversations) { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); } pollIntervalRef.current = window.setInterval(pollMessages, 3000); return () => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); } }; } }, [isOpen, conversationId, settings?.saveConversations, pollMessages]); const validatePreChat = () => { let isValid = true; if (!userName.trim()) { setNameError('لطفا نام خود را وارد کنید.'); isValid = false; } else { setNameError(''); } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!userEmail.trim() || !emailRegex.test(userEmail.trim())) { setEmailError('لطفا یک ایمیل معتبر وارد کنید.'); isValid = false; } else { setEmailError(''); } // More permissive validation for the optional phone number const phoneRegex = /^\+?[0-9\s-()]{7,20}$/; if (userPhone.trim() && !phoneRegex.test(userPhone.trim())) { setPhoneError('لطفا شماره تلفن معتبری وارد کنید.'); isValid = false; } else { setPhoneError(''); } return isValid; }; const handleStartChat = () => { if (validatePreChat()) { setIsPreChat(false); } }; const handleSendMessage = async (messageText = userInput) => { if (!messageText.trim()) return; setIsLoading(true); setSuggestedActions([]); const newUserMessage: Message = { id: `user-${Date.now()}`, role: Role.USER, text: messageText, }; const updatedMessages = [...messages, newUserMessage]; setMessages(updatedMessages); setUserInput(''); try { const { text, suggestions, citations } = await getChatResponse({ history: messages, message: messageText, conversationId, userName, userEmail, phoneNumber: userPhone }); const aiResponseMessage: Message = { id: `ai-${Date.now()}`, role: Role.AI, text: text, citations, }; setMessages([...updatedMessages, aiResponseMessage]); if (suggestions && suggestions.length > 0) { setSuggestedActions(suggestions); } } catch (error) { const errorMessage = (error instanceof Error) ? error.message : 'An unknown error occurred.'; const errorResponseMessage: Message = { id: `error-${Date.now()}`, role: Role.AI, text: errorMessage + (settings?.humanSupportEnabled ? `\n\n${settings.humanSupportFallbackMessage}` : ''), }; setMessages([...updatedMessages, errorResponseMessage]); } finally { setIsLoading(false); inputRef.current?.focus(); } }; const handleRequestSupport = async () => { if (supportRequested) return; // Prevent multiple requests // If user hasn't filled pre-chat form, we must ask for it now. if (isPreChat) { if (!validatePreChat()) return; setIsPreChat(false); // Hide the form after validation. Now they are in the chat. } try { await requestHumanSupport(conversationId, userName, userEmail, userPhone); setSupportRequested(true); const supportMessage: Message = { id: `support-request-${Date.now()}`, role: Role.AI, // Displayed as a system/AI message text: settings?.humanSupportMessage || 'Support requested. An agent will be with you shortly.', }; setMessages([...messages, supportMessage]); } catch (error) { const supportErrorMessage: Message = { id: `support-error-${Date.now()}`, role: Role.AI, text: 'Could not request human support. Please try again later.', }; setMessages([...messages, supportErrorMessage]); } }; const positionClass = settings?.widgetPosition === 'left' ? 'end-4' : 'start-4'; const animationClass = isOpen ? 'animate-fade-in-up' : 'animate-fade-out-down'; if (!settings || (!isOpen && !isInline)) { return null; } const containerClass = isInline ? "jaguar-chatbot-inline-container w-full h-[600px] max-h-full" : `jaguar-chatbot-container fixed bottom-[74px] sm:bottom-6 ${positionClass} z-50 w-[calc(100%-2rem)] max-w-sm h-[70vh] max-h-[600px] ${animationClass}`; return (
{settings.humanSupportEnabled && ( )}

{settings.chatbotTitle}

{!isInline && ( )}
{isPreChat ? (

شروع گفتگو

برای شروع، لطفاً اطلاعات زیر را وارد کنید.

setUserName(e.target.value)} className="w-full p-2 bg-gray-800 border border-gray-600 rounded-md focus:ring-orange-500 focus:border-orange-500 text-sm"/> {nameError &&

{nameError}

}
setUserEmail(e.target.value)} className="w-full p-2 bg-gray-800 border border-gray-600 rounded-md focus:ring-orange-500 focus:border-orange-500 text-sm"/> {emailError &&

{emailError}

}
setUserPhone(e.target.value)} className="w-full p-2 bg-gray-800 border border-gray-600 rounded-md focus:ring-orange-500 focus:border-orange-500 text-sm" placeholder="مثال: 09123456789"/> {phoneError &&

{phoneError}

}
) : ( <>
{messages.map((msg, index) => ( ))} {isLoading && (
)}
{suggestedActions.length > 0 && (
{suggestedActions.map((action, index) => ( ))}
)}
{ e.preventDefault(); handleSendMessage(); }} className="flex items-center gap-2" > setUserInput(e.target.value)} placeholder={isLoading ? 'در حال پردازش...' : 'پیام خود را بنویسید...'} className="flex-1 w-full px-4 py-2 text-sm text-gray-200 bg-gray-700 border-gray-600 rounded-full focus:outline-none focus:ring-2 focus:ring-orange-500" disabled={isLoading} aria-label="ورودی پیام" />
)}
); }; export default ChatWindow;