- 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
298 lines
9.5 KiB
TypeScript
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'] })
|
|
},
|
|
})
|
|
}
|