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 };
};