返回 DeepSeek-Reasonix
ComposerGuidanceShelf.tsx
根目录 / desktop / frontend / src / components / ComposerGuidanceShelf.tsx
1 import { useState } from "react";
2 import { Check, ChevronDown, ChevronUp, CornerDownRight, Pencil, Trash2, X } from "lucide-react";
3 import { guidanceHasKnownPendingState, guidanceIsEditable, guidanceIsInFlight, guidanceNeedsRetry } from "../lib/composerGuidance";
4 import { useI18n } from "../lib/i18n";
5 import type { StructuredInvocationSubmit } from "../lib/invocationDisplay";
6 import { InboxRecoveryBanner } from "./InboxRecoveryBanner";
7 import { Tooltip } from "./Tooltip";
8
9 export type PendingGuidance = {
10 id: string;
11 text: string;
12 submitText: string;
13 state?: string;
14 intent?: string;
15 source?: string;
16 blockReason?: string;
17 paused?: boolean;
18 recoveredCount?: number;
19 structured?: StructuredInvocationSubmit;
20 };
21
22 export type InboxRecoveryNotice = {
23 draftKey: string;
24 tabId: string;
25 count: number;
26 recovered: boolean;
27 };
28
29 export function ComposerGuidanceShelf({
30 recovery,
31 recoveryDisabled,
32 items,
33 expanded,
34 running,
35 disabled,
36 readOnly,
37 sendingId,
38 onReview,
39 onRecoveryResumed,
40 onRecoveryError,
41 onToggleExpanded,
42 onSend,
43 onDismiss,
44 onEdit,
45 }: {
46 recovery: InboxRecoveryNotice | null;
47 recoveryDisabled: boolean;
48 items: PendingGuidance[];
49 expanded: boolean;
50 running: boolean;
51 disabled: boolean;
52 readOnly: boolean;
53 sendingId: string | null;
54 onReview: () => void;
55 onRecoveryResumed: () => void;
56 onRecoveryError: (error: unknown) => void;
57 onToggleExpanded: () => void;
58 onSend: (item: PendingGuidance) => void;
59 onDismiss: (item: PendingGuidance) => void;
60 onEdit?: (item: PendingGuidance, text: string) => void | Promise<void>;
61 }) {
62 const { t } = useI18n();
63 const visible = expanded ? items : items.slice(0, 2);
64 const hiddenCount = Math.max(0, items.length - 2);
65 const [editingId, setEditingId] = useState<string | null>(null);
66 const [editDraft, setEditDraft] = useState("");
67 const [editBusy, setEditBusy] = useState(false);
68
69 const startEdit = (item: PendingGuidance) => {
70 setEditingId(item.id);
71 setEditDraft(item.submitText.trim() || item.text);
72 };
73
74 const cancelEdit = () => {
75 setEditingId(null);
76 setEditDraft("");
77 setEditBusy(false);
78 };
79
80 const saveEdit = async (item: PendingGuidance) => {
81 if (!onEdit || editBusy) return;
82 const next = editDraft.trim();
83 if (!next) return;
84 setEditBusy(true);
85 try {
86 await onEdit(item, next);
87 cancelEdit();
88 } catch {
89 setEditBusy(false);
90 }
91 };
92
93 return (
94 <>
95 {recovery && (
96 <InboxRecoveryBanner
97 key={`${recovery.draftKey}:${recovery.tabId}`}
98 count={recovery.count}
99 recovered={recovery.recovered}
100 disabled={recoveryDisabled}
101 tabId={recovery.tabId}
102 onReview={onReview}
103 onResumed={onRecoveryResumed}
104 onError={onRecoveryError}
105 />
106 )}
107 {items.length > 0 && (
108 <div className="composer-guidance-shelf" aria-label={t("composer.guidanceQueue")}>
109 <div className="composer-guidance-head">
110 <span className="composer-guidance-head__label">
111 <CornerDownRight size={14} />
112 <span>{t("composer.guidanceCount", { n: items.length })}</span>
113 </span>
114 </div>
115 <div className="composer-guidance-list">
116 {visible.map((item, index) => {
117 const inFlight = guidanceIsInFlight(item.state);
118 const unknownState = !guidanceHasKnownPendingState(item.state);
119 const needsRetry = guidanceNeedsRetry(item.state);
120 const waitingForEarlier = !running && !inFlight && index > 0;
121 const canEdit = Boolean(onEdit) && !readOnly && !disabled && guidanceIsEditable(item) && !waitingForEarlier && sendingId === null;
122 const editing = editingId === item.id;
123 const actionLabel = inFlight
124 ? t("composer.guidanceInFlight")
125 : waitingForEarlier
126 ? t("composer.guidanceWaiting")
127 : needsRetry
128 ? t("composer.guidanceRetry")
129 : t("composer.guidanceSend");
130 return (
131 <div className={`composer-guidance-item${editing ? " composer-guidance-item--editing" : ""}`} key={item.id}>
132 <CornerDownRight size={14} className="composer-guidance-item__icon" />
133 {editing ? (
134 <input
135 className="composer-guidance-item__editor"
136 value={editDraft}
137 onChange={(event) => setEditDraft(event.target.value)}
138 aria-label={t("composer.guidanceEdit")}
139 disabled={editBusy}
140 onKeyDown={(event) => {
141 if (event.key === "Enter") {
142 event.preventDefault();
143 void saveEdit(item);
144 }
145 if (event.key === "Escape") {
146 event.preventDefault();
147 cancelEdit();
148 }
149 }}
150 />
151 ) : (
152 <span className="composer-guidance-item__text">{item.text.trim() || t("composer.guidanceEmptyPreview")}</span>
153 )}
154 {editing ? (
155 <>
156 <Tooltip label={t("composer.guidanceSaveEdit")}>
157 <button
158 className="composer-guidance-item__action"
159 type="button"
160 aria-label={t("composer.guidanceSaveEdit")}
161 disabled={editBusy || !editDraft.trim()}
162 onClick={() => void saveEdit(item)}
163 >
164 <Check size={14} />
165 </button>
166 </Tooltip>
167 <Tooltip label={t("composer.guidanceCancelEdit")}>
168 <button
169 className="composer-guidance-item__action"
170 type="button"
171 aria-label={t("composer.guidanceCancelEdit")}
172 disabled={editBusy}
173 onClick={cancelEdit}
174 >
175 <X size={14} />
176 </button>
177 </Tooltip>
178 </>
179 ) : (
180 <>
181 {canEdit && (
182 <Tooltip label={t("composer.guidanceEdit")}>
183 <button
184 className="composer-guidance-item__action"
185 type="button"
186 aria-label={t("composer.guidanceEdit")}
187 onClick={() => startEdit(item)}
188 >
189 <Pencil size={14} />
190 </button>
191 </Tooltip>
192 )}
193 <Tooltip label={actionLabel}>
194 <button
195 className="composer-guidance-item__guide"
196 type="button"
197 aria-label={actionLabel}
198 disabled={inFlight || unknownState || waitingForEarlier || disabled || readOnly || sendingId !== null || (running && !needsRetry && Boolean(item.structured)) || Boolean(item.paused)}
199 onClick={() => onSend(item)}
200 >
201 <CornerDownRight size={13} />
202 <span>{t(needsRetry ? "composer.guidanceRetryMode" : running ? "composer.guidanceMode" : "composer.guidanceSendMode")}</span>
203 </button>
204 </Tooltip>
205 <Tooltip label={inFlight ? actionLabel : t("composer.guidanceDismiss")}>
206 <button
207 className="composer-guidance-item__action"
208 type="button"
209 aria-label={inFlight ? actionLabel : t("composer.guidanceDismiss")}
210 disabled={disabled || readOnly || inFlight || unknownState || sendingId === item.id}
211 onClick={() => onDismiss(item)}
212 >
213 <Trash2 size={14} />
214 </button>
215 </Tooltip>
216 </>
217 )}
218 </div>
219 );
220 })}
221 {items.length > 2 && (
222 <button
223 className="composer-guidance-more"
224 type="button"
225 aria-expanded={expanded}
226 onClick={onToggleExpanded}
227 >
228 {expanded ? <ChevronUp size={13} /> : <ChevronDown size={13} />}
229 <span>{expanded ? t("composer.guidanceCollapse") : t("composer.guidanceRemaining", { n: hiddenCount })}</span>
230 </button>
231 )}
232 </div>
233 </div>
234 )}
235 </>
236 );
237 }
238
238 lines Plain Text