@restnfeel/agentc-starter-kit
Version:
한국어 기업용 CMS 모듈 - Task Master AI와 함께 빠르게 웹사이트를 구현할 수 있는 재사용 가능한 컴포넌트 시스템
248 lines (241 loc) • 10.2 kB
JavaScript
"use client";
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import { useState, useEffect, useCallback } from 'react';
import { FloatingChatButton } from './floating-chat-button.js';
import { MobileFullscreenChat } from '../../../src/components/chatbot/customer/mobile-fullscreen-chat.tsx';
import { I18nProvider } from './i18n-context.js';
// Hook for mobile detection
function useIsMobile(breakpoint = 768) {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkDevice = () => {
const width = window.innerWidth;
const userAgent = navigator.userAgent;
// Check both screen width and user agent
const isSmallScreen = width <= breakpoint;
const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
setIsMobile(isSmallScreen || isMobileDevice);
};
checkDevice();
window.addEventListener("resize", checkDevice);
return () => window.removeEventListener("resize", checkDevice);
}, [breakpoint]);
return isMobile;
}
// Hook for chat persistence
function useChatPersistence(sessionId, enabled = true) {
const storageKey = `mobile-chat-${sessionId}`;
const saveMessages = useCallback((messages) => {
if (!enabled || typeof window === "undefined")
return;
try {
localStorage.setItem(storageKey, JSON.stringify({
messages,
timestamp: Date.now(),
}));
}
catch (error) {
console.warn("Failed to save chat messages:", error);
}
}, [storageKey, enabled]);
const loadMessages = useCallback(() => {
if (!enabled || typeof window === "undefined")
return [];
try {
const stored = localStorage.getItem(storageKey);
if (!stored)
return [];
const data = JSON.parse(stored);
// Check if data is less than 24 hours old
const isRecent = Date.now() - data.timestamp < 24 * 60 * 60 * 1000;
if (isRecent && Array.isArray(data.messages)) {
return data.messages.map((msg) => ({
...msg,
timestamp: new Date(msg.timestamp),
}));
}
}
catch (error) {
console.warn("Failed to load chat messages:", error);
}
return [];
}, [storageKey, enabled]);
const clearMessages = useCallback(() => {
if (!enabled || typeof window === "undefined")
return;
try {
localStorage.removeItem(storageKey);
}
catch (error) {
console.warn("Failed to clear chat messages:", error);
}
}, [storageKey, enabled]);
return { saveMessages, loadMessages, clearMessages };
}
function MobileChatNavigator({ apiEndpoint = "/api/rag/answer", language = "en", title, placeholder, className = "", disabled = false, showSuggestedQuestions = true, suggestedQuestionsContext, initialMessages = [], onNavigate, onMessage, buttonPosition = "bottom-right", buttonStyle, persistChat = true, sessionId = "default", }) {
const [isChatOpen, setIsChatOpen] = useState(false);
const [messages, setMessages] = useState([]);
const [isTyping, setIsTyping] = useState(false);
const isMobile = useIsMobile();
const { saveMessages, loadMessages, clearMessages } = useChatPersistence(sessionId, persistChat);
// Load persisted messages on mount
useEffect(() => {
if (persistChat) {
const persistedMessages = loadMessages();
if (persistedMessages.length > 0) {
setMessages(persistedMessages);
}
else if (initialMessages.length > 0) {
setMessages(initialMessages);
}
}
else if (initialMessages.length > 0) {
setMessages(initialMessages);
}
}, [persistChat, loadMessages, initialMessages]);
// Save messages when they change
useEffect(() => {
if (persistChat && messages.length > 0) {
saveMessages(messages);
}
}, [messages, saveMessages, persistChat]);
const handleFloatingButtonClick = useCallback(() => {
if (!isMobile) {
// On desktop, don't navigate - this should be handled by desktop modal
console.warn("MobileChatNavigator should only be used on mobile devices");
return;
}
const newState = !isChatOpen;
setIsChatOpen(newState);
onNavigate === null || onNavigate === void 0 ? void 0 : onNavigate(newState);
}, [isChatOpen, isMobile, onNavigate]);
const handleBackPress = useCallback(() => {
setIsChatOpen(false);
onNavigate === null || onNavigate === void 0 ? void 0 : onNavigate(false);
}, [onNavigate]);
const handleSendMessage = useCallback(async (message) => {
if (disabled)
return;
const userMessage = {
id: `user-${Date.now()}`,
text: message,
isUser: true,
timestamp: new Date(),
};
setMessages((prev) => [...prev, userMessage]);
setIsTyping(true);
try {
let aiResponse;
if (onMessage) {
// Use custom message handler
aiResponse = await onMessage(message);
}
else {
// Use default API call
const response = await fetch(apiEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
question: message,
sessionId,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
aiResponse =
data.answer ||
data.response ||
"Sorry, I could not process your request.";
}
const aiMessage = {
id: `ai-${Date.now()}`,
text: aiResponse,
isUser: false,
timestamp: new Date(),
};
setMessages((prev) => [...prev, aiMessage]);
}
catch (error) {
console.error("Error sending message:", error);
const errorMessage = {
id: `error-${Date.now()}`,
text: "Sorry, there was an error processing your message. Please try again.",
isUser: false,
timestamp: new Date(),
};
setMessages((prev) => [...prev, errorMessage]);
}
finally {
setIsTyping(false);
}
}, [disabled, onMessage, apiEndpoint, sessionId]);
const handleClearHistory = useCallback(() => {
setMessages([]);
if (persistChat) {
clearMessages();
}
}, [persistChat, clearMessages]);
// Only render if on mobile
if (!isMobile) {
return null;
}
return (jsx(I18nProvider, { defaultLanguage: language, children: jsxs("div", { className: `mobile-chat-navigator ${className}`, children: [!isChatOpen && (jsx(FloatingChatButton, { onClick: handleFloatingButtonClick, position: buttonPosition, className: buttonStyle ? "custom-styled" : "", disabled: disabled })), isChatOpen && (jsx(MobileFullscreenChat, { onSendMessage: handleSendMessage, onBackPress: handleBackPress, messages: messages, isTyping: isTyping, title: title, placeholder: placeholder, disabled: disabled, showSuggestedQuestions: showSuggestedQuestions, suggestedQuestionsContext: suggestedQuestionsContext, headerContent: jsx("button", { onClick: handleClearHistory, className: "clear-history-button", "aria-label": "Clear chat history", children: "Clear" }) })), jsx("style", { children: `
.mobile-chat-navigator {
position: relative;
}
.clear-history-button {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.clear-history-button:hover {
background: rgba(255, 255, 255, 0.3);
}
.clear-history-button:active {
transform: scale(0.95);
}
.custom-styled {
${buttonStyle
? Object.entries(buttonStyle)
.map(([key, value]) => `${key
.replace(/([A-Z])/g, "-$1")
.toLowerCase()}: ${value};`)
.join(" ")
: ""}
}
` })] }) }));
}
// Higher-order component for easier integration
function withMobileNavigation(WrappedComponent) {
return function MobileNavigationWrapper(props) {
const { apiEndpoint, language, title, placeholder, disabled, showSuggestedQuestions, suggestedQuestionsContext, initialMessages, onNavigate, onMessage, buttonPosition, buttonStyle, persistChat, sessionId, ...componentProps } = props;
const navigatorProps = {
apiEndpoint,
language,
title,
placeholder,
disabled,
showSuggestedQuestions,
suggestedQuestionsContext,
initialMessages,
onNavigate,
onMessage,
buttonPosition,
buttonStyle,
persistChat,
sessionId,
};
return (jsxs(Fragment, { children: [jsx(WrappedComponent, { ...componentProps }), jsx(MobileChatNavigator, { ...navigatorProps })] }));
};
}
export { MobileChatNavigator, withMobileNavigation };
//# sourceMappingURL=mobile-chat-navigator.js.map