import { useCallback, useEffect, useState } from "react"; import { useApi } from "@/contexts/ApiContext"; import { EpisodeDetail, EpisodeListResponse, getEpisode, listEpisodes, } from "@/lib/datasetApi"; /** Episode listing for one local dataset. `repoId` null → idle. */ export const useEpisodes = (repoId: string | null) => { const { baseUrl, fetchWithHeaders } = useApi(); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [version, setVersion] = useState(0); useEffect(() => { setData(null); setError(null); if (!repoId) { setLoading(false); return; } const controller = new AbortController(); setLoading(true); listEpisodes(baseUrl, fetchWithHeaders, repoId, controller.signal) .then((r) => { if (controller.signal.aborted) return; // The backend reports a readable dataset it couldn't parse as // success:false rather than an HTTP error, so check the flag too. if (!r.success) { setData(null); setError(r.message ?? "Could not read dataset"); return; } setData(r); }) .catch((e) => { if (controller.signal.aborted) return; setData(null); setError(e instanceof Error ? e.message : "Could not read dataset"); }) .finally(() => { if (!controller.signal.aborted) setLoading(false); }); return () => controller.abort(); }, [baseUrl, fetchWithHeaders, repoId, version]); const refresh = useCallback(() => setVersion((v) => v + 1), []); /** Show a task edit right away, before the backend has written it. */ const applyTask = useCallback( (task: string, episodeIndex: number | null) => setData((d) => d && { ...d, episodes: d.episodes.map((ep) => episodeIndex === null || ep.episode_index === episodeIndex ? { ...ep, tasks: [task] } : ep, ), }, ), [], ); // Effects run after render: do not let the new dataset auto-select an // episode from the old list before this effect clears it. return { data: data?.repo_id === repoId ? data : null, loading, error, refresh, applyTask }; }; /** One episode's detail. Either arg null → idle. */ export const useEpisodeDetail = (repoId: string | null, episodeIndex: number | null) => { const { baseUrl, fetchWithHeaders } = useApi(); const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (!repoId || episodeIndex === null) { setDetail(null); return; } const controller = new AbortController(); setLoading(true); setError(null); getEpisode(baseUrl, fetchWithHeaders, repoId, episodeIndex, controller.signal) .then((r) => { if (controller.signal.aborted) return; if (!r.success) { setDetail(null); setError(r.message ?? "Could not load episode"); return; } setDetail(r); }) .catch((e) => { if (controller.signal.aborted) return; setDetail(null); setError(e instanceof Error ? e.message : "Could not load episode"); }) .finally(() => { if (!controller.signal.aborted) setLoading(false); }); // Clicking through episodes quickly would otherwise let a slow earlier // response land after a faster later one. return () => controller.abort(); }, [baseUrl, fetchWithHeaders, repoId, episodeIndex]); return { detail, loading, error }; };