Spaces:
Sleeping
Sleeping
| 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> | |
| ); | |
| } | |