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(null); const audioWorkletNodeRef = useRef(null); const audioContextRef = useRef(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 ( <>
{isAudioReady && ( )}
); }