Avatar / frontend /src /App.jsx
DataSage12's picture
Initial commit - HOLOKIA-AVATAR v2.2
de63014
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>
);
}