UnivAi / src /components /ChatInterface.tsx
oceddyyy's picture
Upload 59 files
13a0af9 verified
import { useState, useRef, useEffect, type ReactNode } from 'react';
import { Send, MessageCircle, Settings, Search, BarChart3, User, Bot, Sparkles, HelpCircle, Moon, Sun, Globe, Database, BookOpen, Crown, Zap, Menu, ChevronLeft, ChevronRight, ThumbsUp, ThumbsDown, MessageSquare } from 'lucide-react';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Message } from './Message';
import { TypingIndicator } from './TypingIndicator';
import { Sheet, SheetContent, SheetTrigger, SheetTitle, SheetDescription } from './ui/sheet';
import { Textarea } from './ui/textarea';
import { motion, AnimatePresence } from 'motion/react';
interface ChatMessage {
id: string;
content: string;
isUser: boolean;
timestamp: Date;
isPlusResponse?: boolean;
sources?: Array<{ name: string; icon: ReactNode; url?: string }>;
}
export function ChatInterface() {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [inputValue, setInputValue] = useState('');
const [isTyping, setIsTyping] = useState(false);
const [showComingSoon, setShowComingSoon] = useState(false);
const [isDarkMode, setIsDarkMode] = useState(false);
const [isUnivAiPlusMode, setIsUnivAiPlusMode] = useState(false);
const [hasUsedPlusResponse, setHasUsedPlusResponse] = useState(false);
const [currentSources, setCurrentSources] = useState<Array<{ name: string; icon: ReactNode; url?: string }>>([]);
const [showFeedback, setShowFeedback] = useState(false);
const [feedbackType, setFeedbackType] = useState<'positive' | 'negative' | null>(null);
const [feedbackComment, setFeedbackComment] = useState('');
const [lastBotMessageId, setLastBotMessageId] = useState<string | null>(null);
const [leftSheetOpen, setLeftSheetOpen] = useState(false);
const [rightSheetOpen, setRightSheetOpen] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
// Initialize messages based on current mode
useEffect(() => {
const initialMessage: ChatMessage = {
id: '1',
content: isUnivAiPlusMode
? "Feel free to explore UnivAi+++ for a richer, more responsive conversation experience. Please note: due to limited computational resources, the AI can provide only one response per session. Make it count!"
: "Hello! I'm your AI assistant. How can I help you today? Feel free to ask me anything!",
isUser: false,
timestamp: new Date(),
isPlusResponse: isUnivAiPlusMode
};
setMessages([initialMessage]);
setHasUsedPlusResponse(false);
setCurrentSources([]); // Reset sources when switching modes
setShowFeedback(false);
setFeedbackType(null);
setFeedbackComment('');
}, [isUnivAiPlusMode]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages, isTyping]);
useEffect(() => {
// Apply dark mode class to document
if (isDarkMode) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, [isDarkMode]);
const handleSendMessage = async (e: React.FormEvent) => {
e.preventDefault();
if (!inputValue.trim()) return;
if (isUnivAiPlusMode && hasUsedPlusResponse) return; // Prevent sending if already used Plus response
const userMessage: ChatMessage = {
id: Date.now().toString(),
content: inputValue,
isUser: true,
timestamp: new Date(),
};
setMessages(prev => [...prev, userMessage]);
setInputValue('');
setIsTyping(true);
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: inputValue,
dev_mode: isUnivAiPlusMode,
}),
});
const data = await res.json();
// Optionally, you can extract sources from data if provided
const botResponse: ChatMessage = {
id: (Date.now() + 1).toString(),
content: data.response,
isUser: false,
timestamp: new Date(),
isPlusResponse: isUnivAiPlusMode,
sources: [], // Optionally fill from data.source
};
setMessages(prev => [...prev, botResponse]);
setCurrentSources([]); // Optionally update if sources are available
setIsTyping(false);
setLastBotMessageId(botResponse.id);
// Show feedback form after bot response
setShowFeedback(true);
setFeedbackType(null);
setFeedbackComment('');
// Mark Plus response as used
if (isUnivAiPlusMode) {
setHasUsedPlusResponse(true);
}
} catch (err) {
setIsTyping(false);
}
};
const handleFeedbackSubmit = async () => {
console.log('Feedback submitted:', { messageId: lastBotMessageId, feedbackType, feedbackComment });
if (!lastBotMessageId) return;
const lastBotMsg = messages.find(m => m.id === lastBotMessageId);
if (!lastBotMsg) return;
try {
await fetch('/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: lastBotMsg.content,
response: lastBotMsg.content,
feedback: feedbackType,
}),
});
setFeedbackType(null);
setFeedbackComment('');
} catch (err) {
// Optionally handle error
}
};
const handleComingSoonClick = () => {
setShowComingSoon(true);
setTimeout(() => {
setShowComingSoon(false);
}, 2000);
};
const toggleDarkMode = () => {
setIsDarkMode(!isDarkMode);
};
const toggleUnivAiMode = () => {
setIsUnivAiPlusMode(!isUnivAiPlusMode);
};
const sidebarItems = [
{ icon: <MessageCircle size={20} />, active: true },
{ icon: <BarChart3 size={20} />, active: false },
{ icon: <Search size={20} />, active: false },
{ icon: <User size={20} />, active: false },
{ icon: <Settings size={20} />, active: false },
];
const isInputDisabled = isTyping || (isUnivAiPlusMode && hasUsedPlusResponse);
// Sidebar content components
const LeftSidebarContent = () => (
<>
{/* Logo */}
<div className={`w-9 h-9 rounded-lg flex items-center justify-center mb-6 shadow-lg transition-all duration-300 ${
isUnivAiPlusMode
? 'bg-gradient-to-r from-purple-500 to-pink-500 shadow-purple-500/30'
: 'bg-gradient-to-r from-red-500 to-orange-500 shadow-red-500/30'
}`}>
{isUnivAiPlusMode ? <Crown className="text-white" size={20} /> : <Bot className="text-white" size={20} />}
</div>
{/* Navigation */}
<div className="flex flex-col gap-3">
{sidebarItems.map((item, index) => (
<motion.button
key={index}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
onClick={!item.active ? handleComingSoonClick : undefined}
className={`
w-9 h-9 rounded-lg flex items-center justify-center transition-all duration-200
${item.active
? isUnivAiPlusMode
? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-lg shadow-purple-500/30'
: 'bg-gradient-to-r from-red-500 to-orange-500 text-white shadow-lg shadow-red-500/30'
: 'bg-white/80 text-red-600 hover:bg-white hover:text-red-700 shadow-sm cursor-pointer dark:bg-gray-800/80 dark:text-red-400 dark:hover:bg-gray-700'
}
`}
>
{item.active ? item.icon : <HelpCircle size={18} />}
</motion.button>
))}
</div>
</>
);
const RightSidebarContent = () => (
<>
{/* AI Status Card - More compact */}
<div className={`rounded-xl p-4 border backdrop-blur-sm shadow-lg transition-all duration-300 min-h-[200px] ${
isUnivAiPlusMode
? 'bg-gradient-to-br from-white/90 to-purple-50/90 border-purple-200/50 shadow-purple-100/30 dark:from-gray-800/90 dark:to-purple-900/90 dark:border-purple-600/50 dark:shadow-purple-900/30'
: 'bg-gradient-to-br from-white/90 to-red-50/90 border-red-200/50 shadow-red-100/30 dark:from-gray-800/90 dark:to-gray-700/90 dark:border-gray-600/50 dark:shadow-gray-900/30'
}`}>
<div className="flex items-center justify-center mb-3">
<div className={`w-16 h-16 rounded-full flex items-center justify-center shadow-lg transition-all duration-300 ${
isUnivAiPlusMode
? 'bg-gradient-to-r from-purple-400 to-pink-500 shadow-purple-500/30'
: 'bg-gradient-to-r from-yellow-400 to-orange-500 shadow-yellow-500/30'
}`}>
{isUnivAiPlusMode ? <Crown className="text-white" size={24} /> : <Bot className="text-white" size={24} />}
</div>
</div>
<div className="text-center">
{/* Fixed height container for title */}
<div className="h-6 overflow-hidden flex items-center justify-center mb-1">
<motion.h3
key={isUnivAiPlusMode ? 'plus' : 'regular'}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
className={`text-sm transition-all duration-300 whitespace-nowrap flex items-center ${
isUnivAiPlusMode
? 'text-purple-900 dark:text-purple-100'
: 'text-red-900 dark:text-red-100'
}`}
>
{isUnivAiPlusMode ? 'UnivAi+++' : 'UnivAi'}
{isUnivAiPlusMode && <Crown className="ml-1" size={12} />}
</motion.h3>
</div>
{/* Fixed height container for description */}
<div className="h-5 overflow-hidden flex items-center justify-center mb-3">
<motion.p
key={isUnivAiPlusMode ? 'plus-desc' : 'regular-desc'}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
className={`text-xs text-center transition-all duration-300 ${
isUnivAiPlusMode
? 'text-purple-700/80 dark:text-purple-300/80'
: 'text-red-700/80 dark:text-red-300/80'
}`}
>
{isUnivAiPlusMode
? 'Smarter with human-like responses'
: 'Any PUP-Related Queries?'
}
</motion.p>
</div>
{/* Fixed button */}
<div className="flex justify-center">
<Button
size="sm"
onClick={toggleUnivAiMode}
className={`text-white border-0 shadow-lg transition-all duration-300 text-xs px-3 py-1 w-40 h-7 ${
isUnivAiPlusMode
? 'bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 shadow-purple-500/20'
: 'bg-gradient-to-r from-red-500 to-orange-500 hover:from-red-600 hover:to-orange-600 shadow-red-500/20'
}`}
>
<span className="truncate">
{isUnivAiPlusMode ? 'Switch to UnivAi' : 'Try the New UnivAi+++'}
</span>
</Button>
</div>
</div>
</div>
{/* Chat Stats - Equalized for both modes */}
<div className={`rounded-xl p-3 border backdrop-blur-sm shadow-lg transition-all duration-300 ${
isUnivAiPlusMode
? 'bg-gradient-to-br from-white/90 to-pink-50/90 border-pink-200/50 shadow-pink-100/30 dark:from-gray-800/90 dark:to-pink-900/90 dark:border-pink-600/50 dark:shadow-pink-900/30'
: 'bg-gradient-to-br from-white/90 to-orange-50/90 border-orange-200/50 shadow-orange-100/30 dark:from-gray-800/90 dark:to-gray-700/90 dark:border-gray-600/50 dark:shadow-gray-900/30'
}`}>
<h4 className={`mb-2 text-sm transition-all duration-300 ${
isUnivAiPlusMode
? 'text-purple-900 dark:text-purple-100'
: 'text-red-900 dark:text-red-100'
}`}>Chat Statistics</h4>
<div className="space-y-2">
<div className="flex justify-between">
<span className={`text-xs transition-all duration-300 ${
isUnivAiPlusMode
? 'text-purple-700/80 dark:text-purple-300/80'
: 'text-red-700/80 dark:text-red-300/80'
}`}>Response Time</span>
<span className={`text-xs transition-all duration-300 ${
isUnivAiPlusMode
? 'text-purple-900 dark:text-purple-100'
: 'text-red-900 dark:text-red-100'
}`}>{isUnivAiPlusMode ? '2.5s' : '1.2s'}</span>
</div>
<div className="flex justify-between">
<span className={`text-xs transition-all duration-300 ${
isUnivAiPlusMode
? 'text-purple-700/80 dark:text-purple-300/80'
: 'text-red-700/80 dark:text-red-300/80'
}`}>Status</span>
<span className="text-green-600 text-xs">{isUnivAiPlusMode ? 'Premium' : 'Online'}</span>
</div>
<div className="flex justify-between">
<span className={`text-xs transition-all duration-300 ${
isUnivAiPlusMode
? 'text-purple-700/80 dark:text-purple-300/80'
: 'text-red-700/80 dark:text-red-300/80'
}`}>{isUnivAiPlusMode ? 'Responses Left' : 'Queries Processed'}</span>
<span className={`text-xs transition-all duration-300 ${
isUnivAiPlusMode
? 'text-purple-900 dark:text-purple-100'
: 'text-red-900 dark:text-red-100'
}`}>{isUnivAiPlusMode ? (hasUsedPlusResponse ? '0' : '1') : '∞'}</span>
</div>
</div>
</div>
{/* Sources - More compact */}
<div className={`rounded-xl p-3 border backdrop-blur-sm shadow-lg transition-all duration-300 ${
isUnivAiPlusMode
? 'bg-gradient-to-br from-white/90 to-amber-50/90 border-amber-200/50 shadow-amber-100/30 dark:from-gray-800/90 dark:to-amber-900/90 dark:border-amber-600/50 dark:shadow-amber-900/30'
: 'bg-gradient-to-br from-white/90 to-yellow-50/90 border-yellow-200/50 shadow-yellow-100/30 dark:from-gray-800/90 dark:to-gray-700/90 dark:border-gray-600/50 dark:shadow-gray-900/30'
}`}>
<h4 className={`mb-2 text-sm transition-all duration-300 ${
isUnivAiPlusMode
? 'text-purple-900 dark:text-purple-100'
: 'text-red-900 dark:text-red-100'
}`}>Sources</h4>
<div className="space-y-2 min-h-[40px]">
{currentSources.length === 0 ? (
<div className="flex items-center justify-center py-2">
<p className={`text-xs text-center transition-all duration-300 ${
isUnivAiPlusMode
? 'text-purple-600/60 dark:text-purple-400/60'
: 'text-red-600/60 dark:text-red-400/60'
}`}>
Sources will appear here after AI responses
</p>
</div>
) : (
currentSources.map((source, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
className="flex items-center justify-between"
>
<div className="flex items-center gap-2 min-w-0 flex-1">
<div className={`transition-all duration-300 flex-shrink-0 ${
isUnivAiPlusMode
? 'text-purple-600 dark:text-purple-400'
: 'text-red-600 dark:text-red-400'
}`}>
{source.icon}
</div>
<span className={`text-xs transition-all duration-300 truncate ${
isUnivAiPlusMode
? 'text-purple-700/80 dark:text-purple-300/80'
: 'text-red-700/80 dark:text-red-300/80'
}`}>{source.name}</span>
</div>
{source.url && (
<button
onClick={() => window.open(source.url, '_blank')}
className={`text-xs px-2 py-1 rounded-full transition-all duration-300 flex-shrink-0 hover:opacity-80 ${
isUnivAiPlusMode
? 'bg-gradient-to-r from-purple-100 to-pink-100 text-purple-700 dark:from-purple-900/30 dark:to-pink-900/30 dark:text-purple-400'
: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
}`}
>
View
</button>
)}
</motion.div>
))
)}
</div>
</div>
</>
);
return (
<div className={`h-screen flex p-1 md:p-2 transition-all duration-500 ${
isUnivAiPlusMode
? 'bg-gradient-to-br from-purple-100 via-pink-50 to-amber-50 dark:from-purple-900 dark:via-indigo-900 dark:to-amber-900'
: 'bg-gradient-to-br from-orange-100 via-red-50 to-yellow-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900'
}`}>
{/* Coming Soon Toast */}
<AnimatePresence>
{showComingSoon && (
<motion.div
initial={{ opacity: 0, y: -50, x: '-50%' }}
animate={{ opacity: 1, y: 0, x: '-50%' }}
exit={{ opacity: 0, y: -50, x: '-50%' }}
className="fixed top-4 left-1/2 z-50 bg-gradient-to-r from-red-500 to-orange-500 text-white px-6 py-3 rounded-lg shadow-lg shadow-red-500/30"
>
<p className="text-sm">Coming soon!</p>
</motion.div>
)}
</AnimatePresence>
{/* Usage Limit Warning */}
<AnimatePresence>
{isUnivAiPlusMode && hasUsedPlusResponse && (
<motion.div
initial={{ opacity: 0, y: -50, x: '-50%' }}
animate={{ opacity: 1, y: 0, x: '-50%' }}
exit={{ opacity: 0, y: -50, x: '-50%' }}
className="fixed top-16 left-1/2 z-50 bg-gradient-to-r from-purple-500 to-pink-500 text-white px-6 py-3 rounded-lg shadow-lg shadow-purple-500/30"
>
<p className="text-sm">UnivAi+++ limit reached. Switch to UnivAi to continue.</p>
</motion.div>
)}
</AnimatePresence>
{/* Dark Mode Toggle - Top Right */}
<motion.button
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
onClick={toggleDarkMode}
className={`fixed top-2 right-2 md:top-3 md:right-3 z-40 w-8 h-8 md:w-10 md:h-10 rounded-full text-white flex items-center justify-center shadow-lg transition-all duration-200 hover:scale-110 ${
isUnivAiPlusMode
? 'bg-gradient-to-r from-purple-500 to-pink-500 shadow-purple-500/30 hover:shadow-purple-500/50'
: 'bg-gradient-to-r from-red-500 to-orange-500 shadow-red-500/30 hover:shadow-red-500/50'
}`}
>
<motion.div
key={isDarkMode ? 'dark' : 'light'}
initial={{ rotate: -180, opacity: 0 }}
animate={{ rotate: 0, opacity: 1 }}
transition={{ duration: 0.3 }}
>
{isDarkMode ? <Sun size={16} className="md:w-[18px] md:h-[18px]" /> : <Moon size={16} className="md:w-[18px] md:h-[18px]" />}
</motion.div>
</motion.button>
{/* Mobile Left Sheet Trigger */}
<Sheet open={leftSheetOpen} onOpenChange={setLeftSheetOpen}>
<SheetTrigger asChild>
<motion.button
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className={`md:hidden fixed left-0 top-1/2 -translate-y-1/2 z-30 w-8 h-16 rounded-r-lg flex items-center justify-center shadow-lg transition-all duration-200 ${
isUnivAiPlusMode
? 'bg-gradient-to-r from-purple-500 to-pink-500 shadow-purple-500/30'
: 'bg-gradient-to-r from-red-500 to-orange-500 shadow-red-500/30'
}`}
>
<ChevronRight className="text-white" size={20} />
</motion.button>
</SheetTrigger>
<SheetContent side="left" className={`w-20 p-3 transition-all duration-300 ${
isUnivAiPlusMode
? 'bg-gradient-to-br from-white/95 to-purple-50/95 border-purple-200/50'
: 'bg-gradient-to-br from-white/95 to-red-50/95 border-red-200/50'
}`}>
<SheetTitle className="sr-only">Navigation Menu</SheetTitle>
<SheetDescription className="sr-only">
Access chat navigation, statistics, search, profile, and settings
</SheetDescription>
<div className="flex flex-col items-center py-3">
<LeftSidebarContent />
</div>
</SheetContent>
</Sheet>
{/* Sidebar - Desktop only */}
<motion.div
initial={{ x: -20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
className="hidden md:flex w-14 flex-col items-center py-3 px-1 mr-2"
>
<LeftSidebarContent />
</motion.div>
{/* Main Container */}
<div className={`flex-1 flex flex-col rounded-2xl backdrop-blur-sm border overflow-hidden shadow-xl transition-all duration-300 ${
isUnivAiPlusMode
? 'bg-white/95 border-purple-200/50 shadow-purple-100/50 dark:bg-gray-900/95 dark:border-purple-700/50 dark:shadow-purple-900/50'
: 'bg-white/90 border-red-200/50 shadow-red-100/50 dark:bg-gray-900/90 dark:border-gray-700/50 dark:shadow-gray-900/50'
}`}>
{/* Header */}
<motion.div
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
className={`p-3 md:p-4 border-b backdrop-blur-sm transition-all duration-300 flex-shrink-0 ${
isUnivAiPlusMode
? 'border-purple-200/50 bg-gradient-to-r from-white/95 to-purple-50/95 dark:from-gray-900/95 dark:to-purple-900/95 dark:border-purple-700/50'
: 'border-red-200/50 bg-gradient-to-r from-white/95 to-red-50/95 dark:from-gray-900/95 dark:to-gray-800/95 dark:border-gray-700/50'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 md:gap-3 min-w-0 flex-1">
<div className="flex items-center gap-2 md:gap-3 min-w-0 flex-1">
<div className={`w-8 h-8 md:w-10 md:h-10 rounded-full flex items-center justify-center shadow-lg transition-all duration-300 flex-shrink-0 ${
isUnivAiPlusMode
? 'bg-gradient-to-r from-purple-400 to-pink-500 shadow-purple-500/30'
: 'bg-gradient-to-r from-yellow-400 to-orange-500 shadow-yellow-500/30'
}`}>
{isUnivAiPlusMode ? <Crown className="text-white" size={16} /> : <Bot className="text-white" size={16} />}
</div>
<div className="min-w-0 flex-1">
<div className="overflow-hidden">
<motion.h1
key={isUnivAiPlusMode ? 'plus-header' : 'regular-header'}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2 }}
className={`transition-all duration-300 truncate whitespace-nowrap ${
isUnivAiPlusMode
? 'text-purple-900 dark:text-purple-100'
: 'text-red-900 dark:text-red-100'
}`}
>
{isUnivAiPlusMode ? 'UnivAi+++' : 'UnivAi'}
{isUnivAiPlusMode && <Crown className="inline ml-1 md:ml-2" size={12} />}
</motion.h1>
</div>
<p className={`text-xs md:text-sm transition-all duration-300 truncate ${
isUnivAiPlusMode
? 'text-purple-700/80 dark:text-purple-300/80'
: 'text-red-700/80 dark:text-red-300/80'
}`}>
{isUnivAiPlusMode ? 'Premium AI Experience' : 'Always ready to help'}
</p>
</div>
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<div className={`w-5 h-5 md:w-6 md:h-6 rounded-full shadow-lg transition-all duration-300 ${
isUnivAiPlusMode
? 'bg-gradient-to-r from-purple-400 to-pink-500 shadow-purple-500/20'
: 'bg-gradient-to-r from-yellow-400 to-orange-500 shadow-yellow-500/20'
}`}></div>
</div>
</div>
</motion.div>
{/* Chat Container */}
<div className="flex-1 flex min-h-0">
{/* Messages Area */}
<div className="flex-1 flex flex-col min-h-0">
{/* Messages */}
<div className={`flex-1 overflow-y-auto p-2 md:p-4 transition-all duration-300 ${
isUnivAiPlusMode
? 'bg-gradient-to-br from-white to-purple-50/30 dark:from-gray-900 dark:to-purple-900/30'
: 'bg-gradient-to-br from-white to-red-50/30 dark:from-gray-900 dark:to-gray-800/30'
}`}>
<div className="max-w-4xl mx-auto space-y-3 md:space-y-4">
{messages.map((message) => (
<Message
key={message.id}
content={message.content}
isUser={message.isUser}
timestamp={message.timestamp}
isPlusResponse={message.isPlusResponse}
isUnivAiPlusMode={isUnivAiPlusMode}
/>
))}
{isTyping && <TypingIndicator isUnivAiPlusMode={isUnivAiPlusMode} />}
<div ref={messagesEndRef} />
</div>
</div>
{/* Feedback Section - Appears after AI responses */}
<AnimatePresence>
{showFeedback && messages.length > 1 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
className={`px-2 md:px-4 pb-2 md:pb-3 transition-all duration-300 flex-shrink-0 ${
isUnivAiPlusMode
? 'bg-gradient-to-r from-white to-purple-50/30 dark:from-gray-900 dark:to-purple-900/30'
: 'bg-gradient-to-r from-white to-red-50/30 dark:from-gray-900 dark:to-gray-800/30'
}`}
>
<div className={`max-w-4xl mx-auto rounded-xl p-3 md:p-4 border backdrop-blur-sm shadow-lg transition-all duration-300 ${
isUnivAiPlusMode
? 'bg-gradient-to-br from-white/95 to-purple-50/95 border-purple-200/50 shadow-purple-100/30 dark:from-gray-800/95 dark:to-purple-900/95 dark:border-purple-600/50'
: 'bg-gradient-to-br from-white/95 to-red-50/95 border-red-200/50 shadow-red-100/30 dark:from-gray-800/95 dark:to-gray-700/95 dark:border-gray-600/50'
}`}>
<div className="flex items-center gap-2 mb-3">
<MessageSquare className={`${
isUnivAiPlusMode
? 'text-purple-600 dark:text-purple-400'
: 'text-red-600 dark:text-red-400'
}`} size={18} />
<h4 className={`text-sm transition-all duration-300 ${
isUnivAiPlusMode
? 'text-purple-900 dark:text-purple-100'
: 'text-red-900 dark:text-red-100'
}`}>How was this response?</h4>
</div>
<div className="flex gap-2 mb-3">
<Button
variant={feedbackType === 'positive' ? 'default' : 'outline'}
size="sm"
onClick={() => setFeedbackType('positive')}
className={`flex items-center gap-1.5 transition-all duration-300 text-xs ${
feedbackType === 'positive'
? isUnivAiPlusMode
? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-purple-500/30'
: 'bg-gradient-to-r from-red-500 to-orange-500 text-white shadow-red-500/30'
: isUnivAiPlusMode
? 'border-purple-200 text-purple-700 hover:bg-purple-50 dark:border-purple-600 dark:text-purple-300'
: 'border-red-200 text-red-700 hover:bg-red-50 dark:border-red-600 dark:text-red-300'
}`}
>
<ThumbsUp size={14} />
Helpful
</Button>
<Button
variant={feedbackType === 'negative' ? 'default' : 'outline'}
size="sm"
onClick={() => setFeedbackType('negative')}
className={`flex items-center gap-1.5 transition-all duration-300 text-xs ${
feedbackType === 'negative'
? isUnivAiPlusMode
? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-purple-500/30'
: 'bg-gradient-to-r from-red-500 to-orange-500 text-white shadow-red-500/30'
: isUnivAiPlusMode
? 'border-purple-200 text-purple-700 hover:bg-purple-50 dark:border-purple-600 dark:text-purple-300'
: 'border-red-200 text-red-700 hover:bg-red-50 dark:border-red-600 dark:text-red-300'
}`}
>
<ThumbsDown size={14} />
Not helpful
</Button>
</div>
<Textarea
value={feedbackComment}
onChange={(e) => setFeedbackComment(e.target.value)}
placeholder="Additional comments (optional)..."
className={`mb-2 text-sm transition-all duration-300 ${
isUnivAiPlusMode
? 'border-purple-200 focus:border-purple-400 dark:border-purple-600'
: 'border-red-200 focus:border-red-400 dark:border-red-600'
}`}
rows={2}
/>
<div className="flex justify-end">
<Button
size="sm"
onClick={handleFeedbackSubmit}
disabled={!feedbackType}
className={`text-xs transition-all duration-300 ${
isUnivAiPlusMode
? 'bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white shadow-purple-500/30'
: 'bg-gradient-to-r from-red-500 to-orange-500 hover:from-red-600 hover:to-orange-600 text-white shadow-red-500/30'
}`}
>
Submit Feedback
</Button>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Input */}
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
className={`p-2 md:p-4 border-t backdrop-blur-sm transition-all duration-300 flex-shrink-0 ${
isUnivAiPlusMode
? 'border-purple-200/50 bg-gradient-to-r from-white/95 to-purple-50/95 dark:from-gray-900/95 dark:to-purple-900/95 dark:border-purple-700/50'
: 'border-red-200/50 bg-gradient-to-r from-white/95 to-red-50/95 dark:from-gray-900/95 dark:to-gray-800/95 dark:border-gray-700/50'
}`}
>
<div className="max-w-4xl mx-auto">
<form onSubmit={handleSendMessage} className="flex gap-2 md:gap-3">
<div className="flex-1 relative">
<Input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder={
isUnivAiPlusMode && hasUsedPlusResponse
? "Switch to UnivAi to continue..."
: "Type your message..."
}
className={`backdrop-blur-sm transition-all duration-200 shadow-sm text-sm ${
isUnivAiPlusMode
? 'bg-white/90 border-purple-200 text-purple-900 placeholder:text-purple-500/60 focus:bg-white focus:border-purple-400 dark:bg-gray-800/90 dark:border-purple-600 dark:text-purple-100 dark:placeholder:text-purple-400/60 dark:focus:bg-gray-800'
: 'bg-white/90 border-red-200 text-red-900 placeholder:text-red-500/60 focus:bg-white focus:border-red-400 dark:bg-gray-800/90 dark:border-gray-600 dark:text-red-100 dark:placeholder:text-red-400/60 dark:focus:bg-gray-800'
}`}
disabled={isInputDisabled}
/>
</div>
<Button
type="submit"
disabled={!inputValue.trim() || isInputDisabled}
className={`text-white border-0 shadow-lg transition-all duration-300 px-3 md:px-4 ${
isUnivAiPlusMode
? 'bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 shadow-purple-500/30'
: 'bg-gradient-to-r from-red-500 to-orange-500 hover:from-red-600 hover:to-orange-600 shadow-red-500/30'
}`}
>
<Send size={16} className="md:w-[18px] md:h-[18px]" />
</Button>
</form>
</div>
</motion.div>
</div>
{/* Stats Sidebar - Desktop & Tablet */}
<motion.div
initial={{ x: 20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
className={`hidden md:flex w-72 flex-shrink-0 border-l p-4 space-y-4 transition-all duration-300 overflow-y-auto flex-col ${
isUnivAiPlusMode
? 'border-purple-200/50 bg-gradient-to-br from-white/50 to-purple-50/50 dark:from-gray-900/50 dark:to-purple-900/50 dark:border-purple-700/50'
: 'border-red-200/50 bg-gradient-to-br from-white/50 to-yellow-50/50 dark:from-gray-900/50 dark:to-gray-800/50 dark:border-gray-700/50'
}`}
>
<RightSidebarContent />
</motion.div>
</div>
</div>
{/* Mobile Right Sheet Trigger */}
<Sheet open={rightSheetOpen} onOpenChange={setRightSheetOpen}>
<SheetTrigger asChild>
<motion.button
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
className={`md:hidden fixed right-0 top-1/2 -translate-y-1/2 z-30 w-8 h-16 rounded-l-lg flex items-center justify-center shadow-lg transition-all duration-200 ${
isUnivAiPlusMode
? 'bg-gradient-to-r from-purple-500 to-pink-500 shadow-purple-500/30'
: 'bg-gradient-to-r from-red-500 to-orange-500 shadow-red-500/30'
}`}
>
<ChevronLeft className="text-white" size={20} />
</motion.button>
</SheetTrigger>
<SheetContent side="right" className={`w-80 p-4 transition-all duration-300 overflow-y-auto ${
isUnivAiPlusMode
? 'bg-gradient-to-br from-white/95 to-purple-50/95 border-purple-200/50'
: 'bg-gradient-to-br from-white/95 to-red-50/95 border-red-200/50'
}`}>
<SheetTitle className="sr-only">Chat Information Panel</SheetTitle>
<SheetDescription className="sr-only">
View AI status, chat statistics, and source references
</SheetDescription>
<div className="space-y-4">
<RightSidebarContent />
</div>
</SheetContent>
</Sheet>
</div>
);
}