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