Files
postgenerator/frontend/src/api/hooks.ts
Michele f449d945e9 feat(03-02): picker Swipe File nel form Genera Calendario + mark-used
- CalendarRequest in types.ts: aggiunto topic_overrides?: Record<number, string>
- hooks.ts: aggiunto useMarkSwipeUsed hook (POST /swipe/{id}/mark-used)
- GenerateCalendar.tsx: sezione Topic Override con griglia 13 slot
  - Bottone "Da Swipe File" per aprire picker inline per ogni slot
  - Picker mostra lista idee con nicchia badge e badge Usato
  - Selezione assegna topic allo slot e chiama mark-used
  - Bottone X per rimuovere override da uno slot
  - Override inclusi in CalendarRequest.topic_overrides al submit
  - Riepilogo counter override selezionati
2026-03-09 00:33:00 +01:00

298 lines
9.5 KiB
TypeScript

/**
* 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<Settings>({
queryKey: ['settings'],
queryFn: () => apiGet<Settings>('/settings'),
staleTime: 60_000,
})
}
/** Verifica se API key è configurata e quale modello è attivo. */
export function useSettingsStatus() {
return useQuery<SettingsStatus>({
queryKey: ['settings', 'status'],
queryFn: () => apiGet<SettingsStatus>('/settings/status'),
staleTime: 30_000,
refetchOnWindowFocus: true,
})
}
/** Aggiorna le impostazioni via PUT /api/settings. */
export function useUpdateSettings() {
const queryClient = useQueryClient()
return useMutation<Settings, Error, Partial<Settings>>({
mutationFn: (settings) => apiPut<Settings>('/settings', settings),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['settings'] })
},
})
}
// ---------------------------------------------------------------------------
// Calendario
// ---------------------------------------------------------------------------
/** Recupera i formati narrativi disponibili (7 formati). */
export function useFormats() {
return useQuery<FormatsResponse>({
queryKey: ['calendar', 'formats'],
queryFn: () => apiGet<FormatsResponse>('/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<JobStatus>({
queryKey: ['jobs', jobId, 'status'],
queryFn: () => apiGet<JobStatus>(`/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<GenerateResponse>({
queryKey: ['jobs', jobId, 'results'],
queryFn: () => apiGet<GenerateResponse>(`/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<PostResult, Error, GenerateRequest>({
mutationFn: (request) => apiPost<PostResult>('/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<CalendarResponse, Error, CalendarRequest>({
mutationFn: (request) =>
apiPost<CalendarResponse>('/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<void, Error, string>({
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<PromptListResponse>({
queryKey: ['prompts'],
queryFn: () => apiGet<PromptListResponse>('/prompts'),
staleTime: 30_000,
})
}
/** Carica contenuto + variabili di un singolo prompt. */
export function usePrompt(name: string | null) {
return useQuery<PromptDetail>({
queryKey: ['prompts', name],
queryFn: () => apiGet<PromptDetail>(`/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<PromptDetail, Error, { name: string; content: string }>({
mutationFn: ({ name, content }) =>
apiPut<PromptDetail>(`/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<PromptDetail, Error, string>({
mutationFn: (name) => apiPost<PromptDetail>(`/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<SwipeListResponse>({
queryKey: ['swipe'],
queryFn: () => apiGet<SwipeListResponse>('/swipe/'),
staleTime: 30_000,
})
}
/** Aggiunge una nuova idea allo Swipe File. */
export function useAddSwipeItem() {
const queryClient = useQueryClient()
return useMutation<SwipeItem, Error, SwipeItemCreate>({
mutationFn: (item) => apiPost<SwipeItem>('/swipe/', item),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['swipe'] })
},
})
}
/** Aggiorna una voce esistente dello Swipe File. */
export function useUpdateSwipeItem() {
const queryClient = useQueryClient()
return useMutation<SwipeItem, Error, { id: string; data: SwipeItemUpdate }>({
mutationFn: ({ id, data }) => apiPut<SwipeItem>(`/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<SwipeItem, Error, string>({
mutationFn: (id) => apiPost<SwipeItem>(`/swipe/${id}/mark-used`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['swipe'] })
},
})
}