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>
  );
}