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