203 lines
6.9 KiB
JavaScript
203 lines
6.9 KiB
JavaScript
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>
|
||
)}
|
||
</>
|
||
);
|
||
};
|