UNPKG

@restnfeel/agentc-starter-kit

Version:

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

549 lines (489 loc) 18.8 kB
"use client"; import { jsxs, jsx, Fragment } from 'react/jsx-runtime'; import { useState, useRef, useEffect, useCallback } from 'react'; import { ArrowLeft, X, Menu, Send } from 'lucide-react'; import { Button } from '../../ui/Button.js'; import { SuggestedQuestions } from './suggested-questions.js'; import { useI18n, RTLText } from './i18n-context.js'; // Hook for detecting viewport changes and keyboard function useViewport() { const [viewport, setViewport] = useState({ height: typeof window !== "undefined" ? window.innerHeight : 0, width: typeof window !== "undefined" ? window.innerWidth : 0, keyboardVisible: false, keyboardHeight: 0, }); useEffect(() => { if (typeof window === "undefined") return; const updateViewport = () => { const newHeight = window.innerHeight; const newWidth = window.innerWidth; // Estimate keyboard visibility based on height change const heightDiff = viewport.height - newHeight; const keyboardVisible = heightDiff > 150; // Threshold for keyboard detection setViewport({ height: newHeight, width: newWidth, keyboardVisible, keyboardHeight: keyboardVisible ? heightDiff : 0, }); }; const handleResize = () => { updateViewport(); }; const handleVisualViewportChange = () => { if ("visualViewport" in window && window.visualViewport) { const vvHeight = window.visualViewport.height; const windowHeight = window.innerHeight; const keyboardHeight = windowHeight - vvHeight; const keyboardVisible = keyboardHeight > 150; setViewport((prev) => ({ ...prev, height: vvHeight, keyboardVisible, keyboardHeight, })); } }; window.addEventListener("resize", handleResize); // Use Visual Viewport API if available (better for keyboard detection) if ("visualViewport" in window && window.visualViewport) { window.visualViewport.addEventListener("resize", handleVisualViewportChange); } // Initial update updateViewport(); return () => { window.removeEventListener("resize", handleResize); if ("visualViewport" in window && window.visualViewport) { window.visualViewport.removeEventListener("resize", handleVisualViewportChange); } }; }, [viewport.height]); return viewport; } function MobileFullscreenChat({ onSendMessage, onBackPress, messages = [], isTyping = false, title, placeholder, className = "", disabled = false, showSuggestedQuestions = true, suggestedQuestionsContext, headerContent, keyboardAdjustment = true, mobileBreakpoint = 768, pullToRefresh = false, }) { const [inputMessage, setInputMessage] = useState(""); const [isMenuOpen, setIsMenuOpen] = useState(false); const [refreshing, setRefreshing] = useState(false); const messagesEndRef = useRef(null); const inputRef = useRef(null); const messagesContainerRef = useRef(null); const { t} = useI18n(); const viewport = useViewport(); // Auto-scroll to bottom when new messages arrive useEffect(() => { var _a; (_a = messagesEndRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: "smooth" }); }, [messages]); // Handle keyboard appearance adjustments useEffect(() => { if (!keyboardAdjustment) return; const chatContainer = messagesContainerRef.current; if (chatContainer && viewport.keyboardVisible) { // Scroll to show latest messages when keyboard appears setTimeout(() => { var _a; (_a = messagesEndRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: "smooth" }); }, 100); } }, [viewport.keyboardVisible, keyboardAdjustment]); const handleSendMessage = useCallback(() => { var _a; if (inputMessage.trim() && !disabled) { onSendMessage(inputMessage.trim()); setInputMessage(""); (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); } }, [inputMessage, disabled, onSendMessage]); const handleKeyPress = useCallback((e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } }, [handleSendMessage]); const handleSuggestedQuestionClick = useCallback((question) => { var _a; setInputMessage(question); (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, []); const handleBackPress = useCallback(() => { if (onBackPress) { onBackPress(); } else if (typeof window !== "undefined" && window.history.length > 1) { window.history.back(); } }, [onBackPress]); // Pull-to-refresh handler useCallback(async () => { if (!pullToRefresh) return; setRefreshing(true); // Simulate refresh action await new Promise((resolve) => setTimeout(resolve, 1000)); setRefreshing(false); }, [pullToRefresh]); // Calculate dynamic styles for keyboard adjustment const containerStyle = { height: keyboardAdjustment ? `${viewport.height}px` : "100vh", width: "100vw", maxHeight: keyboardAdjustment ? `${viewport.height}px` : "100vh", overflow: "hidden", }; const messagesAreaHeight = keyboardAdjustment && viewport.keyboardVisible ? viewport.height - 64 - 80 // Header height - Input area height : viewport.height - 64 - 80; return (jsxs("div", { className: `mobile-fullscreen-chat ${className}`, style: containerStyle, dir: dir, children: [jsxs("header", { className: "mobile-chat-header", children: [jsxs("div", { className: "header-content", children: [jsx(Button, { variant: "ghost", size: "sm", onClick: handleBackPress, className: "back-button", "aria-label": t("chat.back", "Back"), children: jsx(ArrowLeft, { size: 24 }) }), jsx("div", { className: "header-title", children: jsx("h1", { children: title || t("chat.title", "Chat") }) }), headerContent || (jsx(Button, { variant: "ghost", size: "sm", onClick: () => setIsMenuOpen(!isMenuOpen), className: "menu-button", "aria-label": t("chat.menu", "Menu"), children: isMenuOpen ? jsx(X, { size: 24 }) : jsx(Menu, { size: 24 }) }))] }), isMenuOpen && (jsxs("div", { className: "mobile-menu", children: [jsx("div", { className: "menu-item", children: jsx("button", { onClick: () => setIsMenuOpen(false), children: t("chat.clearHistory", "Clear History") }) }), jsx("div", { className: "menu-item", children: jsx("button", { onClick: () => setIsMenuOpen(false), children: t("chat.settings", "Settings") }) })] }))] }), jsxs("div", { className: "messages-area", style: { height: `${messagesAreaHeight}px` }, ref: messagesContainerRef, children: [refreshing && (jsxs("div", { className: "refresh-indicator", children: [jsx("div", { className: "refresh-spinner" }), jsx("span", { children: t("chat.refreshing", "Refreshing...") })] })), jsxs("div", { className: "messages-container", children: [messages.length === 0 && !isTyping ? (jsxs("div", { className: "empty-state", children: [jsx("div", { className: "empty-icon", children: "\uD83D\uDCAC" }), jsx("h2", { children: t("chat.welcome", "Welcome to Chat") }), jsx("p", { children: t("chat.welcomeMessage", "Start a conversation by typing a message below.") }), showSuggestedQuestions && (jsx("div", { className: "suggested-questions-mobile", children: jsx(SuggestedQuestions, { context: suggestedQuestionsContext, onQuestionClick: handleSuggestedQuestionClick, className: "mobile-suggestions" }) }))] })) : (jsxs(Fragment, { children: [messages.map((message) => (jsxs("div", { className: `message ${message.isUser ? "user-message" : "ai-message"}`, children: [jsx("div", { className: "message-content", children: jsx(RTLText, { text: message.text }) }), jsx("div", { className: "message-time", children: message.timestamp.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", }) })] }, message.id))), isTyping && (jsx("div", { className: "message ai-message typing", children: jsx("div", { className: "message-content", children: jsxs("div", { className: "typing-indicator", children: [jsx("span", {}), jsx("span", {}), jsx("span", {})] }) }) }))] })), jsx("div", { ref: messagesEndRef })] })] }), jsx("div", { className: "input-area", children: jsxs("div", { className: "input-container", children: [jsx("input", { ref: inputRef, type: "text", value: inputMessage, onChange: (e) => setInputMessage(e.target.value), onKeyPress: handleKeyPress, placeholder: placeholder || t("chat.placeholder", "Type your message..."), disabled: disabled, className: "message-input", "aria-label": t("chat.inputLabel", "Message input") }), jsx(Button, { onClick: handleSendMessage, disabled: !inputMessage.trim() || disabled, className: "send-button", "aria-label": t("chat.send", "Send message"), children: jsx(Send, { size: 20 }) })] }) }), jsx("style", { jsx: true, children: ` .mobile-fullscreen-chat { position: fixed; top: 0; left: 0; background: white; display: flex; flex-direction: column; z-index: 9999; } .mobile-chat-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 0; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); position: relative; min-height: 64px; } .header-content { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; height: 64px; box-sizing: border-box; } .back-button, .menu-button { color: white; min-width: 48px; min-height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; } .back-button:hover, .menu-button:hover { background: rgba(255, 255, 255, 0.1); } .header-title { flex: 1; text-align: center; margin: 0 16px; } .header-title h1 { margin: 0; font-size: 18px; font-weight: 600; } .mobile-menu { position: absolute; top: 100%; right: 0; background: white; border-radius: 8px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); margin: 8px; min-width: 200px; z-index: 1000; } .menu-item { border-bottom: 1px solid #f0f0f0; } .menu-item:last-child { border-bottom: none; } .menu-item button { width: 100%; padding: 16px; text-align: left; background: none; border: none; font-size: 16px; color: #333; cursor: pointer; } .menu-item button:hover { background: #f5f5f5; } .messages-area { flex: 1; overflow: hidden; background: #f8f9fa; position: relative; } .refresh-indicator { position: absolute; top: 16px; left: 50%; transform: translateX(-50%); display: flex; align-items: center; gap: 8px; background: white; padding: 8px 16px; border-radius: 20px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); z-index: 10; } .refresh-spinner { width: 16px; height: 16px; border: 2px solid #f3f3f3; border-top: 2px solid #667eea; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .messages-container { height: 100%; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; -webkit-overflow-scrolling: touch; } .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; text-align: center; padding: 20px; } .empty-icon { font-size: 48px; margin-bottom: 16px; } .empty-state h2 { font-size: 24px; margin: 0 0 8px 0; color: #333; } .empty-state p { font-size: 16px; color: #666; margin: 0 0 24px 0; line-height: 1.5; } .suggested-questions-mobile { width: 100%; max-width: 400px; } .message { display: flex; flex-direction: column; max-width: 85%; word-wrap: break-word; margin-bottom: 4px; } .user-message { align-self: flex-end; align-items: flex-end; } .ai-message { align-self: flex-start; align-items: flex-start; } .message-content { padding: 16px 20px; border-radius: 20px; font-size: 16px; line-height: 1.4; min-height: 20px; word-break: break-word; } .user-message .message-content { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-bottom-right-radius: 8px; } .ai-message .message-content { background: white; color: #333; border: 1px solid #e1e5e9; border-bottom-left-radius: 8px; } .message-time { font-size: 12px; color: #888; margin: 4px 8px 0; } .typing-indicator { display: flex; gap: 4px; align-items: center; } .typing-indicator span { height: 8px; width: 8px; background: #999; border-radius: 50%; animation: typing 1.4s infinite ease-in-out; } .typing-indicator span:nth-child(1) { animation-delay: -0.32s; } .typing-indicator span:nth-child(2) { animation-delay: -0.16s; } @keyframes typing { 0%, 80%, 100% { transform: scale(0.8); opacity: 0.5; } 40% { transform: scale(1); opacity: 1; } } .input-area { background: white; border-top: 1px solid #e1e5e9; padding: 16px; min-height: 80px; display: flex; align-items: center; } .input-container { display: flex; gap: 12px; width: 100%; align-items: flex-end; } .message-input { flex: 1; min-height: 48px; padding: 12px 16px; border: 2px solid #e1e5e9; border-radius: 24px; font-size: 16px; outline: none; background: #f8f9fa; resize: none; box-sizing: border-box; } .message-input:focus { border-color: #667eea; background: white; } .send-button { min-width: 48px; min-height: 48px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; } .send-button:enabled:hover { transform: scale(1.05); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); } .send-button:disabled { background: #ccc; cursor: not-allowed; transform: none; box-shadow: none; } /* Dark mode support */ @media (prefers-color-scheme: dark) { .mobile-fullscreen-chat { background: #1a1a1a; color: white; } .messages-area { background: #2d2d2d; } .ai-message .message-content { background: #3a3a3a; color: white; border-color: #555; } .empty-state h2 { color: white; } .empty-state p { color: #ccc; } .message-input { background: #3a3a3a; color: white; border-color: #555; } .message-input:focus { background: #4a4a4a; } .input-area { background: #2d2d2d; border-color: #555; } .mobile-menu { background: #3a3a3a; color: white; } .menu-item button { color: white; } .menu-item button:hover { background: #4a4a4a; } } /* High contrast mode */ @media (prefers-contrast: high) { .message-content { border-width: 2px; } .send-button:focus { outline: 3px solid #fff; outline-offset: 2px; } .back-button:focus, .menu-button:focus { outline: 2px solid #fff; outline-offset: 2px; } } /* Reduced motion */ @media (prefers-reduced-motion: reduce) { .typing-indicator span { animation: none; } .refresh-spinner { animation: none; } .send-button:enabled:hover { transform: none; } } ` })] })); } export { MobileFullscreenChat }; //# sourceMappingURL=mobile-fullscreen-chat.js.map