@prettier-ai/dsh-client-ui-user-questions 0.1.2-alpha.1
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 +6 -0
- package/README.md +103 -0
- package/README.zh.md +103 -0
- package/lib/client.js +891 -0
- package/lib/index.js +16 -0
- package/lib/invariant.js +24 -0
- package/lib/types/client/PlanReviewPanel.d.ts +14 -0
- package/lib/types/client/QuestionComposer.d.ts +26 -0
- package/lib/types/client/contract/slots.d.ts +109 -0
- package/lib/types/client/draft-store.d.ts +37 -0
- package/lib/types/client/index.d.ts +34 -0
- package/lib/types/client/locales.d.ts +40 -0
- package/lib/types/index.d.ts +14 -0
- package/lib/types/invariant.d.ts +16 -0
- package/package.json +94 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@prettier-ai/dsh-client-ui-user-questions",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let _prettier_ai_dsh_client_store = require("@prettier-ai/dsh-client-store");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
let react = require("react");
|
|
10
|
+
let _prettier_ai_dsh_client_ui_primitives = require("@prettier-ai/dsh-client-ui-primitives");
|
|
11
|
+
//#region lib/types/client/contract/slots.js
|
|
12
|
+
function settlePendingComposer(settle, failureMessage) {
|
|
13
|
+
try {
|
|
14
|
+
settle();
|
|
15
|
+
return Promise.resolve();
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return Promise.reject(error instanceof Error ? error : new Error(failureMessage, { cause: error }));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Narrow a request to a renderable plan review, or return undefined to leave it
|
|
22
|
+
* to the generic question flow.
|
|
23
|
+
*
|
|
24
|
+
* The card is one decision over one plan, and it claims a request only when it
|
|
25
|
+
* can send every answer that request allows — an intent changes the layout,
|
|
26
|
+
* never which answers are reachable. So the batch must be a single question
|
|
27
|
+
* that declares the intent, carries the plan as its detail, offers the approve
|
|
28
|
+
* label the intent names, and is a binary single choice: at most one option
|
|
29
|
+
* besides approve, and not multi-select. A third option or a multi-select batch
|
|
30
|
+
* has answers two buttons cannot express, so the generic flow keeps it — as it
|
|
31
|
+
* keeps any request whose intent the asker's own service would have rejected,
|
|
32
|
+
* because the client sits downstream of a wire boundary and every request must
|
|
33
|
+
* stay answerable.
|
|
34
|
+
*
|
|
35
|
+
* @param questions - the request's whole question batch.
|
|
36
|
+
* @returns The narrowed review, or undefined when the generic flow owns it.
|
|
37
|
+
*/
|
|
38
|
+
function planReviewOf(questions) {
|
|
39
|
+
if (questions.length !== 1) return void 0;
|
|
40
|
+
const question = questions[0];
|
|
41
|
+
const intent = question.intent;
|
|
42
|
+
if (intent?.kind !== "plan-review" || question.detail === void 0) return void 0;
|
|
43
|
+
if (question.multiSelect === true) return void 0;
|
|
44
|
+
const options = question.options ?? [];
|
|
45
|
+
if (options.length > 2) return void 0;
|
|
46
|
+
const approve = options.find((option) => option.label === intent.approve);
|
|
47
|
+
if (approve === void 0) return void 0;
|
|
48
|
+
const decline = options.find((option) => option.label !== intent.approve);
|
|
49
|
+
return {
|
|
50
|
+
id: question.id,
|
|
51
|
+
question: question.question,
|
|
52
|
+
plan: question.detail,
|
|
53
|
+
approve,
|
|
54
|
+
...decline === void 0 ? {} : { decline }
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
let nextQuestionKey = 0;
|
|
58
|
+
/** Create a wire-preserved user-question rejection. */
|
|
59
|
+
function questionError(message, code) {
|
|
60
|
+
const error = new Error(message);
|
|
61
|
+
error.name = "UserQuestionError";
|
|
62
|
+
error.code = code;
|
|
63
|
+
return error;
|
|
64
|
+
}
|
|
65
|
+
/** One answerable Client presentation of a pending Host waterfall. */
|
|
66
|
+
var PendingQuestion = class {
|
|
67
|
+
sessionId;
|
|
68
|
+
/** Presentation discriminator used by Session pending-interaction consumers. */
|
|
69
|
+
kind;
|
|
70
|
+
/** Opaque render identity and request key for the Session-scoped draft store. */
|
|
71
|
+
key;
|
|
72
|
+
/** The request's question list. */
|
|
73
|
+
questions;
|
|
74
|
+
/** Result returned by the Remote Event listener to the Host waterfall. */
|
|
75
|
+
result;
|
|
76
|
+
#resolve;
|
|
77
|
+
#reject;
|
|
78
|
+
#signal;
|
|
79
|
+
#onAbort;
|
|
80
|
+
#delegated = Symbol("pending question delegated");
|
|
81
|
+
#settled = false;
|
|
82
|
+
/**
|
|
83
|
+
* @param sessionId - Agent/Session identity owning the scoped request.
|
|
84
|
+
* @param questions - complete question batch.
|
|
85
|
+
* @param signal - Host request and delivery lifetime.
|
|
86
|
+
*/
|
|
87
|
+
constructor(sessionId, questions, signal) {
|
|
88
|
+
this.sessionId = sessionId;
|
|
89
|
+
nextQuestionKey += 1;
|
|
90
|
+
this.key = `question:${String(nextQuestionKey)}`;
|
|
91
|
+
this.questions = questions;
|
|
92
|
+
this.kind = planReviewOf(questions) === void 0 ? "question" : "plan-review";
|
|
93
|
+
const completion = Promise.withResolvers();
|
|
94
|
+
this.result = completion.promise;
|
|
95
|
+
this.#resolve = completion.resolve;
|
|
96
|
+
this.#reject = completion.reject;
|
|
97
|
+
this.#signal = signal;
|
|
98
|
+
if (signal === void 0) {
|
|
99
|
+
this.#onAbort = void 0;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const onAbort = () => {
|
|
103
|
+
this.abort(questionError("ask_user_question was aborted before the user answered", "ASK_ABORTED"));
|
|
104
|
+
};
|
|
105
|
+
this.#onAbort = onAbort;
|
|
106
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
107
|
+
if (signal.aborted) onAbort();
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Resolve the Host waterfall with the whole answer batch.
|
|
111
|
+
* @param answer - complete structured answer batch.
|
|
112
|
+
*/
|
|
113
|
+
answer(answer) {
|
|
114
|
+
return settlePendingComposer(() => {
|
|
115
|
+
this.finish(() => {
|
|
116
|
+
this.#resolve(answer);
|
|
117
|
+
});
|
|
118
|
+
}, "pending question settlement failed");
|
|
119
|
+
}
|
|
120
|
+
/** Delegate an unanswered request to the next waterfall listener. */
|
|
121
|
+
delegate() {
|
|
122
|
+
if (this.#settled) return;
|
|
123
|
+
this.finish(() => {
|
|
124
|
+
this.#reject(this.#delegated);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Test whether a rejection requests waterfall delegation.
|
|
129
|
+
* @param reason - rejection received from {@link PendingQuestion.result}.
|
|
130
|
+
* @returns whether {@link PendingQuestion.delegate} produced it.
|
|
131
|
+
*/
|
|
132
|
+
isDelegation(reason) {
|
|
133
|
+
return reason === this.#delegated;
|
|
134
|
+
}
|
|
135
|
+
/** Reject the Host waterfall because the user closed the question. */
|
|
136
|
+
cancel() {
|
|
137
|
+
return settlePendingComposer(() => {
|
|
138
|
+
this.finish(() => {
|
|
139
|
+
this.#reject(questionError("the user cancelled ask_user_question", "ASK_CANCELLED"));
|
|
140
|
+
});
|
|
141
|
+
}, "pending question cancellation failed");
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* End an unanswered presentation when its transport, scope, or plugin lifetime ends.
|
|
145
|
+
* @param reason - rejection exposed to the waiting Remote Event listener.
|
|
146
|
+
*/
|
|
147
|
+
abort(reason) {
|
|
148
|
+
if (this.#settled) return;
|
|
149
|
+
this.finish(() => {
|
|
150
|
+
this.#reject(reason);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
finish(settle) {
|
|
154
|
+
if (this.#settled) throw new Error(`pending question ${this.key} is already settled`);
|
|
155
|
+
this.#settled = true;
|
|
156
|
+
if (this.#signal !== void 0 && this.#onAbort !== void 0) this.#signal.removeEventListener("abort", this.#onAbort);
|
|
157
|
+
settle();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region lib/types/client/draft-store.js
|
|
162
|
+
/**
|
|
163
|
+
* Session-scoped draft state for the generic question composer. The Slot
|
|
164
|
+
* registry owns store instances; this module exports only the factory so a
|
|
165
|
+
* plugin reload cannot reuse a module-global handle.
|
|
166
|
+
*/
|
|
167
|
+
const emptyProgress = () => ({
|
|
168
|
+
index: 0,
|
|
169
|
+
drafts: []
|
|
170
|
+
});
|
|
171
|
+
/**
|
|
172
|
+
* Declare the question composer's transient Session store.
|
|
173
|
+
* @returns a non-persisted store handle whose instance is owned by the Slot registry.
|
|
174
|
+
*/
|
|
175
|
+
function createQuestionDraftStore() {
|
|
176
|
+
return (0, _prettier_ai_dsh_client_store.defineStore)({
|
|
177
|
+
init: () => ({ progress: emptyProgress() }),
|
|
178
|
+
actions: {
|
|
179
|
+
replace: (draft, requestKey, progress) => {
|
|
180
|
+
draft.requestKey = requestKey;
|
|
181
|
+
draft.progress = progress;
|
|
182
|
+
},
|
|
183
|
+
clear: (draft, requestKey) => {
|
|
184
|
+
if (draft.requestKey !== requestKey) return;
|
|
185
|
+
delete draft.requestKey;
|
|
186
|
+
draft.progress = emptyProgress();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
193
|
+
function r(e) {
|
|
194
|
+
var t, f, n = "";
|
|
195
|
+
if ("string" == typeof e || "number" == typeof e) n += e;
|
|
196
|
+
else if ("object" == typeof e) if (Array.isArray(e)) {
|
|
197
|
+
var o = e.length;
|
|
198
|
+
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
|
|
199
|
+
} else for (f in e) e[f] && (n && (n += " "), n += f);
|
|
200
|
+
return n;
|
|
201
|
+
}
|
|
202
|
+
function clsx() {
|
|
203
|
+
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
|
|
204
|
+
return n;
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region \0dsh-css:/home/runner/work/dsh-publisher/dsh-publisher/upstream/packages/client/ui-user-questions/src/client/PlanReviewPanel.module.css.mjs
|
|
208
|
+
const css$1 = ".bnoQSG_frame{padding:6px calc(var(--dsh-composer-side-clearance) + 16px) 10px;justify-content:center;display:flex}.bnoQSG_card{width:100%;max-width:var(--dsh-chat-content-width);border:1px solid var(--dsw-alias-state-warn-secondary);background:var(--dsw-specific-input-major);max-height:min(60vh,520px);box-shadow:var(--dsw-shadow-lv2);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:20px;flex-direction:column;display:flex;overflow:hidden}.bnoQSG_card,.bnoQSG_card *{box-sizing:border-box}.bnoQSG_strip{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-primary);flex-shrink:0;align-items:center;gap:8px;padding:10px 16px;font-size:13px;line-height:18px;display:flex}.bnoQSG_dot{background:var(--dsw-alias-state-warn-primary);border-radius:50%;width:8px;height:8px}.bnoQSG_body{overscroll-behavior:contain;flex:auto;min-height:0;padding:12px 16px 4px;font-size:14px;line-height:22px;overflow-y:auto}.bnoQSG_footer{flex-shrink:0;justify-content:space-between;align-items:center;gap:12px;padding:8px 16px 12px;display:flex}.bnoQSG_feedback{min-height:16px;color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px}.bnoQSG_actions{flex-shrink:0;align-items:center;gap:8px;display:flex}.bnoQSG_discuss{color:var(--dsw-alias-label-secondary);gap:6px}.bnoQSG_discuss:hover:not(:disabled){color:var(--dsw-alias-label-primary)}@media (width<=720px){.bnoQSG_card{border-radius:16px}.bnoQSG_body{padding:10px 12px 4px}.bnoQSG_footer{align-items:flex-end;padding:8px 12px 10px}}";
|
|
209
|
+
const tagId$1 = "@prettier-ai/dsh-client-ui-user-questions/PlanReviewPanel.module.css";
|
|
210
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
211
|
+
const tag = document.createElement("style");
|
|
212
|
+
tag.dataset.plugin = "@prettier-ai/dsh-client-ui-user-questions";
|
|
213
|
+
tag.dataset.pluginCss = tagId$1;
|
|
214
|
+
tag.textContent = css$1;
|
|
215
|
+
document.head.appendChild(tag);
|
|
216
|
+
}
|
|
217
|
+
var PlanReviewPanel_module_css_default = {
|
|
218
|
+
"actions": "bnoQSG_actions",
|
|
219
|
+
"body": "bnoQSG_body",
|
|
220
|
+
"card": "bnoQSG_card",
|
|
221
|
+
"discuss": "bnoQSG_discuss",
|
|
222
|
+
"dot": "bnoQSG_dot",
|
|
223
|
+
"feedback": "bnoQSG_feedback",
|
|
224
|
+
"footer": "bnoQSG_footer",
|
|
225
|
+
"frame": "bnoQSG_frame",
|
|
226
|
+
"strip": "bnoQSG_strip"
|
|
227
|
+
};
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region lib/types/client/PlanReviewPanel.js
|
|
230
|
+
/**
|
|
231
|
+
* Optional-prop spread for a decision button's tooltip: `title` is optional on
|
|
232
|
+
* the DOM props, and exactOptionalPropertyTypes rejects an explicit undefined.
|
|
233
|
+
*
|
|
234
|
+
* @param description - the asker's option description, when it carries one.
|
|
235
|
+
* @returns The `title` prop to spread, or nothing.
|
|
236
|
+
*/
|
|
237
|
+
function tooltip(description) {
|
|
238
|
+
return description === void 0 ? {} : { title: description };
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Render a plan review as a decision card.
|
|
242
|
+
*
|
|
243
|
+
* @param props - the question domain face, the narrowed plan review, and `t`.
|
|
244
|
+
* @returns The plan-review takeover for this request.
|
|
245
|
+
*/
|
|
246
|
+
function PlanReviewPanel({ pending, review, t }) {
|
|
247
|
+
const markdownLabels = (0, react.useMemo)(() => ({
|
|
248
|
+
code: {
|
|
249
|
+
copyLabel: t("copy"),
|
|
250
|
+
copiedLabel: t("copied")
|
|
251
|
+
},
|
|
252
|
+
footnotes: t("markdown.footnotes")
|
|
253
|
+
}), [t]);
|
|
254
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
255
|
+
const [error, setError] = (0, react.useState)(null);
|
|
256
|
+
const settle = (send) => {
|
|
257
|
+
setBusy(true);
|
|
258
|
+
setError(null);
|
|
259
|
+
send().catch((cause) => {
|
|
260
|
+
setBusy(false);
|
|
261
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
262
|
+
});
|
|
263
|
+
};
|
|
264
|
+
const decide = (label) => {
|
|
265
|
+
settle(() => pending.answer({ answers: [{
|
|
266
|
+
id: review.id,
|
|
267
|
+
selected: [label]
|
|
268
|
+
}] }));
|
|
269
|
+
};
|
|
270
|
+
const decline = review.decline;
|
|
271
|
+
return (0, react_jsx_runtime.jsx)("div", {
|
|
272
|
+
className: PlanReviewPanel_module_css_default.frame,
|
|
273
|
+
"data-plan-review-key": pending.key,
|
|
274
|
+
children: (0, react_jsx_runtime.jsxs)("section", {
|
|
275
|
+
className: PlanReviewPanel_module_css_default.card,
|
|
276
|
+
"aria-label": review.question,
|
|
277
|
+
children: [
|
|
278
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
279
|
+
className: PlanReviewPanel_module_css_default.strip,
|
|
280
|
+
children: [(0, react_jsx_runtime.jsx)("span", { className: PlanReviewPanel_module_css_default.dot }), t("plan.header")]
|
|
281
|
+
}),
|
|
282
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
283
|
+
className: PlanReviewPanel_module_css_default.body,
|
|
284
|
+
"data-plan-review-scroll": true,
|
|
285
|
+
children: (0, react_jsx_runtime.jsx)(_prettier_ai_dsh_client_ui_primitives.MarkdownText, {
|
|
286
|
+
text: review.plan,
|
|
287
|
+
labels: markdownLabels
|
|
288
|
+
})
|
|
289
|
+
}),
|
|
290
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
291
|
+
className: PlanReviewPanel_module_css_default.footer,
|
|
292
|
+
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
293
|
+
className: PlanReviewPanel_module_css_default.feedback,
|
|
294
|
+
role: "status",
|
|
295
|
+
children: error
|
|
296
|
+
}), (0, react_jsx_runtime.jsxs)("div", {
|
|
297
|
+
className: PlanReviewPanel_module_css_default.actions,
|
|
298
|
+
children: [
|
|
299
|
+
(0, react_jsx_runtime.jsx)(_prettier_ai_dsh_client_ui_primitives.Button, {
|
|
300
|
+
variant: "ghost",
|
|
301
|
+
className: PlanReviewPanel_module_css_default.discuss,
|
|
302
|
+
icon: (0, react_jsx_runtime.jsx)(_prettier_ai_dsh_client_ui_primitives.IconEditOutline16, { size: 14 }),
|
|
303
|
+
disabled: busy,
|
|
304
|
+
onClick: () => {
|
|
305
|
+
settle(() => pending.cancel());
|
|
306
|
+
},
|
|
307
|
+
children: t("plan.discuss")
|
|
308
|
+
}),
|
|
309
|
+
decline !== void 0 && (0, react_jsx_runtime.jsx)(_prettier_ai_dsh_client_ui_primitives.Button, {
|
|
310
|
+
variant: "outline",
|
|
311
|
+
...tooltip(decline.description),
|
|
312
|
+
disabled: busy,
|
|
313
|
+
onClick: () => {
|
|
314
|
+
decide(decline.label);
|
|
315
|
+
},
|
|
316
|
+
children: t("plan.decline")
|
|
317
|
+
}),
|
|
318
|
+
(0, react_jsx_runtime.jsx)(_prettier_ai_dsh_client_ui_primitives.Button, {
|
|
319
|
+
variant: "primary",
|
|
320
|
+
...tooltip(review.approve.description),
|
|
321
|
+
disabled: busy,
|
|
322
|
+
onClick: () => {
|
|
323
|
+
decide(review.approve.label);
|
|
324
|
+
},
|
|
325
|
+
children: t("plan.approve")
|
|
326
|
+
})
|
|
327
|
+
]
|
|
328
|
+
})]
|
|
329
|
+
})
|
|
330
|
+
]
|
|
331
|
+
})
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region \0dsh-css:/home/runner/work/dsh-publisher/dsh-publisher/upstream/packages/client/ui-user-questions/src/client/QuestionComposer.module.css.mjs
|
|
336
|
+
const css = ".DM9ViG_frame{padding:6px calc(var(--dsh-composer-side-clearance) + 16px) 10px;justify-content:center;display:flex}.DM9ViG_card{width:100%;max-width:var(--dsh-chat-content-width);border:1px solid var(--dsw-alias-border-l2-darkmode-thin);background:var(--dsw-specific-input-major);max-height:min(60vh,520px);box-shadow:var(--dsw-shadow-lv2);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:20px;flex-direction:column;padding:0 0 10px;display:flex;overflow:hidden}.DM9ViG_card,.DM9ViG_card *{box-sizing:border-box}.DM9ViG_cardMinimized{max-height:none}.DM9ViG_cardMinimized .DM9ViG_header{padding-bottom:14px}.DM9ViG_headerActions{flex-shrink:0;align-items:center;gap:4px;display:flex}.DM9ViG_header{flex-shrink:0;justify-content:space-between;align-items:flex-start;gap:16px;padding:20px 16px 0 24px;display:flex}.DM9ViG_headingBlock{min-width:0}.DM9ViG_eyebrow{color:var(--dsw-alias-label-tertiary);margin-bottom:5px;font-size:11px;line-height:16px}.DM9ViG_title{margin:0;font-size:16px;font-weight:500;line-height:22px}.DM9ViG_detail{margin:0 2px 8px}.DM9ViG_footerActions{flex-shrink:0;align-items:center;gap:12px;display:flex}.DM9ViG_pager{flex-shrink:0;align-items:center;gap:6px;display:flex}.DM9ViG_progress{color:var(--dsw-alias-label-secondary);white-space:nowrap;word-spacing:-2px;padding:0 4px;font-size:14px;font-weight:500;line-height:24px}.DM9ViG_iconButton{width:24px;height:24px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:999px;place-items:center;padding:0;display:grid}.DM9ViG_iconButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.DM9ViG_iconButton:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.DM9ViG_body{overscroll-behavior:contain;flex-direction:column;flex:auto;min-height:0;display:flex;overflow-y:auto}.DM9ViG_options{flex-direction:column;gap:1px;margin:8px 0 0;padding:4px 12px;display:flex}.DM9ViG_option{width:100%;min-height:40px;color:inherit;text-align:left;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:12px;flex-shrink:0;align-items:flex-start;gap:8px;padding:8px 12px 8px 8px;transition:background-color .12s,border-color .12s;display:flex}.DM9ViG_option:hover:not(:disabled),.DM9ViG_optionSelected{background:var(--dsw-alias-interactive-bg-hover)}.DM9ViG_optionSelected{border-color:var(--dsw-alias-border-l2)}.DM9ViG_option:disabled{cursor:default}.DM9ViG_number{background:var(--dsw-alias-bg-overlay);width:20px;height:20px;color:var(--dsw-alias-label-secondary);border-radius:6px;flex:0 0 20px;place-items:center;margin-top:2px;font-size:12px;font-weight:500;line-height:18px;display:grid}.DM9ViG_checkbox{flex:0 0 20px;place-items:center;width:20px;height:20px;margin-top:2px;display:grid}.DM9ViG_checkbox:before{content:\"\";border:1px solid var(--dsw-alias-border-l4);border-radius:4px;grid-area:1/1;width:14px;height:14px;transition:background-color .12s,border-color .12s}.DM9ViG_checkbox>svg{grid-area:1/1}.DM9ViG_checkboxChecked{color:var(--dsw-alias-label-primary-foreground)}.DM9ViG_checkboxChecked:before{border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-label-primary)}.DM9ViG_optionCopy{flex:1;min-width:0}.DM9ViG_optionLine{flex-wrap:wrap;align-items:baseline;gap:2px 6px;display:flex}.DM9ViG_optionLabel{font-size:14px;font-weight:500;line-height:24px}.DM9ViG_badge{background:var(--dsw-specific-sidebar-nav-item-active-accent);color:var(--dsw-alias-button-info-fill);border-radius:6px;padding:0 4px;font-size:11px;font-weight:600;line-height:18px}.DM9ViG_description{color:var(--dsw-alias-label-tertiary);font-size:14px;font-weight:400;line-height:24px}.DM9ViG_customRow{border:1px solid #0000;border-radius:12px;flex-shrink:0;align-items:flex-start;gap:8px;width:100%;min-height:40px;padding:8px 12px 8px 8px;transition:background-color .12s,border-color .12s;display:flex}.DM9ViG_customRow:hover,.DM9ViG_customRow:focus-within,.DM9ViG_customRowActive{background:var(--dsw-alias-interactive-bg-hover)}.DM9ViG_customRow:focus-within,.DM9ViG_customRowActive{border-color:var(--dsw-alias-border-l2)}.DM9ViG_field{--dsh-answer-field-padding:0;min-width:0;display:grid}.DM9ViG_field>*{min-width:0;padding:var(--dsh-answer-field-padding);font:inherit;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;grid-area:1/1;font-size:14px;line-height:24px}.DM9ViG_fieldMirror{box-sizing:content-box;visibility:hidden;max-height:144px;overflow:hidden}.DM9ViG_fieldInput{resize:none;color:var(--dsw-alias-label-primary);caret-color:var(--dsw-alias-state-business-primary);background:0 0;border:none;outline:none;overflow-y:auto}.DM9ViG_fieldInput::placeholder{color:var(--dsw-alias-label-caption)}.DM9ViG_customInline{flex:1}.DM9ViG_customBlock{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-module-platform);--dsh-answer-field-padding:8px 12px;border-radius:10px;flex-shrink:0;min-height:64px;margin:0 12px}.DM9ViG_customBlock:focus-within{border-color:var(--dsw-alias-state-business-primary)}.DM9ViG_footer{flex-shrink:0;justify-content:space-between;align-items:center;gap:12px;margin-top:12px;padding:0 10px 0 18px;display:flex}.DM9ViG_feedback{min-height:16px;color:var(--dsw-alias-state-error-primary);text-align:right;flex:1;font-size:11px;line-height:16px}@media (width<=720px){.DM9ViG_card{border-radius:16px}.DM9ViG_header{padding:10px 12px 0 18px}.DM9ViG_options{padding:4px 8px}.DM9ViG_title{font-size:15px;line-height:21px}.DM9ViG_option,.DM9ViG_customRow{padding:8px 6px}.DM9ViG_footer{align-items:flex-end;padding:0 10px}.DM9ViG_footerActions{flex-shrink:0}}@media (prefers-reduced-motion:reduce){.DM9ViG_option,.DM9ViG_customRow{transition:none}}";
|
|
337
|
+
const tagId = "@prettier-ai/dsh-client-ui-user-questions/QuestionComposer.module.css";
|
|
338
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
339
|
+
const tag = document.createElement("style");
|
|
340
|
+
tag.dataset.plugin = "@prettier-ai/dsh-client-ui-user-questions";
|
|
341
|
+
tag.dataset.pluginCss = tagId;
|
|
342
|
+
tag.textContent = css;
|
|
343
|
+
document.head.appendChild(tag);
|
|
344
|
+
}
|
|
345
|
+
var QuestionComposer_module_css_default = {
|
|
346
|
+
"badge": "DM9ViG_badge",
|
|
347
|
+
"body": "DM9ViG_body",
|
|
348
|
+
"card": "DM9ViG_card",
|
|
349
|
+
"cardMinimized": "DM9ViG_cardMinimized",
|
|
350
|
+
"checkbox": "DM9ViG_checkbox",
|
|
351
|
+
"checkboxChecked": "DM9ViG_checkboxChecked",
|
|
352
|
+
"customBlock": "DM9ViG_customBlock",
|
|
353
|
+
"customInline": "DM9ViG_customInline",
|
|
354
|
+
"customRow": "DM9ViG_customRow",
|
|
355
|
+
"customRowActive": "DM9ViG_customRowActive",
|
|
356
|
+
"description": "DM9ViG_description",
|
|
357
|
+
"detail": "DM9ViG_detail",
|
|
358
|
+
"eyebrow": "DM9ViG_eyebrow",
|
|
359
|
+
"feedback": "DM9ViG_feedback",
|
|
360
|
+
"field": "DM9ViG_field",
|
|
361
|
+
"fieldInput": "DM9ViG_fieldInput",
|
|
362
|
+
"fieldMirror": "DM9ViG_fieldMirror",
|
|
363
|
+
"footer": "DM9ViG_footer",
|
|
364
|
+
"footerActions": "DM9ViG_footerActions",
|
|
365
|
+
"frame": "DM9ViG_frame",
|
|
366
|
+
"header": "DM9ViG_header",
|
|
367
|
+
"headerActions": "DM9ViG_headerActions",
|
|
368
|
+
"headingBlock": "DM9ViG_headingBlock",
|
|
369
|
+
"iconButton": "DM9ViG_iconButton",
|
|
370
|
+
"number": "DM9ViG_number",
|
|
371
|
+
"option": "DM9ViG_option",
|
|
372
|
+
"optionCopy": "DM9ViG_optionCopy",
|
|
373
|
+
"optionLabel": "DM9ViG_optionLabel",
|
|
374
|
+
"optionLine": "DM9ViG_optionLine",
|
|
375
|
+
"optionSelected": "DM9ViG_optionSelected",
|
|
376
|
+
"options": "DM9ViG_options",
|
|
377
|
+
"pager": "DM9ViG_pager",
|
|
378
|
+
"progress": "DM9ViG_progress",
|
|
379
|
+
"title": "DM9ViG_title"
|
|
380
|
+
};
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region lib/types/client/QuestionComposer.js
|
|
383
|
+
/**
|
|
384
|
+
* Split the conventional recommendation suffix without changing the answer value.
|
|
385
|
+
* @param label - Original option label returned if selected.
|
|
386
|
+
* @returns Display label plus recommendation state.
|
|
387
|
+
*/
|
|
388
|
+
function parseRecommendedLabel(label) {
|
|
389
|
+
const suffix = /\s*(?:\((?:recommended|推荐)\)|((?:recommended|推荐)))\s*$/i;
|
|
390
|
+
return suffix.test(label) ? {
|
|
391
|
+
label: label.replace(suffix, ""),
|
|
392
|
+
recommended: true
|
|
393
|
+
} : {
|
|
394
|
+
label,
|
|
395
|
+
recommended: false
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
/** Return whether a text-field key event belongs to an active IME composition. */
|
|
399
|
+
function isComposing(event) {
|
|
400
|
+
return event.nativeEvent.isComposing || event.nativeEvent.keyCode === 229;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Auto-growing free-text answer: a textarea, so a long answer soft-wraps and
|
|
404
|
+
* Shift+Enter breaks a line, over a hidden mirror that owns the height.
|
|
405
|
+
*
|
|
406
|
+
* The mirror renders the draft plus a trailing newline in normal flow and so
|
|
407
|
+
* sizes the grid row (counting rows by '\n' cannot see soft wraps); the
|
|
408
|
+
* textarea shares that one cell and stretches to it, and `rows={1}` keeps the
|
|
409
|
+
* control's own intrinsic height out of the row sizing so the mirror alone
|
|
410
|
+
* decides. Past the mirror's cap the textarea scrolls itself — it is the only
|
|
411
|
+
* scrollport in the stack, there being no second glyph layer to keep aligned.
|
|
412
|
+
* Mirror and textarea MUST share font, line-height, padding and wrapping rules
|
|
413
|
+
* or the two heights diverge.
|
|
414
|
+
*
|
|
415
|
+
* @param props - visual variant, draft text, and the field's event handlers.
|
|
416
|
+
* @returns The mirrored auto-growing field.
|
|
417
|
+
*/
|
|
418
|
+
function AnswerField(props) {
|
|
419
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
420
|
+
className: clsx(QuestionComposer_module_css_default.field, props.variant === "inline" ? QuestionComposer_module_css_default.customInline : QuestionComposer_module_css_default.customBlock),
|
|
421
|
+
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
422
|
+
"aria-hidden": true,
|
|
423
|
+
className: QuestionComposer_module_css_default.fieldMirror,
|
|
424
|
+
children: `${props.value}\n`
|
|
425
|
+
}), (0, react_jsx_runtime.jsx)("textarea", {
|
|
426
|
+
autoFocus: props.autoFocus,
|
|
427
|
+
className: QuestionComposer_module_css_default.fieldInput,
|
|
428
|
+
value: props.value,
|
|
429
|
+
disabled: props.disabled,
|
|
430
|
+
rows: 1,
|
|
431
|
+
placeholder: props.placeholder,
|
|
432
|
+
onFocus: props.onFocus,
|
|
433
|
+
onChange: props.onChange,
|
|
434
|
+
onKeyDown: props.onKeyDown
|
|
435
|
+
})]
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Composer takeover router. Generic-question drafts live in this entry's
|
|
440
|
+
* Session-scoped Slot store, keyed by the pending carrier, so a strict Session
|
|
441
|
+
* entry remount restores the same request without exposing it to another one.
|
|
442
|
+
*
|
|
443
|
+
* One takeover, two presentations: a request that declares a presentation intent this
|
|
444
|
+
* package renders uses that presentation (a plan review is one decision over one
|
|
445
|
+
* plan, not a question set), and every other request takes the generic flow.
|
|
446
|
+
* The routing lives here, at the one entry that owns the composer seat, so
|
|
447
|
+
* neither presentation can claim a request the other is already rendering.
|
|
448
|
+
*
|
|
449
|
+
* @param props - the selector-matched pending question carrier plus the framework standard kit.
|
|
450
|
+
* @returns The question flow, or the intent's own surface, for this request.
|
|
451
|
+
*/
|
|
452
|
+
function QuestionComposer(props) {
|
|
453
|
+
const question = props.matched;
|
|
454
|
+
const review = (0, react.useMemo)(() => planReviewOf(question.questions), [question]);
|
|
455
|
+
return review === void 0 ? (0, react_jsx_runtime.jsx)(QuestionFlow, {
|
|
456
|
+
pending: question,
|
|
457
|
+
t: props.t,
|
|
458
|
+
useStore: props.useStore,
|
|
459
|
+
actions: props.actions
|
|
460
|
+
}, question.key) : (0, react_jsx_runtime.jsx)(PlanReviewPanel, {
|
|
461
|
+
pending: question,
|
|
462
|
+
review,
|
|
463
|
+
t: props.t
|
|
464
|
+
}, question.key);
|
|
465
|
+
}
|
|
466
|
+
function QuestionFlow({ pending, t, useStore, actions }) {
|
|
467
|
+
const questions = pending.questions;
|
|
468
|
+
const markdownLabels = (0, react.useMemo)(() => ({
|
|
469
|
+
code: {
|
|
470
|
+
copyLabel: t("copy"),
|
|
471
|
+
copiedLabel: t("copied")
|
|
472
|
+
},
|
|
473
|
+
footnotes: t("markdown.footnotes")
|
|
474
|
+
}), [t]);
|
|
475
|
+
const initialProgress = (0, react.useMemo)(() => ({
|
|
476
|
+
index: 0,
|
|
477
|
+
drafts: questions.map(() => ({
|
|
478
|
+
selected: [],
|
|
479
|
+
custom: "",
|
|
480
|
+
skipped: false
|
|
481
|
+
}))
|
|
482
|
+
}), [questions]);
|
|
483
|
+
const { index, drafts } = useStore((state) => state.requestKey === pending.key && state.progress.drafts.length === questions.length ? state.progress : void 0) ?? initialProgress;
|
|
484
|
+
const [busy, setBusy] = (0, react.useState)(null);
|
|
485
|
+
const [error, setError] = (0, react.useState)(null);
|
|
486
|
+
const [minimized, setMinimized] = (0, react.useState)(false);
|
|
487
|
+
const focusedQuestions = (0, react.useRef)(/* @__PURE__ */ new Set());
|
|
488
|
+
const question = questions[index];
|
|
489
|
+
const draft = drafts[index];
|
|
490
|
+
const hasOptions = (question.options?.length ?? 0) > 0;
|
|
491
|
+
const replaceProgress = (nextIndex, nextDrafts) => {
|
|
492
|
+
actions.replace(pending.key, {
|
|
493
|
+
index: nextIndex,
|
|
494
|
+
drafts: nextDrafts
|
|
495
|
+
});
|
|
496
|
+
};
|
|
497
|
+
const cancelFlow = () => {
|
|
498
|
+
setBusy("cancel");
|
|
499
|
+
setError(null);
|
|
500
|
+
pending.cancel().then(() => {
|
|
501
|
+
actions.clear(pending.key);
|
|
502
|
+
}).catch((cause) => {
|
|
503
|
+
setBusy(null);
|
|
504
|
+
setError({ text: cause instanceof Error ? cause.message : String(cause) });
|
|
505
|
+
});
|
|
506
|
+
};
|
|
507
|
+
const updateDraft = (update, nextIndex = index) => {
|
|
508
|
+
replaceProgress(nextIndex, drafts.map((item, itemIndex) => itemIndex === index ? update(item) : item));
|
|
509
|
+
setError(null);
|
|
510
|
+
};
|
|
511
|
+
const choose = (label) => {
|
|
512
|
+
updateDraft((current) => {
|
|
513
|
+
if (question.multiSelect === true) {
|
|
514
|
+
const selected = current.selected.includes(label) ? current.selected.filter((item) => item !== label) : [...current.selected, label];
|
|
515
|
+
return {
|
|
516
|
+
...current,
|
|
517
|
+
selected,
|
|
518
|
+
skipped: false
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
selected: [label],
|
|
523
|
+
custom: "",
|
|
524
|
+
skipped: false
|
|
525
|
+
};
|
|
526
|
+
}, question.multiSelect !== true && index < questions.length - 1 ? index + 1 : index);
|
|
527
|
+
};
|
|
528
|
+
const answered = (item) => item.selected.length > 0 || item.custom.trim() !== "";
|
|
529
|
+
const completed = (item) => answered(item) || item.skipped;
|
|
530
|
+
const submitDrafts = (values) => {
|
|
531
|
+
const missing = values.findIndex((item) => !completed(item));
|
|
532
|
+
if (missing >= 0) {
|
|
533
|
+
replaceProgress(missing, values);
|
|
534
|
+
setError({ key: "error.incomplete" });
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const answer = { answers: questions.map((item, itemIndex) => {
|
|
538
|
+
const value = values[itemIndex];
|
|
539
|
+
if (value.skipped) return {
|
|
540
|
+
id: item.id,
|
|
541
|
+
selected: []
|
|
542
|
+
};
|
|
543
|
+
const custom = value.custom.trim();
|
|
544
|
+
return {
|
|
545
|
+
id: item.id,
|
|
546
|
+
selected: custom === "" || item.multiSelect === true ? value.selected : [],
|
|
547
|
+
...custom === "" ? {} : { custom }
|
|
548
|
+
};
|
|
549
|
+
}) };
|
|
550
|
+
setBusy("answer");
|
|
551
|
+
setError(null);
|
|
552
|
+
pending.answer(answer).then(() => {
|
|
553
|
+
actions.clear(pending.key);
|
|
554
|
+
}).catch((cause) => {
|
|
555
|
+
setBusy(null);
|
|
556
|
+
setError({ text: cause instanceof Error ? cause.message : String(cause) });
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
const continueFlow = () => {
|
|
560
|
+
if (!answered(draft)) {
|
|
561
|
+
setError({ key: "error.unanswered" });
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (index < questions.length - 1) {
|
|
565
|
+
replaceProgress(index + 1, drafts);
|
|
566
|
+
setError(null);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
submitDrafts(drafts);
|
|
570
|
+
};
|
|
571
|
+
const draftCustom = (event) => {
|
|
572
|
+
const value = event.target.value;
|
|
573
|
+
updateDraft((current) => ({
|
|
574
|
+
...current,
|
|
575
|
+
selected: question.multiSelect === true ? current.selected : [],
|
|
576
|
+
custom: value,
|
|
577
|
+
skipped: false
|
|
578
|
+
}));
|
|
579
|
+
};
|
|
580
|
+
const continueFromCustom = (event) => {
|
|
581
|
+
if (event.key !== "Enter" || event.shiftKey || isComposing(event)) return;
|
|
582
|
+
event.preventDefault();
|
|
583
|
+
continueFlow();
|
|
584
|
+
};
|
|
585
|
+
const skipQuestion = () => {
|
|
586
|
+
const nextDrafts = drafts.map((item, itemIndex) => itemIndex === index ? {
|
|
587
|
+
selected: [],
|
|
588
|
+
custom: "",
|
|
589
|
+
skipped: true
|
|
590
|
+
} : item);
|
|
591
|
+
replaceProgress(index < questions.length - 1 ? index + 1 : index, nextDrafts);
|
|
592
|
+
setError(null);
|
|
593
|
+
if (index < questions.length - 1) return;
|
|
594
|
+
submitDrafts(nextDrafts);
|
|
595
|
+
};
|
|
596
|
+
return (0, react_jsx_runtime.jsx)("div", {
|
|
597
|
+
className: QuestionComposer_module_css_default.frame,
|
|
598
|
+
"data-question-key": pending.key,
|
|
599
|
+
children: (0, react_jsx_runtime.jsxs)("section", {
|
|
600
|
+
className: clsx(QuestionComposer_module_css_default.card, minimized && QuestionComposer_module_css_default.cardMinimized),
|
|
601
|
+
"aria-labelledby": `question-${pending.key}-${String(index)}`,
|
|
602
|
+
children: [(0, react_jsx_runtime.jsxs)("header", {
|
|
603
|
+
className: QuestionComposer_module_css_default.header,
|
|
604
|
+
children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
605
|
+
className: QuestionComposer_module_css_default.headingBlock,
|
|
606
|
+
children: [question.header !== void 0 && (0, react_jsx_runtime.jsx)("div", {
|
|
607
|
+
className: QuestionComposer_module_css_default.eyebrow,
|
|
608
|
+
children: question.header
|
|
609
|
+
}), (0, react_jsx_runtime.jsx)("h2", {
|
|
610
|
+
className: QuestionComposer_module_css_default.title,
|
|
611
|
+
id: `question-${pending.key}-${String(index)}`,
|
|
612
|
+
children: question.question
|
|
613
|
+
})]
|
|
614
|
+
}), (0, react_jsx_runtime.jsxs)("div", {
|
|
615
|
+
className: QuestionComposer_module_css_default.headerActions,
|
|
616
|
+
children: [(0, react_jsx_runtime.jsx)("button", {
|
|
617
|
+
type: "button",
|
|
618
|
+
className: QuestionComposer_module_css_default.iconButton,
|
|
619
|
+
"aria-label": t(minimized ? "nav.maximize" : "nav.minimize"),
|
|
620
|
+
title: t(minimized ? "nav.maximize" : "nav.minimize"),
|
|
621
|
+
"aria-expanded": !minimized,
|
|
622
|
+
disabled: busy !== null,
|
|
623
|
+
onClick: () => {
|
|
624
|
+
setMinimized((current) => !current);
|
|
625
|
+
},
|
|
626
|
+
children: minimized ? (0, react_jsx_runtime.jsx)(_prettier_ai_dsh_client_ui_primitives.IconChevronUpOutline14, {}) : (0, react_jsx_runtime.jsx)(_prettier_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
|
|
627
|
+
}), (0, react_jsx_runtime.jsx)("button", {
|
|
628
|
+
type: "button",
|
|
629
|
+
className: QuestionComposer_module_css_default.iconButton,
|
|
630
|
+
"aria-label": t("nav.cancel"),
|
|
631
|
+
title: t("nav.cancel"),
|
|
632
|
+
disabled: busy !== null,
|
|
633
|
+
onClick: cancelFlow,
|
|
634
|
+
children: (0, react_jsx_runtime.jsx)(_prettier_ai_dsh_client_ui_primitives.IconCloseOutline16, {})
|
|
635
|
+
})]
|
|
636
|
+
})]
|
|
637
|
+
}), !minimized && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
638
|
+
className: QuestionComposer_module_css_default.body,
|
|
639
|
+
"data-question-scroll": true,
|
|
640
|
+
children: [question.detail !== void 0 && (0, react_jsx_runtime.jsx)("div", {
|
|
641
|
+
className: QuestionComposer_module_css_default.detail,
|
|
642
|
+
children: (0, react_jsx_runtime.jsx)(_prettier_ai_dsh_client_ui_primitives.MarkdownText, {
|
|
643
|
+
text: question.detail,
|
|
644
|
+
labels: markdownLabels
|
|
645
|
+
})
|
|
646
|
+
}), (0, react_jsx_runtime.jsxs)("div", {
|
|
647
|
+
className: QuestionComposer_module_css_default.options,
|
|
648
|
+
role: question.multiSelect === true ? "group" : "radiogroup",
|
|
649
|
+
children: [(question.options ?? []).map((option, optionIndex) => {
|
|
650
|
+
const selected = draft.selected.includes(option.label);
|
|
651
|
+
const display = parseRecommendedLabel(option.label);
|
|
652
|
+
return (0, react_jsx_runtime.jsxs)("button", {
|
|
653
|
+
type: "button",
|
|
654
|
+
className: clsx(QuestionComposer_module_css_default.option, selected && question.multiSelect !== true && QuestionComposer_module_css_default.optionSelected),
|
|
655
|
+
role: question.multiSelect === true ? "checkbox" : "radio",
|
|
656
|
+
"aria-checked": selected,
|
|
657
|
+
"aria-label": display.label,
|
|
658
|
+
disabled: busy !== null,
|
|
659
|
+
onClick: () => {
|
|
660
|
+
choose(option.label);
|
|
661
|
+
},
|
|
662
|
+
onKeyDown: (event) => {
|
|
663
|
+
if (event.key !== "Enter" || !drafts.every(completed)) return;
|
|
664
|
+
event.preventDefault();
|
|
665
|
+
submitDrafts(drafts);
|
|
666
|
+
},
|
|
667
|
+
children: [question.multiSelect === true ? (0, react_jsx_runtime.jsx)("span", {
|
|
668
|
+
className: clsx(QuestionComposer_module_css_default.checkbox, selected && QuestionComposer_module_css_default.checkboxChecked),
|
|
669
|
+
"aria-hidden": "true",
|
|
670
|
+
children: selected && (0, react_jsx_runtime.jsx)(_prettier_ai_dsh_client_ui_primitives.IconCheckOutline14, { size: 12 })
|
|
671
|
+
}) : (0, react_jsx_runtime.jsx)("span", {
|
|
672
|
+
className: QuestionComposer_module_css_default.number,
|
|
673
|
+
children: optionIndex + 1
|
|
674
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
675
|
+
className: QuestionComposer_module_css_default.optionCopy,
|
|
676
|
+
children: (0, react_jsx_runtime.jsxs)("span", {
|
|
677
|
+
className: QuestionComposer_module_css_default.optionLine,
|
|
678
|
+
children: [
|
|
679
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
680
|
+
className: QuestionComposer_module_css_default.optionLabel,
|
|
681
|
+
children: display.label
|
|
682
|
+
}),
|
|
683
|
+
display.recommended && (0, react_jsx_runtime.jsx)("span", {
|
|
684
|
+
className: QuestionComposer_module_css_default.badge,
|
|
685
|
+
children: t("option.recommended")
|
|
686
|
+
}),
|
|
687
|
+
option.description !== void 0 && (0, react_jsx_runtime.jsx)("span", {
|
|
688
|
+
className: QuestionComposer_module_css_default.description,
|
|
689
|
+
children: option.description
|
|
690
|
+
})
|
|
691
|
+
]
|
|
692
|
+
})
|
|
693
|
+
})]
|
|
694
|
+
}, `${option.label}-${String(optionIndex)}`);
|
|
695
|
+
}), hasOptions ? (0, react_jsx_runtime.jsxs)("div", {
|
|
696
|
+
className: clsx(QuestionComposer_module_css_default.customRow, draft.custom !== "" && QuestionComposer_module_css_default.customRowActive),
|
|
697
|
+
children: [question.multiSelect === true ? (0, react_jsx_runtime.jsx)("span", {
|
|
698
|
+
className: clsx(QuestionComposer_module_css_default.checkbox, draft.custom !== "" && QuestionComposer_module_css_default.checkboxChecked),
|
|
699
|
+
"aria-hidden": "true",
|
|
700
|
+
children: draft.custom !== "" && (0, react_jsx_runtime.jsx)(_prettier_ai_dsh_client_ui_primitives.IconCheckOutline14, { size: 12 })
|
|
701
|
+
}) : (0, react_jsx_runtime.jsx)("span", {
|
|
702
|
+
className: QuestionComposer_module_css_default.number,
|
|
703
|
+
"aria-hidden": "true",
|
|
704
|
+
children: (0, react_jsx_runtime.jsx)(_prettier_ai_dsh_client_ui_primitives.IconEditOutline16, { size: 12 })
|
|
705
|
+
}), (0, react_jsx_runtime.jsx)(AnswerField, {
|
|
706
|
+
variant: "inline",
|
|
707
|
+
value: draft.custom,
|
|
708
|
+
disabled: busy !== null,
|
|
709
|
+
placeholder: t("custom.placeholder"),
|
|
710
|
+
onChange: draftCustom,
|
|
711
|
+
onKeyDown: continueFromCustom
|
|
712
|
+
})]
|
|
713
|
+
}) : (0, react_jsx_runtime.jsx)(AnswerField, {
|
|
714
|
+
autoFocus: !focusedQuestions.current.has(index),
|
|
715
|
+
variant: "block",
|
|
716
|
+
value: draft.custom,
|
|
717
|
+
disabled: busy !== null,
|
|
718
|
+
placeholder: t("custom.placeholder"),
|
|
719
|
+
onFocus: () => {
|
|
720
|
+
focusedQuestions.current.add(index);
|
|
721
|
+
},
|
|
722
|
+
onChange: draftCustom,
|
|
723
|
+
onKeyDown: continueFromCustom
|
|
724
|
+
})]
|
|
725
|
+
})]
|
|
726
|
+
}), (0, react_jsx_runtime.jsxs)("footer", {
|
|
727
|
+
className: QuestionComposer_module_css_default.footer,
|
|
728
|
+
children: [
|
|
729
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
730
|
+
className: QuestionComposer_module_css_default.pager,
|
|
731
|
+
children: [
|
|
732
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
733
|
+
type: "button",
|
|
734
|
+
className: QuestionComposer_module_css_default.iconButton,
|
|
735
|
+
"aria-label": t("nav.prev"),
|
|
736
|
+
disabled: index === 0 || busy !== null,
|
|
737
|
+
onClick: () => {
|
|
738
|
+
replaceProgress(index - 1, drafts);
|
|
739
|
+
setError(null);
|
|
740
|
+
},
|
|
741
|
+
children: (0, react_jsx_runtime.jsx)(_prettier_ai_dsh_client_ui_primitives.IconChevronLeftOutline14, {})
|
|
742
|
+
}),
|
|
743
|
+
(0, react_jsx_runtime.jsxs)("span", {
|
|
744
|
+
className: QuestionComposer_module_css_default.progress,
|
|
745
|
+
children: [
|
|
746
|
+
index + 1,
|
|
747
|
+
" / ",
|
|
748
|
+
questions.length
|
|
749
|
+
]
|
|
750
|
+
}),
|
|
751
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
752
|
+
type: "button",
|
|
753
|
+
className: QuestionComposer_module_css_default.iconButton,
|
|
754
|
+
"aria-label": t("nav.next"),
|
|
755
|
+
disabled: index === questions.length - 1 || busy !== null,
|
|
756
|
+
onClick: () => {
|
|
757
|
+
replaceProgress(index + 1, drafts);
|
|
758
|
+
setError(null);
|
|
759
|
+
},
|
|
760
|
+
children: (0, react_jsx_runtime.jsx)(_prettier_ai_dsh_client_ui_primitives.IconChevronRightOutline14, {})
|
|
761
|
+
})
|
|
762
|
+
]
|
|
763
|
+
}),
|
|
764
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
765
|
+
className: QuestionComposer_module_css_default.feedback,
|
|
766
|
+
role: "status",
|
|
767
|
+
children: error === null ? null : "key" in error ? t(error.key) : error.text
|
|
768
|
+
}),
|
|
769
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
770
|
+
className: QuestionComposer_module_css_default.footerActions,
|
|
771
|
+
children: [(0, react_jsx_runtime.jsx)(_prettier_ai_dsh_client_ui_primitives.Button, {
|
|
772
|
+
variant: "outline",
|
|
773
|
+
disabled: busy !== null,
|
|
774
|
+
onClick: skipQuestion,
|
|
775
|
+
children: t("action.skip")
|
|
776
|
+
}), (0, react_jsx_runtime.jsx)(_prettier_ai_dsh_client_ui_primitives.Button, {
|
|
777
|
+
variant: "primary",
|
|
778
|
+
disabled: busy !== null || !answered(draft),
|
|
779
|
+
onClick: continueFlow,
|
|
780
|
+
children: busy === "answer" ? t("submitting") : index === questions.length - 1 ? t("submit") : t("action.next")
|
|
781
|
+
})]
|
|
782
|
+
})
|
|
783
|
+
]
|
|
784
|
+
})] })]
|
|
785
|
+
})
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
//#endregion
|
|
789
|
+
//#region lib/types/client/locales.js
|
|
790
|
+
/** `question` namespace dictionaries. */
|
|
791
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
792
|
+
const zh = {
|
|
793
|
+
"error.incomplete": "请先完成这道问题。",
|
|
794
|
+
"error.unanswered": "请选择一个选项或填写自定义答案。",
|
|
795
|
+
"nav.prev": "上一题",
|
|
796
|
+
"nav.next": "下一题",
|
|
797
|
+
"nav.minimize": "收起问题卡片",
|
|
798
|
+
"nav.maximize": "展开问题卡片",
|
|
799
|
+
"nav.cancel": "放弃整组问题",
|
|
800
|
+
"option.recommended": "推荐",
|
|
801
|
+
"custom.placeholder": "输入你的答案",
|
|
802
|
+
"action.skip": "跳过本题",
|
|
803
|
+
"action.next": "下一题",
|
|
804
|
+
"plan.header": "计划待审",
|
|
805
|
+
"plan.approve": "确认执行",
|
|
806
|
+
"plan.decline": "拒绝",
|
|
807
|
+
"plan.discuss": "去聊天里说"
|
|
808
|
+
};
|
|
809
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
810
|
+
const en = {
|
|
811
|
+
"error.incomplete": "Please complete this question first.",
|
|
812
|
+
"error.unanswered": "Please select an option or enter a custom answer.",
|
|
813
|
+
"nav.prev": "Previous question",
|
|
814
|
+
"nav.next": "Next question",
|
|
815
|
+
"nav.minimize": "Collapse the question card",
|
|
816
|
+
"nav.maximize": "Expand the question card",
|
|
817
|
+
"nav.cancel": "Dismiss all questions",
|
|
818
|
+
"option.recommended": "Recommended",
|
|
819
|
+
"custom.placeholder": "Type your answer",
|
|
820
|
+
"action.skip": "Skip this question",
|
|
821
|
+
"action.next": "Next",
|
|
822
|
+
"plan.header": "Plan review",
|
|
823
|
+
"plan.approve": "Approve",
|
|
824
|
+
"plan.decline": "Refuse",
|
|
825
|
+
"plan.discuss": "Chat about it"
|
|
826
|
+
};
|
|
827
|
+
//#endregion
|
|
828
|
+
//#region lib/types/client/index.js
|
|
829
|
+
/** Dictionary namespace owned by this plugin. */
|
|
830
|
+
const NS = "question";
|
|
831
|
+
/** Required services: Agent scopes, Remote Events, Session UI, Slot registry, and copy. */
|
|
832
|
+
const inject = [
|
|
833
|
+
"sessions",
|
|
834
|
+
"remote",
|
|
835
|
+
"uiSession",
|
|
836
|
+
"slots",
|
|
837
|
+
"locale"
|
|
838
|
+
];
|
|
839
|
+
/** Present one request until the user answers, cancels, or its lifetime ends. */
|
|
840
|
+
async function answerQuestion(ctx, owner, request, next, registerPendingInteraction) {
|
|
841
|
+
const sessionId = ctx.sessions.scopeOf(owner);
|
|
842
|
+
if (sessionId === void 0) return next();
|
|
843
|
+
const pending = new PendingQuestion(sessionId, request.questions, request.signal);
|
|
844
|
+
const completed = Promise.withResolvers();
|
|
845
|
+
const remove = registerPendingInteraction(pending, async () => {
|
|
846
|
+
pending.delegate();
|
|
847
|
+
await completed.promise;
|
|
848
|
+
});
|
|
849
|
+
try {
|
|
850
|
+
try {
|
|
851
|
+
return await pending.result;
|
|
852
|
+
} catch (error) {
|
|
853
|
+
if (pending.isDelegation(error)) return await next();
|
|
854
|
+
throw error;
|
|
855
|
+
}
|
|
856
|
+
} finally {
|
|
857
|
+
remove();
|
|
858
|
+
completed.resolve();
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Client plugin body: register the `question` dictionaries and the question
|
|
863
|
+
* composer into the composer chain. Zero business face — data and verbs live
|
|
864
|
+
* on the matched carrier; t rides the standard locale seat.
|
|
865
|
+
* @param ctx - client root context.
|
|
866
|
+
*/
|
|
867
|
+
function apply(ctx) {
|
|
868
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
869
|
+
zh,
|
|
870
|
+
en
|
|
871
|
+
}), "ui-user-questions: dictionaries");
|
|
872
|
+
const questionDraftStore = createQuestionDraftStore();
|
|
873
|
+
const registerPendingInteraction = ctx.uiSession.registerPendingInteraction((pending) => pending.kind === "plan-review" ? 2 : 1);
|
|
874
|
+
ctx.slots.inject("conversation.composer", () => ctx.slots.register({
|
|
875
|
+
name: "conversation.composer",
|
|
876
|
+
select: ({ pendingInteraction }) => pendingInteraction instanceof PendingQuestion ? pendingInteraction : null,
|
|
877
|
+
locale: NS,
|
|
878
|
+
store: questionDraftStore
|
|
879
|
+
}, QuestionComposer));
|
|
880
|
+
ctx.remote.$on("user-questions/request", function(request, next) {
|
|
881
|
+
return answerQuestion(ctx, this, request, next, registerPendingInteraction);
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
//#endregion
|
|
885
|
+
exports.apply = apply;
|
|
886
|
+
exports.inject = inject;
|
|
887
|
+
return module.exports;
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
//# sourceMappingURL=client.js.map
|