Spaces:
Runtime error
Runtime error
File size: 6,110 Bytes
442a1fe | 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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | import { useQueryClient } from '@tanstack/react-query';
import { useEffect, useRef } from 'react';
import { useToast } from '@/components/ui/use-toast';
import { apiClient } from '@/lib/api/client';
import { useGenerationSettings } from '@/lib/hooks/useSettings';
import { useGenerationStore } from '@/stores/generationStore';
import { usePlayerStore } from '@/stores/playerStore';
interface GenerationStatusEvent {
id: string;
status: 'loading_model' | 'generating' | 'completed' | 'failed' | 'not_found';
duration?: number;
error?: string;
source?: string;
}
// Agent-initiated generations are played by the floating pill, not the
// main-window AudioPlayer. Skip autoplay here to avoid double-playback.
const AGENT_SOURCES = new Set(['mcp', 'rest']);
/**
* Subscribes to SSE for all pending generations. When a generation completes,
* invalidates the history query, removes it from pending, and auto-plays
* if the player is idle.
*/
export function useGenerationProgress() {
const queryClient = useQueryClient();
const { toast } = useToast();
const pendingIds = useGenerationStore((s) => s.pendingGenerationIds);
const removePendingGeneration = useGenerationStore((s) => s.removePendingGeneration);
const removePendingStoryAdd = useGenerationStore((s) => s.removePendingStoryAdd);
const isPlaying = usePlayerStore((s) => s.isPlaying);
const setAudioWithAutoPlay = usePlayerStore((s) => s.setAudioWithAutoPlay);
const { settings: genSettings } = useGenerationSettings();
const autoplayOnGenerate = genSettings?.autoplay_on_generate ?? true;
// Keep refs to avoid stale closures in EventSource handlers
const isPlayingRef = useRef(isPlaying);
const autoplayRef = useRef(autoplayOnGenerate);
isPlayingRef.current = isPlaying;
autoplayRef.current = autoplayOnGenerate;
// Track active EventSource instances
const eventSourcesRef = useRef<Map<string, EventSource>>(new Map());
// Unmount-only cleanup — close all SSE connections when the hook is torn down
useEffect(() => {
const sources = eventSourcesRef.current;
return () => {
for (const source of sources.values()) {
source.close();
}
sources.clear();
};
}, []);
useEffect(() => {
const currentSources = eventSourcesRef.current;
// Close SSE connections for IDs no longer pending
for (const [id, source] of currentSources.entries()) {
if (!pendingIds.has(id)) {
source.close();
currentSources.delete(id);
}
}
// Open SSE connections for new pending IDs
for (const id of pendingIds) {
if (currentSources.has(id)) continue;
const url = apiClient.getGenerationStatusUrl(id);
const source = new EventSource(url);
source.onmessage = (event) => {
try {
const data: GenerationStatusEvent = JSON.parse(event.data);
if (data.status === 'completed') {
source.close();
currentSources.delete(id);
removePendingGeneration(id);
// Refetch history to pick up the completed generation
queryClient.refetchQueries({ queryKey: ['history'] });
// If this generation was queued for a story, add it now
const storyId = removePendingStoryAdd(id);
if (storyId) {
apiClient
.addStoryItem(storyId, { generation_id: id })
.then(() => {
queryClient.invalidateQueries({ queryKey: ['stories'] });
queryClient.invalidateQueries({ queryKey: ['stories', storyId] });
toast({
title: 'Added to story',
description: data.duration
? `Audio generated (${data.duration.toFixed(2)}s) and added to story`
: 'Audio generated and added to story',
});
})
.catch(() => {
toast({
title: 'Generation complete',
description: 'Audio generated but failed to add to story',
variant: 'destructive',
});
});
} else {
// toast({
// title: 'Generation complete!',
// description: data.duration
// ? `Audio generated (${data.duration.toFixed(2)}s)`
// : 'Audio generated',
// });
}
// Auto-play if enabled and nothing is currently playing.
// Skip agent-initiated sources — the floating pill window
// plays those itself.
const isAgentSpeak = data.source ? AGENT_SOURCES.has(data.source) : false;
if (autoplayRef.current && !isPlayingRef.current && !isAgentSpeak) {
const genAudioUrl = apiClient.getAudioUrl(id);
setAudioWithAutoPlay(genAudioUrl, id, '', '');
}
} else if (data.status === 'failed' || data.status === 'not_found') {
source.close();
currentSources.delete(id);
removePendingGeneration(id);
removePendingStoryAdd(id);
queryClient.refetchQueries({ queryKey: ['history'] });
toast({
title: data.status === 'not_found' ? 'Generation not found' : 'Generation failed',
description: data.error || 'An error occurred during generation',
variant: 'destructive',
});
}
} catch {
// Ignore parse errors from heartbeats etc
}
};
source.onerror = () => {
// SSE connection dropped — clean up and refresh history so any
// completed/failed generation still appears in the list
source.close();
currentSources.delete(id);
removePendingGeneration(id);
queryClient.refetchQueries({ queryKey: ['history'] });
};
currentSources.set(id, source);
}
}, [
pendingIds,
removePendingGeneration,
removePendingStoryAdd,
queryClient,
toast,
setAudioWithAutoPlay,
]);
}
|