File size: 3,739 Bytes
84ecff3 | 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 105 106 107 108 109 110 111 | 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<EpisodeListResponse | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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<EpisodeDetail | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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 };
};
|