Files
my-loves-calendar/components/AssistantView.tsx
drjones c373bbdab4 first commit
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 21:05:27 -08:00

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>
);
};