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