Spaces:
Sleeping
Sleeping
| import React, { useState, useRef, Suspense } from "react"; | |
| import { motion } from "framer-motion"; | |
| import { Canvas } from "@react-three/fiber"; | |
| import { OrbitControls } from '@react-three/drei'; | |
| import AssistantChat from "./components/AssistantChat"; | |
| import AvatarModel from "./components/AvatarModel"; | |
| import ErrorBoundary from "./components/ErrorBoundary"; | |
| import Sidebar from "./components/Sidebar"; | |
| import Header from "./components/Header"; | |
| export default function App() { | |
| const [error, setError] = useState(null); | |
| const [sidebarOpen, setSidebarOpen] = useState(false); | |
| const [activeTab, setActiveTab] = useState("chat"); | |
| const [audioUrl, setAudioUrl] = useState(null); | |
| // Audio partagé pour Avatar + Lipsync | |
| const audioRef = useRef(new Audio()); | |
| // --- Gestion audio stable --- | |
| const handleAudioGenerated = (url) => { | |
| setAudioUrl(url); | |
| if (!audioRef.current) return; | |
| // Pause et réinitialisation de l'audio actuel | |
| audioRef.current.pause(); | |
| audioRef.current.currentTime = 0; | |
| // Mettre à jour la source | |
| audioRef.current.src = url; | |
| audioRef.current.crossOrigin = "anonymous"; | |
| const playAudio = async () => { | |
| try { | |
| await audioRef.current.play(); | |
| } catch (err) { | |
| console.error("Erreur lecture audio:", err); | |
| } finally { | |
| audioRef.current.oncanplaythrough = null; | |
| } | |
| }; | |
| // Attendre que l'audio soit chargé | |
| audioRef.current.oncanplaythrough = playAudio; | |
| audioRef.current.onerror = () => { | |
| console.error("Erreur de chargement audio"); | |
| audioRef.current.oncanplaythrough = null; | |
| }; | |
| }; | |
| if (error) { | |
| return ( | |
| <div className="h-screen flex items-center justify-center bg-gradient-to-br from-red-50 to-white p-4"> | |
| <motion.div | |
| initial={{ opacity: 0, scale: 0.9 }} | |
| animate={{ opacity: 1, scale: 1 }} | |
| className="max-w-md bg-white/90 backdrop-blur-lg rounded-2xl shadow-2xl p-8 text-center" | |
| > | |
| <h2 className="text-2xl font-bold text-red-600 mb-4">Erreur d'application</h2> | |
| <p className="mb-6 text-gray-700">{error.message}</p> | |
| <button | |
| className="px-6 py-2 rounded-lg bg-gradient-to-r from-red-500 to-red-600 text-white shadow-md hover:shadow-lg transition" | |
| onClick={() => window.location.reload()} | |
| > | |
| Recharger l'application | |
| </button> | |
| </motion.div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <ErrorBoundary onError={setError}> | |
| <div className="h-screen flex flex-col bg-gradient-to-br from-gray-50 to-white overflow-hidden"> | |
| <Header onMenuClick={() => setSidebarOpen(true)} /> | |
| <div className="flex flex-1 overflow-hidden"> | |
| <Sidebar | |
| isOpen={sidebarOpen} | |
| onClose={() => setSidebarOpen(false)} | |
| activeTab={activeTab} | |
| setActiveTab={setActiveTab} | |
| /> | |
| <div className="flex-1 flex flex-col md:flex-row overflow-hidden"> | |
| <div className="flex-1 p-4 md:p-6 flex flex-col space-y-6 overflow-hidden"> | |
| <motion.div | |
| className="flex-1 card overflow-hidden flex flex-col" | |
| initial={{ opacity: 0, y: 10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| > | |
| <h2 className="text-xl font-semibold mb-3 text-gray-800">Assistante Intelligent</h2> | |
| <div className="flex-1 min-h-0"> | |
| <AssistantChat | |
| onAudioGenerated={handleAudioGenerated} | |
| audioRef={audioRef} | |
| /> | |
| </div> | |
| </motion.div> | |
| </div> | |
| <div className="flex-1 p-4 md:p-6"> | |
| <motion.div | |
| className="w-full h-full rounded-2xl overflow-hidden shadow-2xl bg-white/50 backdrop-blur-lg" | |
| initial={{ opacity: 0, x: 20 }} | |
| animate={{ opacity: 1, x: 0 }} | |
| > | |
| <Canvas | |
| shadows | |
| camera={{ position: [0, 1.3, 2], fov: 45 }} | |
| className="w-full h-full" | |
| > | |
| <ambientLight intensity={1.7} /> | |
| <directionalLight position={[0, 5, 5]} intensity={2} castShadow /> | |
| <directionalLight position={[-5, -10, -5]} intensity={0.7} /> | |
| <Suspense fallback={null}> | |
| <AvatarModel audioRef={audioRef} audioUrl={audioUrl} /> | |
| </Suspense> | |
| <OrbitControls /> | |
| </Canvas> | |
| </motion.div> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="md:hidden fixed bottom-0 left-0 right-0 bg-white/95 backdrop-blur-lg border-t border-gray-200 z-10"> | |
| <div className="flex justify-around py-3"> | |
| <button | |
| className={`flex flex-col items-center px-4 py-2 rounded-lg transition-colors ${ | |
| activeTab === "chat" ? "text-blue-600" : "text-gray-500" | |
| }`} | |
| onClick={() => setActiveTab("chat")} | |
| > | |
| <div className="w-6 h-6 bg-blue-500/20 rounded-full mb-1" /> | |
| <span className="text-xs font-medium">Assistant</span> | |
| </button> | |
| <button | |
| className={`flex flex-col items-center px-4 py-2 rounded-lg transition-colors ${ | |
| activeTab === "avatar" ? "text-blue-600" : "text-gray-500" | |
| }`} | |
| onClick={() => setActiveTab("avatar")} | |
| > | |
| <div className="w-6 h-6 bg-indigo-500/20 rounded-full mb-1" /> | |
| <span className="text-xs font-medium">Avatar</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </ErrorBoundary> | |
| ); | |
| } |