Spaces:
Sleeping
Sleeping
File size: 5,764 Bytes
de63014 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
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>
);
} |