@qilin-agent/client-ui-message-feedback 0.0.0-stage → 3.1.3
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/LICENSE +21 -0
- package/README.i18n.yaml +40 -0
- package/README.md +90 -2
- package/README.zh.md +91 -0
- package/lib/client.js +880 -0
- package/lib/index.js +11 -0
- package/lib/types/client/FeedbackDialog.d.ts +16 -0
- package/lib/types/client/MessageFeedbackActions.d.ts +17 -0
- package/lib/types/client/controller.d.ts +127 -0
- package/lib/types/client/dialog.d.ts +83 -0
- package/lib/types/client/index.d.ts +22 -0
- package/lib/types/client/locales.d.ts +56 -0
- package/lib/types/client/slots.d.ts +70 -0
- package/lib/types/client/surface.d.ts +30 -0
- package/lib/types/index.d.ts +9 -0
- package/package.json +78 -3
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
|