taskflow-frontend / components /ChatInterface.tsx
Tahasaif3's picture
'change'
c49988e
'use client';
import { useState, useRef, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Send, Bot, User, Sparkles, Loader2, ArrowDown, Calendar, Layers, Hash } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import ReactMarkdown from 'react-markdown';
interface Message {
id: string;
content: string;
role: 'user' | 'assistant';
timestamp: Date;
}
interface ChatInterfaceProps {
userId?: string;
conversationId?: number;
compact?: boolean;
}
export default function ChatInterface({ userId, conversationId, compact = false }: ChatInterfaceProps) {
const [inputMessage, setInputMessage] = useState('');
const [messages, setMessages] = useState<Message[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [showScrollButton, setShowScrollButton] = useState(false);
const scrollAreaRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (messages.length === 0) {
setMessages([
{
id: 'welcome',
content: "Welcome to **TaskFlow Elite**. I'm your high-fidelity coordination agent.\n\nI can now manage your **Projects**, **Tasks**, and **Calendar** with geometric precision. \n\n### Quick Capabilities:\n* **Projects**: \"Create a new project called 'Hackathon'\"\n* **Coordination**: \"Add 'Update UI' to my Hackathon project\"\n* **Calendar**: \"What's on my schedule for this week?\"",
role: 'assistant',
timestamp: new Date(),
}
]);
}
}, [messages.length]);
const scrollToBottom = () => {
if (scrollAreaRef.current) {
scrollAreaRef.current.scrollTo({
top: scrollAreaRef.current.scrollHeight,
behavior: 'smooth'
});
}
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const handleScroll = () => {
if (scrollAreaRef.current) {
const { scrollTop, scrollHeight, clientHeight } = scrollAreaRef.current;
setShowScrollButton(scrollHeight - scrollTop - clientHeight > 100);
}
};
const handleSendMessage = async () => {
if (!inputMessage.trim() || isLoading || !userId) return;
const userMessage: Message = {
id: Date.now().toString(),
content: inputMessage,
role: 'user',
timestamp: new Date(),
};
setMessages(prev => [...prev, userMessage]);
setInputMessage('');
setIsLoading(true);
try {
const API_URL = 'https://tahasaif3-ai-taskflow-backend.hf.space';
const storedToken = localStorage.getItem('auth_token');
const response = await fetch(`${API_URL}/api/${userId}/chat/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(storedToken ? { 'Authorization': `Bearer ${storedToken}` } : {}),
},
credentials: 'include',
body: JSON.stringify({
message: inputMessage,
conversation_id: conversationId || null
})
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
setMessages(prev => [...prev, {
id: `resp-${Date.now()}`,
content: data.response,
role: 'assistant',
timestamp: new Date(),
}]);
} catch (error) {
setMessages(prev => [...prev, {
id: `error-${Date.now()}`,
content: "**System Overload.** I couldn't process that request. Please try again or check your connection.",
role: 'assistant',
timestamp: new Date(),
}]);
} finally {
setIsLoading(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
};
return (
<div className={`flex flex-col relative h-full bg-[#050505]/60 backdrop-blur-2xl border border-white/5 rounded-3xl overflow-hidden shadow-[0_22px_70px_-15px_rgba(0,0,0,0.8)] transition-all duration-500`}>
{/* Dynamic Background Accents */}
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-indigo-600/10 blur-[120px] rounded-full pointer-events-none" />
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-purple-600/10 blur-[120px] rounded-full pointer-events-none" />
{/* Top Refinement Line */}
<div className="absolute top-0 inset-x-0 h-[1px] bg-gradient-to-r from-transparent via-indigo-500/30 to-transparent z-10" />
{/* Messages Feed */}
<div
ref={scrollAreaRef}
onScroll={handleScroll}
className="flex-1 p-6 space-y-8 overflow-y-auto scrollbar-hide selection:bg-indigo-500/30"
>
<AnimatePresence initial={false}>
{messages.map((message) => (
<motion.div
key={message.id}
initial={{ opacity: 0, y: 30, filter: "blur(10px)" }}
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
transition={{ duration: 0.5, ease: [0.19, 1, 0.22, 1] }}
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div className={`flex gap-4 max-w-[90%] ${message.role === 'user' ? 'flex-row-reverse' : 'flex-row'}`}>
{/* Avatar with Glow */}
<div className={`relative flex-shrink-0 w-9 h-9 rounded-xl flex items-center justify-center transition-all duration-300 ${message.role === 'user'
? 'bg-gradient-to-tr from-indigo-500 to-blue-600 shadow-[0_0_20px_-5px_rgba(79,70,229,0.5)]'
: 'bg-white/5 border border-white/10 ring-1 ring-white/5'
}`}>
{message.role === 'user' ? (
<User className="w-5 h-5 text-white" />
) : (
<Bot className="w-5 h-5 text-indigo-400" />
)}
{message.role === 'assistant' && (
<span className="absolute -top-0.5 -right-0.5 w-2 h-2 bg-green-500 rounded-full border border-black animate-pulse" />
)}
</div>
{/* Message Content Container */}
<div className={`flex flex-col ${message.role === 'user' ? 'items-end' : 'items-start'} space-y-2`}>
<motion.div
whileHover={{ scale: 1.005 }}
className={`rounded-[22px] px-5 py-3.5 shadow-2xl transition-all duration-300 ${message.role === 'user'
? 'bg-gradient-to-br from-indigo-600/90 to-blue-700/90 text-white rounded-tr-none border border-white/10'
: 'bg-white/[0.03] backdrop-blur-xl border border-white/10 text-gray-200 rounded-tl-none ring-1 ring-white/5'
}`}
>
<div className="text-[14px] leading-relaxed prose prose-invert prose-sm max-w-none
prose-headings:text-white prose-headings:font-bold prose-headings:mb-2 prose-headings:mt-4
prose-strong:text-indigo-300 prose-strong:font-semibold
prose-p:text-gray-300 prose-p:leading-relaxed
prose-ul:my-2 prose-li:my-1
">
<ReactMarkdown>{message.content}</ReactMarkdown>
</div>
</motion.div>
<span className="text-[11px] font-medium text-gray-500 tracking-tight px-1 flex items-center gap-2">
{message.role === 'assistant' && <Sparkles className="w-3 h-3 text-indigo-500/50" />}
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
</div>
</motion.div>
))}
</AnimatePresence>
{/* Advanced Loading State */}
{isLoading && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="flex justify-start"
>
<div className="flex gap-4 items-center bg-white/[0.03] backdrop-blur-md border border-white/10 rounded-2xl rounded-tl-none px-5 py-3.5 ring-1 ring-white/5 shadow-xl">
<div className="flex gap-1.5 item-center">
{[0, 1, 2].map((i) => (
<motion.div
key={i}
animate={{
scale: [1, 1.4, 1],
opacity: [0.3, 1, 0.3],
backgroundColor: ['#6366f1', '#a855f7', '#6366f1']
}}
transition={{ repeat: Infinity, duration: 1.5, delay: i * 0.2 }}
className="w-1.5 h-1.5 rounded-full"
/>
))}
</div>
<span className="text-[13px] text-gray-400 font-medium tracking-wide italic">Synthesizing...</span>
</div>
</motion.div>
)}
</div>
{/* Floating Scroll Hub */}
<AnimatePresence>
{showScrollButton && (
<motion.button
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
onClick={scrollToBottom}
className="absolute bottom-28 right-8 w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-2xl border border-white/10 rounded-full flex items-center justify-center text-white shadow-2xl z-20 group transition-all"
>
<ArrowDown className="w-5 h-5 group-hover:translate-y-0.5 transition-transform" />
</motion.button>
)}
</AnimatePresence>
{/* Futuristic Input Control */}
<div className="px-6 py-6 bg-gradient-to-t from-black via-black/80 to-transparent border-t border-white/5 relative">
<div className="relative group max-w-3xl mx-auto">
<div className="absolute -inset-1 bg-gradient-to-r from-indigo-500/20 to-purple-500/20 rounded-2xl blur opacity-0 group-hover:opacity-100 transition duration-500" />
<div className="relative flex items-center gap-3">
<Input
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Architect your day..."
className="h-14 pl-6 pr-14 bg-[#0a0a0a] border-white/10 focus:border-indigo-500/50 focus:ring-4 focus:ring-indigo-500/10 text-white placeholder:text-gray-600 rounded-2xl transition-all duration-300 text-[15px] font-medium"
disabled={isLoading}
/>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handleSendMessage}
disabled={isLoading || !inputMessage.trim()}
className="absolute right-2.5 top-2.5 w-10 h-10 bg-white text-black hover:bg-gray-200 disabled:bg-gray-800 disabled:text-gray-500 disabled:cursor-not-allowed rounded-xl flex items-center justify-center transition-all duration-300 shadow-xl"
>
{isLoading ? <Loader2 className="w-5 h-5 animate-spin" /> : <Send className="w-5 h-5" />}
</motion.button>
</div>
</div>
{/* Status Bar */}
<div className="flex items-center justify-center gap-6 mt-5 opacity-40 hover:opacity-100 transition-opacity duration-300">
<div className="flex items-center gap-2">
<Calendar className="w-3.5 h-3.5 text-indigo-400" />
<span className="text-[10px] text-gray-300 font-bold uppercase tracking-widest">Calendar Sync</span>
</div>
<div className="w-[1px] h-3 bg-white/20" />
<div className="flex items-center gap-2">
<Layers className="w-3.5 h-3.5 text-purple-400" />
<span className="text-[10px] text-gray-300 font-bold uppercase tracking-widest">Projects Node</span>
</div>
<div className="w-[1px] h-3 bg-white/20" />
<div className="flex items-center gap-2">
<Hash className="w-3.5 h-3.5 text-blue-400" />
<span className="text-[10px] text-gray-300 font-bold uppercase tracking-widest">v2.1 Elite</span>
</div>
</div>
</div>
</div>
);
}