Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 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 | 'use client' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { api } from '@/lib/queryClient' import { collectedClipKeys } from '@/lib/queryKeys' // Re-export keys for consumers export { collectedClipKeys } from '@/lib/queryKeys' // ============================================================================ // Types // ============================================================================ export interface CollectedClipEntry { id: string tone: string say: Record<string, string> | null playCount: number firstSeenAt: string lastSeenAt: string } interface CollectedClipsResponse { clips: CollectedClipEntry[] generatedFor?: Record<string, boolean> deactivatedFor?: Record<string, boolean> } // ============================================================================ // API Functions // ============================================================================ async function fetchCollectedClips(voice?: string): Promise<CollectedClipsResponse> { const path = voice ? `audio/collected-clips?voice=${encodeURIComponent(voice)}` : 'audio/collected-clips' const res = await api(path) if (!res.ok) throw new Error('Failed to fetch collected clips') return res.json() } async function generateCollectedClips(params: { voice: string clipIds: string[] }): Promise<{ taskId: string }> { console.log('[useCollectedClips] generateCollectedClips called', params) const res = await api('admin/audio/generate-collected', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(params), }) if (!res.ok) { const errData = await res.json().catch(() => ({})) console.error('[useCollectedClips] generateCollectedClips failed', res.status, errData) throw new Error(errData.error || 'Generation failed') } const result = await res.json() console.log('[useCollectedClips] generateCollectedClips success', result) return result } // ============================================================================ // Query Hooks // ============================================================================ /** * Fetch collected clips with optional per-voice generation status. * * When `voice` is provided, the response includes `generatedFor` — * a map of clipId → boolean indicating whether an mp3 exists for that voice. */ export function useCollectedClips(voice?: string, options?: { enabled?: boolean }) { return useQuery({ queryKey: collectedClipKeys.list(voice), queryFn: () => fetchCollectedClips(voice), enabled: options?.enabled ?? true, }) } // ============================================================================ // Mutation Hooks // ============================================================================ /** * Trigger background generation of OpenAI TTS mp3s for collected clips. * * On success, invalidates the collected clips query so the status dots * refresh once the background task finishes. */ export function useGenerateCollectedClips(voice?: string) { const queryClient = useQueryClient() return useMutation({ mutationFn: generateCollectedClips, onSuccess: () => { // The task runs in the background; we invalidate so that when // the caller re-fetches (e.g. after task completion) the cache is stale. queryClient.invalidateQueries({ queryKey: collectedClipKeys.list(voice), }) }, }) } |