@deepseek-ai/dsh-client-ui-message-feedback 0.1.3-alpha.2 → 0.1.5-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -6,8 +6,277 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react_jsx_runtime = require("react/jsx-runtime");
8
8
  let react = require("react");
9
- let react_dom = require("react-dom");
10
9
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
+ let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
11
+ //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-message-feedback/src/client/FeedbackDialog.module.css.mjs
12
+ const css$1 = "._6_jNJa_dialog._6_jNJa_dialog{border-radius:18px;gap:38px;width:min(488px,100%)}._6_jNJa_categories{flex-wrap:wrap;gap:8px;margin-top:-14px;display:flex}._6_jNJa_chip{border:.5px solid var(--dsw-alias-border-l4);corner-shape:round;height:28px;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;background:0 0;border-radius:14px;padding:0 12px;font-size:13px;line-height:18px;transition:background-color .12s,border-color .12s,color .12s}._6_jNJa_chip:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}._6_jNJa_chip:disabled{cursor:default;opacity:.4}._6_jNJa_chip:focus-visible{outline:2px solid var(--dsw-alias-button-primary-fill);outline-offset:2px}._6_jNJa_chipActive,._6_jNJa_chipActive:hover:not(:disabled){border-color:var(--dsw-alias-button-primary-fill);background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}._6_jNJa_detail{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1);width:100%;min-height:116px;max-height:280px;color:var(--dsw-alias-label-primary);font:inherit;field-sizing:content;resize:none;border-radius:16px;margin-top:18px;padding:12px 14px;font-size:14px;line-height:22px;transition:border-color .12s,box-shadow .12s;display:block}._6_jNJa_detail::placeholder{color:var(--dsw-alias-label-caption)}._6_jNJa_detail:focus{border-color:var(--dsw-alias-border-l3);box-shadow:0 0 0 1px var(--dsw-alias-border-l3);outline:none}._6_jNJa_failure{color:var(--dsw-alias-label-tertiary);margin-top:8px;font-size:13px;line-height:20px;display:block}._6_jNJa_submit{border-radius:18px;width:100%;height:44px;font-size:14px;font-weight:500}._6_jNJa_toastIcon{border:1.5px solid var(--dsw-alias-state-success-primary);corner-shape:round;width:18px;height:18px;color:var(--dsw-alias-state-success-primary);border-radius:50%;place-items:center;display:grid}@media (prefers-reduced-motion:reduce){._6_jNJa_chip,._6_jNJa_detail{transition:none}}";
13
+ const tagId$1 = "@deepseek-ai/dsh-client-ui-message-feedback/FeedbackDialog.module.css";
14
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
15
+ const tag = document.createElement("style");
16
+ tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-message-feedback";
17
+ tag.dataset.pluginCss = tagId$1;
18
+ tag.textContent = css$1;
19
+ document.head.appendChild(tag);
20
+ }
21
+ var FeedbackDialog_module_css_default = {
22
+ "categories": "_6_jNJa_categories",
23
+ "chip": "_6_jNJa_chip",
24
+ "chipActive": "_6_jNJa_chipActive",
25
+ "detail": "_6_jNJa_detail",
26
+ "dialog": "_6_jNJa_dialog",
27
+ "failure": "_6_jNJa_failure",
28
+ "submit": "_6_jNJa_submit",
29
+ "toastIcon": "_6_jNJa_toastIcon"
30
+ };
31
+ //#endregion
32
+ //#region lib/types/client/FeedbackDialog.js
33
+ /**
34
+ * The feedback dialog and its acknowledgement toast, rendered as one entry
35
+ * of `conversation.input.overlay` so each Session owns exactly one of each.
36
+ * The Modal and the Toast both portal to `document.body`; the overlay slot
37
+ * only supplies the per-session controller and the composer card the toast
38
+ * centers over.
39
+ * @module @deepseek-ai/dsh-client-ui-message-feedback/client/FeedbackDialog
40
+ */
41
+ const CATEGORIES = Object.keys({
42
+ "task-result": true,
43
+ "instruction-following": true,
44
+ "product-interaction": true,
45
+ "service-stability": true,
46
+ "resource-cost": true,
47
+ "security-privacy-permission": true,
48
+ "other": true
49
+ });
50
+ /** Failure codes with their own copy; every other code reads the generic line. */
51
+ const FAILURE_COPY = {
52
+ "version-conflict": "error.conflict",
53
+ "note-too-large": "error.noteTooLarge"
54
+ };
55
+ /**
56
+ * Render one Session's feedback dialog and toast.
57
+ * @param props - the dialog hook, the draft verbs, and the locale seat.
58
+ * @returns the modal while a target is open, the toast while one is showing.
59
+ */
60
+ function FeedbackDialog({ useDialog, edit, submit, dismiss, dismissToast, t }) {
61
+ const state = useDialog((s) => s);
62
+ const probeRef = (0, react.useRef)(null);
63
+ const [card, setCard] = (0, react.useState)(null);
64
+ (0, react.useLayoutEffect)(() => {
65
+ setCard(probeRef.current?.closest("[data-composer-card]") ?? null);
66
+ }, []);
67
+ const toast = state.toast;
68
+ const onToastDone = (0, react.useCallback)(() => {
69
+ dismissToast(toast);
70
+ }, [dismissToast, toast]);
71
+ (0, react.useEffect)(() => () => {
72
+ dismissToast(toast);
73
+ }, [dismissToast, toast]);
74
+ const failure = state.failure === null ? null : t(FAILURE_COPY[state.failure] ?? "error.generic");
75
+ return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
76
+ (0, react_jsx_runtime.jsx)("span", {
77
+ ref: probeRef,
78
+ hidden: true
79
+ }),
80
+ toast > 0 && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
81
+ text: t("toast.recorded"),
82
+ icon: (0, react_jsx_runtime.jsx)("span", {
83
+ className: FeedbackDialog_module_css_default.toastIcon,
84
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, { size: 12 })
85
+ }),
86
+ anchor: card,
87
+ onDone: onToastDone
88
+ }, toast),
89
+ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
90
+ open: state.target !== null,
91
+ title: t("dialog.title"),
92
+ closeLabel: t("close"),
93
+ onClose: dismiss,
94
+ className: FeedbackDialog_module_css_default.dialog,
95
+ footer: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
96
+ variant: "primary",
97
+ className: FeedbackDialog_module_css_default.submit,
98
+ disabled: state.submitting,
99
+ onClick: () => {
100
+ submit();
101
+ },
102
+ children: state.submitting ? t("submitting") : t("submit")
103
+ }),
104
+ children: [
105
+ (0, react_jsx_runtime.jsx)("div", {
106
+ className: FeedbackDialog_module_css_default.categories,
107
+ role: "group",
108
+ "aria-label": t("dialog.categories"),
109
+ children: CATEGORIES.map((category) => (0, react_jsx_runtime.jsx)("button", {
110
+ type: "button",
111
+ className: state.category === category ? `${FeedbackDialog_module_css_default.chip} ${FeedbackDialog_module_css_default.chipActive}` : FeedbackDialog_module_css_default.chip,
112
+ "aria-pressed": state.category === category,
113
+ disabled: state.submitting,
114
+ onClick: () => {
115
+ edit({ category: state.category === category ? null : category });
116
+ },
117
+ children: t(`category.${category}`)
118
+ }, category))
119
+ }),
120
+ (0, react_jsx_runtime.jsx)("textarea", {
121
+ className: FeedbackDialog_module_css_default.detail,
122
+ "aria-label": t("dialog.detail"),
123
+ placeholder: t("dialog.hint"),
124
+ value: state.text,
125
+ readOnly: state.submitting,
126
+ onChange: (event) => {
127
+ edit({ text: event.target.value });
128
+ }
129
+ }),
130
+ failure !== null && (0, react_jsx_runtime.jsx)("span", {
131
+ className: FeedbackDialog_module_css_default.failure,
132
+ role: "status",
133
+ children: failure
134
+ })
135
+ ]
136
+ })
137
+ ] });
138
+ }
139
+ //#endregion
140
+ //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-message-feedback/src/client/MessageFeedbackActions.module.css.mjs
141
+ const css = "._8_XoUG_action{width:calc(28px + var(--dsh-content-font-delta,0px));height:calc(28px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}._8_XoUG_action svg{width:calc(15px + var(--dsh-content-font-delta,0px));height:calc(15px + var(--dsh-content-font-delta,0px))}._8_XoUG_action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}._8_XoUG_action:disabled{cursor:default;opacity:.4}._8_XoUG_action[data-active]{color:var(--dsw-alias-label-tertiary)}._8_XoUG_failure{color:var(--dsw-alias-label-tertiary);padding-left:4px;font-size:13px;line-height:20px}";
142
+ const tagId = "@deepseek-ai/dsh-client-ui-message-feedback/MessageFeedbackActions.module.css";
143
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
144
+ const tag = document.createElement("style");
145
+ tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-message-feedback";
146
+ tag.dataset.pluginCss = tagId;
147
+ tag.textContent = css;
148
+ document.head.appendChild(tag);
149
+ }
150
+ var MessageFeedbackActions_module_css_default = {
151
+ "action": "_8_XoUG_action",
152
+ "failure": "_8_XoUG_failure"
153
+ };
154
+ //#endregion
155
+ //#region lib/types/client/MessageFeedbackActions.js
156
+ /**
157
+ * Per-message feedback controls: the Like/Dislike pair inside the assistant
158
+ * message's IconActions row, between copy and branch. Like records at once
159
+ * and raises the Session's acknowledgement toast; Dislike opens the Session's
160
+ * feedback dialog, whose submission records the negative judgment with its
161
+ * category and text. Clicking the recorded rating retracts it. A recorded
162
+ * rating shows the filled glyph so the signal survives a pointer leaving the row.
163
+ * @module @deepseek-ai/dsh-client-ui-message-feedback/client/MessageFeedbackActions
164
+ */
165
+ /**
166
+ * One message's feedback controls.
167
+ * @param props - the owner's message identity, the injected verbs, and the
168
+ * shared feedback hook.
169
+ * @returns the rating buttons with any failure notice beside them.
170
+ */
171
+ function MessageFeedbackActions({ messageId, ensure, current, toggle, openDialog, acknowledge, useFeedback, t }) {
172
+ const item = useFeedback((view) => view.items.get(messageId));
173
+ const loadFailed = useFeedback((view) => view.status === "error");
174
+ const rating = item?.rating;
175
+ const [pending, setPending] = (0, react.useState)(false);
176
+ const [failure, setFailure] = (0, react.useState)(null);
177
+ const seeded = (0, react.useRef)(false);
178
+ const seed = (0, react.useCallback)(() => {
179
+ if (seeded.current) return;
180
+ seeded.current = true;
181
+ ensure();
182
+ }, [ensure]);
183
+ const alive = (0, react.useRef)(true);
184
+ (0, react.useEffect)(() => () => {
185
+ alive.current = false;
186
+ }, []);
187
+ const errorCopy = (0, react.useCallback)((result) => {
188
+ return result.error.code === "version-conflict" ? t("error.conflict") : t("error.generic");
189
+ }, [t]);
190
+ const onLike = (0, react.useCallback)(() => {
191
+ setPending(true);
192
+ setFailure(null);
193
+ toggle(messageId, "positive").then((result) => {
194
+ if (!alive.current) return;
195
+ setPending(false);
196
+ if (!result.ok) {
197
+ setFailure(errorCopy(result));
198
+ return;
199
+ }
200
+ if (result.rating === "positive") acknowledge();
201
+ });
202
+ }, [
203
+ acknowledge,
204
+ errorCopy,
205
+ messageId,
206
+ toggle
207
+ ]);
208
+ const onDislike = (0, react.useCallback)(() => {
209
+ setPending(true);
210
+ setFailure(null);
211
+ ensure().then((loaded) => {
212
+ if (!alive.current) return;
213
+ if (!loaded.ok || current(messageId)?.rating !== "negative") {
214
+ setPending(false);
215
+ openDialog(messageId);
216
+ return;
217
+ }
218
+ toggle(messageId, "negative").then((result) => {
219
+ if (!alive.current) return;
220
+ setPending(false);
221
+ if (!result.ok) setFailure(errorCopy(result));
222
+ });
223
+ });
224
+ }, [
225
+ current,
226
+ ensure,
227
+ errorCopy,
228
+ messageId,
229
+ openDialog,
230
+ toggle
231
+ ]);
232
+ const likeLabel = rating === "positive" ? t("action.likeActive") : t("action.like");
233
+ const dislikeLabel = rating === "negative" ? t("action.dislikeActive") : t("action.dislike");
234
+ return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
235
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
236
+ label: likeLabel,
237
+ side: "bottom",
238
+ children: (0, react_jsx_runtime.jsx)("button", {
239
+ type: "button",
240
+ className: MessageFeedbackActions_module_css_default.action,
241
+ "aria-label": likeLabel,
242
+ "aria-pressed": rating === "positive",
243
+ "data-active": rating === "positive" || void 0,
244
+ disabled: pending,
245
+ onFocus: seed,
246
+ onPointerEnter: seed,
247
+ onClick: onLike,
248
+ children: rating === "positive" ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconLikeFill16, {}) : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconLikeOutline16, {})
249
+ })
250
+ }),
251
+ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
252
+ label: dislikeLabel,
253
+ side: "bottom",
254
+ children: (0, react_jsx_runtime.jsx)("button", {
255
+ type: "button",
256
+ className: MessageFeedbackActions_module_css_default.action,
257
+ "aria-label": dislikeLabel,
258
+ "aria-pressed": rating === "negative",
259
+ "data-active": rating === "negative" || void 0,
260
+ disabled: pending,
261
+ onFocus: seed,
262
+ onPointerEnter: seed,
263
+ onClick: onDislike,
264
+ children: rating === "negative" ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDislikeFill16, {}) : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDislikeOutline16, {})
265
+ })
266
+ }),
267
+ failure === null && loadFailed && (0, react_jsx_runtime.jsx)("span", {
268
+ className: MessageFeedbackActions_module_css_default.failure,
269
+ role: "status",
270
+ children: t("error.load")
271
+ }),
272
+ failure !== null && (0, react_jsx_runtime.jsx)("span", {
273
+ className: MessageFeedbackActions_module_css_default.failure,
274
+ role: "status",
275
+ children: failure
276
+ })
277
+ ] });
278
+ }
279
+ //#endregion
11
280
  //#region lib/types/client/controller.js
12
281
  const INITIAL_VIEW = Object.freeze({
13
282
  status: "cold",
@@ -22,7 +291,11 @@ window.__ModuleLoader__.load({
22
291
  message: "feedback controller is disposed"
23
292
  })
24
293
  });
25
- /** Human-readable text for one business failure code. */
294
+ /**
295
+ * Human-readable text for one business failure code.
296
+ * @param code - the Host's business failure code.
297
+ * @returns the developer-facing description carried in the failure branch.
298
+ */
26
299
  function describe(code) {
27
300
  switch (code) {
28
301
  case "session-not-found": return "this session is no longer persisted";
@@ -123,22 +396,17 @@ window.__ModuleLoader__.load({
123
396
  }
124
397
  /**
125
398
  * Create or replace feedback for one message, comparing against the version
126
- * this controller last observed.
127
- *
128
- * The note is resolved here rather than by the caller: `mutate` awaits the
129
- * one list read first, so this body always sees the committed item, while a
130
- * control that rendered before that read completed would still be holding
131
- * `undefined`. Omitting `note` therefore keeps whatever is stored; only
132
- * {@link clearNote} removes one.
399
+ * this controller last observed. The item stores exactly `entry`: an entry
400
+ * without a note or category replaces whatever the stored item carried.
133
401
  * @param messageId - target assistant message.
134
402
  * @param rating - desired judgment.
135
- * @param note - replacement explanation; omitted keeps the stored note.
403
+ * @param entry - explanation and category to store with the judgment.
136
404
  * @returns the settled mutation result.
137
405
  */
138
- rate(messageId, rating, note) {
406
+ rate(messageId, rating, entry = {}) {
139
407
  return this.mutate(async () => {
140
408
  const observed = this.view.items.get(messageId);
141
- return await this.putCommitted(messageId, rating, note ?? observed?.note, observed);
409
+ return await this.putCommitted(messageId, rating, entry, observed);
142
410
  });
143
411
  }
144
412
  /**
@@ -146,50 +414,31 @@ window.__ModuleLoader__.load({
146
414
  * the committed rating already matches. The decision reads the committed item
147
415
  * inside the serialized mutation, so a click that lands before the first list
148
416
  * read still toggles against the stored value rather than the empty view a
149
- * cold control rendered.
417
+ * cold control rendered. A replacement stores the bare judgment; the note
418
+ * and category of the judgment it replaces do not carry over.
150
419
  * @param messageId - target assistant message.
151
420
  * @param rating - the judgment the human asked for.
152
- * @returns the settled mutation result.
421
+ * @returns the settled mutation result with the rating now committed.
153
422
  */
154
423
  toggle(messageId, rating) {
155
424
  return this.mutate(async () => {
156
425
  const observed = this.view.items.get(messageId);
157
- if (observed?.rating === rating) return await this.deleteCommitted(messageId, observed);
158
- return await this.putCommitted(messageId, rating, observed?.note, observed);
159
- });
160
- }
161
- /**
162
- * Drop the note while keeping the rating. Absent feedback needs no call.
163
- * @param messageId - target assistant message.
164
- * @returns the settled mutation result.
165
- */
166
- clearNote(messageId) {
167
- return this.mutate(async () => {
168
- const observed = this.view.items.get(messageId);
169
- if (observed === void 0 || observed.note === void 0) return OK;
170
- return await this.putCommitted(messageId, observed.rating, void 0, observed);
171
- });
172
- }
173
- /**
174
- * Remove feedback for one message. A message with no known item is already
175
- * in the requested state, so no call is made.
176
- * @param messageId - target assistant message.
177
- * @returns the settled mutation result.
178
- */
179
- clear(messageId) {
180
- return this.mutate(async () => {
181
- const observed = this.view.items.get(messageId);
182
- if (observed === void 0) return OK;
183
- return await this.deleteCommitted(messageId, observed);
426
+ const retract = observed?.rating === rating;
427
+ const result = retract ? await this.deleteCommitted(messageId, observed) : await this.putCommitted(messageId, rating, {}, observed);
428
+ return result.ok ? {
429
+ ok: true,
430
+ rating: retract ? null : rating
431
+ } : result;
184
432
  });
185
433
  }
186
434
  /** Commit one put against the observed version and reconcile a conflict. */
187
- async putCommitted(messageId, rating, note, observed) {
435
+ async putCommitted(messageId, rating, entry, observed) {
188
436
  const carried = await this.ctx.remote.messageFeedback.put({
189
437
  sessionId: this.sessionId,
190
438
  messageId,
191
439
  rating,
192
- ...note === void 0 ? {} : { note },
440
+ ...entry.text === void 0 ? {} : { note: entry.text },
441
+ ...entry.category === void 0 ? {} : { category: entry.category },
193
442
  ifVersion: observed?.version ?? null
194
443
  });
195
444
  if (!carried.ok) return carrierFailure(carried.error);
@@ -297,297 +546,185 @@ window.__ModuleLoader__.load({
297
546
  }
298
547
  };
299
548
  //#endregion
300
- //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-message-feedback/src/client/MessageFeedbackActions.module.css.mjs
301
- const css = "._8_XoUG_action{width:calc(28px + var(--dsh-content-font-delta,0px));height:calc(28px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}._8_XoUG_action svg{width:calc(15px + var(--dsh-content-font-delta,0px));height:calc(15px + var(--dsh-content-font-delta,0px))}._8_XoUG_action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}._8_XoUG_action:disabled{cursor:default;opacity:.4}._8_XoUG_action[data-active]{color:var(--dsw-alias-label-primary)}._8_XoUG_noteOpen{max-width:220px;color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(28px + var(--dsh-content-font-delta,0px));white-space:nowrap;text-overflow:ellipsis;cursor:pointer;background:0 0;border:none;border-radius:14px;padding:0 8px;overflow:hidden}._8_XoUG_noteOpen:hover,._8_XoUG_noteOpen[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}._8_XoUG_notePanel{z-index:1100;box-sizing:border-box;background:var(--dsw-specific-menu);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);width:320px;max-width:min(360px,100vw - 24px);max-height:calc(100vh - 24px);box-shadow:var(--dsw-elevation-prominent);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border:0;border-radius:12px;flex-direction:column;gap:8px;padding:8px;display:flex;position:fixed;overflow-y:auto}._8_XoUG_noteInput{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1);width:100%;color:var(--dsw-alias-label-primary);font:inherit;resize:vertical;border-radius:8px;padding:6px 8px;font-size:13px}._8_XoUG_noteActions{justify-content:flex-end;gap:6px;display:flex}._8_XoUG_noteSave,._8_XoUG_noteCancel{cursor:pointer;border:none;border-radius:14px;height:28px;padding:0 10px;font-size:13px}._8_XoUG_noteSave{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}._8_XoUG_noteSave:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}._8_XoUG_noteSave:disabled{cursor:default;opacity:.4}._8_XoUG_noteCancel{color:var(--dsw-alias-label-tertiary);background:0 0}._8_XoUG_noteCancel:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}._8_XoUG_failure{color:var(--dsw-alias-label-tertiary);padding-left:4px;font-size:13px;line-height:20px}";
302
- const tagId = "@deepseek-ai/dsh-client-ui-message-feedback/MessageFeedbackActions.module.css";
303
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
304
- const tag = document.createElement("style");
305
- tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-message-feedback";
306
- tag.dataset.pluginCss = tagId;
307
- tag.textContent = css;
308
- document.head.appendChild(tag);
309
- }
310
- var MessageFeedbackActions_module_css_default = {
311
- "action": "_8_XoUG_action",
312
- "failure": "_8_XoUG_failure",
313
- "noteActions": "_8_XoUG_noteActions",
314
- "noteCancel": "_8_XoUG_noteCancel",
315
- "noteInput": "_8_XoUG_noteInput",
316
- "noteOpen": "_8_XoUG_noteOpen",
317
- "notePanel": "_8_XoUG_notePanel",
318
- "noteSave": "_8_XoUG_noteSave"
319
- };
320
- //#endregion
321
- //#region lib/types/client/MessageFeedbackActions.js
549
+ //#region lib/types/client/dialog.js
322
550
  /**
323
- * Per-message feedback controls: a Like/Dislike pair plus an optional note.
324
- * The buttons render inside the assistant message's IconActions row, so they
325
- * reuse that row's chrome and sit between copy and branch. The note editor is
326
- * a popover (portaled to `document.body`) anchored to the note trigger, not an
327
- * inline expansion: a 260px textarea plus buttons cannot fit the row at any
328
- * viewport, and an inline element pushed the branch action and clock out of the
329
- * conversation column. Portaling out of the column also escapes its `overflow`
330
- * clip, so the panel cannot be cropped or detached from the message it annotates.
331
- * @module @deepseek-ai/dsh-client-ui-message-feedback/client/MessageFeedbackActions
332
- */
333
- /** Safe distance kept between the panel and the viewport edges (the Menu portal margin). */
334
- const PANEL_MARGIN = 12;
335
- /** Distance between the trigger's bottom edge and the panel's top. */
336
- const PANEL_GAP = 4;
337
- /**
338
- * Unplaced portal panel: hidden but laid out so `offsetWidth` is real for the
339
- * clamp. The explicit insets match `Menu`'s measure style — a `position: fixed`
340
- * element with auto insets otherwise sits at its static position, a different
341
- * origin than the one the first placement measures from.
551
+ * Headless state of one Session's feedback dialog and its acknowledgement
552
+ * toast. One form serves two targets: the Session itself (a bare `/feedback`)
553
+ * and one assistant message (Dislike). The overlay view renders from the
554
+ * store; the message controls raise the toast after a Like through
555
+ * {@link FeedbackDialogController.acknowledge}.
556
+ * @module @deepseek-ai/dsh-client-ui-message-feedback/client/dialog
342
557
  */
343
- const MEASURE_STYLE = {
344
- visibility: "hidden",
345
- left: 0,
346
- top: 0
558
+ const CLOSED = {
559
+ target: null,
560
+ category: null,
561
+ text: "",
562
+ submitting: false,
563
+ failure: null
347
564
  };
348
- /**
349
- * One message's feedback controls.
350
- * @param props - the owner's message identity, the injected verbs, and the
351
- * shared feedback hook.
352
- * @returns the rating buttons and the note trigger, with the note editor
353
- * portal-open beneath the trigger while it is open.
354
- */
355
- function MessageFeedbackActions({ messageId, ensure, rate, toggle, clearNote, useFeedback, t }) {
356
- const item = useFeedback((view) => view.items.get(messageId));
357
- const loadFailed = useFeedback((view) => view.status === "error");
358
- const rating = item?.rating;
359
- const [noteOpen, setNoteOpen] = (0, react.useState)(false);
360
- const [draft, setDraft] = (0, react.useState)("");
361
- const [pending, setPending] = (0, react.useState)(false);
362
- const [rowFailure, setRowFailure] = (0, react.useState)(null);
363
- const [noteFailure, setNoteFailure] = (0, react.useState)(null);
364
- const triggerRef = (0, react.useRef)(null);
365
- const panelRef = (0, react.useRef)(null);
366
- const inputRef = (0, react.useRef)(null);
367
- const seeded = (0, react.useRef)(false);
368
- const seed = (0, react.useCallback)(() => {
369
- if (seeded.current) return;
370
- seeded.current = true;
371
- ensure();
372
- }, [ensure]);
373
- const alive = (0, react.useRef)(true);
374
- (0, react.useEffect)(() => () => {
375
- alive.current = false;
376
- }, []);
377
- /** Bumped whenever an editing session ends, so a late save can tell it is stale. */
378
- const noteGeneration = (0, react.useRef)(0);
379
- /** Current panel open-state, readable from a stale closure via a ref. */
380
- const noteOpenRef = (0, react.useRef)(false);
381
- (0, react.useEffect)(() => {
382
- noteOpenRef.current = noteOpen;
383
- }, [noteOpen]);
384
- const errorCopy = (0, react.useCallback)((result) => {
385
- return result.error?.code === "version-conflict" ? t("error.conflict") : t("error.generic");
386
- }, [t]);
387
- const settleRating = (0, react.useCallback)((result) => {
388
- if (!alive.current) return;
389
- setPending(false);
390
- setRowFailure(result.ok ? null : errorCopy(result));
391
- }, [errorCopy]);
392
- const closeNote = (0, react.useCallback)(() => {
393
- noteGeneration.current += 1;
394
- setNoteOpen(false);
395
- }, []);
396
- const onRate = (0, react.useCallback)((next) => {
397
- setPending(true);
398
- setRowFailure(null);
399
- closeNote();
400
- toggle(messageId, next).then(settleRating);
401
- }, [
402
- closeNote,
403
- messageId,
404
- settleRating,
405
- toggle
406
- ]);
407
- const onSaveNote = (0, react.useCallback)((current) => {
408
- const trimmed = draft.trim();
409
- setPending(true);
410
- setNoteFailure(null);
411
- const generation = noteGeneration.current;
412
- const staleSeed = item?.note ?? "";
413
- (trimmed.length === 0 ? clearNote(messageId) : rate(messageId, current, trimmed)).then((result) => {
414
- if (!alive.current) return;
415
- setPending(false);
416
- if (result.ok) {
417
- if (generation === noteGeneration.current) {
418
- setNoteFailure(null);
419
- setNoteOpen(false);
420
- return;
421
- }
422
- setDraft((draftNow) => draftNow === staleSeed ? trimmed : draftNow);
423
- return;
424
- }
425
- if (generation === noteGeneration.current || !noteOpenRef.current) setNoteFailure(errorCopy(result));
565
+ /** Per-session dialog controller; one instance backs the overlay entry and every message control. */
566
+ var FeedbackDialogController = class {
567
+ submit;
568
+ /** Dialog state store (the overlay entry subscribes here). */
569
+ state = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({
570
+ ...CLOSED,
571
+ toast: 0
572
+ });
573
+ /** Bumped by every open, dismiss, and dispose so a late settlement can tell its draft is gone. */
574
+ generation = 0;
575
+ toastSeq = 0;
576
+ /**
577
+ * @param submit - records one submission; the owner routes it by target.
578
+ */
579
+ constructor(submit) {
580
+ this.submit = submit;
581
+ }
582
+ /**
583
+ * Open the dialog with an empty draft, replacing any open draft.
584
+ * @param target - what the submission records against.
585
+ */
586
+ open(target) {
587
+ this.generation += 1;
588
+ this.state.set({
589
+ ...CLOSED,
590
+ target,
591
+ toast: this.state.getSnapshot().toast
426
592
  });
427
- }, [
428
- clearNote,
429
- draft,
430
- errorCopy,
431
- item?.note,
432
- messageId,
433
- noteOpenRef,
434
- rate
435
- ]);
436
- const toggleNote = (0, react.useCallback)(() => {
437
- if (noteOpen) {
438
- closeNote();
593
+ }
594
+ /** Close the dialog and discard the draft; a toast on screen stays. */
595
+ dismiss() {
596
+ this.generation += 1;
597
+ this.state.set({
598
+ ...CLOSED,
599
+ toast: this.state.getSnapshot().toast
600
+ });
601
+ }
602
+ /**
603
+ * Replace part of the draft while it is editable.
604
+ * @param draft - the category (null clears it) or the text as typed.
605
+ */
606
+ edit(draft) {
607
+ const s = this.state.getSnapshot();
608
+ if (s.target === null || s.submitting) return;
609
+ this.state.set({
610
+ ...s,
611
+ ...draft
612
+ });
613
+ }
614
+ /**
615
+ * Submit the draft; an empty draft is a valid submission. Success closes the
616
+ * dialog and raises the toast; a failure keeps the dialog open with its code.
617
+ * @returns after the submission settles.
618
+ */
619
+ async submitDraft() {
620
+ const s = this.state.getSnapshot();
621
+ if (s.target === null || s.submitting) return;
622
+ const generation = this.generation;
623
+ this.state.set({
624
+ ...s,
625
+ submitting: true,
626
+ failure: null
627
+ });
628
+ const text = s.text.trim();
629
+ const result = await this.submit(s.target, {
630
+ ...text.length === 0 ? {} : { text },
631
+ ...s.category === null ? {} : { category: s.category }
632
+ });
633
+ if (result.ok) {
634
+ if (generation === this.generation) this.dismiss();
635
+ this.acknowledge();
439
636
  return;
440
637
  }
441
- setDraft(item?.note ?? "");
442
- setNoteFailure(null);
443
- setNoteOpen(true);
444
- }, [
445
- noteOpen,
446
- closeNote,
447
- item?.note
448
- ]);
449
- const pos = (0, _deepseek_ai_dsh_client_ui_primitives.useAnchoredPosition)({
450
- open: noteOpen,
451
- anchorRef: triggerRef,
452
- panelRef,
453
- gap: PANEL_GAP,
454
- margin: PANEL_MARGIN
455
- });
456
- (0, react.useEffect)(() => {
457
- if (!noteOpen) return;
458
- inputRef.current?.focus();
459
- const onPointerDown = (e) => {
460
- if (!(e.target instanceof Node)) return;
461
- if (triggerRef.current?.contains(e.target) === true) return;
462
- if (panelRef.current?.contains(e.target) === true) return;
463
- closeNote();
464
- };
465
- const onKeyDown = (e) => {
466
- if (e.key === "Escape") closeNote();
638
+ if (generation !== this.generation) return;
639
+ this.state.set({
640
+ ...this.state.getSnapshot(),
641
+ submitting: false,
642
+ failure: result.error.code
643
+ });
644
+ }
645
+ /** Show the acknowledgement toast; a toast already on screen restarts. */
646
+ acknowledge() {
647
+ this.toastSeq += 1;
648
+ this.state.set({
649
+ ...this.state.getSnapshot(),
650
+ toast: this.toastSeq
651
+ });
652
+ }
653
+ /**
654
+ * Retire one toast after its fade; a newer toast is left alone.
655
+ * @param seq - the toast sequence the view finished showing.
656
+ */
657
+ dismissToast(seq) {
658
+ const s = this.state.getSnapshot();
659
+ if (s.toast === seq) this.state.set({
660
+ ...s,
661
+ toast: 0
662
+ });
663
+ }
664
+ /** Scope-teardown disposer: drop the draft and the toast, orphan in-flight work. */
665
+ dispose() {
666
+ this.generation += 1;
667
+ this.state.set({
668
+ ...CLOSED,
669
+ toast: 0
670
+ });
671
+ }
672
+ };
673
+ //#endregion
674
+ //#region lib/types/client/surface.js
675
+ /**
676
+ * One Session's feedback surface: the message-feedback object layer and the
677
+ * dialog controller, plus the routing between them. A message target puts a
678
+ * negative judgment through the message controller; the Session target
679
+ * records through the `sessionFeedback` Remote.
680
+ * @module @deepseek-ai/dsh-client-ui-message-feedback/client/surface
681
+ */
682
+ /** The per-session pair behind every entry of one Session. */
683
+ var FeedbackSurface = class {
684
+ ctx;
685
+ sessionId;
686
+ /** The Session's message-feedback object layer, shared by every message control. */
687
+ feedback;
688
+ /** The Session's dialog and toast state, shared by the overlay entry and the message controls. */
689
+ dialog;
690
+ /**
691
+ * @param ctx - the browser plugin context carrying both feedback Remotes.
692
+ * @param sessionId - Session owning the transcript and the remark.
693
+ */
694
+ constructor(ctx, sessionId) {
695
+ this.ctx = ctx;
696
+ this.sessionId = sessionId;
697
+ this.feedback = new MessageFeedbackController(ctx, sessionId);
698
+ this.dialog = new FeedbackDialogController((target, entry) => target.kind === "message" ? this.feedback.rate(target.messageId, "negative", entry) : this.recordSession(entry));
699
+ }
700
+ /** Record one Session-level remark through the sessionFeedback Remote. */
701
+ async recordSession(entry) {
702
+ const carried = await this.ctx.remote.sessionFeedback.record({
703
+ sessionId: this.sessionId,
704
+ ...entry
705
+ });
706
+ if (!carried.ok) return {
707
+ ok: false,
708
+ error: {
709
+ code: carried.error.code,
710
+ message: carried.error.message
711
+ }
467
712
  };
468
- document.addEventListener("pointerdown", onPointerDown);
469
- document.addEventListener("keydown", onKeyDown);
470
- return () => {
471
- document.removeEventListener("pointerdown", onPointerDown);
472
- document.removeEventListener("keydown", onKeyDown);
713
+ if (carried.value.ok) return { ok: true };
714
+ return {
715
+ ok: false,
716
+ error: {
717
+ code: carried.value.error.code,
718
+ message: describe(carried.value.error.code)
719
+ }
473
720
  };
474
- }, [noteOpen, closeNote]);
475
- const wasOpen = (0, react.useRef)(false);
476
- (0, react.useEffect)(() => {
477
- if (noteOpen) {
478
- wasOpen.current = true;
479
- return;
480
- }
481
- if (wasOpen.current) triggerRef.current?.focus();
482
- wasOpen.current = false;
483
- }, [noteOpen]);
484
- const likeLabel = rating === "positive" ? t("action.likeActive") : t("action.like");
485
- const dislikeLabel = rating === "negative" ? t("action.dislikeActive") : t("action.dislike");
486
- return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
487
- (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
488
- label: likeLabel,
489
- side: "bottom",
490
- children: (0, react_jsx_runtime.jsx)("button", {
491
- type: "button",
492
- className: MessageFeedbackActions_module_css_default.action,
493
- "aria-label": likeLabel,
494
- "aria-pressed": rating === "positive",
495
- "data-active": rating === "positive" || void 0,
496
- disabled: pending,
497
- onFocus: seed,
498
- onPointerEnter: seed,
499
- onClick: () => {
500
- onRate("positive");
501
- },
502
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconLikeOutline16, {})
503
- })
504
- }),
505
- (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
506
- label: dislikeLabel,
507
- side: "bottom",
508
- children: (0, react_jsx_runtime.jsx)("button", {
509
- type: "button",
510
- className: MessageFeedbackActions_module_css_default.action,
511
- "aria-label": dislikeLabel,
512
- "aria-pressed": rating === "negative",
513
- "data-active": rating === "negative" || void 0,
514
- disabled: pending,
515
- onFocus: seed,
516
- onPointerEnter: seed,
517
- onClick: () => {
518
- onRate("negative");
519
- },
520
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDislikeOutline16, {})
521
- })
522
- }),
523
- rating !== void 0 && (0, react_jsx_runtime.jsx)("button", {
524
- ref: triggerRef,
525
- type: "button",
526
- className: MessageFeedbackActions_module_css_default.noteOpen,
527
- "aria-haspopup": "dialog",
528
- "aria-expanded": noteOpen,
529
- onClick: toggleNote,
530
- children: item?.note === void 0 ? t("note.open") : item.note
531
- }),
532
- rowFailure === null && loadFailed && (0, react_jsx_runtime.jsx)("span", {
533
- className: MessageFeedbackActions_module_css_default.failure,
534
- role: "status",
535
- children: t("error.load")
536
- }),
537
- rowFailure !== null && (0, react_jsx_runtime.jsx)("span", {
538
- className: MessageFeedbackActions_module_css_default.failure,
539
- role: "status",
540
- children: rowFailure
541
- }),
542
- !(rating !== void 0 && noteOpen) && noteFailure !== null && (0, react_jsx_runtime.jsx)("span", {
543
- className: MessageFeedbackActions_module_css_default.failure,
544
- role: "status",
545
- children: noteFailure
546
- }),
547
- rating !== void 0 && noteOpen && (0, react_dom.createPortal)((0, react_jsx_runtime.jsxs)("div", {
548
- ref: panelRef,
549
- className: MessageFeedbackActions_module_css_default.notePanel,
550
- role: "dialog",
551
- "aria-label": t("note.dialog"),
552
- style: pos ?? MEASURE_STYLE,
553
- children: [
554
- (0, react_jsx_runtime.jsx)("textarea", {
555
- ref: inputRef,
556
- className: MessageFeedbackActions_module_css_default.noteInput,
557
- "aria-label": t("note.aria"),
558
- placeholder: t("note.placeholder"),
559
- value: draft,
560
- rows: 3,
561
- onChange: (event) => {
562
- setDraft(event.target.value);
563
- }
564
- }),
565
- (0, react_jsx_runtime.jsxs)("div", {
566
- className: MessageFeedbackActions_module_css_default.noteActions,
567
- children: [(0, react_jsx_runtime.jsx)("button", {
568
- type: "button",
569
- className: MessageFeedbackActions_module_css_default.noteSave,
570
- disabled: pending,
571
- onClick: () => {
572
- onSaveNote(rating);
573
- },
574
- children: t("note.save")
575
- }), (0, react_jsx_runtime.jsx)("button", {
576
- type: "button",
577
- className: MessageFeedbackActions_module_css_default.noteCancel,
578
- onClick: closeNote,
579
- children: t("note.cancel")
580
- })]
581
- }),
582
- noteFailure !== null && (0, react_jsx_runtime.jsx)("span", {
583
- className: MessageFeedbackActions_module_css_default.failure,
584
- role: "status",
585
- children: noteFailure
586
- })
587
- ]
588
- }), document.body)
589
- ] });
590
- }
721
+ }
722
+ /** Drop both controllers when the owning fiber unloads. */
723
+ dispose() {
724
+ this.feedback.dispose();
725
+ this.dialog.dispose();
726
+ }
727
+ };
591
728
  //#endregion
592
729
  //#region lib/types/client/locales.js
593
730
  /** `feedback` namespace dictionaries. */
@@ -597,15 +734,22 @@ window.__ModuleLoader__.load({
597
734
  "action.likeActive": "取消标记",
598
735
  "action.dislike": "有问题的回答",
599
736
  "action.dislikeActive": "取消标记",
600
- "note.open": "补充说明",
601
- "note.dialog": "反馈",
602
- "note.placeholder": "这条回答哪里好,或哪里有问题?(可选)",
603
- "note.save": "保存",
604
- "note.cancel": "取消",
605
- "note.aria": "反馈说明",
737
+ "dialog.title": "提交反馈",
738
+ "dialog.categories": "反馈分类",
739
+ "dialog.detail": "反馈详情",
740
+ "dialog.hint": "填写详情以帮助我们改进体验,提交内容会包括当前对话的日志",
741
+ "category.task-result": "任务结果",
742
+ "category.instruction-following": "指令理解与遵循",
743
+ "category.product-interaction": "产品功能与交互",
744
+ "category.service-stability": "服务稳定性",
745
+ "category.resource-cost": "资源使用与费用",
746
+ "category.security-privacy-permission": "安全隐私与权限",
747
+ "category.other": "其他",
748
+ "toast.recorded": "感谢你的反馈",
606
749
  "error.conflict": "这条反馈已在别处改动,已显示最新状态",
607
750
  "error.load": "反馈状态加载失败",
608
- "error.generic": "反馈保存失败"
751
+ "error.generic": "反馈保存失败",
752
+ "error.noteTooLarge": "描述太长,请缩短后再提交"
609
753
  };
610
754
  /** English dictionary, checked complete against the zh key set. */
611
755
  const en = {
@@ -613,38 +757,46 @@ window.__ModuleLoader__.load({
613
757
  "action.likeActive": "Remove rating",
614
758
  "action.dislike": "Bad response",
615
759
  "action.dislikeActive": "Remove rating",
616
- "note.open": "Add a note",
617
- "note.dialog": "Feedback",
618
- "note.placeholder": "What was good, or what went wrong? (optional)",
619
- "note.save": "Save",
620
- "note.cancel": "Cancel",
621
- "note.aria": "Feedback note",
760
+ "dialog.title": "Submit feedback",
761
+ "dialog.categories": "Feedback category",
762
+ "dialog.detail": "Feedback details",
763
+ "dialog.hint": "Add details to help us improve. Your submission will include the current conversation log.",
764
+ "category.task-result": "Task result",
765
+ "category.instruction-following": "Instruction understanding and following",
766
+ "category.product-interaction": "Product features and interaction",
767
+ "category.service-stability": "Service stability",
768
+ "category.resource-cost": "Resource usage and cost",
769
+ "category.security-privacy-permission": "Security, privacy, and permissions",
770
+ "category.other": "Other",
771
+ "toast.recorded": "Thanks for your feedback",
622
772
  "error.conflict": "This feedback changed elsewhere; the latest state is shown",
623
773
  "error.load": "Could not load feedback",
624
- "error.generic": "Could not save feedback"
774
+ "error.generic": "Could not save feedback",
775
+ "error.noteTooLarge": "The description is too long; shorten it and submit again"
625
776
  };
626
777
  //#endregion
627
778
  //#region lib/types/client/index.js
628
779
  /**
629
- * Message feedback plugin, browser half: the Like/Dislike entry in the
630
- * conversation.chat.assistant-actions strip. One MessageFeedbackController per
631
- * Session backs every message control in that Session, so a single list read
632
- * seeds the whole transcript. Mutations go through the generated
633
- * messageFeedback Remote; the Host owns per-item compare-and-set.
780
+ * Feedback surface plugin, browser half: the Like/Dislike entry in the
781
+ * conversation.chat.assistant-actions strip, the feedback dialog and its
782
+ * acknowledgement toast in conversation.input.overlay, and the `/feedback`
783
+ * decoration that opens the dialog from the composer menu or a bare typed
784
+ * command. One FeedbackSurface per Session backs every entry in that Session.
634
785
  * @module @deepseek-ai/dsh-client-ui-message-feedback/client
635
786
  */
636
787
  /** Dictionary namespace owned by this plugin. */
637
788
  const NS = "feedback";
638
- /** Required services: the slot registry, the Remote namespace, and the copy. */
789
+ /** Required services: the slot registry, the two Remote namespaces, and the copy. */
639
790
  const inject = [
640
791
  "slots",
641
792
  "remote",
642
793
  "remote.messageFeedback",
794
+ "remote.sessionFeedback",
643
795
  "locale"
644
796
  ];
645
797
  /**
646
- * Client plugin body: the per-message feedback entry and its per-session
647
- * object layer.
798
+ * Client plugin body: the per-message feedback entry, the Session's dialog
799
+ * entry, the `/feedback` decoration, and their per-session surfaces.
648
800
  * @param ctx - client root context.
649
801
  */
650
802
  function apply(ctx) {
@@ -652,41 +804,79 @@ window.__ModuleLoader__.load({
652
804
  zh,
653
805
  en
654
806
  }), "ui-message-feedback: dictionaries");
655
- const controllers = /* @__PURE__ */ new Map();
656
- const controllerFor = (sessionId) => {
657
- let controller = controllers.get(sessionId);
658
- if (controller === void 0) {
659
- controller = new MessageFeedbackController(ctx, sessionId);
660
- controllers.set(sessionId, controller);
807
+ const surfaces = /* @__PURE__ */ new Map();
808
+ const surfaceFor = (sessionId) => {
809
+ let surface = surfaces.get(sessionId);
810
+ if (surface === void 0) {
811
+ surface = new FeedbackSurface(ctx, sessionId);
812
+ surfaces.set(sessionId, surface);
661
813
  }
662
- return controller;
814
+ return surface;
663
815
  };
816
+ ctx.effect(() => () => {
817
+ for (const surface of surfaces.values()) surface.dispose();
818
+ surfaces.clear();
819
+ }, "ui-message-feedback: per-session surfaces");
664
820
  ctx.on("connection/reset", () => {
665
- for (const controller of controllers.values()) if (controller.getSnapshot().status !== "cold") controller.resync();
821
+ for (const { feedback } of surfaces.values()) if (feedback.getSnapshot().status !== "cold") feedback.resync();
666
822
  });
667
- ctx.slots.inject("conversation.chat.assistant-actions", () => {
668
- const dispose = ctx.slots.register({
669
- name: "conversation.chat.assistant-actions",
670
- id: "feedback",
671
- order: 10,
672
- locale: NS,
673
- inject: (sessionId) => {
674
- const controller = controllerFor(sessionId);
675
- return {
676
- hooks: { feedback: controller },
677
- ensure: () => controller.ensure(),
678
- rate: (messageId, rating, note) => controller.rate(messageId, rating, note),
679
- toggle: (messageId, rating) => controller.toggle(messageId, rating),
680
- clearNote: (messageId) => controller.clearNote(messageId),
681
- clear: (messageId) => controller.clear(messageId)
682
- };
823
+ ctx.slots.inject("conversation.chat.assistant-actions", () => ctx.slots.register({
824
+ name: "conversation.chat.assistant-actions",
825
+ id: "feedback",
826
+ order: 10,
827
+ locale: NS,
828
+ inject: (sessionId) => {
829
+ const { feedback, dialog } = surfaceFor(sessionId);
830
+ return {
831
+ hooks: { feedback },
832
+ ensure: () => feedback.ensure(),
833
+ current: (messageId) => feedback.getSnapshot().items.get(messageId),
834
+ toggle: (messageId, rating) => feedback.toggle(messageId, rating),
835
+ openDialog: (messageId) => {
836
+ dialog.open({
837
+ kind: "message",
838
+ messageId
839
+ });
840
+ },
841
+ acknowledge: () => {
842
+ dialog.acknowledge();
843
+ }
844
+ };
845
+ }
846
+ }, MessageFeedbackActions));
847
+ ctx.slots.inject("conversation.input.overlay", () => ctx.slots.register({
848
+ name: "conversation.input.overlay",
849
+ id: "feedback-dialog",
850
+ order: 2,
851
+ locale: NS,
852
+ inject: (sessionId) => {
853
+ const { dialog } = surfaceFor(sessionId);
854
+ return {
855
+ hooks: { dialog: dialog.state },
856
+ edit: (draft) => {
857
+ dialog.edit(draft);
858
+ },
859
+ submit: () => dialog.submitDraft(),
860
+ dismiss: () => {
861
+ dialog.dismiss();
862
+ },
863
+ dismissToast: (seq) => {
864
+ dialog.dismissToast(seq);
865
+ }
866
+ };
867
+ }
868
+ }, FeedbackDialog));
869
+ ctx.inject(["commandUi"], (scope) => {
870
+ scope.effect(() => scope.commandUi.decorate({
871
+ name: "feedback",
872
+ available: () => true,
873
+ ui: {
874
+ kind: "action",
875
+ run: (session) => {
876
+ surfaceFor(session.sessionId).dialog.open({ kind: "session" });
877
+ }
683
878
  }
684
- }, MessageFeedbackActions);
685
- return () => {
686
- dispose();
687
- for (const controller of controllers.values()) controller.dispose();
688
- controllers.clear();
689
- };
879
+ }), "ui-message-feedback: /feedback decoration");
690
880
  });
691
881
  }
692
882
  //#endregion