@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
206 lines (203 loc) • 9.49 kB
JavaScript
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