返回 DeepSeek-Reasonix
HistoricalImportList.tsx
根目录 / desktop / frontend / src / components / HistoricalImportList.tsx
1 import { useCallback, useEffect, useRef, useState } from "react";
2 import { app } from "../lib/bridge";
3 import { asArray } from "../lib/array";
4 import { useT } from "../lib/i18n";
5 import type { HistoricalImportStatus, HistoricalSessionView, HistoricalSourceUpdateView } from "../generated/desktopContract.generated";
6 import type { SessionRef } from "../lib/sessionRef";
7 import { desktopHost } from "../lib/desktopHost";
8 import { useManagementT, type ManagementKey } from "../lib/managementLocale";
9
10 const emptyHistoricalStatus: HistoricalImportStatus = { items: [], running: false, paused: false, remaining: 0, completed: 0, blocked: 0, failed: 0 };
11 const historicalStatusKeys: Record<string, ManagementKey> = {
12 available: "historicalAvailable", queued: "historicalQueued", importing: "historicalImporting", imported: "historicalImported",
13 blocked: "historicalBlocked", failed: "historicalNeedsAttention", archived: "historicalArchived", deleted: "historicalDeleted",
14 };
15
16 export function HistoricalImportList({ active, onOpenSession }: { active: boolean; onOpenSession?: (ref: SessionRef) => Promise<void> }) {
17 const t = useT();
18 const m = useManagementT();
19 const [status, setStatus] = useState<HistoricalImportStatus>(emptyHistoricalStatus);
20 const [error, setError] = useState("");
21 const [busy, setBusy] = useState("");
22 const [query, setQuery] = useState("");
23 const [updates, setUpdates] = useState<Record<string, HistoricalSourceUpdateView>>({});
24 const generation = useRef(0);
25 const requests = useRef(0);
26 const reload = useCallback(async (scan = false) => {
27 const current = generation.current;
28 const request = ++requests.current;
29 if (desktopHost().kind === "none") { setStatus(emptyHistoricalStatus); return; }
30 try {
31 const next = await (scan ? app.ListHistoricalSessions?.() : app.GetHistoricalImportStatus?.()) ?? emptyHistoricalStatus;
32 if (current === generation.current && request === requests.current) setStatus({ ...next, items: asArray<HistoricalSessionView>(next.items) });
33 } catch (err) { if (current === generation.current) setError(String(err)); }
34 }, []);
35 useEffect(() => {
36 if (!active) return;
37 setBusy("");
38 void reload(true);
39 const timer = setInterval(() => void reload(), 2000);
40 return () => { generation.current++; clearInterval(timer); };
41 }, [active, reload]);
42 const open = async (item: HistoricalSessionView) => {
43 if (busy) return;
44 const current = generation.current;
45 setBusy(item.id); setError("");
46 try {
47 const ref = item.session ?? (await app.ImportHistoricalSession!(item.id)).session;
48 if (current === generation.current && onOpenSession) await onOpenSession(ref);
49 } catch (err) { if (current === generation.current) setError(String(err)); }
50 finally { if (current === generation.current) setBusy(""); await reload(); }
51 };
52 const control = async (action: string) => {
53 const current = generation.current;
54 ++requests.current;
55 try {
56 setError("");
57 const next = action === "start" ? await app.StartHistoricalImport!([]) : await app.ControlHistoricalImport!(action);
58 if (current === generation.current) setStatus(next);
59 } catch (err) { if (current === generation.current) setError(String(err)); }
60 };
61 const checkUpdate = async (item: HistoricalSessionView) => {
62 if (!item.source) return;
63 setBusy(item.id); setError("");
64 try {
65 let update = await app.CheckHistoricalSourceUpdate!({ source: item.source });
66 while (update.status === "checking") {
67 await new Promise(resolve => setTimeout(resolve, 300));
68 update = await app.CheckHistoricalSourceUpdate!({ source: item.source });
69 }
70 setUpdates(current => ({ ...current, [item.id]: update }));
71 } catch (err) { setError(String(err)); }
72 finally { setBusy(""); }
73 };
74 const prepareUpdate = async (item: HistoricalSessionView, update: HistoricalSourceUpdateView) => {
75 if (!item.source || !update.version) return;
76 setBusy(item.id); setError("");
77 try { await app.PrepareHistoricalSourceVersion!(item.source, update.version); await reload(); }
78 catch (err) { setError(String(err)); }
79 finally { setBusy(""); }
80 };
81 return <section className="archived-sessions" aria-label={m("historicalTitle")}>
82 <h3>{m("historicalTitle")}</h3>
83 <p>{m("historicalImportDescription")}</p>
84 <input aria-label={t("history.searchPlaceholder")} placeholder={t("history.searchPlaceholder")} value={query} onChange={event => setQuery(event.target.value)} />
85 <div>
86 <button className="btn btn--small" disabled={status.running || !!busy} onClick={() => void control("start")}>{m("historicalImportAll")}</button>
87 {(status.running || (status.paused && status.remaining > 0)) && <>
88 <button className="btn btn--small" onClick={() => void control(status.paused ? "resume" : "pause")}>{m(status.paused ? "historicalImportResume" : "historicalImportPause")}</button>
89 <button className="btn btn--small" onClick={() => void control("cancel")}>{m("historicalImportCancel")}</button>
90 <span role="status">{m("historicalRemaining")} {status.remaining} · {m("historicalImported")} {status.completed} · {m("historicalBlocked")} {status.blocked} · {m("historicalNeedsAttention")} {status.failed}</span>
91 </>}
92 <button className="btn btn--small" onClick={() => void reload(true)}>{t("common.retry")}</button>
93 </div>
94 {error && <p role="alert">{error}</p>}
95 {status.items.filter(item => item.title.toLowerCase().includes(query.toLowerCase())).map(item =>
96 <div className="archived-sessions__row" key={item.id}>
97 <span>{item.title}</span><small>{item.format}</small>
98 <span>{m(historicalStatusKeys[item.status] ?? "historicalNeedsAttention")}</span>
99 {item.errorCode && <small>{m(item.errorCode === "source_busy" ? "historicalSourceBusy" : "historicalImportFailed")}</small>}
100 <button className="btn btn--small" disabled={!!busy || item.status === "importing" || item.status === "queued" || item.status === "archived" || item.status === "deleted"}
101 onClick={() => void open(item)}>{item.session && onOpenSession ? t("history.openRestored") : m("historicalImportOpen")}</button>
102 {item.session && item.source && <button className="btn btn--small" disabled={!!busy} onClick={() => void checkUpdate(item)}>{t("common.retry")} · {m("historicalTitle")}</button>}
103 {updates[item.id]?.status === "available" && <button className="btn btn--small" disabled={!!busy} onClick={() => void prepareUpdate(item, updates[item.id])}>{m("historicalImportOpen")} · {m("branch")}</button>}
104 {updates[item.id]?.status === "unchanged" && <small>{m("historicalImported")}</small>}
105 </div>)}
106 {status.items.length === 0 && <p>{m("noHistoricalSessions")}</p>}
107 </section>;
108 }
109
109 lines Plain Text