UNPKG

@restnfeel/agentc-starter-kit

Version:

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

263 lines (260 loc) 16.9 kB
"use client"; import { jsxs, jsx } from 'react/jsx-runtime'; import { useState, useRef, useEffect, useCallback } from 'react'; import { Button } from '../../ui/Button.js'; import { SuggestedQuestions } from './suggested-questions.js'; import { useI18n, RTLText } from './i18n-context.js'; import { useModalState } from './modal-state-context.js'; function DesktopChatModal({ onSendMessage, isTyping = false, title, placeholder, className = "", disabled = false, showSuggestedQuestions = true, suggestedQuestionsContext = "welcome", allowMinimize = true, width = "800px", height = "600px", announceMessages = true, breakpoints = { mobile: "95vw", tablet: "600px", desktop: "800px", }, }) { // Use modal state context const { modalState, closeModal, minimizeModal, maximizeModal, bringToFront, currentSession, addMessage, } = useModalState(); // Use i18n context const { t, isRTL, currentLanguage } = useI18n(); // Use default values from translations if not provided const displayTitle = title || t("chatbot"); const displayPlaceholder = placeholder || t("placeholder"); const [inputValue, setInputValue] = useState(""); const [screenSize, setScreenSize] = useState("desktop"); const messagesEndRef = useRef(null); const inputRef = useRef(null); const messagesContainerRef = useRef(null); const modalRef = useRef(null); const announcementRef = useRef(null); // Get messages from current session const messages = (currentSession === null || currentSession === void 0 ? void 0 : currentSession.messages) || []; // Responsive design detection useEffect(() => { const handleResize = () => { const width = window.innerWidth; if (width < 768) { setScreenSize("mobile"); } else if (width < 1024) { setScreenSize("tablet"); } else { setScreenSize("desktop"); } }; // Initial check handleResize(); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); // Auto-scroll to bottom when new messages arrive useEffect(() => { if (messagesEndRef.current && !modalState.isMinimized) { messagesEndRef.current.scrollIntoView({ behavior: "smooth" }); } }, [messages, modalState.isMinimized]); // Announce new messages to screen readers useEffect(() => { if (announceMessages && messages.length > 0 && announcementRef.current) { const lastMessage = messages[messages.length - 1]; if (lastMessage.sender === "ai") { announcementRef.current.textContent = `AI responded: ${lastMessage.content}`; // Clear after a delay to avoid cluttering screen readers setTimeout(() => { if (announcementRef.current) { announcementRef.current.textContent = ""; } }, 1000); } } }, [messages, announceMessages]); // Focus input when modal opens useEffect(() => { if (modalState.isOpen && !modalState.isMinimized && inputRef.current) { const timer = setTimeout(() => { var _a; (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, 100); return () => clearTimeout(timer); } }, [modalState.isOpen, modalState.isMinimized]); // Handle escape key to close modal useEffect(() => { const handleEscape = (event) => { if (event.key === "Escape" && modalState.isOpen && !modalState.isMinimized) { closeModal(); } }; if (modalState.isOpen) { document.addEventListener("keydown", handleEscape); // Prevent body scroll when modal is open document.body.style.overflow = "hidden"; } return () => { document.removeEventListener("keydown", handleEscape); document.body.style.overflow = "unset"; }; }, [modalState.isOpen, modalState.isMinimized, closeModal]); // Enhanced focus trap for accessibility useEffect(() => { if (modalState.isOpen && !modalState.isMinimized && modalRef.current) { const focusableElements = modalRef.current.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])'); const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; const handleTabKey = (e) => { if (e.key === "Tab") { if (e.shiftKey) { if (document.activeElement === firstElement) { lastElement === null || lastElement === void 0 ? void 0 : lastElement.focus(); e.preventDefault(); } } else { if (document.activeElement === lastElement) { firstElement === null || firstElement === void 0 ? void 0 : firstElement.focus(); e.preventDefault(); } } } }; document.addEventListener("keydown", handleTabKey); return () => document.removeEventListener("keydown", handleTabKey); } }, [modalState.isOpen, modalState.isMinimized]); const handleSendMessage = useCallback(() => { const trimmedValue = inputValue.trim(); if (trimmedValue && !disabled) { // Add user message to state addMessage({ content: trimmedValue, sender: "user", type: "text", }); // Call external handler onSendMessage(trimmedValue); setInputValue(""); } }, [inputValue, onSendMessage, disabled, addMessage]); const handleKeyPress = useCallback((event) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); handleSendMessage(); } }, [handleSendMessage]); const handleMinimize = useCallback(() => { if (modalState.isMinimized) { maximizeModal(); } else { minimizeModal(); } }, [modalState.isMinimized, minimizeModal, maximizeModal]); const handleBackdropClick = useCallback((event) => { if (event.target === event.currentTarget && !modalState.isMinimized) { closeModal(); } }, [closeModal, modalState.isMinimized]); const handleModalClick = useCallback(() => { bringToFront(); }, [bringToFront]); const formatTimestamp = (timestamp) => { return timestamp.toLocaleTimeString(currentLanguage === "ko" ? "ko-KR" : "en-US", { hour: "2-digit", minute: "2-digit", }); }; // Get responsive modal dimensions const getModalDimensions = () => { if (modalState.isMinimized) { return { width: "320px", height: "64px" }; } switch (screenSize) { case "mobile": return { width: breakpoints.mobile, height: "85vh", maxWidth: "95vw", maxHeight: "85vh", }; case "tablet": return { width: breakpoints.tablet, height: "70vh", maxWidth: "90vw", maxHeight: "80vh", }; default: return { width: modalState.size.width || width, height: modalState.size.height || height, maxWidth: "90vw", maxHeight: "90vh", }; } }; const modalDimensions = getModalDimensions(); const TypingIndicator = () => (jsxs("div", { className: "flex items-center space-x-2 px-6 py-4", role: "status", "aria-live": "polite", children: [jsxs("div", { className: "flex space-x-1", children: [jsx("div", { className: "w-2 h-2 bg-gray-400 rounded-full animate-bounce [animation-delay:-0.3s]" }), jsx("div", { className: "w-2 h-2 bg-gray-400 rounded-full animate-bounce [animation-delay:-0.15s]" }), jsx("div", { className: "w-2 h-2 bg-gray-400 rounded-full animate-bounce" })] }), jsx(RTLText, { className: "text-sm text-gray-500", children: t("typing") })] })); const MessageBubble = ({ message }) => { const isUser = message.sender === "user"; const isError = message.type === "error"; const isSystem = message.type === "system"; return (jsx("div", { className: `flex ${isUser ? "justify-end" : "justify-start"} mb-6 animate-in slide-in-from-bottom-1 duration-300`, role: "article", "aria-label": `${isUser ? "Your message" : "AI message"}: ${message.content}`, children: jsxs("div", { className: ` max-w-[85%] sm:max-w-[70%] px-4 sm:px-5 py-3 sm:py-4 rounded-2xl shadow-sm ${isUser ? "bg-gradient-to-r from-blue-500 to-blue-600 text-white" : isError ? "bg-red-50 border border-red-200 text-red-800" : isSystem ? "bg-gray-50 border border-gray-200 text-gray-600" : "bg-gray-50 border border-gray-200 text-gray-800"} ${isUser ? "rounded-br-md" : "rounded-bl-md"} `, children: [jsx(RTLText, { className: "text-sm leading-relaxed whitespace-pre-wrap break-words", children: message.content }), jsx("div", { className: `text-xs mt-2 opacity-70 ${isUser ? "text-blue-100" : "text-gray-500"}`, children: jsx("time", { dateTime: message.timestamp.toISOString(), children: formatTimestamp(message.timestamp) }) })] }) })); }; if (!modalState.isOpen) return null; return (jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-0", onClick: handleBackdropClick, style: { zIndex: modalState.zIndex }, role: "presentation", children: [jsx("div", { className: "absolute inset-0 bg-black bg-opacity-50 backdrop-blur-sm transition-opacity duration-300" }), jsx("div", { ref: announcementRef, className: "sr-only", "aria-live": "polite", "aria-atomic": "true" }), jsxs("div", { ref: modalRef, className: ` relative bg-white rounded-2xl sm:rounded-3xl shadow-2xl border border-gray-200 transition-all duration-300 ease-in-out ${screenSize === "mobile" ? "mx-2" : ""} ${className} `, style: { width: modalDimensions.width, height: modalDimensions.height, maxWidth: modalDimensions.maxWidth, maxHeight: modalDimensions.maxHeight, }, role: "dialog", "aria-labelledby": "desktop-chat-title", "aria-describedby": "desktop-chat-description", "aria-modal": "true", onClick: handleModalClick, children: [jsx("div", { id: "desktop-chat-description", className: "sr-only", children: "AI chatbot conversation interface. Use Tab to navigate between elements, Escape to close." }), jsxs("div", { className: ` flex items-center justify-between px-4 sm:px-6 py-3 sm:py-4 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-t-2xl sm:rounded-t-3xl ${modalState.isMinimized ? "rounded-b-2xl sm:rounded-b-3xl" : ""} `, children: [jsxs("div", { className: "flex items-center space-x-2 sm:space-x-3 min-w-0", children: [jsx("div", { className: "w-3 h-3 bg-green-400 rounded-full animate-pulse flex-shrink-0" }), jsx("h2", { id: "desktop-chat-title", className: "text-base sm:text-lg font-semibold truncate", title: displayTitle, children: displayTitle }), currentSession && screenSize !== "mobile" && (jsxs("span", { className: "text-xs sm:text-sm opacity-75 hidden sm:inline", children: ["(", messages.length, " messages)"] }))] }), jsxs("div", { className: "flex items-center space-x-1 sm:space-x-2 flex-shrink-0", children: [allowMinimize && (jsx(Button, { variant: "ghost", size: "sm", onClick: handleMinimize, className: "text-white hover:bg-white/20 p-1.5 sm:p-2 rounded-full min-w-0", "aria-label": modalState.isMinimized ? t("maximize") : t("minimize"), children: jsx("svg", { className: `w-3 h-3 sm:w-4 sm:h-4 transition-transform duration-200 ${modalState.isMinimized ? "rotate-180" : ""}`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" }) }) })), jsx(Button, { variant: "ghost", size: "sm", onClick: closeModal, className: "text-white hover:bg-white/20 p-1.5 sm:p-2 rounded-full min-w-0", "aria-label": t("close"), children: jsx("svg", { className: "w-3 h-3 sm:w-4 sm:h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })] })] }), !modalState.isMinimized && (jsxs("div", { className: "flex flex-col h-full", children: [jsxs("div", { ref: messagesContainerRef, className: "flex-1 overflow-y-auto p-3 sm:p-6 space-y-3 sm:space-y-4 min-h-0", style: { maxHeight: screenSize === "mobile" ? "calc(100% - 100px)" : "calc(100% - 140px)", }, role: "log", "aria-live": "polite", "aria-label": "Chat conversation", children: [messages.length === 0 ? (jsxs("div", { className: "text-center text-gray-500 py-8 sm:py-12", children: [jsx("div", { className: "text-4xl sm:text-6xl mb-4 sm:mb-6", children: "\uD83D\uDCAC" }), jsx(RTLText, { className: "text-base sm:text-lg mb-6 sm:mb-8 font-medium", children: t("welcomeMessage") }), showSuggestedQuestions && (jsx("div", { className: "mt-6 sm:mt-8", children: jsx(SuggestedQuestions, { onQuestionSelect: onSendMessage, context: suggestedQuestionsContext, variant: "chips", maxQuestions: screenSize === "mobile" ? 2 : 4, className: "justify-center" }) }))] })) : (messages.map((message) => (jsx(MessageBubble, { message: message }, message.id)))), isTyping && jsx(TypingIndicator, {}), jsx("div", { ref: messagesEndRef })] }), jsxs("div", { className: "border-t border-gray-200 p-3 sm:p-6", children: [jsxs("div", { className: "flex space-x-2 sm:space-x-4", children: [jsx("input", { ref: inputRef, type: "text", value: inputValue, onChange: (e) => setInputValue(e.target.value), onKeyPress: handleKeyPress, placeholder: displayPlaceholder, disabled: disabled, className: ` flex-1 px-3 sm:px-4 py-2 sm:py-3 border border-gray-300 rounded-xl sm:rounded-2xl text-sm sm:text-base focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed transition-colors ${isRTL ? "text-right" : "text-left"} `, dir: isRTL ? "rtl" : "ltr", "aria-label": displayPlaceholder, "aria-describedby": "input-instructions" }), jsx(Button, { onClick: handleSendMessage, disabled: !inputValue.trim() || disabled, className: "px-3 sm:px-6 py-2 sm:py-3 bg-blue-500 hover:bg-blue-600 text-white rounded-xl sm:rounded-2xl disabled:opacity-50 disabled:cursor-not-allowed transition-colors min-w-0", "aria-label": t("send"), children: jsx("svg", { className: "w-4 h-4 sm:w-5 sm:h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 19l9 2-9-18-9 18 9-2zm0 0v-8" }) }) })] }), jsx("div", { id: "input-instructions", className: "sr-only", children: "Type your message and press Enter or click Send button to send. Press Escape to close the chat." }), screenSize === "mobile" && (jsx("p", { className: "text-xs text-gray-500 mt-2 text-center", children: "Press Enter to send \u2022 Escape to close" }))] })] }))] })] })); } export { DesktopChatModal, DesktopChatModal as default }; //# sourceMappingURL=desktop-chat-modal.js.map