返回 DeepSeek-Reasonix
TopicbarSessionActions.tsx
根目录 / desktop / frontend / src / components / TopicbarSessionActions.tsx
1 import { lazy, Suspense, useEffect, useRef, useState } from "react";
2 import { Activity, Download, TerminalSquare } from "lucide-react";
3 import { CopyButton } from "./CopyButton";
4 import { Tooltip } from "./Tooltip";
5 import { t } from "../lib/i18n";
6
7 const loadExportMenu = () => import("./TopicbarExportMenu");
8 const TopicbarExportMenu = lazy(async () => ({ default: (await loadExportMenu()).TopicbarExportMenu }));
9 const SessionExportProgress = lazy(async () => ({ default: (await import("./SessionExportProgress")).SessionExportProgress }));
10
11 export interface TopicbarSessionActionsProps {
12 sessionHasContent: boolean;
13 getSessionMarkdown: () => string | Promise<string>;
14 exportSession: (format: "markdown" | "json" | "pdf" | "image" | "diagnostic") => void;
15 toggleTerminal: () => void;
16 terminalEnabled?: boolean;
17 terminalOpen: boolean;
18 prefetchTerminal?: () => void;
19 openSessionSummary: () => void;
20 tasksOpen: boolean;
21 }
22
23 const actionClass = "topicbar__action-btn topicbar__action-btn--icon topicbar__action-btn--utility";
24
25 /** Keep session actions directly accessible; only export formats need a menu. */
26 export function TopicbarSessionActions({
27 sessionHasContent, getSessionMarkdown, exportSession,
28 toggleTerminal, terminalEnabled = true, terminalOpen, prefetchTerminal,
29 openSessionSummary, tasksOpen,
30 }: TopicbarSessionActionsProps) {
31 const exportRootRef = useRef<HTMLDivElement>(null);
32 const exportTriggerRef = useRef<HTMLButtonElement>(null);
33 const [exportFocus, setExportFocus] = useState<"first" | "last" | null>(null);
34 const exportVisible = exportFocus !== null && sessionHasContent;
35
36 const closeExport = (restoreFocus: boolean) => {
37 setExportFocus(null);
38 if (restoreFocus) exportTriggerRef.current?.focus();
39 };
40
41 useEffect(() => {
42 if (!exportVisible) return;
43 const dismissOutside = (event: Event) => {
44 if (!exportRootRef.current?.contains(event.target as Node)) setExportFocus(null);
45 };
46 const onKeyDown = (event: KeyboardEvent) => {
47 if (event.key !== "Escape") return;
48 event.preventDefault();
49 setExportFocus(null);
50 exportTriggerRef.current?.focus();
51 };
52 document.addEventListener("pointerdown", dismissOutside);
53 document.addEventListener("focusin", dismissOutside);
54 document.addEventListener("keydown", onKeyDown);
55 return () => {
56 document.removeEventListener("pointerdown", dismissOutside);
57 document.removeEventListener("focusin", dismissOutside);
58 document.removeEventListener("keydown", onKeyDown);
59 };
60 }, [exportVisible]);
61
62 return (
63 <>
64 <Suspense fallback={null}><SessionExportProgress /></Suspense>
65 <Tooltip label={t("topicBar.copyAll")}>
66 <CopyButton getText={getSessionMarkdown} label={t("topicBar.copyAll")} className={actionClass} showInlineLabel={false} />
67 </Tooltip>
68 <div ref={exportRootRef} className={`topicbar__export${exportVisible ? " topicbar__export--open" : ""}`}>
69 <Tooltip label={t("topicBar.export")}>
70 <button
71 ref={exportTriggerRef} className={actionClass} type="button"
72 aria-label={t("topicBar.export")} aria-haspopup="menu" aria-expanded={exportVisible}
73 disabled={!sessionHasContent}
74 onPointerEnter={() => { void loadExportMenu(); }}
75 onFocus={() => { void loadExportMenu(); }}
76 onKeyDown={(event) => {
77 if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
78 event.preventDefault();
79 setExportFocus(event.key === "ArrowUp" ? "last" : "first");
80 }}
81 onClick={() => setExportFocus((focus) => focus ? null : "first")}
82 >
83 <Download size={14} />
84 </button>
85 </Tooltip>
86 {exportVisible && (
87 <Suspense fallback={null}>
88 <TopicbarExportMenu initialFocus={exportFocus!} exportSession={exportSession} onClose={closeExport} />
89 </Suspense>
90 )}
91 </div>
92 <Tooltip label={t("rightDock.terminal")}>
93 <button
94 className={actionClass} type="button" aria-label={t("rightDock.terminal")}
95 aria-pressed={terminalOpen} disabled={!terminalEnabled}
96 onPointerEnter={terminalEnabled ? prefetchTerminal : undefined}
97 onFocus={terminalEnabled ? prefetchTerminal : undefined}
98 onClick={toggleTerminal}
99 >
100 <TerminalSquare size={14} />
101 </button>
102 </Tooltip>
103 <Tooltip label={t("summary.session")}>
104 <button className={actionClass} type="button" aria-label={t("summary.session")} aria-expanded={tasksOpen} onClick={openSessionSummary}>
105 <Activity size={14} />
106 </button>
107 </Tooltip>
108 </>
109 );
110 }
111
111 lines Plain Text