返回 DeepSeek-Reasonix
useSessionExportCommands.ts
根目录 / desktop / frontend / src / app-runtime / useSessionExportCommands.ts
1 import { useEffect } from "react";
2 import type { SessionSelector } from "../generated/desktopContract.generated";
3 import { useCommittedCommand } from "../lib/useCommittedCommand";
4 import { applyThemeScene } from "../lib/themePack";
5 import { useOverlayStore } from "../store/overlays";
6 import type { Translator } from "../lib/i18n";
7 import type { Item, LiveStream } from "../lib/useController";
8
9 export type SessionExportFormat = "markdown" | "json" | "pdf" | "image" | "diagnostic";
10
11 /**
12 * Owns the session export commands (markdown/json/pdf/image file pickers and
13 * writers), the export popover outside-click close and the theme scene that
14 * switches between the empty home and the content task scene. Each command
15 * captures the source identity and title of the render that published
16 * it; resident items are used only for diagnostic counters; the renderer chunks stay lazy behind the file dialog.
17 */
18 export function useSessionExportCommands(input: {
19 tabId?: string;
20 selector?: SessionSelector;
21 remote: boolean;
22 sessionTitle: string;
23 items: readonly Item[];
24 live: LiveStream | undefined;
25 hasContent: boolean;
26 t: Translator;
27 showToast: (message: string, kind: "info" | "warn" | "error", options?: { durationMs?: number }) => void;
28 }) {
29 const { tabId, remote, sessionTitle, items, live, hasContent, t, showToast } = input;
30 const topicExportOpen = useOverlayStore((state) => state.topicExportOpen);
31 const setTopicExportOpen = useOverlayStore((state) => state.setTopicExportOpen);
32
33 // Theme pack scene: home when the session is empty, task once content exists.
34 useEffect(() => {
35 applyThemeScene(hasContent ? "task" : "home");
36 }, [hasContent]);
37
38 useEffect(() => {
39 if (!topicExportOpen) return;
40 const onDown = (event: MouseEvent) => {
41 const target = event.target as Element | null;
42 if (!target?.closest(".topicbar__export")) setTopicExportOpen(false);
43 };
44 document.addEventListener("mousedown", onDown);
45 return () => document.removeEventListener("mousedown", onDown);
46 }, [setTopicExportOpen, topicExportOpen]);
47
48 const run = useCommittedCommand(async (format: SessionExportFormat | "clipboard") => (await import("../lib/sessionExportOperation")).runSessionExport({
49 selector: input.selector ?? {}, tabId: tabId ?? "", format, title: sessionTitle, remote,
50 residentItems: items.length, runningStream: Boolean(live), unresolvedTools: items.filter(item => item.kind === "tool" && item.resultMissing).length,
51 }));
52 const getSessionMarkdown = useCommittedCommand(async () => {
53 try {
54 const result = await run("clipboard");
55 if (!result || result.cancelled) throw new DOMException("Export cancelled", "AbortError");
56 return result.text ?? "";
57 } catch (error) {
58 if (!(error instanceof DOMException && error.name === "AbortError")) showToast(t("topicBar.exportFailed", { error: error instanceof Error ? error.message : String(error) }), "error", { durationMs: 8000 });
59 throw error;
60 }
61 });
62 const exportSession = useCommittedCommand(async (format: SessionExportFormat) => {
63 setTopicExportOpen(false);
64 try {
65 const result = await run(format);
66 if (result && !result.cancelled) showToast(`${sessionTitle} · ${t("topicBar.exportSuccess", { count: result.files })}`, "info");
67 } catch (err) {
68 showToast(t("topicBar.exportFailed", { error: err instanceof Error ? err.message : String(err) }), "error", { durationMs: 8000 });
69 }
70 });
71
72 return { getSessionMarkdown, exportSession };
73 }
74
74 lines TYPESCRIPT