UNPKG

@restnfeel/agentc-starter-kit

Version:

한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템

206 lines (203 loc) 9.49 kB
import { jsx, jsxs } from 'react/jsx-runtime'; import React, { useState, useMemo, useEffect, useCallback } from 'react'; import { FloatingChatButton } from './floating-chat-button.js'; import { ChatDock } from './chat-dock.js'; import { useSuggestedQuestions } from './suggested-questions.js'; import { ChatbotAPI, withRetry, ChatbotAPIError } from '../../../../services/chatbot-api.js'; import { useDebounce } from '../../../../hooks/usePerformance.js'; import { I18nProvider, useI18n } from './i18n-context.js'; import { useTheme, applyTheme } from './theme-config.js'; // 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, }) { // Use i18n context const { t} = 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"); 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) => { const newMessage = { ...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) => { 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 (jsxs("div", { className: `chatbot-widget ${className}`, children: [jsx(FloatingChatButton, { onClick: handleToggleChat, isOpen: isOpen, unreadCount: unreadCount, position: position, size: buttonSize, color: hasErrors ? "#EF4444" : getConnectionStatusColor(), disabled: disabled, ariaLabel: disabled ? t("offline") : isOpen ? t("closeChat") : t("openChat") }), jsx(ChatDock, { isOpen: isOpen, onClose: handleCloseChat, onSendMessage: handleSendMessage, messages: messages, isTyping: enableTypingIndicator && debouncedTyping, title: displayTitle, placeholder: disabled ? t("offline") : displayPlaceholder, position: position === "top-right" || position === "top-left" ? "bottom-right" : position, disabled: disabled || isTyping, showSuggestedQuestions: enableSuggestedQuestions, suggestedQuestionsContext: suggestedQuestionsContext }), hasErrors && enableRetry && (jsx("div", { className: "fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50", children: jsxs("div", { className: "bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-lg shadow-lg flex items-center space-x-3", children: [jsx("span", { className: "text-sm", children: t("errorNetwork") }), jsx("button", { onClick: handleRetry, className: "bg-red-500 hover:bg-red-600 text-white px-3 py-1 rounded text-sm transition-colors", children: t("retry") })] }) }))] })); } // Main ChatbotWidget component with providers function ChatbotWidget({ theme = "default", language, ...props }) { const { theme: currentTheme } = useTheme(theme); // Apply theme on mount and when theme changes React.useEffect(() => { applyTheme(currentTheme); }, [currentTheme]); return (jsx(I18nProvider, { defaultLanguage: language, enableAutoDetect: !language, children: jsx("div", { "data-chatbot": true, className: "chatbot-root", children: jsx(ChatbotWidgetInternal, { ...props }) }) })); } export { ChatbotWidget, ChatbotWidget as default }; //# sourceMappingURL=chatbot-widget.js.map