File size: 4,707 Bytes
59c3ada
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { useState, useEffect, useRef, useCallback } from "react";
import { Volume2, VolumeX } from "lucide-react";

import { useLLM } from "./hooks/useLLM";
import { useTTS } from "./hooks/useTTS";
import useAudioPlayer from "./hooks/useAudioPlayer";

import LandingScreen from "./components/LandingScreen";
import ProgressScreen from "./components/ProgressScreen";
import ErrorScreen from "./components/ErrorScreen";

import WORKLET from "./play-worklet.js?raw";
import MainApplication from "./components/MainApplication";

export default function App() {
  const llm = useLLM();
  const tts = useTTS();

  const { initAudio, playPopSound, playHoverSound, toggleMusic, playMusic, isMusicPlaying, isAudioReady } =
    useAudioPlayer();
  const [appState, setAppState] = useState<"landing" | "loading" | "main" | "error">(
    navigator.gpu ? "landing" : "error",
  );
  const [error, setError] = useState<string | null>(null);
  const audioWorkletNodeRef = useRef<AudioWorkletNode | null>(null);
  const audioContextRef = useRef<AudioContext | null>(null);

  const audioGlobal = (globalThis as any).__AUDIO__ || {
    ctx: null as AudioContext | null,
    node: null as AudioWorkletNode | null,
    loaded: false as boolean,
  };
  (globalThis as any).__AUDIO__ = audioGlobal;

  const allowAutoplayRef = useRef(true);
  const handleToggleMusic = useCallback(() => {
    if (isMusicPlaying) allowAutoplayRef.current = false;
    toggleMusic();
  }, [isMusicPlaying, toggleMusic]);

  const handleLoadApp = async () => {
    setAppState("loading");
    initAudio();

    if (audioGlobal.ctx && audioGlobal.node) {
      audioContextRef.current = audioGlobal.ctx;
      audioWorkletNodeRef.current = audioGlobal.node;
      await audioContextRef.current?.resume();
    } else {
      try {
        const audioContext = new AudioContext({ sampleRate: 24000 });
        audioContextRef.current = audioContext;
        await audioContext.resume();

        if (!audioGlobal.loaded) {
          const blob = new Blob([WORKLET], { type: "application/javascript" });
          const url = URL.createObjectURL(blob);
          await audioContext.audioWorklet.addModule(url);
          URL.revokeObjectURL(url);
          audioGlobal.loaded = true;
        }

        const workletNode = new AudioWorkletNode(audioContext, "buffered-audio-worklet-processor");
        workletNode.connect(audioContext.destination);

        audioWorkletNodeRef.current = workletNode;
        audioGlobal.ctx = audioContext;
        audioGlobal.node = workletNode;
      } catch {}
    }

    await audioContextRef.current?.resume();
    llm.load();
    tts.load();
  };

  const handleLoadingComplete = useCallback(() => {
    setAppState("main");
    if (allowAutoplayRef.current && !isMusicPlaying) playMusic();
  }, [playMusic, isMusicPlaying]);

  const handleRetry = () => {
    setError(null);
    handleLoadApp();
  };

  useEffect(() => {
    if (llm.error) {
      setError(`LLM Error: ${llm.error}`);
      setAppState("error");
    } else if (tts.error) {
      setError(`TTS Error: ${tts.error}`);
      setAppState("error");
    } else if (llm.isReady && tts.isReady) {
      handleLoadingComplete();
    }
  }, [llm.isReady, tts.isReady, llm.error, tts.error, handleLoadingComplete]);

  useEffect(() => {
    if (!navigator.gpu) {
      setError("WebGPU is not supported in this browser.");
      setAppState("error");
      return;
    }
    return () => {
      audioWorkletNodeRef.current?.disconnect();
    };
  }, []);

  return (
    <>
      <div className="bg-pattern h-screen text-black relative overflow-hidden">
        {isAudioReady && (
          <button
            onClick={handleToggleMusic}
            className="absolute top-4 right-4 z-20 p-2 bg-white/50 border-2 border-black rounded-full shadow-[2px_2px_0px_#000] hover:bg-white/80 transition-colors"
          >
            {isMusicPlaying ? <Volume2 /> : <VolumeX />}
          </button>
        )}
        <LandingScreen isVisible={appState === "landing"} onLoad={handleLoadApp} playHoverSound={playHoverSound} />
        <ProgressScreen isVisible={appState === "loading"} progress={(llm.progress + tts.progress) / 2} />
        <ErrorScreen isVisible={appState === "error"} error={error} onRetry={handleRetry} />
        <MainApplication
          isVisible={appState === "main"}
          playPopSound={playPopSound}
          playHoverSound={playHoverSound}
          generate={llm.generate}
          streamTTS={tts.stream}
          isTTSReady={tts.isReady}
          audioWorkletNode={audioWorkletNodeRef.current}
          toggleMusic={handleToggleMusic}
          isMusicPlaying={isMusicPlaying}
        />
      </div>
    </>
  );
}