返回 DeepSeek-Reasonix
ClearContextCard.tsx
根目录 / desktop / frontend / src / components / ClearContextCard.tsx
1 import { useEffect, useId, useRef, useState } from "react";
2 import { useT } from "../lib/i18n";
3 import {
4 DecisionConfirmBar,
5 PromptAction,
6 PromptBadge,
7 PromptDescriptionDisclosure,
8 PromptShelf,
9 } from "./PromptShelf";
10
11 export function ClearContextCard({
12 onCancel,
13 onConfirm,
14 }: {
15 onCancel: () => void;
16 onConfirm: () => void;
17 }) {
18 const t = useT();
19 const shelfRef = useRef<HTMLDivElement | null>(null);
20 // Default safe choice: cancel.
21 const [selectedIndex, setSelectedIndex] = useState(0);
22 const [expandedDescriptionId, setExpandedDescriptionId] = useState<string | null>(null);
23 const [descriptionTruncated, setDescriptionTruncated] = useState(false);
24 const [submitting, setSubmitting] = useState(false);
25 const instanceId = useId();
26 const selectedIndexRef = useRef(0);
27 selectedIndexRef.current = selectedIndex;
28 const submittingRef = useRef(false);
29 submittingRef.current = submitting;
30 const onCancelRef = useRef(onCancel);
31 onCancelRef.current = onCancel;
32 const onConfirmRef = useRef(onConfirm);
33 onConfirmRef.current = onConfirm;
34
35 const actions = [
36 {
37 key: "1",
38 label: t("common.cancel"),
39 desc: t("clearContext.cancelDesc"),
40 tone: "default" as const,
41 },
42 {
43 key: "2",
44 label: t("clearContext.clear"),
45 desc: t("clearContext.clearDesc"),
46 tone: "danger" as const,
47 },
48 ];
49 const selectedDescriptionId = `${instanceId}-description-${selectedIndex}`;
50 const descriptionExpanded = expandedDescriptionId === selectedDescriptionId;
51
52 useEffect(() => {
53 shelfRef.current?.focus();
54 setSelectedIndex(0);
55 setSubmitting(false);
56 }, []);
57
58 const runSelected = () => {
59 if (submittingRef.current) return;
60 submittingRef.current = true;
61 setSubmitting(true);
62 if (selectedIndexRef.current === 1) onConfirmRef.current();
63 else onCancelRef.current();
64 };
65
66 useEffect(() => {
67 const onKeyDown = (event: globalThis.KeyboardEvent) => {
68 if (submittingRef.current) return;
69 const target = event.target instanceof Element ? event.target : null;
70 const tag = target?.tagName.toLowerCase();
71 if (tag === "input" || tag === "textarea" || (target instanceof HTMLElement && target.isContentEditable)) return;
72
73 if (event.key === "Escape") {
74 event.preventDefault();
75 submittingRef.current = true;
76 setSubmitting(true);
77 onCancelRef.current();
78 return;
79 }
80 if (event.key === "ArrowUp" || event.key === "ArrowDown") {
81 event.preventDefault();
82 setSelectedIndex((i) => (i === 0 ? 1 : 0));
83 return;
84 }
85 if (event.key === "Enter") {
86 event.preventDefault();
87 runSelected();
88 return;
89 }
90 if (event.key === "1") {
91 event.preventDefault();
92 setSelectedIndex(0);
93 return;
94 }
95 if (event.key === "2") {
96 event.preventDefault();
97 setSelectedIndex(1);
98 }
99 };
100 document.addEventListener("keydown", onKeyDown);
101 return () => document.removeEventListener("keydown", onKeyDown);
102 }, []);
103
104 return (
105 <PromptShelf
106 decision
107 className="prompt-shelf--clear-context"
108 barRef={shelfRef}
109 titleId="clear-context-shelf-title"
110 title={t("clearContext.title")}
111 badges={<PromptBadge tone="amber">{t("clearContext.badge")}</PromptBadge>}
112 meta={t("clearContext.prompt")}
113 actions={
114 <>
115 {actions.map((action, index) => (
116 <PromptAction
117 key={action.key}
118 keyLabel={action.key}
119 label={action.label}
120 description={action.desc}
121 descriptionId={`${instanceId}-description-${index}`}
122 descriptionDisclosure
123 onDescriptionOverflowChange={selectedIndex === index ? setDescriptionTruncated : undefined}
124 onClick={() => {
125 if (submitting) return;
126 setSelectedIndex(index);
127 }}
128 selected={selectedIndex === index}
129 tone={action.tone}
130 disabled={submitting}
131 />
132 ))}
133 </>
134 }
135 note={
136 descriptionTruncated ? (
137 <PromptDescriptionDisclosure
138 descriptionId={`${selectedDescriptionId}-detail`}
139 label={actions[selectedIndex]?.label}
140 description={actions[selectedIndex]?.desc ?? ""}
141 expanded={descriptionExpanded}
142 onToggle={() => setExpandedDescriptionId((current) => current === selectedDescriptionId ? null : selectedDescriptionId)}
143 disabled={submitting}
144 alwaysVisible={actions[selectedIndex]?.tone === "danger"}
145 />
146 ) : undefined
147 }
148 footer={
149 <DecisionConfirmBar
150 hint={t("decision.selectHint")}
151 confirmLabel={t("decision.confirm")}
152 onConfirm={runSelected}
153 disabled={submitting}
154 danger={selectedIndex === 1}
155 />
156 }
157 />
158 );
159 }
160
160 lines Plain Text