import { createContext, useContext, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'; import type { ReactNode } from 'react'; import { StreamingAudioManager } from '@/utils/streamingAudio'; import type { StreamingAudioState } from '@/utils/streamingAudio'; const AudioContext = createContext(null); interface ReplayController { play: (el: HTMLAudioElement) => void; stop: () => void; } const ReplayContext = createContext(null); /** * Provides a {@link StreamingAudioManager} to the component tree. The manager * collects DATA_BLOCK_* events for audio blocks and exposes per-block state * that ``MessageBubble`` consumes via {@link useAudioBlock}. * * Mount this once around any subtree that renders assistant messages. */ export function AudioProvider({ children }: { children: ReactNode }) { const manager = useMemo(() => new StreamingAudioManager(), []); useEffect(() => () => manager.disposeAll(), [manager]); const currentRef = useRef(null); const replay: ReplayController = useMemo( () => ({ play(el: HTMLAudioElement) { manager.stopLivePlayback(); if (currentRef.current && currentRef.current !== el) { currentRef.current.pause(); currentRef.current.currentTime = 0; } currentRef.current = el; }, stop() { currentRef.current = null; }, }), [manager], ); return ( {children} ); } /** * Access the streaming audio manager. Returns ``null`` outside of an * ``AudioProvider`` — callers must handle that, since audio handling is * an optional feature. */ export function useAudioManager(): StreamingAudioManager | null { return useContext(AudioContext); } /** * Access the replay controller that ensures only one audio element plays * at a time across all message bubbles. */ export function useReplayController(): ReplayController | null { return useContext(ReplayContext); } /** * Subscribe to the streaming state for a single audio DataBlock. * Re-renders when the block transitions from ``streaming`` to ``ready``. * * Returns ``null`` if the block isn't being tracked (e.g. a historical * message loaded from the server, where the bytes are already complete). */ export function useAudioBlock(blockId: string | undefined): StreamingAudioState | null { const manager = useAudioManager(); return useSyncExternalStore( (fn) => { if (!manager || !blockId) return () => undefined; return manager.subscribe(blockId, fn); }, () => (manager && blockId ? manager.getState(blockId) : null), () => null, ); }