mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-17 22:07:20 +00:00
251 lines
8.7 KiB
TypeScript
251 lines
8.7 KiB
TypeScript
import { useState, useRef, useEffect } from "react";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import { MessageCircle, X, Send, Bot, User, Loader2 } from "lucide-react";
|
|
import { useLocation } from "wouter";
|
|
import { isMobile } from "@/lib/platform";
|
|
|
|
interface Message {
|
|
id: string;
|
|
role: "user" | "assistant";
|
|
content: string;
|
|
timestamp: Date;
|
|
}
|
|
|
|
export function Chatbot() {
|
|
const [location] = useLocation();
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [messages, setMessages] = useState<Message[]>([
|
|
{
|
|
id: "welcome",
|
|
role: "assistant",
|
|
content: "AEGIS ONLINE. Security protocols initialized. Neural link established. How can I assist with your security operations today?",
|
|
timestamp: new Date(),
|
|
},
|
|
]);
|
|
const [input, setInput] = useState("");
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
}, [messages]);
|
|
|
|
// Load chat history when opening the chatbot
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
loadChatHistory();
|
|
}
|
|
}, [isOpen]);
|
|
|
|
const loadChatHistory = async () => {
|
|
try {
|
|
const response = await fetch("/api/chat/history", {
|
|
method: "GET",
|
|
credentials: "include",
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.history && data.history.length > 0) {
|
|
const historyMessages: Message[] = data.history.map((msg: any) => ({
|
|
id: msg.id,
|
|
role: msg.role,
|
|
content: msg.content,
|
|
timestamp: new Date(msg.created_at),
|
|
}));
|
|
|
|
// Replace welcome message with loaded history
|
|
setMessages(prev => [...historyMessages, ...prev.slice(1)]);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to load chat history:", error);
|
|
}
|
|
};
|
|
|
|
// Don't render chatbot on the OS page - it has its own environment
|
|
if (location === "/os" || isMobile()) {
|
|
return null;
|
|
}
|
|
|
|
const sendMessage = async () => {
|
|
if (!input.trim() || isLoading) return;
|
|
|
|
const userMessage: Message = {
|
|
id: Date.now().toString(),
|
|
role: "user",
|
|
content: input.trim(),
|
|
timestamp: new Date(),
|
|
};
|
|
|
|
setMessages((prev) => [...prev, userMessage]);
|
|
setInput("");
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
const conversationHistory = messages.slice(-10).map(m => ({
|
|
role: m.role,
|
|
content: m.content
|
|
}));
|
|
|
|
const response = await fetch("/api/chat", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "include",
|
|
body: JSON.stringify({
|
|
message: userMessage.content,
|
|
history: conversationHistory
|
|
}),
|
|
});
|
|
|
|
if (!response.ok) throw new Error("Failed to get response");
|
|
|
|
const data = await response.json();
|
|
|
|
const assistantMessage: Message = {
|
|
id: (Date.now() + 1).toString(),
|
|
role: "assistant",
|
|
content: data.response || "I apologize, but I'm having trouble responding right now. Please try again.",
|
|
timestamp: new Date(),
|
|
};
|
|
|
|
setMessages((prev) => [...prev, assistantMessage]);
|
|
} catch (error) {
|
|
const errorMessage: Message = {
|
|
id: (Date.now() + 1).toString(),
|
|
role: "assistant",
|
|
content: "I'm sorry, I encountered an error. Please try again in a moment.",
|
|
timestamp: new Date(),
|
|
};
|
|
setMessages((prev) => [...prev, errorMessage]);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
e.preventDefault();
|
|
sendMessage();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Chat Button */}
|
|
<AnimatePresence>
|
|
{!isOpen && (
|
|
<motion.button
|
|
initial={{ scale: 0, opacity: 0 }}
|
|
animate={{ scale: 1, opacity: 1 }}
|
|
exit={{ scale: 0, opacity: 0 }}
|
|
onClick={() => setIsOpen(true)}
|
|
className="fixed bottom-6 left-6 z-50 bg-secondary text-background p-4 rounded-full shadow-lg hover:bg-secondary/90 transition-colors"
|
|
data-testid="button-open-chatbot"
|
|
>
|
|
<MessageCircle className="w-6 h-6" />
|
|
</motion.button>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* Chat Window */}
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
exit={{ opacity: 0, y: 20, scale: 0.95 }}
|
|
className="fixed bottom-6 left-6 z-50 w-96 max-w-[calc(100vw-3rem)] h-[500px] max-h-[70vh] bg-card border border-white/10 shadow-2xl flex flex-col"
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 bg-red-500/20 rounded-full flex items-center justify-center">
|
|
<Bot className="w-4 h-4 text-red-500" />
|
|
</div>
|
|
<div>
|
|
<div className="text-sm font-bold text-white">AEGIS</div>
|
|
<div className="text-xs text-red-500 flex items-center gap-1">
|
|
<span className="w-1.5 h-1.5 bg-red-500 rounded-full animate-pulse" /> Security Active
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsOpen(false)}
|
|
className="text-muted-foreground hover:text-white transition-colors"
|
|
data-testid="button-close-chatbot"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Messages */}
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
{messages.map((message) => (
|
|
<div
|
|
key={message.id}
|
|
className={`flex gap-3 ${message.role === "user" ? "flex-row-reverse" : ""}`}
|
|
>
|
|
<div
|
|
className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
|
|
message.role === "user" ? "bg-primary/20" : "bg-secondary/20"
|
|
}`}
|
|
>
|
|
{message.role === "user" ? (
|
|
<User className="w-4 h-4 text-primary" />
|
|
) : (
|
|
<Bot className="w-4 h-4 text-secondary" />
|
|
)}
|
|
</div>
|
|
<div
|
|
className={`max-w-[75%] p-3 text-sm ${
|
|
message.role === "user"
|
|
? "bg-primary/10 text-white border border-primary/30"
|
|
: "bg-white/5 text-muted-foreground border border-white/10"
|
|
}`}
|
|
>
|
|
{message.content}
|
|
</div>
|
|
</div>
|
|
))}
|
|
{isLoading && (
|
|
<div className="flex gap-3">
|
|
<div className="w-8 h-8 bg-secondary/20 rounded-full flex items-center justify-center">
|
|
<Bot className="w-4 h-4 text-secondary" />
|
|
</div>
|
|
<div className="bg-white/5 border border-white/10 p-3 text-sm text-muted-foreground">
|
|
<Loader2 className="w-4 h-4 animate-spin" />
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div ref={messagesEndRef} />
|
|
</div>
|
|
|
|
{/* Input */}
|
|
<div className="p-4 border-t border-white/10">
|
|
<div className="flex gap-2">
|
|
<input
|
|
type="text"
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
onKeyPress={handleKeyPress}
|
|
placeholder="Ask me anything..."
|
|
className="flex-1 bg-black/20 border border-white/10 px-4 py-2 text-sm text-white placeholder:text-muted-foreground focus:outline-none focus:border-secondary/50"
|
|
data-testid="input-chat-message"
|
|
/>
|
|
<button
|
|
onClick={sendMessage}
|
|
disabled={!input.trim() || isLoading}
|
|
className="bg-secondary text-background px-4 py-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-secondary/90 transition-colors"
|
|
data-testid="button-send-message"
|
|
>
|
|
<Send className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</>
|
|
);
|
|
}
|