UNPKG

@ieltsrealtest/ui

Version:

Reusable UI components for IELTS Real Test platform, built with React and TypeScript.

349 lines (348 loc) 14.3 kB
"use client"; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { useEffect, useState, useRef } from "react"; import { Send } from "lucide-react"; import { RxCross2 } from "react-icons/rx"; import { FaRegSmile } from "react-icons/fa"; const emojis = [ "😀", "😃", "😄", "😁", "😆", "😅", "😂", "🤣", "😊", "😇", "🙂", "🙃", "😉", "😌", "😍", "🥰", "😘", "😗", "😙", "😚", "😋", "😛", "😝", "😜", "🤪", "🤨", "🧐", "🤓", "😎", "🤩", "🥳", "😏", "😒", "😞", "😔", "😟", "😕", "🙁", "☹️", "😣", "😖", "😫", "😩", "🥺", "😢", "😭", "😤", "😠", "😡", "🤬", "🤯", "😳", "🥵", "🥶", "😱", "😨", "😰", "😥", "😓", "🤗", "🤔", "🤭", "🤫", "🤥", "😶", "😐", "😑", "😬", "🙄", "😯", "😦", "😧", "😮", "😲", "🥱", "😴", "🤤", "😪", "😵", "🤐", "🥴", "🤢", "🤮", "🤧", "😷", "🤒", "🤕", "🤑", "🤠", "😈", "👿", "👹", "👺", "🤡", "💩", "👻", "💀", "☠️", "👽", "👾", "🤖", "🎃", "😺", "😸", "😹", "😻", "😼", "😽", "🙀", "😿", "😾", "👋", "🤚", "🖐️", "✋", "🖖", "👌", "🤌", "🤏", "✌️", "🤞", "🤟", "🤘", "🤙", "👈", "👉", "👆", "🖕", "👇", "☝️", "👍", "👎", "👊", "✊", "🤛", "🤜", "👏", "🙌", "👐", "🤲", "🤝", "🙏", "✍️", "💅", "🤳", "💪", "🦾", "🦿", "🦵", "🦶", "👂", "🦻", "👃", "🧠", "🫀", "🫁", "🦷", "🦴", "👀", "👁️", "👅", "👄", "💋", "🩸", "❤️", "🧡", "💛", "💚", "💙", "💜", "🤎", "🖤", "🤍", "💔", "❣️", "💕", ]; export default function Chatbot() { const [messages, setMessages] = useState([]); const [inputValue, setInputValue] = useState(""); const [loading, setLoading] = useState(false); const [showEmojiPicker, setShowEmojiPicker] = useState(false); const [showChatbox, setShowChatbox] = useState(false); const [userId, setUserId] = useState(null); const [isLoggedIn, setIsLoggedIn] = useState(true); const emojiPickerRef = useRef(null); const inputRef = useRef(null); const messagesEndRef = useRef(null); const [inputDisabled, setInputDisabled] = useState(false); useEffect(() => { fetch("/api/chatbot/seed") //fetch(`${process.env.NEXT_PUBLIC_API_URL}/chatbot/seed`) .then((res) => res.json()) .then((data) => { setMessages(data.map((msg) => ({ ...msg, timestamp: new Date(msg.timestamp), }))); }); }, []); useEffect(() => { const fetchUserIdAndData = async () => { try { const res = await fetch(`https://api.youready.net/ielts/user/api/auth/get_user_id/`, { method: "GET", credentials: "include", headers: { "Content-Type": "application/json", }, }); const data = await res.json(); if (res.ok && data.user_id) { setUserId(data.user_id); setIsLoggedIn(true); } else { setIsLoggedIn(false); } } catch (error) { setIsLoggedIn(false); } }; fetchUserIdAndData(); }, []); // Close emoji picker when clicking outside useEffect(() => { const handleClickOutside = (event) => { if (emojiPickerRef.current && !emojiPickerRef.current.contains(event.target)) { setShowEmojiPicker(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, []); const handleSendMessage = async () => { if (!inputValue.trim()) return; const userMessage = { id: Date.now().toString(), content: inputValue, sender: "user", timestamp: new Date(), }; setMessages((prev) => [...prev, userMessage]); setInputValue(""); setLoading(true); try { // Simulate delay for bot response (e.g., 2 seconds) await new Promise((resolve) => setTimeout(resolve, 2000)); // Lấy JWT từ localStorage (hoặc context/cookie) // Gửi JWT lên backend để xác thực và kiểm tra giới hạn token/ngày //const jwt = localStorage.getItem("jwt_token") // Gửi 6 tin nhắn gần nhất (bao gồm cả user và bot) để AI có ngữ cảnh trả lời tốt hơn const res = await fetch("https://api.youready.net/ielts/ai/chatbot/reply", { method: "POST", headers: { "Content-Type": "application/json", //"Authorization": `Bearer ${jwt}` // Gửi JWT lên backend }, body: JSON.stringify({ message: userMessage.content, history: messages.slice(-6), // Truyền 6 tin nhắn gần nhất user_id: userId // Thêm user_id vào body gửi backend }), }); if (res.status === 429) { setInputDisabled(true); setMessages((prev) => [ ...prev, { id: Date.now().toString(), content: "Bạn đã sử dụng hết lượt chat trong ngày hôm nay! Hãy quay lại vào ngày mai nhé! Cảm ơn bạn!", sender: "bot", timestamp: new Date(), }, ]); return; } if (!res.ok) { throw new Error("API trả về lỗi: " + res.status); } const data = await res.json(); const botMessage = { id: data.id, content: data.content, sender: "bot", timestamp: new Date(data.timestamp), }; setMessages((prev) => [...prev, botMessage]); } catch (err) { setMessages((prev) => [ ...prev, { id: Date.now().toString(), content: "Xin lỗi, hệ thống đang gặp sự cố. Vui lòng thử lại sau.", sender: "bot", timestamp: new Date(), }, ]); console.error("Lỗi gọi API:", err); } finally { setLoading(false); } }; const handleKeyPress = (e) => { if (e.key === "Enter") handleSendMessage(); }; const handleEmojiClick = (emoji) => { setInputValue((prev) => prev + emoji); setShowEmojiPicker(false); // Focus back to input after selecting emoji if (inputRef.current) { inputRef.current.focus(); } }; const toggleEmojiPicker = () => { setShowEmojiPicker((prev) => !prev); }; // Tự động cuộn xuống khi có tin nhắn mới hoặc loading useEffect(() => { if (messagesEndRef.current) { messagesEndRef.current.scrollIntoView({ behavior: "smooth" }); } }, [messages, loading]); return (_jsx(_Fragment, { children: showChatbox ? (_jsxs("div", { className: "fixed bottom-20 right-2 z-50 max-w-[75vw] sm:max-w-4xl mx-auto bg-white rounded-lg shadow-lg overflow-hidden border-2 border-[#A71E34]", children: [_jsxs("div", { className: "bg-[#FFF3E0] border-b-2 border-[#A71E34] p-4 flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center space-x-3", children: [_jsx("div", { className: "w-10 h-10 rounded-full bg-white border-2 border-[#C71F37] flex items-center justify-center overflow-hidden shadow", children: _jsx("img", { src: "https://ieltsrealtest.s3.us-east-1.amazonaws.com/public/common/youready.jpg", alt: "AI Avatar", className: "w-full h-full object-contain" }) }), _jsx("div", { children: _jsx("h3", { className: "font-semibold text-gray-800", children: "YouReady" }) })] }), _jsx("div", { className: "flex items-center space-x-2", children: _jsx("button", { className: "p-2 rounded-full hover:bg-gray-200 transition-colors duration-200", onClick: () => setShowChatbox(false), "aria-label": "\u0110\u00F3ng chat", children: _jsx(RxCross2, { className: "w-5 h-5 text-[#C71F37]" }) }) })] }), _jsxs("div", { className: "h-64 overflow-y-auto p-4 space-y-4 bg-[#FFFBF5]", children: [messages.map((message) => (_jsx("div", { className: `flex ${message.sender === "user" ? "justify-end" : "justify-start"}`, children: _jsxs("div", { className: "flex items-start space-x-2 max-w-xs", children: [message.sender === "bot" && (_jsx("div", { className: "w-8 h-8 rounded-full bg-white border-2 border-[#C71F37] flex items-center justify-center flex-shrink-0 overflow-hidden shadow", children: _jsx("img", { src: "https://ieltsrealtest.s3.us-east-1.amazonaws.com/public/common/youready.jpg", alt: "AI Avatar", className: "w-full h-full object-contain" }) })), _jsx("div", { className: `px-4 py-2 rounded-2xl ${message.sender === "user" ? "bg-[#E7BC91] text-gray-800 rounded-br-sm" : "bg-[#FFF3E0] text-gray-800 rounded-bl-sm border border-gray-200"}`, children: _jsx("p", { className: "text-sm whitespace-pre-line break-words", style: { overflowWrap: "anywhere" }, children: message.content }) })] }) }, message.id))), loading && (_jsx("div", { className: "flex justify-start", children: _jsxs("div", { className: "flex items-center space-x-2 max-w-xs", children: [_jsx("div", { className: "w-8 h-8 rounded-full bg-white border-2 border-[#C71F37] flex items-center justify-center flex-shrink-0 overflow-hidden shadow", children: _jsx("img", { src: "https://ieltsrealtest.s3.us-east-1.amazonaws.com/public/common/youready.jpg", alt: "AI Avatar", className: "w-full h-full object-contain" }) }), _jsxs("div", { className: "px-4 py-2 rounded-2xl bg-[#FFF3E0] text-gray-800 rounded-bl-sm border border-gray-200 flex items-center", children: [_jsx("span", { className: "animate-bounce inline-block w-2 h-2 bg-gray-400 rounded-full mr-1" }), _jsx("span", { className: "animate-bounce inline-block w-2 h-2 bg-gray-400 rounded-full mr-1", style: { animationDelay: '0.2s' } }), _jsx("span", { className: "animate-bounce inline-block w-2 h-2 bg-gray-400 rounded-full", style: { animationDelay: '0.4s' } })] })] }) })), _jsx("div", { ref: messagesEndRef })] }), _jsxs("div", { className: "p-4 bg-[#FFF3E0] border-t border-gray-200 relative", children: [_jsxs("div", { className: "flex items-center space-x-2", children: [_jsxs("div", { className: "flex-1 relative", children: [_jsx("input", { ref: inputRef, type: "text", placeholder: "Nh\u1EADp tin nh\u1EAFn...", value: inputValue, disabled: inputDisabled, onFocus: (e) => { if (inputDisabled) e.target.blur(); }, onChange: (e) => { if (e.target.value.length <= 250) setInputValue(e.target.value); }, onKeyDown: handleKeyPress, maxLength: 250, className: "w-full px-4 py-2 pr-12 bg-white rounded-full border border-gray-300 focus:border-orange-300 focus:ring-2 focus:ring-orange-200 focus:outline-none text-gray-800" }), _jsx("button", { onClick: toggleEmojiPicker, className: "absolute right-2 top-1/2 transform -translate-y-1/2 p-1 rounded-full hover:bg-gray-100 transition-colors duration-200", children: _jsx(FaRegSmile, { className: "w-5 h-5 text-[#C71F37]" }) })] }), _jsx("button", { onClick: handleSendMessage, disabled: loading, className: "p-2 rounded-full bg-[#C71F37] hover:bg-orange-600 text-white transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-orange-300 disabled:opacity-50", children: _jsx(Send, { className: "w-4 h-4" }) })] }), showEmojiPicker && (_jsx("div", { ref: emojiPickerRef, className: "absolute bottom-16 right-4 bg-white border border-gray-300 rounded-lg shadow-lg p-4 w-80 h-64 overflow-y-auto z-10", children: _jsx("div", { className: "grid grid-cols-8 gap-2", children: emojis.map((emoji, index) => (_jsx("button", { onClick: () => handleEmojiClick(emoji), className: "text-2xl hover:bg-gray-100 rounded p-1 transition-colors duration-200", children: emoji }, index))) }) }))] })] })) : ( // Chat bubble _jsx("button", { className: "fixed bottom-20 right-8 z-50 bg-[#C71F37] hover:bg-orange-600 rounded-full w-16 h-16 flex items-center justify-center shadow-lg transition-colors duration-200 border-4 border-white", onClick: () => setShowChatbox(true), "aria-label": "M\u1EDF chat", children: _jsx("span", { className: "w-12 h-12 rounded-full bg-white border-2 border-[#C71F37] flex items-center justify-center shadow-md transition-all duration-200", children: _jsx("img", { src: "https://ieltsrealtest.s3.us-east-1.amazonaws.com/public/common/youready.jpg", alt: "Chatbot", className: "w-10 h-10 rounded-full object-contain" }) }) })) })); }