|
|
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);
|
|
|
|
|
|
|
|
|
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([]);
|
|
|
setShowFeedback(false);
|
|
|
setFeedbackType(null);
|
|
|
setFeedbackComment('');
|
|
|
}, [isUnivAiPlusMode]);
|
|
|
|
|
|
const scrollToBottom = () => {
|
|
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
|
};
|
|
|
|
|
|
useEffect(() => {
|
|
|
scrollToBottom();
|
|
|
}, [messages, isTyping]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
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;
|
|
|
|
|
|
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();
|
|
|
|
|
|
const botResponse: ChatMessage = {
|
|
|
id: (Date.now() + 1).toString(),
|
|
|
content: data.response,
|
|
|
isUser: false,
|
|
|
timestamp: new Date(),
|
|
|
isPlusResponse: isUnivAiPlusMode,
|
|
|
sources: [],
|
|
|
};
|
|
|
|
|
|
setMessages(prev => [...prev, botResponse]);
|
|
|
setCurrentSources([]);
|
|
|
setIsTyping(false);
|
|
|
setLastBotMessageId(botResponse.id);
|
|
|
|
|
|
|
|
|
setShowFeedback(true);
|
|
|
setFeedbackType(null);
|
|
|
setFeedbackComment('');
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
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>
|
|
|
);
|
|
|
}
|
|
|
|