Basdat/web/js/components/AIChatbot.js
2025-12-20 00:01:08 +07:00

203 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const AIChatbot = () => {
const { useState, useEffect, useRef } = React;
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const messagesEndRef = useRef(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
useEffect(() => {
if (isOpen && messages.length === 0) {
loadHistory();
}
}, [isOpen]);
const loadHistory = async () => {
try {
const data = await ApiUtils.get(
`${CONFIG.API_ENDPOINTS.AI.HISTORY}?limit=10`
);
if (data.data && data.data.history) {
const formatted = data.data.history.reverse().flatMap((chat) => [
{ text: chat.message, sender: "user", time: chat.created_at },
{ text: chat.response, sender: "ai", time: chat.created_at },
]);
setMessages(formatted);
}
} catch (error) {
console.error("Failed to load history:", error);
}
};
const sendMessage = async () => {
if (!input.trim() || loading) return;
const userMessage = { text: input, sender: "user", time: new Date() };
setMessages((prev) => [...prev, userMessage]);
setInput("");
setLoading(true);
try {
const data = await ApiUtils.post(CONFIG.API_ENDPOINTS.AI.CHAT, {
message: input,
history: messages.slice(-10).map((msg) => ({
text: msg.text,
sender: msg.sender,
})),
});
const aiMessage = {
text:
data.data.message?.content ||
data.data.response ||
"Maaf, tidak ada respons.",
sender: "ai",
time: new Date(),
};
setMessages((prev) => [...prev, aiMessage]);
} catch (error) {
console.error("AI Chat error:", error);
const errorMessage = {
text: "Maaf, terjadi kesalahan. Silakan coba lagi.",
sender: "ai",
time: new Date(),
};
setMessages((prev) => [...prev, errorMessage]);
} finally {
setLoading(false);
}
};
const clearHistory = async () => {
if (!confirm("Hapus semua riwayat chat?")) return;
try {
await ApiUtils.delete(CONFIG.API_ENDPOINTS.AI.HISTORY);
setMessages([]);
} catch (error) {
console.error("Failed to clear history:", error);
alert("Gagal menghapus riwayat");
}
};
return (
<>
{/* Floating Button */}
<button
onClick={() => setIsOpen(!isOpen)}
className="fixed bottom-6 right-6 w-16 h-16 bg-gradient-to-r from-blue-600 to-blue-700 text-white rounded-full shadow-2xl hover:scale-110 transition-transform z-50 flex items-center justify-center"
>
{isOpen ? (
<span className="text-2xl"></span>
) : (
<span className="text-3xl">🤖</span>
)}
</button>
{/* Chat Window - REDUCED HEIGHT */}
{isOpen && (
<div className="fixed bottom-24 right-6 w-96 h-[500px] bg-gradient-to-br from-slate-800 to-slate-900 rounded-2xl shadow-2xl border border-slate-700 flex flex-col z-50">
{/* Header - REDUCED PADDING */}
<div className="bg-gradient-to-r from-blue-600 to-blue-700 p-3 rounded-t-2xl flex justify-between items-center">
<div>
<h3 className="text-white font-bold text-base">
🤖 AI Assistant
</h3>
<p className="text-blue-100 text-xs">
Tanya apa saja tentang Lost & Found
</p>
</div>
<button
onClick={clearHistory}
className="text-white hover:text-blue-200 text-sm"
title="Clear History"
>
🗑
</button>
</div>
{/* Messages Area - ADJUSTED HEIGHT */}
<div className="flex-1 overflow-y-auto p-3 space-y-2">
{messages.length === 0 && (
<div className="text-center text-slate-400 mt-6">
<p className="text-3xl mb-2">👋</p>
<p className="text-sm">Halo! Ada yang bisa saya bantu?</p>
<div className="mt-3 text-xs space-y-1">
<p>💡 Coba tanya:</p>
<p>"Ada dompet yang ditemukan?"</p>
<p>"Bagaimana cara lapor kehilangan?"</p>
</div>
</div>
)}
{messages.map((msg, idx) => (
<div
key={idx}
className={`flex ${
msg.sender === "user" ? "justify-end" : "justify-start"
}`}
>
<div
className={`max-w-[80%] p-2.5 rounded-2xl text-sm ${
msg.sender === "user"
? "bg-blue-600 text-white rounded-br-none"
: "bg-slate-700 text-slate-100 rounded-bl-none"
}`}
>
<p className="whitespace-pre-wrap">{msg.text}</p>
{msg.intent && (
<span className="text-xs opacity-70 mt-1 block">
🏷 {msg.intent}
</span>
)}
</div>
</div>
))}
{loading && (
<div className="flex justify-start">
<div className="bg-slate-700 text-slate-100 p-2.5 rounded-2xl rounded-bl-none">
<span className="inline-block animate-pulse text-sm">
💭 Sedang berpikir...
</span>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input Area - REDUCED PADDING */}
<div className="p-3 border-t border-slate-700">
<div className="flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && sendMessage()}
placeholder="Ketik pesan..."
className="flex-1 px-3 py-2 bg-slate-700 text-white text-sm rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500"
disabled={loading}
/>
<button
onClick={sendMessage}
disabled={loading || !input.trim()}
className="px-5 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition"
>
📤
</button>
</div>
</div>
</div>
)}
</>
);
};