@qilin-agent/client-ui-message-feedback 0.0.0-stage → 3.1.4

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