"use client"; import React, { useState, useCallback, useEffect, useMemo } from "react"; import FloatingChatButton from "./floating-chat-button"; import ChatDock, { Message } from "./chat-dock"; import { useSuggestedQuestions } from "./suggested-questions"; import { ChatbotAPI, ChatbotAPIError, withRetry, } from "../../../../services/chatbot-api"; import { useDebounce } from "../../../../hooks/usePerformance"; import { I18nProvider, useI18n } from "./i18n-context"; import { useTheme, ThemeName, applyTheme } from "./theme-config"; interface ChatbotWidgetProps { apiBaseUrl?: string; apiKey?: string; position?: "bottom-right" | "bottom-left" | "top-right" | "top-left"; buttonSize?: "sm" | "md" | "lg"; buttonColor?: string; title?: string; placeholder?: string; enableSuggestedQuestions?: boolean; enableTypingIndicator?: boolean; enableRetry?: boolean; maxRetries?: number; className?: string; disabled?: boolean; onError?: (error: ChatbotAPIError) => void; onMessageSent?: (message: string) => void; onMessageReceived?: (message: Message) => void; // Theme and i18n props theme?: ThemeName; language?: string; enableLanguageSelector?: boolean; enableThemeToggle?: boolean; } // Internal widget component that uses contexts function ChatbotWidgetInternal({ apiBaseUrl, apiKey, position = "bottom-right", buttonSize = "md", buttonColor = "#3B82F6", title, placeholder, enableSuggestedQuestions = true, enableTypingIndicator = true, enableRetry = true, maxRetries = 3, className = "", disabled = false, onError, onMessageSent, onMessageReceived, enableLanguageSelector = false, enableThemeToggle = false, }: Omit) { // Use i18n context const { t, currentLanguage, isRTL } = useI18n(); // Use default values from translations if not provided const displayTitle = title || t("chatbot"); const displayPlaceholder = placeholder || t("placeholder"); const [isOpen, setIsOpen] = useState(false); const [messages, setMessages] = useState([]); const [isTyping, setIsTyping] = useState(false); const [unreadCount, setUnreadCount] = useState(0); const [connectionStatus, setConnectionStatus] = useState< "connected" | "connecting" | "error" >("connected"); const [lastError, setLastError] = useState(null); const { context: suggestedQuestionsContext, updateContext: updateSuggestedQuestionsContext, resetQuestions, } = useSuggestedQuestions(); // Debounce typing indicator const debouncedTyping = useDebounce(isTyping, 300); // Initialize ChatbotAPI const chatbotAPI = useMemo(() => { return new ChatbotAPI(apiBaseUrl, apiKey); }, [apiBaseUrl, apiKey]); // Handle health check on mount useEffect(() => { const checkConnection = async () => { try { setConnectionStatus("connecting"); await chatbotAPI.healthCheck(); setConnectionStatus("connected"); } catch (error) { console.warn("챗봇 연결 확인 실패:", error); setConnectionStatus("error"); } }; checkConnection(); }, [chatbotAPI]); // Generate unique message ID const generateMessageId = useCallback(() => { return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; }, []); // Handle opening/closing chat const handleToggleChat = useCallback(() => { setIsOpen((prev) => { const newIsOpen = !prev; if (newIsOpen) { setUnreadCount(0); resetQuestions(); } return newIsOpen; }); }, [resetQuestions]); const handleCloseChat = useCallback(() => { setIsOpen(false); }, []); // Add message to chat const addMessage = useCallback( (message: Omit) => { const newMessage: Message = { ...message, id: generateMessageId(), }; setMessages((prev) => [...prev, newMessage]); // Update unread count if chat is closed if (!isOpen && message.sender === "ai") { setUnreadCount((prev) => prev + 1); } // Callback for message received if (message.sender === "ai" && onMessageReceived) { onMessageReceived(newMessage); } return newMessage; }, [isOpen, generateMessageId, onMessageReceived] ); // Handle sending message const handleSendMessage = useCallback( async (content: string) => { if (!content.trim() || disabled || isTyping) return; try { // Clear any previous errors setLastError(null); // Add user message const userMessage = addMessage({ content, sender: "user", timestamp: new Date(), type: "text", }); // Callback for message sent if (onMessageSent) { onMessageSent(content); } // Update suggested questions context based on content if ( content.toLowerCase().includes("가격") || content.toLowerCase().includes("요금") ) { updateSuggestedQuestionsContext("pricing"); } else if ( content.toLowerCase().includes("문제") || content.toLowerCase().includes("오류") ) { updateSuggestedQuestionsContext("error"); } // Show typing indicator setIsTyping(true); // Send message to API with retry const operation = () => chatbotAPI.sendMessage({ message: content, context: { messageHistory: messages.slice(-5), // Send last 5 messages for context }, }); const response = enableRetry ? await withRetry(operation, maxRetries, 1000) : await operation(); // Add AI response addMessage({ content: response.content, sender: "ai", timestamp: response.timestamp, type: "text", }); } catch (error) { console.error("메시지 전송 실패:", error); const apiError = error instanceof ChatbotAPIError ? error : new ChatbotAPIError( "메시지 전송에 실패했습니다", 0, "SEND_ERROR" ); setLastError(apiError.message); // Add error message to chat addMessage({ content: `${t("errorGeneric")}: ${apiError.message}`, sender: "ai", timestamp: new Date(), type: "error", }); // Call error callback if (onError) { onError(apiError); } } finally { setIsTyping(false); } }, [ disabled, isTyping, addMessage, onMessageSent, messages, updateSuggestedQuestionsContext, chatbotAPI, enableRetry, maxRetries, onError, ] ); // Handle retry last message const handleRetry = useCallback(() => { if (messages.length >= 2) { const lastUserMessage = messages .slice() .reverse() .find((msg) => msg.sender === "user"); if (lastUserMessage) { handleSendMessage(lastUserMessage.content); } } }, [messages, handleSendMessage]); // Determine if there are any error messages const hasErrors = useMemo(() => { return messages.some((msg) => msg.type === "error") || lastError !== null; }, [messages, lastError]); // Connection status indicator const getConnectionStatusColor = () => { switch (connectionStatus) { case "connected": return "#10B981"; // green case "connecting": return "#F59E0B"; // yellow case "error": return "#EF4444"; // red default: return "#6B7280"; // gray } }; return (
{/* Floating Chat Button */} {/* Chat Dock */} {/* Error Recovery */} {hasErrors && enableRetry && (
{t("errorNetwork")}
)}
); } // Hook for managing chatbot widget state export function useChatbotWidget() { const [isOpen, setIsOpen] = useState(false); const [messages, setMessages] = useState([]); const [unreadCount, setUnreadCount] = useState(0); const openChat = useCallback(() => { setIsOpen(true); setUnreadCount(0); }, []); const closeChat = useCallback(() => { setIsOpen(false); }, []); const clearMessages = useCallback(() => { setMessages([]); setUnreadCount(0); }, []); const addMessage = useCallback( (message: Omit) => { const newMessage: Message = { ...message, id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, }; setMessages((prev) => [...prev, newMessage]); if (!isOpen && message.sender === "ai") { setUnreadCount((prev) => prev + 1); } return newMessage; }, [isOpen] ); return { isOpen, messages, unreadCount, openChat, closeChat, clearMessages, addMessage, }; } // Main ChatbotWidget component with providers export function ChatbotWidget({ theme = "default", language, ...props }: ChatbotWidgetProps) { const { theme: currentTheme } = useTheme(theme); // Apply theme on mount and when theme changes React.useEffect(() => { applyTheme(currentTheme); }, [currentTheme]); return (
); } export default ChatbotWidget;