Spaces:
Build error
Build error
import React, { useState, useEffect, useRef } from 'react'; | |
import { motion } from 'framer-motion'; | |
import type { Chat, Message } from '@shared/schema'; | |
import { useToast } from '@/hooks/use-toast'; | |
import { Button } from "@/components/ui/button"; | |
import { Plus } from "lucide-react"; | |
interface VisionOSCarouselProps { | |
chats: Chat[]; | |
onSendMessage: (chatId: number, content: string) => void; | |
onUpdateSettings: (chatId: number, settings: any) => void; | |
onCreateChat: (title: string) => void; | |
isLoading: boolean; | |
} | |
const VisionOSCarousel = ({ | |
chats, | |
onSendMessage, | |
onUpdateSettings, | |
onCreateChat, | |
isLoading | |
}: VisionOSCarouselProps) => { | |
const { toast } = useToast(); | |
const [activeChat, setActiveChat] = useState(0); | |
const [showModelDropdown, setShowModelDropdown] = useState(false); | |
const [showHistoryDropdown, setShowHistoryDropdown] = useState(false); | |
const [activeModel, setActiveModel] = useState('openai/gpt-3.5-turbo'); | |
const [messageInput, setMessageInput] = useState(''); | |
const carouselRef = useRef<HTMLDivElement>(null); | |
const touchStart = useRef(0); | |
const touchEnd = useRef(0); | |
// AI models available in the system | |
const aiModels = [ | |
{ id: 1, name: 'openai/gpt-3.5-turbo', icon: '⚡', description: 'Fast & Efficient' }, | |
{ id: 2, name: 'anthropic/claude-2', icon: '🧠', description: 'Advanced Reasoning' }, | |
{ id: 3, name: 'google/palm-2-chat-bison', icon: '🌿', description: 'Creative & Engaging' } | |
]; | |
// Handle model selection | |
const selectModel = (model: { name: string }) => { | |
setActiveModel(model.name); | |
setShowModelDropdown(false); | |
if (chats[activeChat]) { | |
onUpdateSettings(chats[activeChat].id, { model: model.name }); | |
} | |
}; | |
// Handle message sending | |
const handleSendMessage = () => { | |
if (!messageInput.trim() || isLoading) return; | |
const chatId = chats[activeChat]?.id; | |
if (!chatId) { | |
toast({ | |
title: "Error", | |
description: "No active chat selected", | |
variant: "destructive" | |
}); | |
return; | |
} | |
onSendMessage(chatId, messageInput); | |
setMessageInput(''); | |
}; | |
// Navigation functions | |
const nextChat = () => { | |
if (activeChat < chats.length - 1) { | |
setActiveChat(activeChat + 1); | |
} | |
}; | |
const prevChat = () => { | |
if (activeChat > 0) { | |
setActiveChat(activeChat - 1); | |
} | |
}; | |
// Touch handlers | |
const handleTouchStart = (e: React.TouchEvent) => { | |
touchStart.current = e.touches[0].clientX; | |
}; | |
const handleTouchMove = (e: React.TouchEvent) => { | |
touchEnd.current = e.touches[0].clientX; | |
}; | |
const handleTouchEnd = () => { | |
const diff = touchStart.current - touchEnd.current; | |
if (Math.abs(diff) > 100) { | |
if (diff > 0) { | |
nextChat(); | |
} else { | |
prevChat(); | |
} | |
} | |
touchStart.current = 0; | |
touchEnd.current = 0; | |
}; | |
return ( | |
<div | |
className="flex flex-col h-screen bg-gradient-to-b from-blue-900 to-black text-white" | |
onTouchStart={handleTouchStart} | |
onTouchMove={handleTouchMove} | |
onTouchEnd={handleTouchEnd} | |
> | |
{/* Top Navigation Bar */} | |
<div className="relative z-10 flex justify-between items-center px-4 pt-2"> | |
<Button | |
variant="ghost" | |
size="icon" | |
className="rounded-full" | |
onClick={() => onCreateChat("New Chat")} | |
> | |
<Plus className="h-5 w-5" /> | |
</Button> | |
<motion.div | |
className="mx-auto px-6 py-2 rounded-full bg-black/60 backdrop-blur-md border border-blue-500/30 flex items-center justify-between w-64" | |
initial={{ y: -50 }} | |
animate={{ y: 0 }} | |
transition={{ type: "spring", stiffness: 300, damping: 25 }} | |
> | |
<button | |
onClick={() => { | |
setShowModelDropdown(!showModelDropdown); | |
setShowHistoryDropdown(false); | |
}} | |
className="flex items-center space-x-1 text-sm" | |
> | |
<span className="text-blue-400">{activeModel.split('/')[1]}</span> | |
<span className="text-xs opacity-70">▼</span> | |
</button> | |
<div className="h-4 w-px bg-blue-400/30"></div> | |
<button | |
onClick={() => { | |
setShowHistoryDropdown(!showHistoryDropdown); | |
setShowModelDropdown(false); | |
}} | |
className="flex items-center space-x-1 text-sm" | |
> | |
<span>History</span> | |
<span className="text-xs opacity-70">▼</span> | |
</button> | |
</motion.div> | |
{/* Model Selection Dropdown */} | |
{showModelDropdown && ( | |
<motion.div | |
className="absolute top-12 left-1/2 transform -translate-x-1/2 w-64 bg-black/80 backdrop-blur-xl rounded-xl border border-blue-500/20 p-2 shadow-lg shadow-blue-500/10" | |
initial={{ opacity: 0, y: -10 }} | |
animate={{ opacity: 1, y: 0 }} | |
exit={{ opacity: 0, y: -10 }} | |
> | |
{aiModels.map(model => ( | |
<div | |
key={model.id} | |
onClick={() => selectModel(model)} | |
className="flex items-center space-x-2 p-2 hover:bg-blue-900/30 rounded-lg cursor-pointer transition-colors" | |
> | |
<span className="text-lg">{model.icon}</span> | |
<div className="flex flex-col"> | |
<span className={activeModel === model.name ? "text-blue-400" : ""}>{model.name.split('/')[1]}</span> | |
<span className="text-xs opacity-50">{model.description}</span> | |
</div> | |
{activeModel === model.name && <span className="ml-auto text-blue-400 text-xs">✓</span>} | |
</div> | |
))} | |
</motion.div> | |
)} | |
{/* Chat History Dropdown */} | |
{showHistoryDropdown && ( | |
<motion.div | |
className="absolute top-12 left-1/2 transform -translate-x-1/2 w-64 bg-black/80 backdrop-blur-xl rounded-xl border border-blue-500/20 p-2 shadow-lg shadow-blue-500/10" | |
initial={{ opacity: 0, y: -10 }} | |
animate={{ opacity: 1, y: 0 }} | |
exit={{ opacity: 0, y: -10 }} | |
> | |
{chats.map((chat, index) => ( | |
<div | |
key={chat.id} | |
onClick={() => { | |
setActiveChat(index); | |
setShowHistoryDropdown(false); | |
}} | |
className="flex items-center justify-between p-2 hover:bg-blue-900/30 rounded-lg cursor-pointer transition-colors" | |
> | |
<span>{chat.title}</span> | |
<span className="text-xs opacity-50"> | |
{new Date(chat.createdAt).toLocaleDateString()} | |
</span> | |
</div> | |
))} | |
</motion.div> | |
)} | |
</div> | |
{/* Chat Cards Carousel */} | |
<div className="flex-1 flex items-center justify-center overflow-hidden"> | |
<div className="relative w-full max-w-4xl h-96"> | |
{chats.map((chat, index) => { | |
const offset = index - activeChat; | |
const isActive = index === activeChat; | |
const zIndex = chats.length - Math.abs(offset); | |
return ( | |
<motion.div | |
key={chat.id} | |
className="absolute top-0 w-72 h-80 rounded-2xl bg-gradient-to-br from-blue-900/90 to-indigo-900/90 backdrop-blur-lg border border-blue-500/20 p-4 shadow-lg shadow-blue-500/10" | |
style={{ | |
left: `calc(50% - 144px)`, | |
zIndex | |
}} | |
animate={{ | |
x: offset * 60, | |
rotateY: offset * -15, | |
scale: isActive ? 1 : 0.9 - Math.abs(offset) * 0.05, | |
opacity: 1 - Math.abs(offset) * 0.2 | |
}} | |
transition={{ type: "spring", stiffness: 300, damping: 30 }} | |
> | |
<div className="h-full flex flex-col"> | |
<div className="flex justify-between items-center mb-3"> | |
<h3 className="font-semibold text-lg text-blue-100">{chat.title}</h3> | |
{isLoading && isActive && ( | |
<motion.div | |
className="h-2 w-2 rounded-full bg-blue-400" | |
animate={{ scale: [1, 1.2, 1] }} | |
transition={{ repeat: Infinity, duration: 1 }} | |
/> | |
)} | |
</div> | |
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-blue-500/20 scrollbar-track-transparent"> | |
{/* Messages will be displayed here */} | |
</div> | |
{isActive && ( | |
<motion.div | |
className="mt-2 bg-blue-500/10 rounded-lg p-2 flex items-center" | |
initial={{ opacity: 0 }} | |
animate={{ opacity: 1 }} | |
transition={{ delay: 0.2 }} | |
> | |
<input | |
type="text" | |
value={messageInput} | |
onChange={(e) => setMessageInput(e.target.value)} | |
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()} | |
placeholder="Message Assistant..." | |
className="bg-transparent text-sm w-full outline-none placeholder-blue-300/40" | |
disabled={isLoading} | |
/> | |
<button | |
onClick={handleSendMessage} | |
disabled={isLoading || !messageInput.trim()} | |
className="ml-2 text-blue-300 disabled:opacity-50" | |
> | |
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
<path d="M22 2L11 13" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> | |
<path d="M22 2L15 22L11 13L2 9L22 2Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> | |
</svg> | |
</button> | |
</motion.div> | |
)} | |
</div> | |
</motion.div> | |
); | |
})} | |
</div> | |
</div> | |
{/* Bottom Navigation Bar */} | |
<div className="relative z-10"> | |
<motion.div | |
className="mx-auto mb-6 px-6 py-4 rounded-2xl bg-black/60 backdrop-blur-md border border-blue-500/20 flex items-center justify-between w-72" | |
initial={{ y: 50 }} | |
animate={{ y: 0 }} | |
transition={{ type: "spring", stiffness: 300, damping: 25 }} | |
> | |
<button | |
onClick={prevChat} | |
className={`w-12 h-12 flex items-center justify-center rounded-full ${ | |
activeChat > 0 ? 'bg-blue-900/50 text-blue-300' : 'bg-blue-900/20 text-blue-300/30' | |
}`} | |
disabled={activeChat === 0} | |
> | |
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
<path d="M15 18L9 12L15 6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> | |
</svg> | |
</button> | |
<div className="w-20 h-1 bg-blue-400/30 rounded-full overflow-hidden"> | |
{isLoading && ( | |
<motion.div | |
className="h-full bg-blue-400" | |
initial={{ x: "-100%" }} | |
animate={{ x: "100%" }} | |
transition={{ repeat: Infinity, duration: 1 }} | |
/> | |
)} | |
</div> | |
<button | |
onClick={nextChat} | |
className={`w-12 h-12 flex items-center justify-center rounded-full ${ | |
activeChat < chats.length - 1 ? 'bg-blue-900/50 text-blue-300' : 'bg-blue-900/20 text-blue-300/30' | |
}`} | |
disabled={activeChat === chats.length - 1} | |
> | |
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
<path d="M9 6L15 12L9 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/> | |
</svg> | |
</button> | |
</motion.div> | |
</div> | |
{/* iPhone-style home bar */} | |
<div className="fixed bottom-2 left-1/2 transform -translate-x-1/2 w-32 h-1 bg-white/30 rounded-full" /> | |
</div> | |
); | |
}; | |
export default VisionOSCarousel; |