@ieltsrealtest/ui
Version:
Reusable UI components for IELTS Real Test platform, built with React and TypeScript.
349 lines (348 loc) • 14.3 kB
JavaScript
"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" }) }) })) }));
}