/** * TanStack Query hooks per tutte le API calls del PostGenerator. * * Pattern generale: * - useQuery per lettura dati (GET) * - useMutation per scrittura dati (POST/PUT) * - Polling condizionale: refetchInterval attivo solo quando job in running * * Tutti gli endpoint usano API_BASE='/postgenerator/api' via apiGet/apiPost/apiPut/apiDownload. */ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { apiFetch, apiDownload, apiGet, apiPost, apiPut, triggerDownload } from './client' import type { CalendarRequest, CalendarResponse, FormatsResponse, GenerateRequest, GenerateResponse, JobStatus, PostResult, PromptDetail, PromptListResponse, Settings, SettingsStatus, SwipeItem, SwipeItemCreate, SwipeItemUpdate, SwipeListResponse, } from '../types' // --------------------------------------------------------------------------- // Settings // --------------------------------------------------------------------------- /** Carica le impostazioni correnti. api_key è mascherata (ultimi 4 char). */ export function useSettings() { return useQuery({ queryKey: ['settings'], queryFn: () => apiGet('/settings'), staleTime: 60_000, }) } /** Verifica se API key è configurata e quale modello è attivo. */ export function useSettingsStatus() { return useQuery({ queryKey: ['settings', 'status'], queryFn: () => apiGet('/settings/status'), staleTime: 30_000, refetchOnWindowFocus: true, }) } /** Aggiorna le impostazioni via PUT /api/settings. */ export function useUpdateSettings() { const queryClient = useQueryClient() return useMutation>({ mutationFn: (settings) => apiPut('/settings', settings), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['settings'] }) }, }) } // --------------------------------------------------------------------------- // Calendario // --------------------------------------------------------------------------- /** Recupera i formati narrativi disponibili (7 formati). */ export function useFormats() { return useQuery({ queryKey: ['calendar', 'formats'], queryFn: () => apiGet('/calendar/formats'), staleTime: Infinity, // i formati non cambiano mai }) } // --------------------------------------------------------------------------- // Generazione bulk (async con job_id) // --------------------------------------------------------------------------- /** * Avvia generazione batch di 13 post. * Risponde 202 con {job_id, message} — la generazione gira in background. * Il frontend deve fare polling su useJobStatus(jobId). */ export function useGenerateCalendar() { return useMutation<{ job_id: string; message: string }, Error, CalendarRequest>({ mutationFn: (request) => apiPost<{ job_id: string; message: string }>('/generate/bulk', request), }) } /** * Polling stato job. refetchInterval attivo solo quando status='running'. * Smette di pollare automaticamente quando status diventa 'completed' o 'failed'. */ export function useJobStatus(jobId: string | null) { return useQuery({ queryKey: ['jobs', jobId, 'status'], queryFn: () => apiGet(`/generate/job/${jobId}/status`), enabled: !!jobId, refetchInterval: (query) => { const status = query.state.data?.status // Polla ogni 2s solo quando il job è in esecuzione if (status === 'running') return 2000 return false }, staleTime: 0, }) } /** * Risultati completi di un job completato. * Usato in OutputReview dopo che il job ha status='completed'. */ export function useJobResults(jobId: string | null) { return useQuery({ queryKey: ['jobs', jobId, 'results'], queryFn: () => apiGet(`/generate/job/${jobId}`), enabled: !!jobId, staleTime: 60_000, }) } // --------------------------------------------------------------------------- // Generazione singolo post // --------------------------------------------------------------------------- /** * Genera un singolo post (sync — ritorna PostResult direttamente). * Usato per rigenerare post falliti o in GenerateSingle. */ export function useGenerateSingle() { return useMutation({ mutationFn: (request) => apiPost('/generate/single', request), }) } // --------------------------------------------------------------------------- // Calendar generation (sync, per CalendarRequest -> CalendarResponse) // --------------------------------------------------------------------------- /** * Genera calendario editoriale (sync). * Diverso da useGenerateCalendar: questo chiama POST /api/calendar/generate * che ritorna i slot del calendario senza generare il contenuto LLM. */ export function useGenerateCalendarPlan() { return useMutation({ mutationFn: (request) => apiPost('/calendar/generate', request), }) } // --------------------------------------------------------------------------- // Export CSV // --------------------------------------------------------------------------- /** * Scarica il CSV originale del job (GET /api/export/{jobId}/csv). * Triggera download browser automaticamente. */ export function useDownloadCsv() { return useMutation({ mutationFn: async (jobId: string) => { const blob = await apiDownload(`/export/${jobId}/csv`, 'GET') triggerDownload(blob, `postgenerator-${jobId}.csv`) }, }) } /** * Scarica il CSV con le modifiche inline (POST /api/export/{jobId}/csv). * Invia i risultati modificati e triggera download browser. */ export function useDownloadEditedCsv() { return useMutation< void, Error, { jobId: string; results: PostResult[]; campagna: string } >({ mutationFn: async ({ jobId, results, campagna }) => { const blob = await apiDownload(`/export/${jobId}/csv`, 'POST', { results, campagna, }) triggerDownload(blob, `postgenerator-${jobId}-edited.csv`) }, }) } // --------------------------------------------------------------------------- // Prompt Editor // --------------------------------------------------------------------------- /** Lista tutti i prompt disponibili con flag modificato/default. */ export function usePromptList() { return useQuery({ queryKey: ['prompts'], queryFn: () => apiGet('/prompts'), staleTime: 30_000, }) } /** Carica contenuto + variabili di un singolo prompt. */ export function usePrompt(name: string | null) { return useQuery({ queryKey: ['prompts', name], queryFn: () => apiGet(`/prompts/${name}`), enabled: !!name, staleTime: 0, // Sempre fresco dopo edit }) } /** Salva il contenuto modificato di un prompt. */ export function useSavePrompt() { const queryClient = useQueryClient() return useMutation({ mutationFn: ({ name, content }) => apiPut(`/prompts/${name}`, { content }), onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ['prompts'] }) queryClient.setQueryData(['prompts', data.name], data) }, }) } /** Reset un prompt al default originale. */ export function useResetPrompt() { const queryClient = useQueryClient() return useMutation({ mutationFn: (name) => apiPost(`/prompts/${name}/reset`), onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ['prompts'] }) queryClient.setQueryData(['prompts', data.name], data) }, }) } // --------------------------------------------------------------------------- // Swipe File // --------------------------------------------------------------------------- /** Lista tutte le idee salvate nello Swipe File (ordine: piu' recenti prima). */ export function useSwipeItems() { return useQuery({ queryKey: ['swipe'], queryFn: () => apiGet('/swipe/'), staleTime: 30_000, }) } /** Aggiunge una nuova idea allo Swipe File. */ export function useAddSwipeItem() { const queryClient = useQueryClient() return useMutation({ mutationFn: (item) => apiPost('/swipe/', item), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['swipe'] }) }, }) } /** Aggiorna una voce esistente dello Swipe File. */ export function useUpdateSwipeItem() { const queryClient = useQueryClient() return useMutation({ mutationFn: ({ id, data }) => apiPut(`/swipe/${id}`, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['swipe'] }) }, }) } /** Elimina una voce dallo Swipe File. */ export function useDeleteSwipeItem() { const queryClient = useQueryClient() return useMutation<{ deleted: boolean }, Error, string>({ mutationFn: (id) => apiFetch<{ deleted: boolean }>(`/swipe/${id}`, { method: 'DELETE' }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['swipe'] }) }, }) } /** Marca un'idea dello Swipe File come "usata". */ export function useMarkSwipeUsed() { const queryClient = useQueryClient() return useMutation({ mutationFn: (id) => apiPost(`/swipe/${id}/mark-used`), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['swipe'] }) }, }) }