aether-rides / VisionOSCarousel.tsx
lattmamb's picture
Upload 134 files (#2)
ad160e7 verified
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;