188 lines
7.2 KiB
TypeScript
188 lines
7.2 KiB
TypeScript
import React, { useState, useRef, useEffect } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { Send, Bot, User as UserIcon, X, Loader2 } from 'lucide-react';
|
|
import { ChatMessage, UserRole, AppSettings, CalendarEvent } from '../types';
|
|
import { generateChatResponse } from '../services/aiService';
|
|
|
|
interface AssistantViewProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
currentUser: UserRole;
|
|
settings: AppSettings;
|
|
onEventCreated: (event: CalendarEvent) => void;
|
|
}
|
|
|
|
export const AssistantView: React.FC<AssistantViewProps> = ({
|
|
isOpen,
|
|
onClose,
|
|
currentUser,
|
|
settings,
|
|
onEventCreated
|
|
}) => {
|
|
const [input, setInput] = useState('');
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [messages, setMessages] = useState<ChatMessage[]>([
|
|
{
|
|
id: 'welcome',
|
|
role: 'assistant',
|
|
content: `Hi ${currentUser === 'mom' ? 'Mom' : 'Dad'}! I am your cosmic assistant. How can I help you plan?`,
|
|
timestamp: new Date()
|
|
}
|
|
]);
|
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
|
|
const scrollToBottom = () => {
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
};
|
|
|
|
useEffect(() => {
|
|
scrollToBottom();
|
|
}, [messages, isOpen]);
|
|
|
|
const handleSend = async (e?: React.FormEvent) => {
|
|
e?.preventDefault();
|
|
if (!input.trim() || isLoading) return;
|
|
|
|
const userMsg: ChatMessage = {
|
|
id: Date.now().toString(),
|
|
role: 'user',
|
|
content: input,
|
|
timestamp: new Date()
|
|
};
|
|
|
|
setMessages(prev => [...prev, userMsg]);
|
|
setInput('');
|
|
setIsLoading(true);
|
|
|
|
const { text, event } = await generateChatResponse(input, settings, currentUser);
|
|
|
|
const assistantMsg: ChatMessage = {
|
|
id: (Date.now() + 1).toString(),
|
|
role: 'assistant',
|
|
content: text,
|
|
timestamp: new Date()
|
|
};
|
|
|
|
setMessages(prev => [...prev, assistantMsg]);
|
|
setIsLoading(false);
|
|
|
|
if (event) {
|
|
onEventCreated(event);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<>
|
|
{/* Backdrop */}
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
onClick={onClose}
|
|
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40"
|
|
/>
|
|
|
|
{/* Side Panel */}
|
|
<motion.div
|
|
initial={{ x: '100%' }}
|
|
animate={{ x: 0 }}
|
|
exit={{ x: '100%' }}
|
|
transition={{ type: "spring", stiffness: 300, damping: 30 }}
|
|
className="fixed inset-y-0 right-0 w-full md:w-[450px] bg-gray-900 border-l border-gray-800 shadow-2xl z-50 flex flex-col"
|
|
>
|
|
{/* Header */}
|
|
<div className="p-4 border-b border-gray-800 flex justify-between items-center bg-gray-900/95 backdrop-blur">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-xl bg-gradient-to-tr from-purple-600 to-blue-600 flex items-center justify-center shadow-lg shadow-purple-900/50">
|
|
<Bot className="text-white" size={24} />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-lg font-bold text-white">Home Assistant</h2>
|
|
<div className="flex items-center gap-1.5">
|
|
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
|
|
<span className="text-xs text-gray-400 font-mono">
|
|
{settings.aiProvider === 'ollama' ? 'Local Spirit' : 'Gemini Cloud'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<button onClick={onClose} className="p-2 hover:bg-gray-800 rounded-full text-gray-400 hover:text-white transition-colors">
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Chat Area */}
|
|
<div className="flex-1 overflow-y-auto p-4 space-y-4 custom-scrollbar">
|
|
{messages.map((msg) => (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
key={msg.id}
|
|
className={`flex gap-3 ${msg.role === 'user' ? 'flex-row-reverse' : 'flex-row'}`}
|
|
>
|
|
<div className={`
|
|
w-8 h-8 rounded-full flex items-center justify-center shrink-0
|
|
${msg.role === 'user'
|
|
? (currentUser === 'mom' ? 'bg-mom-primary' : 'bg-dad-primary')
|
|
: 'bg-gray-700'}
|
|
`}>
|
|
{msg.role === 'user' ? <UserIcon size={14} className="text-white" /> : <Bot size={14} className="text-purple-300" />}
|
|
</div>
|
|
|
|
<div className={`
|
|
max-w-[80%] p-3 rounded-2xl text-sm leading-relaxed
|
|
${msg.role === 'user'
|
|
? 'bg-gray-800 text-white rounded-tr-none border border-gray-700'
|
|
: 'bg-purple-900/20 text-gray-200 rounded-tl-none border border-purple-500/20'}
|
|
`}>
|
|
{msg.content}
|
|
<div className="text-[10px] opacity-30 mt-1 text-right">
|
|
{msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
|
|
{isLoading && (
|
|
<div className="flex gap-3">
|
|
<div className="w-8 h-8 rounded-full bg-gray-700 flex items-center justify-center shrink-0">
|
|
<Bot size={14} className="text-purple-300" />
|
|
</div>
|
|
<div className="bg-purple-900/20 p-3 rounded-2xl rounded-tl-none border border-purple-500/20 flex items-center gap-2">
|
|
<Loader2 size={16} className="animate-spin text-purple-400" />
|
|
<span className="text-xs text-purple-300">Consulting the stars...</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div ref={messagesEndRef} />
|
|
</div>
|
|
|
|
{/* Input Area */}
|
|
<div className="p-4 bg-gray-900/95 backdrop-blur border-t border-gray-800">
|
|
<form onSubmit={handleSend} className="relative">
|
|
<input
|
|
type="text"
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
placeholder="Schedule a date on Friday at 7pm..."
|
|
className="w-full bg-gray-800 text-white pl-4 pr-12 py-3 rounded-xl border border-gray-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none placeholder-gray-500 transition-all"
|
|
disabled={isLoading}
|
|
/>
|
|
<button
|
|
type="submit"
|
|
disabled={!input.trim() || isLoading}
|
|
className="absolute right-2 top-2 p-1.5 bg-purple-600 hover:bg-purple-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
<Send size={18} />
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</motion.div>
|
|
</>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
};
|