Spaces:
Paused
Paused
File size: 2,829 Bytes
0b9dc2e | 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 | 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<StreamingAudioManager | null>(null);
interface ReplayController {
play: (el: HTMLAudioElement) => void;
stop: () => void;
}
const ReplayContext = createContext<ReplayController | null>(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<HTMLAudioElement | null>(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 (
<AudioContext.Provider value={manager}>
<ReplayContext.Provider value={replay}>{children}</ReplayContext.Provider>
</AudioContext.Provider>
);
}
/**
* 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,
);
}
|