@qilin-agent/client-ui-user-questions 0.0.0-stage → 3.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.i18n.yaml +52 -0
- package/README.md +104 -2
- package/README.zh.md +105 -0
- package/lib/client.js +1935 -0
- package/lib/index.js +13 -0
- package/lib/types/client/PlanReviewPanel.d.ts +14 -0
- package/lib/types/client/QuestionComposer.d.ts +26 -0
- package/lib/types/client/QuestionReplyView.d.ts +20 -0
- package/lib/types/client/contract/slots.d.ts +276 -0
- package/lib/types/client/draft-store.d.ts +40 -0
- package/lib/types/client/index.d.ts +33 -0
- package/lib/types/client/locales.d.ts +72 -0
- package/lib/types/client/question-reply.d.ts +67 -0
- package/lib/types/index.d.ts +11 -0
- package/package.json +80 -3
package/lib/client.js
ADDED
|
@@ -0,0 +1,1935 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@qilin-agent/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 _qilin_agent_client_store = require("@qilin-agent/client-store");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
let react = require("react");
|
|
10
|
+
let _qilin_agent_client_ui_primitives = require("@qilin-agent/client-ui-primitives");
|
|
11
|
+
//#region ../../util/brand/src/index.ts
|
|
12
|
+
/**
|
|
13
|
+
* Apply a compile-time string brand without changing the value.
|
|
14
|
+
* @param value - string admitted by the domain that owns the target brand.
|
|
15
|
+
* @returns the same string with the requested compile-time brand.
|
|
16
|
+
*/
|
|
17
|
+
function brandString(value) {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region lib/types/client/contract/slots.js
|
|
22
|
+
function settlePendingComposer(settle, failureMessage) {
|
|
23
|
+
try {
|
|
24
|
+
settle();
|
|
25
|
+
return Promise.resolve();
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return Promise.reject(error instanceof Error ? error : new Error(failureMessage, { cause: error }));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Narrow a request to a renderable plan review, or return undefined to leave it
|
|
32
|
+
* to the generic question flow.
|
|
33
|
+
*
|
|
34
|
+
* The card offers approval and a return to the composer for change requests.
|
|
35
|
+
* It accepts one question carrying the plan as detail and the named approve
|
|
36
|
+
* option, with at most one alternative and no multi-select. Larger choices
|
|
37
|
+
* remain in the generic question flow.
|
|
38
|
+
*
|
|
39
|
+
* @param questions - the request's whole question batch.
|
|
40
|
+
* @returns The narrowed review, or undefined when the generic flow owns it.
|
|
41
|
+
*/
|
|
42
|
+
function planReviewOf(questions) {
|
|
43
|
+
if (questions.length !== 1) return void 0;
|
|
44
|
+
const question = questions[0];
|
|
45
|
+
const intent = question.intent;
|
|
46
|
+
if (intent?.kind !== "plan-review" || question.detail === void 0) return void 0;
|
|
47
|
+
if (question.multiSelect === true) return void 0;
|
|
48
|
+
const options = question.options ?? [];
|
|
49
|
+
if (options.length > 2) return void 0;
|
|
50
|
+
const approve = options.find((option) => option.label === intent.approve);
|
|
51
|
+
if (approve === void 0) return void 0;
|
|
52
|
+
const decline = options.find((option) => option.label !== intent.approve);
|
|
53
|
+
return {
|
|
54
|
+
id: question.id,
|
|
55
|
+
question: question.question,
|
|
56
|
+
plan: question.detail,
|
|
57
|
+
...intent.callId === void 0 ? {} : { callId: intent.callId },
|
|
58
|
+
approve,
|
|
59
|
+
...decline === void 0 ? {} : { decline }
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** Reload-unique prefix so an unnamed legacy card cannot reuse a persisted draft. */
|
|
63
|
+
const unnamedQuestionPrefix = Array.from(globalThis.crypto.getRandomValues(new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
64
|
+
let nextQuestionKey = 0;
|
|
65
|
+
const rejectionMessages = {
|
|
66
|
+
ASK_ABORTED: "ask_user_question was aborted before the user answered",
|
|
67
|
+
ASK_CANCELLED: "the user cancelled ask_user_question",
|
|
68
|
+
ASK_TIMED_OUT: "ask_user_question timed out before the user answered"
|
|
69
|
+
};
|
|
70
|
+
/** Create a wire-preserved user-question rejection. */
|
|
71
|
+
function questionError(code) {
|
|
72
|
+
const error = new Error(rejectionMessages[code]);
|
|
73
|
+
error.name = "UserQuestionError";
|
|
74
|
+
error.code = code;
|
|
75
|
+
return error;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Create the deferred one Remote Event listener settles through a card.
|
|
79
|
+
* The request signal ends the channel with `ASK_ABORTED`; the Host ignores that
|
|
80
|
+
* outcome for an event it already finished, so a cancel frame loses nothing.
|
|
81
|
+
* @param deadline - Client-clock deadline in epoch milliseconds derived from the business claim's remaining duration.
|
|
82
|
+
* @param signal - Delivery lifetime of the forwarded request.
|
|
83
|
+
* @param onSettle - Called once with the channel when it settles or aborts.
|
|
84
|
+
* @returns The channel to attach and the promise the listener awaits.
|
|
85
|
+
*/
|
|
86
|
+
function createWaterfallRequest(deadline, signal, onSettle) {
|
|
87
|
+
const completion = Promise.withResolvers();
|
|
88
|
+
const delegated = Symbol("pending question delegated");
|
|
89
|
+
let settled = false;
|
|
90
|
+
const finish = (settle) => {
|
|
91
|
+
if (settled) return;
|
|
92
|
+
settled = true;
|
|
93
|
+
signal?.removeEventListener("abort", onAbort);
|
|
94
|
+
settle();
|
|
95
|
+
onSettle(channel);
|
|
96
|
+
};
|
|
97
|
+
const onAbort = () => {
|
|
98
|
+
finish(() => {
|
|
99
|
+
completion.reject(questionError("ASK_ABORTED"));
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
const channel = {
|
|
103
|
+
deadline,
|
|
104
|
+
resolve: (answer) => {
|
|
105
|
+
finish(() => {
|
|
106
|
+
completion.resolve(answer);
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
reject: (code) => {
|
|
110
|
+
finish(() => {
|
|
111
|
+
completion.reject(questionError(code));
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
delegate: () => {
|
|
115
|
+
finish(() => {
|
|
116
|
+
completion.reject(delegated);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
if (signal !== void 0) {
|
|
121
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
122
|
+
if (signal.aborted) onAbort();
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
channel,
|
|
126
|
+
result: completion.promise,
|
|
127
|
+
isDelegation: (reason) => reason === delegated
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* One answerable Client card. A card keyed by tool call is created by whichever
|
|
132
|
+
* source arrives first, the forwarded waterfall or the Session projection, and
|
|
133
|
+
* removed only when the projection no longer lists the call.
|
|
134
|
+
*/
|
|
135
|
+
var PendingQuestion = class PendingQuestion {
|
|
136
|
+
/** Presentation discriminator used by Session pending-interaction consumers. */
|
|
137
|
+
kind;
|
|
138
|
+
/** Render identity and request key for the Session-scoped draft store. */
|
|
139
|
+
key;
|
|
140
|
+
/** Agent/Session identity owning the request. */
|
|
141
|
+
sessionId;
|
|
142
|
+
/** The request's question list. */
|
|
143
|
+
questions;
|
|
144
|
+
/** Tool call identity; absent for a blocking request that carried no `wait`. */
|
|
145
|
+
callId;
|
|
146
|
+
/**
|
|
147
|
+
* Recorded answers of a call that already settled. Present only on a
|
|
148
|
+
* read-only review card, which the tool call row builds from its own
|
|
149
|
+
* transcript so a finished question can be read back in the panel that
|
|
150
|
+
* asked it. Such a card has no answer channel and no countdown.
|
|
151
|
+
*/
|
|
152
|
+
review;
|
|
153
|
+
/**
|
|
154
|
+
* What closing the panel does. A card keyed by tool call stays reachable
|
|
155
|
+
* from its tool call row, so closing only withdraws the panel (`hide`) and
|
|
156
|
+
* persists nothing. A card the Host never named has no way back, so closing
|
|
157
|
+
* it ends the request (`cancel`).
|
|
158
|
+
*/
|
|
159
|
+
dismissal;
|
|
160
|
+
#state = "open";
|
|
161
|
+
#waterfall;
|
|
162
|
+
#rpc;
|
|
163
|
+
#seat;
|
|
164
|
+
#timedWait = false;
|
|
165
|
+
#timer;
|
|
166
|
+
#deadline;
|
|
167
|
+
#remainingMs;
|
|
168
|
+
#focused = false;
|
|
169
|
+
#engaged = false;
|
|
170
|
+
#held = false;
|
|
171
|
+
#closed = false;
|
|
172
|
+
#siblings;
|
|
173
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
174
|
+
#snapshot;
|
|
175
|
+
/**
|
|
176
|
+
* @param sessionId - Agent/Session identity owning the request.
|
|
177
|
+
* @param questions - complete question batch.
|
|
178
|
+
* @param callId - tool call identity when the Host named one.
|
|
179
|
+
* @param siblings - keys of every card currently registered for the Session, for draft pruning.
|
|
180
|
+
* @param review - recorded answers of a settled call, making this a read-only card.
|
|
181
|
+
*/
|
|
182
|
+
constructor(sessionId, questions, callId, siblings, review) {
|
|
183
|
+
this.sessionId = sessionId;
|
|
184
|
+
this.questions = questions;
|
|
185
|
+
this.kind = planReviewOf(questions) === void 0 ? "question" : "plan-review";
|
|
186
|
+
this.callId = callId;
|
|
187
|
+
this.review = review;
|
|
188
|
+
this.dismissal = callId === void 0 ? "cancel" : "hide";
|
|
189
|
+
this.#siblings = siblings;
|
|
190
|
+
if (callId === void 0) nextQuestionKey += 1;
|
|
191
|
+
this.key = callId === void 0 ? `question:${unnamedQuestionPrefix}:${String(nextQuestionKey)}` : PendingQuestion.keyOf(sessionId, callId);
|
|
192
|
+
this.#snapshot = this.createSnapshot();
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Card key of a tool call, shared by every source that names one. The Host
|
|
196
|
+
* request and the Session projection carry the branded `ToolCallId`; a
|
|
197
|
+
* transcript row carries the same wire value as a plain string.
|
|
198
|
+
* @param sessionId - owning Session.
|
|
199
|
+
* @param callId - tool call identity, branded or as a transcript spells it.
|
|
200
|
+
* @returns the render identity and draft key.
|
|
201
|
+
*/
|
|
202
|
+
static keyOf(sessionId, callId) {
|
|
203
|
+
return `question:${String(sessionId)}:${callId}`;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Draft keys that are still live in this Session, this card included.
|
|
207
|
+
* @returns keys the draft store must keep; everything else is stale.
|
|
208
|
+
*/
|
|
209
|
+
liveKeys() {
|
|
210
|
+
return this.#siblings?.() ?? [this.key];
|
|
211
|
+
}
|
|
212
|
+
/** Subscribe to card state changes. */
|
|
213
|
+
subscribe = (listener) => {
|
|
214
|
+
this.#listeners.add(listener);
|
|
215
|
+
return () => {
|
|
216
|
+
this.#listeners.delete(listener);
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
/** Read the stable current card state. */
|
|
220
|
+
snapshot = () => this.#snapshot;
|
|
221
|
+
/** Observable snapshot read by the renderer's keyed Hook. */
|
|
222
|
+
getSnapshot = this.snapshot;
|
|
223
|
+
createSnapshot() {
|
|
224
|
+
const channel = this.#waterfall !== void 0 ? "waterfall" : this.#state === "continued" && this.#rpc !== void 0 ? "rpc" : "none";
|
|
225
|
+
const waitState = this.#state === "continued" ? "continued" : this.#held ? "waiting" : this.#engaged ? "editing" : this.#focused ? "focused" : "counting";
|
|
226
|
+
return {
|
|
227
|
+
state: this.#state,
|
|
228
|
+
waitState,
|
|
229
|
+
countdown: this.#timedWait ? {
|
|
230
|
+
remainingMs: this.#deadline === void 0 ? this.#remainingMs ?? 0 : Math.max(0, this.#deadline - Date.now()),
|
|
231
|
+
running: this.#deadline !== void 0
|
|
232
|
+
} : void 0,
|
|
233
|
+
channel,
|
|
234
|
+
closed: this.#closed
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
publish() {
|
|
238
|
+
this.#snapshot = this.createSnapshot();
|
|
239
|
+
this.#reschedule();
|
|
240
|
+
for (const listener of this.#listeners) listener();
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Own the countdown here rather than in a mounted component: the panel can be
|
|
244
|
+
* hidden and remounted while the request stands, and a timer that died with
|
|
245
|
+
* the component would leave the tool call waiting past its deadline.
|
|
246
|
+
*/
|
|
247
|
+
#reschedule() {
|
|
248
|
+
const wanted = this.#deadline !== void 0 && !this.#closed;
|
|
249
|
+
if (wanted === (this.#timer !== void 0)) return;
|
|
250
|
+
if (!wanted) {
|
|
251
|
+
clearInterval(this.#timer);
|
|
252
|
+
this.#timer = void 0;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
this.#timer = setInterval(() => {
|
|
256
|
+
this.#tick();
|
|
257
|
+
}, 1e3);
|
|
258
|
+
}
|
|
259
|
+
#tick() {
|
|
260
|
+
const deadline = this.#deadline;
|
|
261
|
+
/* v8 ignore next -- #reschedule clears the interval in the same publish that drops the deadline. */
|
|
262
|
+
if (deadline === void 0) return;
|
|
263
|
+
if (Date.now() >= deadline) {
|
|
264
|
+
this.timeout();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
this.publish();
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Attach the live waterfall of a forwarded request.
|
|
271
|
+
* @param channel - request channel created by {@link createWaterfallRequest}.
|
|
272
|
+
*/
|
|
273
|
+
attachWaterfall(channel) {
|
|
274
|
+
this.#waterfall = channel;
|
|
275
|
+
this.#timedWait = channel.deadline !== void 0;
|
|
276
|
+
if (!this.#held && !this.#engaged) {
|
|
277
|
+
this.#deadline = channel.deadline;
|
|
278
|
+
if (this.#focused && channel.deadline !== void 0) {
|
|
279
|
+
this.#remainingMs = Math.max(0, channel.deadline - Date.now());
|
|
280
|
+
this.#deadline = void 0;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
this.publish();
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Drop a waterfall channel that settled or was cancelled; the card stays.
|
|
287
|
+
* @param channel - the channel that ended.
|
|
288
|
+
*/
|
|
289
|
+
detachWaterfall(channel) {
|
|
290
|
+
if (this.#waterfall !== channel) return;
|
|
291
|
+
this.#waterfall = void 0;
|
|
292
|
+
this.#timedWait = false;
|
|
293
|
+
this.#deadline = void 0;
|
|
294
|
+
this.publish();
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Whether a live waterfall is attached.
|
|
298
|
+
* @returns whether the pending Host request still accepts settlement.
|
|
299
|
+
*/
|
|
300
|
+
hasWaterfall() {
|
|
301
|
+
return this.#waterfall !== void 0;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Attach the Remote answer path used once the question is continued.
|
|
305
|
+
* @param channel - Remote calls bound to this Session and call.
|
|
306
|
+
*/
|
|
307
|
+
attachRpc(channel) {
|
|
308
|
+
this.#rpc = channel;
|
|
309
|
+
this.publish();
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Attach the composer seat this card is published into.
|
|
313
|
+
* @param seat - withdrawal of the published panel, owned by the card registry.
|
|
314
|
+
*/
|
|
315
|
+
attachSeat(seat) {
|
|
316
|
+
this.#seat = seat;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Copy the projection row state.
|
|
320
|
+
* @param state - `open` or `continued`.
|
|
321
|
+
*/
|
|
322
|
+
setState(state) {
|
|
323
|
+
if (this.#state === state) return;
|
|
324
|
+
this.#state = state;
|
|
325
|
+
this.publish();
|
|
326
|
+
}
|
|
327
|
+
/** Stop this Client's countdown indefinitely; the waterfall then waits like a blocking question. */
|
|
328
|
+
takeTime() {
|
|
329
|
+
if (this.#held) return;
|
|
330
|
+
this.#held = true;
|
|
331
|
+
this.#focused = false;
|
|
332
|
+
this.#remainingMs = void 0;
|
|
333
|
+
this.#deadline = void 0;
|
|
334
|
+
this.publish();
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Record focus even before the request arrives, freezing a pristine countdown once attached.
|
|
338
|
+
* @param now - current Client epoch time.
|
|
339
|
+
*/
|
|
340
|
+
holdFocus(now = Date.now()) {
|
|
341
|
+
if (this.#held || this.#engaged || this.#focused) return;
|
|
342
|
+
this.#remainingMs = this.#deadline === void 0 ? void 0 : Math.max(0, this.#deadline - now);
|
|
343
|
+
this.#deadline = void 0;
|
|
344
|
+
this.#focused = true;
|
|
345
|
+
this.publish();
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Resume a pristine countdown after the answer surface loses focus.
|
|
349
|
+
* @param now - current Client epoch time.
|
|
350
|
+
*/
|
|
351
|
+
releaseFocus(now = Date.now()) {
|
|
352
|
+
if (!this.#focused) return;
|
|
353
|
+
this.#focused = false;
|
|
354
|
+
this.#deadline = this.#timedWait && this.#remainingMs !== void 0 ? now + this.#remainingMs : void 0;
|
|
355
|
+
this.#remainingMs = void 0;
|
|
356
|
+
this.publish();
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Keep the first edited draft answerable without a foreground deadline.
|
|
360
|
+
* @param now - current Client epoch time used to preserve the remaining duration.
|
|
361
|
+
*/
|
|
362
|
+
engage(now = Date.now()) {
|
|
363
|
+
if (this.#held || this.#engaged) return;
|
|
364
|
+
if (this.#remainingMs === void 0 && this.#deadline !== void 0) this.#remainingMs = Math.max(0, this.#deadline - now);
|
|
365
|
+
this.#engaged = true;
|
|
366
|
+
this.#focused = false;
|
|
367
|
+
this.#deadline = void 0;
|
|
368
|
+
this.publish();
|
|
369
|
+
}
|
|
370
|
+
/** Local countdown reached zero: settle the waterfall with `ASK_TIMED_OUT`, keep the card. */
|
|
371
|
+
timeout() {
|
|
372
|
+
if (this.#held || this.#engaged || this.#focused) return;
|
|
373
|
+
const channel = this.#waterfall;
|
|
374
|
+
if (channel === void 0) return;
|
|
375
|
+
this.#waterfall = void 0;
|
|
376
|
+
this.#timedWait = false;
|
|
377
|
+
this.#deadline = void 0;
|
|
378
|
+
channel.reject("ASK_TIMED_OUT");
|
|
379
|
+
this.publish();
|
|
380
|
+
}
|
|
381
|
+
/** Hand a live waterfall to the next listener when this presentation domain unloads. */
|
|
382
|
+
delegate() {
|
|
383
|
+
this.#waterfall?.delegate();
|
|
384
|
+
}
|
|
385
|
+
/** Mark the card removed from the registry; the mounted composer clears its draft. */
|
|
386
|
+
close() {
|
|
387
|
+
if (this.#closed) return;
|
|
388
|
+
this.#closed = true;
|
|
389
|
+
this.publish();
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Submit the whole answer batch through the live waterfall, or through the
|
|
393
|
+
* Remote path once the question is continued.
|
|
394
|
+
* @param answer - complete structured answer batch.
|
|
395
|
+
*/
|
|
396
|
+
answer(answer) {
|
|
397
|
+
const waterfall = this.#waterfall;
|
|
398
|
+
if (waterfall !== void 0) return settlePendingComposer(() => {
|
|
399
|
+
waterfall.resolve(answer);
|
|
400
|
+
}, "pending question settlement failed");
|
|
401
|
+
const rpc = this.#rpc;
|
|
402
|
+
if (this.#state === "continued" && rpc !== void 0) return rpc.answer(answer).then((accepted) => {
|
|
403
|
+
if (!accepted) throw new Error("this question is no longer answerable");
|
|
404
|
+
});
|
|
405
|
+
return Promise.reject(/* @__PURE__ */ new Error("no channel accepts an answer yet"));
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Close the panel. A tool-call-keyed card only leaves the composer seat: the
|
|
409
|
+
* request stands, the countdown keeps running here, and the tool call row
|
|
410
|
+
* reopens it. A card the Host never named ends its request instead, because
|
|
411
|
+
* nothing could bring it back.
|
|
412
|
+
*/
|
|
413
|
+
dismiss() {
|
|
414
|
+
if (this.dismissal === "hide") return settlePendingComposer(() => {
|
|
415
|
+
this.#seat?.hide();
|
|
416
|
+
}, "pending question hide failed");
|
|
417
|
+
const waterfall = this.#waterfall;
|
|
418
|
+
if (waterfall === void 0) return Promise.reject(/* @__PURE__ */ new Error("no channel accepts a cancellation yet"));
|
|
419
|
+
return settlePendingComposer(() => {
|
|
420
|
+
waterfall.reject("ASK_CANCELLED");
|
|
421
|
+
}, "pending question cancellation failed");
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
//#endregion
|
|
425
|
+
//#region lib/types/client/draft-store.js
|
|
426
|
+
/**
|
|
427
|
+
* Session-scoped draft state for the generic question composer. The Slot
|
|
428
|
+
* registry owns store instances; this module exports only the factory so a
|
|
429
|
+
* plugin reload cannot reuse a module-global handle.
|
|
430
|
+
*/
|
|
431
|
+
/**
|
|
432
|
+
* Declare the question composer's Session store. Drafts persist per Session so
|
|
433
|
+
* leaving the Session or restarting the Client does not erase an unfinished answer.
|
|
434
|
+
* @returns a persisted store handle whose instance is owned by the Slot registry.
|
|
435
|
+
*/
|
|
436
|
+
function createQuestionDraftStore() {
|
|
437
|
+
return (0, _qilin_agent_client_store.defineStore)({
|
|
438
|
+
init: () => ({ progressByRequest: {} }),
|
|
439
|
+
persist: "dsh.user-questions.drafts.v1",
|
|
440
|
+
actions: {
|
|
441
|
+
replace: (draft, requestKey, progress) => {
|
|
442
|
+
draft.progressByRequest[requestKey] = progress;
|
|
443
|
+
},
|
|
444
|
+
clear: (draft, requestKey) => {
|
|
445
|
+
draft.progressByRequest = Object.fromEntries(Object.entries(draft.progressByRequest).filter(([key]) => key !== requestKey));
|
|
446
|
+
},
|
|
447
|
+
prune: (draft, keep) => {
|
|
448
|
+
const live = new Set(keep);
|
|
449
|
+
draft.progressByRequest = Object.fromEntries(Object.entries(draft.progressByRequest).filter(([key]) => live.has(key)));
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
//#endregion
|
|
455
|
+
//#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
|
|
456
|
+
function r(e) {
|
|
457
|
+
var t, f, n = "";
|
|
458
|
+
if ("string" == typeof e || "number" == typeof e) n += e;
|
|
459
|
+
else if ("object" == typeof e) if (Array.isArray(e)) {
|
|
460
|
+
var o = e.length;
|
|
461
|
+
for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
|
|
462
|
+
} else for (f in e) e[f] && (n && (n += " "), n += f);
|
|
463
|
+
return n;
|
|
464
|
+
}
|
|
465
|
+
function clsx() {
|
|
466
|
+
for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
|
|
467
|
+
return n;
|
|
468
|
+
}
|
|
469
|
+
//#endregion
|
|
470
|
+
//#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-user-questions/src/client/PlanReviewPanel.module.css.mjs
|
|
471
|
+
const css$2 = ".CE6Vxa_frame{padding:6px calc(var(--qilin-composer-side-clearance) + 16px) 10px;justify-content:center;display:flex}.CE6Vxa_card{width:100%;max-width:var(--qilin-chat-content-width);--qilin-elevation-stroke-color:var(--qilin-alias-border-l2);background:var(--qilin-specific-input-major);box-shadow:var(--qilin-elevation-panel);color:var(--qilin-alias-label-primary);border:0;border-radius:20px;flex-direction:column;display:flex;overflow:hidden}.CE6Vxa_card,.CE6Vxa_card *{box-sizing:border-box}.CE6Vxa_strip{background:var(--qilin-alias-state-warn-tertiary);color:var(--qilin-alias-state-warn-primary);flex-shrink:0;align-items:center;gap:8px;padding:12px 16px;font-size:14px;line-height:20px;display:flex}.CE6Vxa_dot{corner-shape:round;background:var(--qilin-alias-state-warn-primary);border-radius:50%;width:8px;height:8px}.CE6Vxa_footer{flex-shrink:0;justify-content:space-between;align-items:center;gap:12px;padding:8px 16px 12px;display:flex}.CE6Vxa_feedback{min-height:16px;color:var(--qilin-alias-state-error-primary);font-size:11px;line-height:16px}.CE6Vxa_actions{flex-shrink:0;align-items:center;gap:8px;display:flex}.CE6Vxa_summary{min-width:0;padding:14px 16px 12px}.CE6Vxa_title{text-overflow:ellipsis;white-space:nowrap;margin:0;font-size:15px;font-weight:500;line-height:22px;overflow:hidden}.CE6Vxa_description{-webkit-line-clamp:2;color:var(--qilin-alias-label-secondary);-webkit-box-orient:vertical;margin:8px 0 0;font-size:14px;line-height:24px;display:-webkit-box;overflow:hidden}.CE6Vxa_discuss{gap:6px}@media (width<=720px){.CE6Vxa_card{border-radius:16px}.CE6Vxa_footer{align-items:flex-end;padding:8px 12px 10px}}.CE6Vxa_previewActions{align-items:center;margin-left:auto;display:flex}";
|
|
472
|
+
const tagId$2 = "@qilin-agent/client-ui-user-questions/PlanReviewPanel.module.css";
|
|
473
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
|
|
474
|
+
const tag = document.createElement("style");
|
|
475
|
+
tag.dataset.plugin = "@qilin-agent/client-ui-user-questions";
|
|
476
|
+
tag.dataset.pluginCss = tagId$2;
|
|
477
|
+
tag.textContent = css$2;
|
|
478
|
+
document.head.appendChild(tag);
|
|
479
|
+
}
|
|
480
|
+
var PlanReviewPanel_module_css_default = {
|
|
481
|
+
"actions": "CE6Vxa_actions",
|
|
482
|
+
"card": "CE6Vxa_card",
|
|
483
|
+
"description": "CE6Vxa_description",
|
|
484
|
+
"discuss": "CE6Vxa_discuss",
|
|
485
|
+
"dot": "CE6Vxa_dot",
|
|
486
|
+
"feedback": "CE6Vxa_feedback",
|
|
487
|
+
"footer": "CE6Vxa_footer",
|
|
488
|
+
"frame": "CE6Vxa_frame",
|
|
489
|
+
"previewActions": "CE6Vxa_previewActions",
|
|
490
|
+
"strip": "CE6Vxa_strip",
|
|
491
|
+
"summary": "CE6Vxa_summary",
|
|
492
|
+
"title": "CE6Vxa_title"
|
|
493
|
+
};
|
|
494
|
+
//#endregion
|
|
495
|
+
//#region lib/types/client/PlanReviewPanel.js
|
|
496
|
+
/**
|
|
497
|
+
* Optional-prop spread for a decision button's tooltip: `title` is optional on
|
|
498
|
+
* the DOM props, and exactOptionalPropertyTypes rejects an explicit undefined.
|
|
499
|
+
*
|
|
500
|
+
* @param description - the asker's option description, when it carries one.
|
|
501
|
+
* @returns The `title` prop to spread, or nothing.
|
|
502
|
+
*/
|
|
503
|
+
function tooltip(description) {
|
|
504
|
+
return description === void 0 ? {} : { title: description };
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Render plan review controls; the submitted document opens in the sidebar.
|
|
508
|
+
*
|
|
509
|
+
* @param props - the question domain face, the narrowed plan review, and `t`.
|
|
510
|
+
* @returns The plan-review takeover for this request.
|
|
511
|
+
*/
|
|
512
|
+
function PlanReviewPanel({ pending, review, t, renderSlot }) {
|
|
513
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
514
|
+
const [error, setError] = (0, react.useState)(null);
|
|
515
|
+
const settle = (send, remote = false) => {
|
|
516
|
+
setBusy(true);
|
|
517
|
+
setError(null);
|
|
518
|
+
send().then(() => {
|
|
519
|
+
if (!remote) return;
|
|
520
|
+
setBusy(false);
|
|
521
|
+
pending.dismiss().catch(() => {
|
|
522
|
+
setError(t("status.sent"));
|
|
523
|
+
});
|
|
524
|
+
}).catch((cause) => {
|
|
525
|
+
setBusy(false);
|
|
526
|
+
setError(cause instanceof Error ? cause.message : String(cause));
|
|
527
|
+
});
|
|
528
|
+
};
|
|
529
|
+
const decide = (label) => {
|
|
530
|
+
settle(() => pending.answer({ answers: [{
|
|
531
|
+
id: review.id,
|
|
532
|
+
selected: [label]
|
|
533
|
+
}] }), pending.snapshot().channel === "rpc");
|
|
534
|
+
};
|
|
535
|
+
const summary = (0, react.useMemo)(() => {
|
|
536
|
+
const title = (0, _qilin_agent_client_ui_primitives.extractMarkdownPlainText)(review.plan, { mode: "first-line" });
|
|
537
|
+
const description = (0, _qilin_agent_client_ui_primitives.extractMarkdownPlainText)(review.plan, { mode: "first-paragraph" });
|
|
538
|
+
return {
|
|
539
|
+
title,
|
|
540
|
+
description: description === title ? "" : description
|
|
541
|
+
};
|
|
542
|
+
}, [review.plan]);
|
|
543
|
+
return (0, react_jsx_runtime.jsx)("div", {
|
|
544
|
+
className: PlanReviewPanel_module_css_default.frame,
|
|
545
|
+
"data-plan-review-key": pending.key,
|
|
546
|
+
children: (0, react_jsx_runtime.jsxs)("section", {
|
|
547
|
+
className: PlanReviewPanel_module_css_default.card,
|
|
548
|
+
"aria-label": review.question,
|
|
549
|
+
"aria-busy": busy,
|
|
550
|
+
children: [
|
|
551
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
552
|
+
className: PlanReviewPanel_module_css_default.strip,
|
|
553
|
+
children: [
|
|
554
|
+
(0, react_jsx_runtime.jsx)("span", { className: PlanReviewPanel_module_css_default.dot }),
|
|
555
|
+
t("plan.header"),
|
|
556
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
557
|
+
className: PlanReviewPanel_module_css_default.previewActions,
|
|
558
|
+
children: renderSlot("conversation.plan-review.actions", {
|
|
559
|
+
review,
|
|
560
|
+
requestKey: pending.key
|
|
561
|
+
})
|
|
562
|
+
})
|
|
563
|
+
]
|
|
564
|
+
}),
|
|
565
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
566
|
+
className: PlanReviewPanel_module_css_default.summary,
|
|
567
|
+
children: [(0, react_jsx_runtime.jsx)("h3", {
|
|
568
|
+
className: PlanReviewPanel_module_css_default.title,
|
|
569
|
+
children: summary.title
|
|
570
|
+
}), summary.description !== "" && (0, react_jsx_runtime.jsx)("p", {
|
|
571
|
+
className: PlanReviewPanel_module_css_default.description,
|
|
572
|
+
children: summary.description
|
|
573
|
+
})]
|
|
574
|
+
}),
|
|
575
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
576
|
+
className: PlanReviewPanel_module_css_default.footer,
|
|
577
|
+
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
578
|
+
className: PlanReviewPanel_module_css_default.feedback,
|
|
579
|
+
role: "status",
|
|
580
|
+
children: error
|
|
581
|
+
}), (0, react_jsx_runtime.jsxs)("div", {
|
|
582
|
+
className: PlanReviewPanel_module_css_default.actions,
|
|
583
|
+
children: [(0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
|
|
584
|
+
variant: "outline",
|
|
585
|
+
className: PlanReviewPanel_module_css_default.discuss,
|
|
586
|
+
icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconEditOutline16, { size: 14 }),
|
|
587
|
+
disabled: busy,
|
|
588
|
+
onClick: () => {
|
|
589
|
+
settle(() => pending.dismiss());
|
|
590
|
+
},
|
|
591
|
+
children: t("plan.discuss")
|
|
592
|
+
}), (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
|
|
593
|
+
variant: "primary",
|
|
594
|
+
...tooltip(review.approve.description),
|
|
595
|
+
disabled: busy,
|
|
596
|
+
onClick: () => {
|
|
597
|
+
decide(review.approve.label);
|
|
598
|
+
},
|
|
599
|
+
children: t("plan.approve")
|
|
600
|
+
})]
|
|
601
|
+
})]
|
|
602
|
+
})
|
|
603
|
+
]
|
|
604
|
+
})
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
//#endregion
|
|
608
|
+
//#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-user-questions/src/client/QuestionComposer.module.css.mjs
|
|
609
|
+
const css$1 = ".vluglG_frame{padding:6px calc(var(--qilin-composer-side-clearance) + 16px) 10px;justify-content:center;display:flex}.vluglG_card{width:100%;max-width:var(--qilin-chat-content-width);--qilin-elevation-stroke-color:var(--qilin-alias-border-l2-darkmode-thin);background:var(--qilin-specific-input-major);max-height:min(60vh,520px);box-shadow:var(--qilin-elevation-panel);color:var(--qilin-alias-label-primary);--qilin-scrollbar-thumb:var(--qilin-alias-scrollbar-bg-l2);--qilin-scrollbar-thumb-hover:var(--qilin-alias-scrollbar-hover-l2);border:0;border-radius:20px;flex-direction:column;padding:0 0 10px;display:flex;overflow:hidden}.vluglG_card,.vluglG_card *{box-sizing:border-box}.vluglG_cardMinimized{max-height:none}.vluglG_cardMinimized .vluglG_header{padding-bottom:14px}.vluglG_headerActions{flex-shrink:0;align-items:center;gap:4px;display:flex}.vluglG_header{flex-shrink:0;justify-content:space-between;align-items:flex-start;gap:16px;padding:20px 16px 0 24px;display:flex}.vluglG_headingBlock{min-width:0}.vluglG_eyebrow{color:var(--qilin-alias-label-tertiary);margin-bottom:5px;font-size:11px;line-height:16px}.vluglG_title{margin:0;font-size:16px;font-weight:500;line-height:22px}.vluglG_detail{margin:0 2px 8px}.vluglG_footerActions{flex-shrink:0;align-items:center;gap:12px;display:flex}.vluglG_pager{flex-shrink:0;align-items:center;gap:6px;display:flex}.vluglG_progress{color:var(--qilin-alias-label-secondary);white-space:nowrap;word-spacing:-2px;padding:0 4px;font-size:14px;font-weight:500;line-height:24px}.vluglG_iconButton{corner-shape:round;width:24px;height:24px;color:var(--qilin-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:999px;place-items:center;padding:0;display:grid}.vluglG_iconButton:hover:not(:disabled){background:var(--qilin-alias-interactive-bg-hover);color:var(--qilin-alias-label-primary)}.vluglG_iconButton:disabled{color:var(--qilin-alias-label-dimmed);cursor:default}.vluglG_body{overscroll-behavior:contain;flex-direction:column;flex:auto;min-height:0;display:flex;overflow-y:auto}.vluglG_options{flex-direction:column;gap:1px;margin:8px 0 0;padding:4px 12px;display:flex}.vluglG_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}.vluglG_option:hover:not(:disabled),.vluglG_optionSelected{background:var(--qilin-alias-interactive-bg-hover)}.vluglG_optionSelected{border-color:var(--qilin-alias-border-l2)}.vluglG_option:disabled{cursor:default}.vluglG_number{background:var(--qilin-alias-bg-overlay);width:20px;height:20px;color:var(--qilin-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}.vluglG_checkbox{flex:0 0 20px;place-items:center;width:20px;height:20px;margin-top:2px;display:grid}.vluglG_checkbox:before{content:\"\";border:.5px solid var(--qilin-alias-border-l4);border-radius:4px;grid-area:1/1;width:14px;height:14px;transition:background-color .12s,border-color .12s}.vluglG_checkbox>svg{grid-area:1/1}.vluglG_checkboxChecked{color:var(--qilin-alias-label-primary-foreground)}.vluglG_checkboxChecked:before{border-color:var(--qilin-alias-label-primary);background:var(--qilin-alias-label-primary)}.vluglG_optionCopy{flex:1;min-width:0}.vluglG_optionLine{flex-wrap:wrap;align-items:baseline;gap:2px 6px;display:flex}.vluglG_optionLabel{font-size:14px;font-weight:500;line-height:24px}.vluglG_badge{background:var(--qilin-specific-sidebar-nav-item-active-accent);color:var(--qilin-alias-button-info-fill);border-radius:6px;padding:0 4px;font-size:11px;font-weight:600;line-height:18px}.vluglG_description{color:var(--qilin-alias-label-tertiary);font-size:14px;font-weight:400;line-height:24px}.vluglG_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}.vluglG_customRow:hover,.vluglG_customRow:focus-within,.vluglG_customRowActive{background:var(--qilin-alias-interactive-bg-hover)}.vluglG_customRow:focus-within,.vluglG_customRowActive{border-color:var(--qilin-alias-border-l2)}.vluglG_field{--qilin-answer-field-padding:0;min-width:0;display:grid}.vluglG_field>*{min-width:0;padding:var(--qilin-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}.vluglG_fieldMirror{box-sizing:content-box;visibility:hidden;max-height:144px;overflow:hidden}.vluglG_fieldInput{resize:none;color:var(--qilin-alias-label-primary);caret-color:var(--qilin-alias-state-business-primary);background:0 0;border:none;outline:none;overflow-y:auto}.vluglG_fieldInput::placeholder{color:var(--qilin-alias-label-caption)}.vluglG_customInline{flex:1}.vluglG_customBlock{border:.5px solid var(--qilin-alias-border-l4);background:var(--qilin-alias-bg-module-platform);--qilin-answer-field-padding:8px 12px;border-radius:10px;flex-shrink:0;min-height:64px;margin:0 12px}.vluglG_customBlock:focus-within{border-color:var(--qilin-alias-state-business-primary)}.vluglG_footer{flex-shrink:0;justify-content:space-between;align-items:center;gap:12px;margin-top:12px;padding:0 10px 0 18px;display:flex}.vluglG_feedback{min-height:16px;color:var(--qilin-alias-state-error-primary);text-align:right;flex:1;font-size:11px;line-height:16px}@media (width<=720px){.vluglG_card{border-radius:16px}.vluglG_header{padding:10px 12px 0 18px}.vluglG_options{padding:4px 8px}.vluglG_title{font-size:15px;line-height:21px}.vluglG_option,.vluglG_customRow{padding:8px 6px}.vluglG_footer{align-items:flex-end;padding:0 10px}.vluglG_footerActions{flex-shrink:0}}@media (prefers-reduced-motion:reduce){.vluglG_option,.vluglG_customRow{transition:none}}";
|
|
610
|
+
const tagId$1 = "@qilin-agent/client-ui-user-questions/QuestionComposer.module.css";
|
|
611
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
612
|
+
const tag = document.createElement("style");
|
|
613
|
+
tag.dataset.plugin = "@qilin-agent/client-ui-user-questions";
|
|
614
|
+
tag.dataset.pluginCss = tagId$1;
|
|
615
|
+
tag.textContent = css$1;
|
|
616
|
+
document.head.appendChild(tag);
|
|
617
|
+
}
|
|
618
|
+
var QuestionComposer_module_css_default = {
|
|
619
|
+
"badge": "vluglG_badge",
|
|
620
|
+
"body": "vluglG_body",
|
|
621
|
+
"card": "vluglG_card",
|
|
622
|
+
"cardMinimized": "vluglG_cardMinimized",
|
|
623
|
+
"checkbox": "vluglG_checkbox",
|
|
624
|
+
"checkboxChecked": "vluglG_checkboxChecked",
|
|
625
|
+
"customBlock": "vluglG_customBlock",
|
|
626
|
+
"customInline": "vluglG_customInline",
|
|
627
|
+
"customRow": "vluglG_customRow",
|
|
628
|
+
"customRowActive": "vluglG_customRowActive",
|
|
629
|
+
"description": "vluglG_description",
|
|
630
|
+
"detail": "vluglG_detail",
|
|
631
|
+
"eyebrow": "vluglG_eyebrow",
|
|
632
|
+
"feedback": "vluglG_feedback",
|
|
633
|
+
"field": "vluglG_field",
|
|
634
|
+
"fieldInput": "vluglG_fieldInput",
|
|
635
|
+
"fieldMirror": "vluglG_fieldMirror",
|
|
636
|
+
"footer": "vluglG_footer",
|
|
637
|
+
"footerActions": "vluglG_footerActions",
|
|
638
|
+
"frame": "vluglG_frame",
|
|
639
|
+
"header": "vluglG_header",
|
|
640
|
+
"headerActions": "vluglG_headerActions",
|
|
641
|
+
"headingBlock": "vluglG_headingBlock",
|
|
642
|
+
"iconButton": "vluglG_iconButton",
|
|
643
|
+
"number": "vluglG_number",
|
|
644
|
+
"option": "vluglG_option",
|
|
645
|
+
"optionCopy": "vluglG_optionCopy",
|
|
646
|
+
"optionLabel": "vluglG_optionLabel",
|
|
647
|
+
"optionLine": "vluglG_optionLine",
|
|
648
|
+
"optionSelected": "vluglG_optionSelected",
|
|
649
|
+
"options": "vluglG_options",
|
|
650
|
+
"pager": "vluglG_pager",
|
|
651
|
+
"progress": "vluglG_progress",
|
|
652
|
+
"title": "vluglG_title"
|
|
653
|
+
};
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region lib/types/client/QuestionComposer.js
|
|
656
|
+
/** A removed card can remain mounted until the composer seat updates. */
|
|
657
|
+
const REMOVED_CARD = {
|
|
658
|
+
state: "open",
|
|
659
|
+
waitState: "counting",
|
|
660
|
+
countdown: void 0,
|
|
661
|
+
channel: "none",
|
|
662
|
+
closed: true
|
|
663
|
+
};
|
|
664
|
+
/**
|
|
665
|
+
* Split the conventional recommendation suffix without changing the answer value.
|
|
666
|
+
* @param label - Original option label returned if selected.
|
|
667
|
+
* @returns Display label plus recommendation state.
|
|
668
|
+
*/
|
|
669
|
+
function parseRecommendedLabel(label) {
|
|
670
|
+
const suffix = /\s*(?:\((?:recommended|推荐)\)|((?:recommended|推荐)))\s*$/i;
|
|
671
|
+
return suffix.test(label) ? {
|
|
672
|
+
label: label.replace(suffix, ""),
|
|
673
|
+
recommended: true
|
|
674
|
+
} : {
|
|
675
|
+
label,
|
|
676
|
+
recommended: false
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
/** Only a marked first choice is an implicit draft; the user still submits it. */
|
|
680
|
+
function recommendedFirstOption(question) {
|
|
681
|
+
const label = question.options?.[0]?.label;
|
|
682
|
+
return label !== void 0 && parseRecommendedLabel(label).recommended ? label : void 0;
|
|
683
|
+
}
|
|
684
|
+
/** Accept persisted progress only when it still describes this question batch. */
|
|
685
|
+
function isQuestionDraftProgress(value, questionCount) {
|
|
686
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
687
|
+
const progress = value;
|
|
688
|
+
if (typeof progress.index !== "number" || !Number.isInteger(progress.index) || progress.index < 0 || progress.index >= questionCount || !Array.isArray(progress.drafts) || progress.drafts.length !== questionCount || progress.wait !== void 0 && progress.wait !== "editing" && progress.wait !== "waiting") return false;
|
|
689
|
+
return progress.drafts.every((item) => {
|
|
690
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) return false;
|
|
691
|
+
const draft = item;
|
|
692
|
+
return Array.isArray(draft.selected) && draft.selected.every((label) => typeof label === "string") && typeof draft.custom === "string" && typeof draft.skipped === "boolean";
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
/** Return whether a text-field key event belongs to an active IME composition. */
|
|
696
|
+
function isComposing(event) {
|
|
697
|
+
return event.nativeEvent.isComposing || Reflect.get(event.nativeEvent, "keyCode") === 229;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Auto-growing free-text answer: a textarea, so a long answer soft-wraps and
|
|
701
|
+
* Shift+Enter breaks a line, over a hidden mirror that owns the height.
|
|
702
|
+
*
|
|
703
|
+
* The mirror renders the draft plus a trailing newline in normal flow and so
|
|
704
|
+
* sizes the grid row (counting rows by '\n' cannot see soft wraps); the
|
|
705
|
+
* textarea shares that one cell and stretches to it, and `rows={1}` keeps the
|
|
706
|
+
* control's own intrinsic height out of the row sizing so the mirror alone
|
|
707
|
+
* decides. Past the mirror's cap the textarea scrolls itself — it is the only
|
|
708
|
+
* scrollport in the stack, there being no second glyph layer to keep aligned.
|
|
709
|
+
* Mirror and textarea MUST share font, line-height, padding and wrapping rules
|
|
710
|
+
* or the two heights diverge.
|
|
711
|
+
*
|
|
712
|
+
* @param props - visual variant, draft text, and the field's event handlers.
|
|
713
|
+
* @returns The mirrored auto-growing field.
|
|
714
|
+
*/
|
|
715
|
+
function AnswerField(props) {
|
|
716
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
717
|
+
className: clsx(QuestionComposer_module_css_default.field, props.variant === "inline" ? QuestionComposer_module_css_default.customInline : QuestionComposer_module_css_default.customBlock),
|
|
718
|
+
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
719
|
+
"aria-hidden": true,
|
|
720
|
+
className: QuestionComposer_module_css_default.fieldMirror,
|
|
721
|
+
children: `${props.value}\n`
|
|
722
|
+
}), (0, react_jsx_runtime.jsx)("textarea", {
|
|
723
|
+
autoFocus: props.autoFocus,
|
|
724
|
+
className: QuestionComposer_module_css_default.fieldInput,
|
|
725
|
+
value: props.value,
|
|
726
|
+
disabled: props.disabled,
|
|
727
|
+
rows: 1,
|
|
728
|
+
placeholder: props.placeholder,
|
|
729
|
+
onFocus: props.onFocus,
|
|
730
|
+
onChange: props.onChange,
|
|
731
|
+
onKeyDown: props.onKeyDown
|
|
732
|
+
})]
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Composer takeover router. Generic-question drafts live in this entry's
|
|
737
|
+
* Session-scoped Slot store, keyed by the pending carrier, so a strict Session
|
|
738
|
+
* entry remount restores the same request without exposing it to another one.
|
|
739
|
+
*
|
|
740
|
+
* One takeover, two presentations: a request that declares a presentation intent this
|
|
741
|
+
* package renders uses that presentation (a plan review is one decision over one
|
|
742
|
+
* plan, not a question set), and every other request takes the generic flow.
|
|
743
|
+
* The routing lives here, at the one entry that owns the composer seat, so
|
|
744
|
+
* neither presentation can claim a request the other is already rendering.
|
|
745
|
+
*
|
|
746
|
+
* @param props - the selector-matched pending question carrier plus the framework standard kit.
|
|
747
|
+
* @returns The question flow, or the intent's own surface, for this request.
|
|
748
|
+
*/
|
|
749
|
+
function QuestionComposer(props) {
|
|
750
|
+
const question = props.matched;
|
|
751
|
+
const review = (0, react.useMemo)(() => planReviewOf(question.questions), [question]);
|
|
752
|
+
return review === void 0 ? (0, react_jsx_runtime.jsx)(QuestionFlow, {
|
|
753
|
+
pending: question,
|
|
754
|
+
t: props.t,
|
|
755
|
+
useStore: props.useStore,
|
|
756
|
+
useQuestionCard: props.useQuestionCard,
|
|
757
|
+
actions: props.actions
|
|
758
|
+
}, question.key) : (0, react_jsx_runtime.jsx)(PlanReviewPanel, {
|
|
759
|
+
pending: question,
|
|
760
|
+
review,
|
|
761
|
+
t: props.t,
|
|
762
|
+
renderSlot: props.renderSlot
|
|
763
|
+
}, question.key);
|
|
764
|
+
}
|
|
765
|
+
function QuestionFlow({ pending, t, useStore, useQuestionCard, actions }) {
|
|
766
|
+
const questions = pending.questions;
|
|
767
|
+
const review = pending.review;
|
|
768
|
+
const markdownLabels = (0, react.useMemo)(() => ({
|
|
769
|
+
code: {
|
|
770
|
+
copyLabel: t("copy"),
|
|
771
|
+
copiedLabel: t("copied")
|
|
772
|
+
},
|
|
773
|
+
mermaid: {
|
|
774
|
+
diagramLabel: t("markdown.diagram"),
|
|
775
|
+
enlargedLabel: t("markdown.enlarged")
|
|
776
|
+
},
|
|
777
|
+
footnotes: t("markdown.footnotes")
|
|
778
|
+
}), [t]);
|
|
779
|
+
const initialDrafts = (0, react.useMemo)(() => {
|
|
780
|
+
const recorded = new Map((review ?? []).map((answer) => [answer.id, answer]));
|
|
781
|
+
return questions.map((item) => {
|
|
782
|
+
const answer = recorded.get(item.id);
|
|
783
|
+
const custom = answer?.custom ?? "";
|
|
784
|
+
const recommended = review === void 0 ? recommendedFirstOption(item) : void 0;
|
|
785
|
+
return {
|
|
786
|
+
selected: answer === void 0 && recommended !== void 0 ? [recommended] : [...answer?.selected ?? []],
|
|
787
|
+
custom,
|
|
788
|
+
skipped: answer !== void 0 && answer.selected.length === 0 && custom === ""
|
|
789
|
+
};
|
|
790
|
+
});
|
|
791
|
+
}, [questions, review]);
|
|
792
|
+
const stored = useStore((state) => state.progressByRequest[pending.key]);
|
|
793
|
+
const validStored = isQuestionDraftProgress(stored, questions.length) ? stored : void 0;
|
|
794
|
+
const storedProgress = review === void 0 ? validStored : void 0;
|
|
795
|
+
const index = validStored?.index ?? 0;
|
|
796
|
+
const drafts = storedProgress?.drafts ?? initialDrafts;
|
|
797
|
+
const restoredWait = storedProgress?.wait ?? (storedProgress?.drafts.some((item, itemIndex) => item.selected.length !== initialDrafts[itemIndex]?.selected.length || item.selected.some((label, labelIndex) => label !== initialDrafts[itemIndex]?.selected[labelIndex]) || item.custom !== "" || item.skipped) === true ? "editing" : void 0);
|
|
798
|
+
const [busy, setBusy] = (0, react.useState)(null);
|
|
799
|
+
const [error, setError] = (0, react.useState)(null);
|
|
800
|
+
const card = useQuestionCard(pending.key, (snapshot) => snapshot ?? REMOVED_CARD);
|
|
801
|
+
const canSubmit = card.channel !== "none";
|
|
802
|
+
const locked = busy !== null || review !== void 0;
|
|
803
|
+
const sentVia = (0, react.useRef)(null);
|
|
804
|
+
(0, react.useEffect)(() => {
|
|
805
|
+
if (sentVia.current !== "waterfall" || card.state !== "continued") return;
|
|
806
|
+
sentVia.current = null;
|
|
807
|
+
setBusy(null);
|
|
808
|
+
setError({ key: "error.resubmit" });
|
|
809
|
+
}, [card.state]);
|
|
810
|
+
(0, react.useEffect)(() => {
|
|
811
|
+
if (card.closed) actions.clear(pending.key);
|
|
812
|
+
}, [
|
|
813
|
+
actions,
|
|
814
|
+
card.closed,
|
|
815
|
+
pending.key
|
|
816
|
+
]);
|
|
817
|
+
(0, react.useEffect)(() => {
|
|
818
|
+
actions.prune(pending.liveKeys());
|
|
819
|
+
}, [actions, pending]);
|
|
820
|
+
const waitDisposition = (0, react.useRef)(restoredWait);
|
|
821
|
+
(0, react.useEffect)(() => {
|
|
822
|
+
if (waitDisposition.current === "waiting") pending.takeTime();
|
|
823
|
+
if (waitDisposition.current === "editing") pending.engage();
|
|
824
|
+
}, [pending]);
|
|
825
|
+
const countdown = card.countdown;
|
|
826
|
+
const [minimized, setMinimized] = (0, react.useState)(false);
|
|
827
|
+
const focusedQuestions = (0, react.useRef)(/* @__PURE__ */ new Set());
|
|
828
|
+
const answerSurface = (0, react.useRef)(null);
|
|
829
|
+
const question = questions[index];
|
|
830
|
+
const draft = drafts[index];
|
|
831
|
+
const hasOptions = (question.options?.length ?? 0) > 0;
|
|
832
|
+
const replaceProgress = (nextIndex, nextDrafts) => {
|
|
833
|
+
actions.replace(pending.key, {
|
|
834
|
+
index: nextIndex,
|
|
835
|
+
drafts: nextDrafts,
|
|
836
|
+
...waitDisposition.current === void 0 ? {} : { wait: waitDisposition.current }
|
|
837
|
+
});
|
|
838
|
+
};
|
|
839
|
+
const takeTime = () => {
|
|
840
|
+
waitDisposition.current = "waiting";
|
|
841
|
+
pending.takeTime();
|
|
842
|
+
replaceProgress(index, drafts);
|
|
843
|
+
};
|
|
844
|
+
const engage = () => {
|
|
845
|
+
if (waitDisposition.current !== void 0) return;
|
|
846
|
+
waitDisposition.current = "editing";
|
|
847
|
+
pending.engage();
|
|
848
|
+
};
|
|
849
|
+
const focusAnswerSurface = (event) => {
|
|
850
|
+
if (!event.currentTarget.contains(event.relatedTarget)) pending.holdFocus();
|
|
851
|
+
};
|
|
852
|
+
const blurAnswerSurface = (event) => {
|
|
853
|
+
if (!event.currentTarget.contains(event.relatedTarget)) pending.releaseFocus();
|
|
854
|
+
};
|
|
855
|
+
(0, react.useEffect)(() => {
|
|
856
|
+
const release = () => {
|
|
857
|
+
pending.releaseFocus();
|
|
858
|
+
};
|
|
859
|
+
const refocus = () => {
|
|
860
|
+
if (waitDisposition.current === "editing") {
|
|
861
|
+
pending.engage();
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (answerSurface.current?.contains(document.activeElement) === true) pending.holdFocus();
|
|
865
|
+
};
|
|
866
|
+
const visibilityChanged = () => {
|
|
867
|
+
if (document.hidden) release();
|
|
868
|
+
else refocus();
|
|
869
|
+
};
|
|
870
|
+
window.addEventListener("blur", release);
|
|
871
|
+
window.addEventListener("focus", refocus);
|
|
872
|
+
document.addEventListener("visibilitychange", visibilityChanged);
|
|
873
|
+
return () => {
|
|
874
|
+
release();
|
|
875
|
+
window.removeEventListener("blur", release);
|
|
876
|
+
window.removeEventListener("focus", refocus);
|
|
877
|
+
document.removeEventListener("visibilitychange", visibilityChanged);
|
|
878
|
+
};
|
|
879
|
+
}, [pending]);
|
|
880
|
+
const dismissFlow = () => {
|
|
881
|
+
if (pending.dismissal === "hide") {
|
|
882
|
+
pending.dismiss();
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (!canSubmit) {
|
|
886
|
+
setError({ key: "error.unavailable" });
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
setBusy("cancel");
|
|
890
|
+
setError(null);
|
|
891
|
+
sentVia.current = "waterfall";
|
|
892
|
+
pending.dismiss().catch((cause) => {
|
|
893
|
+
sentVia.current = null;
|
|
894
|
+
setBusy(null);
|
|
895
|
+
setError({ text: cause instanceof Error ? cause.message : String(cause) });
|
|
896
|
+
});
|
|
897
|
+
};
|
|
898
|
+
const updateDraft = (update, nextIndex = index) => {
|
|
899
|
+
engage();
|
|
900
|
+
replaceProgress(nextIndex, drafts.map((item, itemIndex) => itemIndex === index ? update(item) : item));
|
|
901
|
+
setError(null);
|
|
902
|
+
};
|
|
903
|
+
const choose = (label) => {
|
|
904
|
+
updateDraft((current) => {
|
|
905
|
+
if (question.multiSelect === true) {
|
|
906
|
+
const selected = current.selected.includes(label) ? current.selected.filter((item) => item !== label) : [...current.selected, label];
|
|
907
|
+
return {
|
|
908
|
+
...current,
|
|
909
|
+
selected,
|
|
910
|
+
skipped: false
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
return {
|
|
914
|
+
selected: [label],
|
|
915
|
+
custom: "",
|
|
916
|
+
skipped: false
|
|
917
|
+
};
|
|
918
|
+
}, question.multiSelect !== true && index < questions.length - 1 ? index + 1 : index);
|
|
919
|
+
};
|
|
920
|
+
const answered = (item) => item.selected.length > 0 || item.custom.trim() !== "";
|
|
921
|
+
const completed = (item) => answered(item) || item.skipped;
|
|
922
|
+
const submitDrafts = (values) => {
|
|
923
|
+
const missing = values.findIndex((item) => !completed(item));
|
|
924
|
+
if (missing >= 0) {
|
|
925
|
+
replaceProgress(missing, values);
|
|
926
|
+
setError({ key: "error.incomplete" });
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (!canSubmit) {
|
|
930
|
+
setError({ key: "error.unavailable" });
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
const answer = { answers: questions.map((item, itemIndex) => {
|
|
934
|
+
const value = values[itemIndex];
|
|
935
|
+
if (value.skipped) return {
|
|
936
|
+
id: item.id,
|
|
937
|
+
selected: []
|
|
938
|
+
};
|
|
939
|
+
const custom = value.custom.trim();
|
|
940
|
+
return {
|
|
941
|
+
id: item.id,
|
|
942
|
+
selected: custom === "" || item.multiSelect === true ? value.selected : [],
|
|
943
|
+
...custom === "" ? {} : { custom }
|
|
944
|
+
};
|
|
945
|
+
}) };
|
|
946
|
+
setBusy("answer");
|
|
947
|
+
setError(null);
|
|
948
|
+
const channel = pending.snapshot().channel;
|
|
949
|
+
sentVia.current = channel === "waterfall" ? "waterfall" : null;
|
|
950
|
+
pending.answer(answer).then(() => {
|
|
951
|
+
if (channel !== "rpc") return;
|
|
952
|
+
sentVia.current = null;
|
|
953
|
+
setBusy(null);
|
|
954
|
+
setError(null);
|
|
955
|
+
pending.dismiss().catch(() => {
|
|
956
|
+
setError({ key: "status.sent" });
|
|
957
|
+
});
|
|
958
|
+
}).catch((cause) => {
|
|
959
|
+
sentVia.current = null;
|
|
960
|
+
setBusy(null);
|
|
961
|
+
setError({ text: cause instanceof Error ? cause.message : String(cause) });
|
|
962
|
+
});
|
|
963
|
+
};
|
|
964
|
+
const continueFlow = () => {
|
|
965
|
+
if (!answered(draft)) {
|
|
966
|
+
setError({ key: "error.unanswered" });
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
if (index < questions.length - 1) {
|
|
970
|
+
replaceProgress(index + 1, drafts);
|
|
971
|
+
setError(null);
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
submitDrafts(drafts);
|
|
975
|
+
};
|
|
976
|
+
const draftCustom = (event) => {
|
|
977
|
+
const value = event.target.value;
|
|
978
|
+
updateDraft((current) => ({
|
|
979
|
+
...current,
|
|
980
|
+
selected: question.multiSelect === true ? current.selected : [],
|
|
981
|
+
custom: value,
|
|
982
|
+
skipped: false
|
|
983
|
+
}));
|
|
984
|
+
};
|
|
985
|
+
const continueFromCustom = (event) => {
|
|
986
|
+
if (event.key !== "Enter" || event.shiftKey || isComposing(event)) return;
|
|
987
|
+
event.preventDefault();
|
|
988
|
+
continueFlow();
|
|
989
|
+
};
|
|
990
|
+
const skipQuestion = () => {
|
|
991
|
+
engage();
|
|
992
|
+
const nextDrafts = drafts.map((item, itemIndex) => itemIndex === index ? {
|
|
993
|
+
selected: [],
|
|
994
|
+
custom: "",
|
|
995
|
+
skipped: true
|
|
996
|
+
} : item);
|
|
997
|
+
replaceProgress(index < questions.length - 1 ? index + 1 : index, nextDrafts);
|
|
998
|
+
setError(null);
|
|
999
|
+
if (index < questions.length - 1) return;
|
|
1000
|
+
submitDrafts(nextDrafts);
|
|
1001
|
+
};
|
|
1002
|
+
return (0, react_jsx_runtime.jsx)("div", {
|
|
1003
|
+
className: QuestionComposer_module_css_default.frame,
|
|
1004
|
+
"data-question-key": pending.key,
|
|
1005
|
+
children: (0, react_jsx_runtime.jsxs)("section", {
|
|
1006
|
+
className: clsx(QuestionComposer_module_css_default.card, minimized && QuestionComposer_module_css_default.cardMinimized),
|
|
1007
|
+
"aria-labelledby": `question-${pending.key}-${String(index)}`,
|
|
1008
|
+
children: [(0, react_jsx_runtime.jsxs)("header", {
|
|
1009
|
+
className: QuestionComposer_module_css_default.header,
|
|
1010
|
+
children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
1011
|
+
className: QuestionComposer_module_css_default.headingBlock,
|
|
1012
|
+
children: [question.header !== void 0 && (0, react_jsx_runtime.jsx)("div", {
|
|
1013
|
+
className: QuestionComposer_module_css_default.eyebrow,
|
|
1014
|
+
children: question.header
|
|
1015
|
+
}), (0, react_jsx_runtime.jsx)("h2", {
|
|
1016
|
+
className: QuestionComposer_module_css_default.title,
|
|
1017
|
+
id: `question-${pending.key}-${String(index)}`,
|
|
1018
|
+
children: question.question
|
|
1019
|
+
})]
|
|
1020
|
+
}), (0, react_jsx_runtime.jsxs)("div", {
|
|
1021
|
+
className: QuestionComposer_module_css_default.headerActions,
|
|
1022
|
+
children: [
|
|
1023
|
+
countdown !== void 0 && card.waitState !== "waiting" && card.waitState !== "editing" && card.waitState !== "continued" && (0, react_jsx_runtime.jsx)("span", {
|
|
1024
|
+
className: QuestionComposer_module_css_default.waitStatus,
|
|
1025
|
+
children: t(countdown.running ? "wait.countdown" : "wait.paused", { seconds: Math.ceil(countdown.remainingMs / 1e3) })
|
|
1026
|
+
}),
|
|
1027
|
+
countdown !== void 0 && card.waitState !== "waiting" && card.waitState !== "editing" && card.waitState !== "continued" && (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
|
|
1028
|
+
variant: "outline",
|
|
1029
|
+
className: QuestionComposer_module_css_default.waitButton,
|
|
1030
|
+
onClick: takeTime,
|
|
1031
|
+
children: t("wait.takeTime")
|
|
1032
|
+
}),
|
|
1033
|
+
card.state === "continued" && (0, react_jsx_runtime.jsx)("span", {
|
|
1034
|
+
className: QuestionComposer_module_css_default.waitStatus,
|
|
1035
|
+
children: t("wait.continued")
|
|
1036
|
+
}),
|
|
1037
|
+
countdown !== void 0 && (card.waitState === "waiting" || card.waitState === "editing") && (0, react_jsx_runtime.jsx)("span", {
|
|
1038
|
+
className: QuestionComposer_module_css_default.waitStatus,
|
|
1039
|
+
children: t("wait.held")
|
|
1040
|
+
}),
|
|
1041
|
+
review !== void 0 && (0, react_jsx_runtime.jsx)("span", {
|
|
1042
|
+
className: QuestionComposer_module_css_default.waitStatus,
|
|
1043
|
+
children: t("review.status")
|
|
1044
|
+
}),
|
|
1045
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1046
|
+
type: "button",
|
|
1047
|
+
className: QuestionComposer_module_css_default.iconButton,
|
|
1048
|
+
"aria-label": t(minimized ? "nav.maximize" : "nav.minimize"),
|
|
1049
|
+
title: t(minimized ? "nav.maximize" : "nav.minimize"),
|
|
1050
|
+
"aria-expanded": !minimized,
|
|
1051
|
+
disabled: busy !== null,
|
|
1052
|
+
onClick: () => {
|
|
1053
|
+
setMinimized((current) => !current);
|
|
1054
|
+
},
|
|
1055
|
+
children: minimized ? (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronUpOutline14, {}) : (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronDownOutline14, {})
|
|
1056
|
+
}),
|
|
1057
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1058
|
+
type: "button",
|
|
1059
|
+
className: QuestionComposer_module_css_default.iconButton,
|
|
1060
|
+
"aria-label": t(pending.dismissal === "hide" ? "nav.close" : "nav.cancel"),
|
|
1061
|
+
title: t(pending.dismissal === "hide" ? "nav.close" : "nav.cancel"),
|
|
1062
|
+
disabled: busy !== null,
|
|
1063
|
+
onClick: dismissFlow,
|
|
1064
|
+
children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconCloseOutline16, {})
|
|
1065
|
+
})
|
|
1066
|
+
]
|
|
1067
|
+
})]
|
|
1068
|
+
}), !minimized && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
1069
|
+
ref: answerSurface,
|
|
1070
|
+
className: QuestionComposer_module_css_default.body,
|
|
1071
|
+
"data-question-scroll": true,
|
|
1072
|
+
onFocusCapture: focusAnswerSurface,
|
|
1073
|
+
onBlurCapture: blurAnswerSurface,
|
|
1074
|
+
children: [question.detail !== void 0 && (0, react_jsx_runtime.jsx)("div", {
|
|
1075
|
+
className: QuestionComposer_module_css_default.detail,
|
|
1076
|
+
children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.MarkdownText, {
|
|
1077
|
+
text: question.detail,
|
|
1078
|
+
labels: markdownLabels
|
|
1079
|
+
})
|
|
1080
|
+
}), (0, react_jsx_runtime.jsxs)("div", {
|
|
1081
|
+
className: QuestionComposer_module_css_default.options,
|
|
1082
|
+
role: question.multiSelect === true ? "group" : "radiogroup",
|
|
1083
|
+
children: [
|
|
1084
|
+
(question.options ?? []).map((option, optionIndex) => {
|
|
1085
|
+
const selected = draft.selected.includes(option.label);
|
|
1086
|
+
const display = parseRecommendedLabel(option.label);
|
|
1087
|
+
return (0, react_jsx_runtime.jsxs)("button", {
|
|
1088
|
+
type: "button",
|
|
1089
|
+
className: clsx(QuestionComposer_module_css_default.option, selected && question.multiSelect !== true && QuestionComposer_module_css_default.optionSelected),
|
|
1090
|
+
role: question.multiSelect === true ? "checkbox" : "radio",
|
|
1091
|
+
"aria-checked": selected,
|
|
1092
|
+
"aria-label": display.label,
|
|
1093
|
+
disabled: locked,
|
|
1094
|
+
onClick: () => {
|
|
1095
|
+
choose(option.label);
|
|
1096
|
+
},
|
|
1097
|
+
onKeyDown: (event) => {
|
|
1098
|
+
if (event.key !== "Enter") return;
|
|
1099
|
+
event.preventDefault();
|
|
1100
|
+
submitDrafts(drafts);
|
|
1101
|
+
},
|
|
1102
|
+
children: [question.multiSelect === true ? (0, react_jsx_runtime.jsx)("span", {
|
|
1103
|
+
className: clsx(QuestionComposer_module_css_default.checkbox, selected && QuestionComposer_module_css_default.checkboxChecked),
|
|
1104
|
+
"aria-hidden": "true",
|
|
1105
|
+
children: selected && (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconCheckOutline14, { size: 12 })
|
|
1106
|
+
}) : (0, react_jsx_runtime.jsx)("span", {
|
|
1107
|
+
className: QuestionComposer_module_css_default.number,
|
|
1108
|
+
children: optionIndex + 1
|
|
1109
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
1110
|
+
className: QuestionComposer_module_css_default.optionCopy,
|
|
1111
|
+
children: (0, react_jsx_runtime.jsxs)("span", {
|
|
1112
|
+
className: QuestionComposer_module_css_default.optionLine,
|
|
1113
|
+
children: [
|
|
1114
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
1115
|
+
className: QuestionComposer_module_css_default.optionLabel,
|
|
1116
|
+
children: display.label
|
|
1117
|
+
}),
|
|
1118
|
+
display.recommended && (0, react_jsx_runtime.jsx)("span", {
|
|
1119
|
+
className: QuestionComposer_module_css_default.badge,
|
|
1120
|
+
children: t("option.recommended")
|
|
1121
|
+
}),
|
|
1122
|
+
option.description !== void 0 && (0, react_jsx_runtime.jsx)("span", {
|
|
1123
|
+
className: QuestionComposer_module_css_default.description,
|
|
1124
|
+
children: option.description
|
|
1125
|
+
})
|
|
1126
|
+
]
|
|
1127
|
+
})
|
|
1128
|
+
})]
|
|
1129
|
+
}, `${option.label}-${String(optionIndex)}`);
|
|
1130
|
+
}),
|
|
1131
|
+
review !== void 0 && draft.skipped && (0, react_jsx_runtime.jsx)("p", {
|
|
1132
|
+
className: QuestionComposer_module_css_default.reviewNote,
|
|
1133
|
+
children: t("review.skipped")
|
|
1134
|
+
}),
|
|
1135
|
+
(review === void 0 || draft.custom !== "") && (hasOptions ? (0, react_jsx_runtime.jsxs)("div", {
|
|
1136
|
+
className: clsx(QuestionComposer_module_css_default.customRow, draft.custom !== "" && QuestionComposer_module_css_default.customRowActive),
|
|
1137
|
+
children: [question.multiSelect === true ? (0, react_jsx_runtime.jsx)("span", {
|
|
1138
|
+
className: clsx(QuestionComposer_module_css_default.checkbox, draft.custom !== "" && QuestionComposer_module_css_default.checkboxChecked),
|
|
1139
|
+
"aria-hidden": "true",
|
|
1140
|
+
children: draft.custom !== "" && (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconCheckOutline14, { size: 12 })
|
|
1141
|
+
}) : (0, react_jsx_runtime.jsx)("span", {
|
|
1142
|
+
className: QuestionComposer_module_css_default.number,
|
|
1143
|
+
"aria-hidden": "true",
|
|
1144
|
+
children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconEditOutline16, { size: 12 })
|
|
1145
|
+
}), (0, react_jsx_runtime.jsx)(AnswerField, {
|
|
1146
|
+
variant: "inline",
|
|
1147
|
+
value: draft.custom,
|
|
1148
|
+
disabled: locked,
|
|
1149
|
+
placeholder: t("custom.placeholder"),
|
|
1150
|
+
onChange: draftCustom,
|
|
1151
|
+
onKeyDown: continueFromCustom
|
|
1152
|
+
})]
|
|
1153
|
+
}) : (0, react_jsx_runtime.jsx)(AnswerField, {
|
|
1154
|
+
autoFocus: canSubmit && countdown === void 0 && !locked && !focusedQuestions.current.has(index),
|
|
1155
|
+
variant: "block",
|
|
1156
|
+
value: draft.custom,
|
|
1157
|
+
disabled: locked,
|
|
1158
|
+
placeholder: t("custom.placeholder"),
|
|
1159
|
+
onFocus: () => {
|
|
1160
|
+
focusedQuestions.current.add(index);
|
|
1161
|
+
},
|
|
1162
|
+
onChange: draftCustom,
|
|
1163
|
+
onKeyDown: continueFromCustom
|
|
1164
|
+
}))
|
|
1165
|
+
]
|
|
1166
|
+
})]
|
|
1167
|
+
}), (0, react_jsx_runtime.jsxs)("footer", {
|
|
1168
|
+
className: QuestionComposer_module_css_default.footer,
|
|
1169
|
+
children: [
|
|
1170
|
+
(0, react_jsx_runtime.jsxs)("div", {
|
|
1171
|
+
className: QuestionComposer_module_css_default.pager,
|
|
1172
|
+
children: [
|
|
1173
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1174
|
+
type: "button",
|
|
1175
|
+
className: QuestionComposer_module_css_default.iconButton,
|
|
1176
|
+
"aria-label": t("nav.prev"),
|
|
1177
|
+
disabled: index === 0 || busy !== null,
|
|
1178
|
+
onClick: () => {
|
|
1179
|
+
replaceProgress(index - 1, drafts);
|
|
1180
|
+
setError(null);
|
|
1181
|
+
},
|
|
1182
|
+
children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronLeftOutline14, {})
|
|
1183
|
+
}),
|
|
1184
|
+
(0, react_jsx_runtime.jsxs)("span", {
|
|
1185
|
+
className: QuestionComposer_module_css_default.progress,
|
|
1186
|
+
children: [
|
|
1187
|
+
index + 1,
|
|
1188
|
+
" / ",
|
|
1189
|
+
questions.length
|
|
1190
|
+
]
|
|
1191
|
+
}),
|
|
1192
|
+
(0, react_jsx_runtime.jsx)("button", {
|
|
1193
|
+
type: "button",
|
|
1194
|
+
className: QuestionComposer_module_css_default.iconButton,
|
|
1195
|
+
"aria-label": t("nav.next"),
|
|
1196
|
+
disabled: index === questions.length - 1 || busy !== null,
|
|
1197
|
+
onClick: () => {
|
|
1198
|
+
replaceProgress(index + 1, drafts);
|
|
1199
|
+
setError(null);
|
|
1200
|
+
},
|
|
1201
|
+
children: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronRightOutline14, {})
|
|
1202
|
+
})
|
|
1203
|
+
]
|
|
1204
|
+
}),
|
|
1205
|
+
(0, react_jsx_runtime.jsx)("div", {
|
|
1206
|
+
className: QuestionComposer_module_css_default.feedback,
|
|
1207
|
+
role: "status",
|
|
1208
|
+
children: error === null ? null : "key" in error ? t(error.key) : error.text
|
|
1209
|
+
}),
|
|
1210
|
+
review === void 0 && (0, react_jsx_runtime.jsxs)("div", {
|
|
1211
|
+
className: QuestionComposer_module_css_default.footerActions,
|
|
1212
|
+
children: [(0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
|
|
1213
|
+
variant: "outline",
|
|
1214
|
+
disabled: busy !== null,
|
|
1215
|
+
onClick: skipQuestion,
|
|
1216
|
+
children: t("action.skip")
|
|
1217
|
+
}), (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Button, {
|
|
1218
|
+
variant: "primary",
|
|
1219
|
+
disabled: busy !== null || !answered(draft) || index === questions.length - 1 && !canSubmit,
|
|
1220
|
+
onClick: continueFlow,
|
|
1221
|
+
children: busy === "answer" ? t("submitting") : index === questions.length - 1 ? t("submit") : t("action.next")
|
|
1222
|
+
})]
|
|
1223
|
+
})
|
|
1224
|
+
]
|
|
1225
|
+
})] })]
|
|
1226
|
+
})
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
//#endregion
|
|
1230
|
+
//#region ../../core/session/src/surface.ts
|
|
1231
|
+
/** Runtime counterpart of the message-producing event union. */
|
|
1232
|
+
const SURFACE_EVENT_TYPES = new Set([
|
|
1233
|
+
"system/message",
|
|
1234
|
+
"developer/message",
|
|
1235
|
+
"user/message",
|
|
1236
|
+
"assistant/message",
|
|
1237
|
+
"tool/result"
|
|
1238
|
+
]);
|
|
1239
|
+
/**
|
|
1240
|
+
* Narrow an event to a surface-eligible event carrying its required marker.
|
|
1241
|
+
* @param event - event to test.
|
|
1242
|
+
* @returns true when both the type and marker identify a surface event.
|
|
1243
|
+
*/
|
|
1244
|
+
function isSurfaceEvent(event) {
|
|
1245
|
+
if (!SURFACE_EVENT_TYPES.has(event.type)) return false;
|
|
1246
|
+
return event.surfaceOp !== void 0;
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Narrow an event to an append-origin surface event: one that entered the
|
|
1250
|
+
* surface at its own log position and was never itself a replacement copy.
|
|
1251
|
+
*
|
|
1252
|
+
* The model-visible surface deliberately shadows replaced ranges, so it is the
|
|
1253
|
+
* wrong source for a human transcript — a landed replacement would erase
|
|
1254
|
+
* conversation the user already saw. Append-origin events are that transcript's
|
|
1255
|
+
* durable source material; replacement copies stay model-only.
|
|
1256
|
+
* @param event - event to test.
|
|
1257
|
+
* @returns true when the event appended to the surface tail.
|
|
1258
|
+
*/
|
|
1259
|
+
function isAppendSurfaceEvent(event) {
|
|
1260
|
+
return isSurfaceEvent(event) && event.surfaceOp === "append";
|
|
1261
|
+
}
|
|
1262
|
+
//#endregion
|
|
1263
|
+
//#region lib/types/client/question-reply.js
|
|
1264
|
+
function isRecord(value) {
|
|
1265
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Read the question and answer pairs out of the reply text at the conversation boundary.
|
|
1269
|
+
* @param text - Model-facing JSON payload of the steered message.
|
|
1270
|
+
* @returns The pairs, or empty lists when the payload is unreadable.
|
|
1271
|
+
*/
|
|
1272
|
+
function replyPairsOf(text) {
|
|
1273
|
+
let value;
|
|
1274
|
+
try {
|
|
1275
|
+
value = JSON.parse(text);
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
return {
|
|
1278
|
+
questions: [],
|
|
1279
|
+
answers: []
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
if (!isRecord(value) || !Array.isArray(value.questions)) return {
|
|
1283
|
+
questions: [],
|
|
1284
|
+
answers: []
|
|
1285
|
+
};
|
|
1286
|
+
const questions = [];
|
|
1287
|
+
for (const question of value.questions) {
|
|
1288
|
+
if (!isRecord(question) || typeof question.id !== "string" || typeof question.question !== "string") continue;
|
|
1289
|
+
questions.push({
|
|
1290
|
+
id: question.id,
|
|
1291
|
+
question: question.question,
|
|
1292
|
+
...typeof question.detail === "string" ? { detail: question.detail } : {},
|
|
1293
|
+
...typeof question.header === "string" ? { header: question.header } : {},
|
|
1294
|
+
...Array.isArray(question.options) ? { options: question.options.flatMap((option) => {
|
|
1295
|
+
if (!isRecord(option) || typeof option.label !== "string") return [];
|
|
1296
|
+
return [{
|
|
1297
|
+
label: option.label,
|
|
1298
|
+
...typeof option.description === "string" ? { description: option.description } : {}
|
|
1299
|
+
}];
|
|
1300
|
+
}) } : {},
|
|
1301
|
+
...typeof question.multiSelect === "boolean" ? { multiSelect: question.multiSelect } : {}
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
const answers = [];
|
|
1305
|
+
for (const answer of Array.isArray(value.answers) ? value.answers : []) {
|
|
1306
|
+
if (!isRecord(answer) || typeof answer.id !== "string" || !Array.isArray(answer.selected)) continue;
|
|
1307
|
+
answers.push({
|
|
1308
|
+
id: answer.id,
|
|
1309
|
+
selected: answer.selected.filter((item) => typeof item === "string"),
|
|
1310
|
+
...typeof answer.custom === "string" ? { custom: answer.custom } : {}
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
return {
|
|
1314
|
+
questions,
|
|
1315
|
+
answers
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Answer values of one question in display order: the selected option labels
|
|
1320
|
+
* followed by a non-blank custom answer.
|
|
1321
|
+
* @param data - Projected reply holding the recorded answers.
|
|
1322
|
+
* @param id - Question id to read.
|
|
1323
|
+
* @returns The values, empty when the user skipped that question.
|
|
1324
|
+
*/
|
|
1325
|
+
function replyAnswerValues(data, id) {
|
|
1326
|
+
const answer = data.answers.find((item) => item.id === id);
|
|
1327
|
+
const custom = answer?.custom?.trim() ?? "";
|
|
1328
|
+
return [...answer?.selected ?? [], ...custom === "" ? [] : [custom]];
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Clipboard text of one late reply: every question with the answer the user
|
|
1332
|
+
* gave, in the layout the open bubble shows, without the options nobody chose.
|
|
1333
|
+
* The text does not depend on whether the bubble is open.
|
|
1334
|
+
* @param data - Projected reply to copy.
|
|
1335
|
+
* @param t - Bound `question` namespace translator owning the answer labels.
|
|
1336
|
+
* @returns One block per question, or the model-facing text when the payload was unreadable.
|
|
1337
|
+
*/
|
|
1338
|
+
function replyClipboardText(data, t) {
|
|
1339
|
+
if (data.questions.length === 0) return data.text;
|
|
1340
|
+
return data.questions.map((question) => {
|
|
1341
|
+
const values = replyAnswerValues(data, question.id);
|
|
1342
|
+
return `${question.header && question.header !== question.question ? `${question.header} — ${question.question}` : question.question}\n${values.length === 0 ? t("reply.skipped") : `${t("reply.answerLabel")}${values.join(", ")}`}`;
|
|
1343
|
+
}).join("\n\n");
|
|
1344
|
+
}
|
|
1345
|
+
/** Validate a late reply source at the persisted conversation boundary. */
|
|
1346
|
+
function replySource(event) {
|
|
1347
|
+
const source = event.data.source;
|
|
1348
|
+
return isRecord(source) && source.kind === "user-question-reply" && typeof source.callId === "string" && source.outcome === "answered" ? {
|
|
1349
|
+
callId: source.callId,
|
|
1350
|
+
outcome: "answered"
|
|
1351
|
+
} : null;
|
|
1352
|
+
}
|
|
1353
|
+
/** Late-reply projection owned by the questions UI. */
|
|
1354
|
+
const questionReplyDefinition = {
|
|
1355
|
+
kind: "user-question-reply",
|
|
1356
|
+
target: "chat",
|
|
1357
|
+
match: (event) => {
|
|
1358
|
+
if (event.type !== "user/message" || !isAppendSurfaceEvent(event)) return null;
|
|
1359
|
+
return replySource(event) === null ? null : {
|
|
1360
|
+
id: String(event.data.id),
|
|
1361
|
+
role: "start"
|
|
1362
|
+
};
|
|
1363
|
+
},
|
|
1364
|
+
start: (_context, match) => {
|
|
1365
|
+
if (match.event.type !== "user/message") throw new Error("user-question-reply start requires user/message");
|
|
1366
|
+
const event = match.event;
|
|
1367
|
+
const text = event.data.content.find((item) => item.type === "text")?.text ?? "";
|
|
1368
|
+
const source = replySource(event);
|
|
1369
|
+
if (source === null) throw new Error("user-question-reply start requires a reply source");
|
|
1370
|
+
return {
|
|
1371
|
+
seq: event.seq,
|
|
1372
|
+
time: event.time,
|
|
1373
|
+
callId: source.callId,
|
|
1374
|
+
outcome: source.outcome,
|
|
1375
|
+
text,
|
|
1376
|
+
...replyPairsOf(text)
|
|
1377
|
+
};
|
|
1378
|
+
},
|
|
1379
|
+
update: (context) => context.state,
|
|
1380
|
+
buildViewNode: (context) => {
|
|
1381
|
+
if (context.state === void 0) return null;
|
|
1382
|
+
const { seq, ...data } = context.state;
|
|
1383
|
+
return {
|
|
1384
|
+
key: context.key,
|
|
1385
|
+
kind: "question-reply",
|
|
1386
|
+
id: context.id,
|
|
1387
|
+
target: "chat",
|
|
1388
|
+
anchorSeq: seq,
|
|
1389
|
+
location: context.start?.location ?? { kind: "unresolved" },
|
|
1390
|
+
visibility: "visible",
|
|
1391
|
+
data
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
//#endregion
|
|
1396
|
+
//#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-user-questions/src/client/QuestionReplyView.module.css.mjs
|
|
1397
|
+
const css = ".wgh9Da_row{flex-direction:column;justify-content:flex-end;align-items:flex-end;gap:6px;padding:4px 0;display:flex}.wgh9Da_bubble{border-radius:var(--qilin-radius-xl);background:var(--qilin-specific-bubble);max-width:72%;color:var(--qilin-alias-label-primary);cursor:pointer;font-size:var(--dsh-content-font-size,14px);line-height:calc(22px + var(--dsh-content-font-delta,0px));text-align:left;overflow-wrap:anywhere;white-space:pre-wrap;padding:10px 16px}.wgh9Da_toggle{width:100%;color:inherit;cursor:pointer;font:inherit;text-align:left;background:0 0;border:0;padding:0}.wgh9Da_toggle:focus-visible{outline:2px solid var(--qilin-focus-ring-color,var(--qilin-alias-state-business-primary));outline-offset:2px}.wgh9Da_bubble:has(.wgh9Da_toggle:focus-visible){box-shadow:0 0 0 2px var(--qilin-focus-ring-color,var(--qilin-alias-state-business-primary))}.wgh9Da_labelRow{align-items:center;gap:3px;display:flex}.wgh9Da_caret{color:var(--qilin-alias-label-secondary);display:inline-flex}.wgh9Da_caret svg{width:12px;height:12px}.wgh9Da_label{color:var(--qilin-alias-label-secondary);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(18px + var(--dsh-content-font-delta-secondary,0px));display:block}.wgh9Da_summary{color:var(--qilin-alias-label-primary);font-size:var(--dsh-content-font-size,14px);margin-top:3px;display:block}.wgh9Da_text{margin:8px 0 0}.wgh9Da_details{border-top:1px solid color-mix(in srgb, var(--qilin-alias-label-secondary) 24%, transparent);margin:8px 0 0;padding-top:8px}.wgh9Da_details>div+div{margin-top:12px}.wgh9Da_details dt{color:var(--qilin-alias-label-secondary);font-size:12px}.wgh9Da_question{gap:3px;display:grid}.wgh9Da_header,.wgh9Da_detail,.wgh9Da_options{color:var(--qilin-alias-label-secondary);font-size:11px}.wgh9Da_questionText{color:var(--qilin-alias-label-primary)}.wgh9Da_options{margin:0;padding-left:16px}.wgh9Da_details dd{margin:4px 0 0}.wgh9Da_answer{font-size:14px}.wgh9Da_answerLabel{color:var(--qilin-alias-label-secondary);margin-right:4px;font-size:12px}.wgh9Da_actions{height:28px;color:var(--qilin-alias-label-tertiary);align-items:center;gap:8px;padding-right:4px;display:flex}.wgh9Da_time{white-space:nowrap;padding-right:4px;font-size:13px;line-height:24px}.wgh9Da_action{border-radius:var(--qilin-radius-sm);width:28px;height:28px;color:inherit;cursor:pointer;background:0 0;border:none;justify-content:center;align-items:center;padding:6px;display:inline-flex}.wgh9Da_action svg{width:15px;height:15px}.wgh9Da_action:hover{background:var(--qilin-alias-interactive-bg-hover);color:var(--qilin-alias-label-secondary)}";
|
|
1398
|
+
const tagId = "@qilin-agent/client-ui-user-questions/QuestionReplyView.module.css";
|
|
1399
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
1400
|
+
const tag = document.createElement("style");
|
|
1401
|
+
tag.dataset.plugin = "@qilin-agent/client-ui-user-questions";
|
|
1402
|
+
tag.dataset.pluginCss = tagId;
|
|
1403
|
+
tag.textContent = css;
|
|
1404
|
+
document.head.appendChild(tag);
|
|
1405
|
+
}
|
|
1406
|
+
var QuestionReplyView_module_css_default = {
|
|
1407
|
+
"action": "wgh9Da_action",
|
|
1408
|
+
"actions": "wgh9Da_actions",
|
|
1409
|
+
"answer": "wgh9Da_answer",
|
|
1410
|
+
"answerLabel": "wgh9Da_answerLabel",
|
|
1411
|
+
"bubble": "wgh9Da_bubble",
|
|
1412
|
+
"caret": "wgh9Da_caret",
|
|
1413
|
+
"detail": "wgh9Da_detail",
|
|
1414
|
+
"details": "wgh9Da_details",
|
|
1415
|
+
"header": "wgh9Da_header",
|
|
1416
|
+
"label": "wgh9Da_label",
|
|
1417
|
+
"labelRow": "wgh9Da_labelRow",
|
|
1418
|
+
"options": "wgh9Da_options",
|
|
1419
|
+
"question": "wgh9Da_question",
|
|
1420
|
+
"questionText": "wgh9Da_questionText",
|
|
1421
|
+
"row": "wgh9Da_row",
|
|
1422
|
+
"summary": "wgh9Da_summary",
|
|
1423
|
+
"text": "wgh9Da_text",
|
|
1424
|
+
"time": "wgh9Da_time",
|
|
1425
|
+
"toggle": "wgh9Da_toggle"
|
|
1426
|
+
};
|
|
1427
|
+
//#endregion
|
|
1428
|
+
//#region lib/types/client/QuestionReplyView.js
|
|
1429
|
+
/**
|
|
1430
|
+
* Right-aligned late-reply bubble: a label naming the earlier pending
|
|
1431
|
+
* questions, then one question and answer pair per question. A payload the
|
|
1432
|
+
* Client cannot read falls back to the model-facing text.
|
|
1433
|
+
*/
|
|
1434
|
+
const QuestionReplyView = (0, react.memo)(function QuestionReplyView({ node, t }) {
|
|
1435
|
+
return (0, react_jsx_runtime.jsx)(QuestionReplyBubble, {
|
|
1436
|
+
data: node.data,
|
|
1437
|
+
t
|
|
1438
|
+
});
|
|
1439
|
+
});
|
|
1440
|
+
/**
|
|
1441
|
+
* Render one settled question reply as a compact transcript bubble.
|
|
1442
|
+
* @param props - Reply data and the question locale translator.
|
|
1443
|
+
* @returns The expandable reply bubble.
|
|
1444
|
+
*/
|
|
1445
|
+
function QuestionReplyBubble({ data, t }) {
|
|
1446
|
+
const label = t("reply.label");
|
|
1447
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1448
|
+
const toggleLabel = t(open ? "reply.close" : "reply.open");
|
|
1449
|
+
const summary = data.questions.map((question) => replyAnswerValues(data, question.id)).flat().join(", ");
|
|
1450
|
+
const copyText = replyClipboardText(data, t);
|
|
1451
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
1452
|
+
const onCopy = (0, react.useCallback)(() => {
|
|
1453
|
+
if (copied) return;
|
|
1454
|
+
(0, _qilin_agent_client_ui_primitives.writeClipboard)(copyText).then((ok) => {
|
|
1455
|
+
if (!ok) return;
|
|
1456
|
+
setCopied(true);
|
|
1457
|
+
window.setTimeout(() => {
|
|
1458
|
+
setCopied(false);
|
|
1459
|
+
}, 1e3);
|
|
1460
|
+
});
|
|
1461
|
+
}, [copied, copyText]);
|
|
1462
|
+
return (0, react_jsx_runtime.jsxs)("div", {
|
|
1463
|
+
className: QuestionReplyView_module_css_default.row,
|
|
1464
|
+
"data-question-reply": data.callId,
|
|
1465
|
+
"data-reply-outcome": data.outcome,
|
|
1466
|
+
role: "group",
|
|
1467
|
+
"aria-label": label,
|
|
1468
|
+
children: [(0, react_jsx_runtime.jsxs)("div", {
|
|
1469
|
+
className: QuestionReplyView_module_css_default.bubble,
|
|
1470
|
+
children: [(0, react_jsx_runtime.jsxs)("button", {
|
|
1471
|
+
type: "button",
|
|
1472
|
+
className: QuestionReplyView_module_css_default.toggle,
|
|
1473
|
+
"aria-expanded": open,
|
|
1474
|
+
"aria-label": `${label} · ${toggleLabel}`,
|
|
1475
|
+
onClick: () => {
|
|
1476
|
+
setOpen((value) => !value);
|
|
1477
|
+
},
|
|
1478
|
+
children: [(0, react_jsx_runtime.jsxs)("span", {
|
|
1479
|
+
className: QuestionReplyView_module_css_default.labelRow,
|
|
1480
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
1481
|
+
className: QuestionReplyView_module_css_default.caret,
|
|
1482
|
+
"aria-hidden": "true",
|
|
1483
|
+
children: open ? (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronDownOutline14, {}) : (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconChevronRightOutline14, {})
|
|
1484
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
1485
|
+
className: QuestionReplyView_module_css_default.label,
|
|
1486
|
+
children: label
|
|
1487
|
+
})]
|
|
1488
|
+
}), !open && (0, react_jsx_runtime.jsx)("span", {
|
|
1489
|
+
className: QuestionReplyView_module_css_default.summary,
|
|
1490
|
+
children: summary === "" ? t("reply.skipped") : summary
|
|
1491
|
+
})]
|
|
1492
|
+
}), open && (data.questions.length === 0 ? (0, react_jsx_runtime.jsx)("p", {
|
|
1493
|
+
className: QuestionReplyView_module_css_default.text,
|
|
1494
|
+
children: data.text
|
|
1495
|
+
}) : (0, react_jsx_runtime.jsx)("dl", {
|
|
1496
|
+
className: QuestionReplyView_module_css_default.details,
|
|
1497
|
+
children: data.questions.map((question) => {
|
|
1498
|
+
const values = replyAnswerValues(data, question.id);
|
|
1499
|
+
return (0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsxs)("dt", {
|
|
1500
|
+
className: QuestionReplyView_module_css_default.question,
|
|
1501
|
+
children: [
|
|
1502
|
+
question.header && question.header !== question.question && (0, react_jsx_runtime.jsx)("span", {
|
|
1503
|
+
className: QuestionReplyView_module_css_default.header,
|
|
1504
|
+
children: question.header
|
|
1505
|
+
}),
|
|
1506
|
+
(0, react_jsx_runtime.jsx)("span", {
|
|
1507
|
+
className: QuestionReplyView_module_css_default.questionText,
|
|
1508
|
+
children: question.question
|
|
1509
|
+
}),
|
|
1510
|
+
question.detail && (0, react_jsx_runtime.jsx)("span", {
|
|
1511
|
+
className: QuestionReplyView_module_css_default.detail,
|
|
1512
|
+
children: question.detail
|
|
1513
|
+
}),
|
|
1514
|
+
question.options && question.options.length > 0 && (0, react_jsx_runtime.jsx)("ul", {
|
|
1515
|
+
className: QuestionReplyView_module_css_default.options,
|
|
1516
|
+
children: question.options.map((option) => (0, react_jsx_runtime.jsxs)("li", { children: [option.label, option.description && ` — ${option.description}`] }, option.label))
|
|
1517
|
+
})
|
|
1518
|
+
]
|
|
1519
|
+
}), (0, react_jsx_runtime.jsxs)("dd", {
|
|
1520
|
+
className: QuestionReplyView_module_css_default.answer,
|
|
1521
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
1522
|
+
className: QuestionReplyView_module_css_default.answerLabel,
|
|
1523
|
+
children: t("reply.answerLabel")
|
|
1524
|
+
}), values.length === 0 ? t("reply.skipped") : values.join(", ")]
|
|
1525
|
+
})] }, question.id);
|
|
1526
|
+
})
|
|
1527
|
+
}))]
|
|
1528
|
+
}), (0, react_jsx_runtime.jsxs)("div", {
|
|
1529
|
+
className: QuestionReplyView_module_css_default.actions,
|
|
1530
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
1531
|
+
className: QuestionReplyView_module_css_default.time,
|
|
1532
|
+
children: new Intl.DateTimeFormat(void 0, {
|
|
1533
|
+
hour: "2-digit",
|
|
1534
|
+
minute: "2-digit",
|
|
1535
|
+
hour12: false
|
|
1536
|
+
}).format(data.time)
|
|
1537
|
+
}), (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Tooltip, {
|
|
1538
|
+
label: copied ? t("copied") : t("copy"),
|
|
1539
|
+
side: "bottom",
|
|
1540
|
+
children: (0, react_jsx_runtime.jsx)("button", {
|
|
1541
|
+
type: "button",
|
|
1542
|
+
className: QuestionReplyView_module_css_default.action,
|
|
1543
|
+
"aria-label": copied ? t("copied") : t("copy"),
|
|
1544
|
+
onClick: onCopy,
|
|
1545
|
+
children: copied ? (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconCheckOutline14, {}) : (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconCopyOutline16, {})
|
|
1546
|
+
})
|
|
1547
|
+
})]
|
|
1548
|
+
})]
|
|
1549
|
+
});
|
|
1550
|
+
}
|
|
1551
|
+
//#endregion
|
|
1552
|
+
//#region lib/types/client/locales.js
|
|
1553
|
+
/** `question` namespace dictionaries. */
|
|
1554
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
1555
|
+
const zh = {
|
|
1556
|
+
"error.incomplete": "请先完成这道问题。",
|
|
1557
|
+
"error.unanswered": "请选择一个选项或填写自定义答案。",
|
|
1558
|
+
"error.unavailable": "当前无法提交,请稍候再试。",
|
|
1559
|
+
"error.resubmit": "回答未送达,工作已继续,请再提交一次。",
|
|
1560
|
+
"status.sent": "回答已发送;面板未能关闭。",
|
|
1561
|
+
"wait.takeTime": "慢慢回答",
|
|
1562
|
+
"wait.countdown": "{seconds} 秒后继续工作",
|
|
1563
|
+
"wait.paused": "已暂停 · 剩余 {seconds} 秒",
|
|
1564
|
+
"wait.held": "会一直等你回答",
|
|
1565
|
+
"wait.continued": "已继续工作,仍可回答",
|
|
1566
|
+
"review.status": "已回答",
|
|
1567
|
+
"review.skipped": "这道问题当时被跳过。",
|
|
1568
|
+
"reply.label": "回答先前等待中的问题",
|
|
1569
|
+
"reply.open": "展开问题详情",
|
|
1570
|
+
"reply.close": "收起问题详情",
|
|
1571
|
+
"reply.answerLabel": "回答:",
|
|
1572
|
+
"reply.skipped": "已跳过",
|
|
1573
|
+
"nav.prev": "上一题",
|
|
1574
|
+
"nav.next": "下一题",
|
|
1575
|
+
"nav.minimize": "收起问题卡片",
|
|
1576
|
+
"nav.maximize": "展开问题卡片",
|
|
1577
|
+
"nav.cancel": "放弃整组问题",
|
|
1578
|
+
"nav.close": "收起问题面板,可从工具调用重新打开",
|
|
1579
|
+
"option.recommended": "推荐",
|
|
1580
|
+
"custom.placeholder": "输入你的答案",
|
|
1581
|
+
"action.skip": "跳过",
|
|
1582
|
+
"action.next": "下一题",
|
|
1583
|
+
"plan.header": "计划待审",
|
|
1584
|
+
"plan.approve": "同意执行",
|
|
1585
|
+
"plan.decline": "拒绝",
|
|
1586
|
+
"plan.discuss": "要求修改"
|
|
1587
|
+
};
|
|
1588
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
1589
|
+
const en = {
|
|
1590
|
+
"error.incomplete": "Please complete this question first.",
|
|
1591
|
+
"error.unanswered": "Please select an option or enter a custom answer.",
|
|
1592
|
+
"error.unavailable": "Cannot submit right now; try again in a moment.",
|
|
1593
|
+
"error.resubmit": "The answer did not arrive before work continued; submit it again.",
|
|
1594
|
+
"status.sent": "Reply sent; the panel could not close.",
|
|
1595
|
+
"wait.takeTime": "Take time",
|
|
1596
|
+
"wait.countdown": "Continuing in {seconds}s",
|
|
1597
|
+
"wait.paused": "Paused · {seconds}s remaining",
|
|
1598
|
+
"wait.held": "Waiting until you answer",
|
|
1599
|
+
"wait.continued": "Work continued — you can still answer",
|
|
1600
|
+
"review.status": "Answered",
|
|
1601
|
+
"review.skipped": "This question was skipped.",
|
|
1602
|
+
"reply.label": "Reply to earlier pending questions",
|
|
1603
|
+
"reply.open": "Open question details",
|
|
1604
|
+
"reply.close": "Close question details",
|
|
1605
|
+
"reply.answerLabel": "Answer: ",
|
|
1606
|
+
"reply.skipped": "Skipped",
|
|
1607
|
+
"nav.prev": "Previous question",
|
|
1608
|
+
"nav.next": "Next question",
|
|
1609
|
+
"nav.minimize": "Collapse the question card",
|
|
1610
|
+
"nav.maximize": "Expand the question card",
|
|
1611
|
+
"nav.cancel": "Dismiss all questions",
|
|
1612
|
+
"nav.close": "Close the panel — reopen it from the tool call",
|
|
1613
|
+
"option.recommended": "Recommended",
|
|
1614
|
+
"custom.placeholder": "Type your answer",
|
|
1615
|
+
"action.skip": "Skip",
|
|
1616
|
+
"action.next": "Next",
|
|
1617
|
+
"plan.header": "Plan review",
|
|
1618
|
+
"plan.approve": "Approve",
|
|
1619
|
+
"plan.decline": "Refuse",
|
|
1620
|
+
"plan.discuss": "Request changes"
|
|
1621
|
+
};
|
|
1622
|
+
//#endregion
|
|
1623
|
+
//#region lib/types/client/index.js
|
|
1624
|
+
/** Dictionary namespace owned by this plugin. */
|
|
1625
|
+
const NS = "question";
|
|
1626
|
+
/** Required services: Agent scopes, Remote Events, Session UI, Slot registry, conversation nodes, and copy. */
|
|
1627
|
+
const inject = [
|
|
1628
|
+
"sessions",
|
|
1629
|
+
"remote",
|
|
1630
|
+
"remote.userQuestions",
|
|
1631
|
+
"uiSession",
|
|
1632
|
+
"slots",
|
|
1633
|
+
"locale",
|
|
1634
|
+
"uiConversation"
|
|
1635
|
+
];
|
|
1636
|
+
/**
|
|
1637
|
+
* Cards published to the Session pending-interaction registry, keyed by
|
|
1638
|
+
* `PendingQuestion.key`. A tool-call-keyed card is shared by the forwarded
|
|
1639
|
+
* waterfall and the Session projection.
|
|
1640
|
+
*
|
|
1641
|
+
* A card outlives its seat. Closing the panel only unpublishes it, which keeps
|
|
1642
|
+
* the request answerable from its tool call row, so this registry — not the
|
|
1643
|
+
* pending-interaction registry — decides when a request is over.
|
|
1644
|
+
*/
|
|
1645
|
+
var QuestionCards = class {
|
|
1646
|
+
publish;
|
|
1647
|
+
#cards = /* @__PURE__ */ new Map();
|
|
1648
|
+
constructor(publish) {
|
|
1649
|
+
this.publish = publish;
|
|
1650
|
+
}
|
|
1651
|
+
byCallId(sessionId, callId) {
|
|
1652
|
+
return this.#cards.get(PendingQuestion.keyOf(sessionId, callId));
|
|
1653
|
+
}
|
|
1654
|
+
/** Observable state for one card while the registry owns it. */
|
|
1655
|
+
source(key) {
|
|
1656
|
+
return this.#cards.get(key)?.pending;
|
|
1657
|
+
}
|
|
1658
|
+
values() {
|
|
1659
|
+
return [...this.#cards.values()];
|
|
1660
|
+
}
|
|
1661
|
+
/** Keys of every card registered for one Session. */
|
|
1662
|
+
keysFor(sessionId) {
|
|
1663
|
+
return this.values().filter((card) => card.pending.sessionId === sessionId).map((card) => card.pending.key);
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Return the card of a tool call, creating and publishing it when absent.
|
|
1667
|
+
* A request without a call id always gets a fresh card.
|
|
1668
|
+
*/
|
|
1669
|
+
ensure(sessionId, questions, callId) {
|
|
1670
|
+
if (callId !== void 0) {
|
|
1671
|
+
const existing = this.byCallId(sessionId, callId);
|
|
1672
|
+
if (existing !== void 0) return existing;
|
|
1673
|
+
}
|
|
1674
|
+
return this.#create(new PendingQuestion(sessionId, questions, callId, () => this.keysFor(sessionId)));
|
|
1675
|
+
}
|
|
1676
|
+
/** Publish one carrier into the composer seat and register the card that owns it. */
|
|
1677
|
+
#create(pending) {
|
|
1678
|
+
const requests = /* @__PURE__ */ new Set();
|
|
1679
|
+
const delegate = async () => {
|
|
1680
|
+
pending.delegate();
|
|
1681
|
+
await Promise.all(requests);
|
|
1682
|
+
};
|
|
1683
|
+
let unpublish = this.publish(pending, delegate);
|
|
1684
|
+
const card = {
|
|
1685
|
+
pending,
|
|
1686
|
+
hide: () => {
|
|
1687
|
+
unpublish?.();
|
|
1688
|
+
unpublish = void 0;
|
|
1689
|
+
},
|
|
1690
|
+
reveal: () => {
|
|
1691
|
+
unpublish?.();
|
|
1692
|
+
unpublish = this.publish(pending, delegate);
|
|
1693
|
+
},
|
|
1694
|
+
remove: () => {
|
|
1695
|
+
/* v8 ignore next -- a card leaves the registry once; no caller holds a card the registry already replaced. */
|
|
1696
|
+
if (this.#cards.get(pending.key) !== card) return;
|
|
1697
|
+
this.#cards.delete(pending.key);
|
|
1698
|
+
pending.close();
|
|
1699
|
+
unpublish?.();
|
|
1700
|
+
unpublish = void 0;
|
|
1701
|
+
},
|
|
1702
|
+
trackRequest: (completion) => {
|
|
1703
|
+
requests.add(completion);
|
|
1704
|
+
return () => {
|
|
1705
|
+
requests.delete(completion);
|
|
1706
|
+
};
|
|
1707
|
+
},
|
|
1708
|
+
hasRequest: () => requests.size > 0
|
|
1709
|
+
};
|
|
1710
|
+
pending.attachSeat({ hide: pending.review === void 0 ? card.hide : card.remove });
|
|
1711
|
+
this.#cards.set(pending.key, card);
|
|
1712
|
+
return card;
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Show the panel of one answerable tool call.
|
|
1716
|
+
* @param sessionId - Session the tool call belongs to.
|
|
1717
|
+
* @param callId - `ask_user_question` call whose panel to show, as its
|
|
1718
|
+
* transcript row spells it.
|
|
1719
|
+
* @returns whether a card for that call is still answerable.
|
|
1720
|
+
*/
|
|
1721
|
+
reveal(sessionId, callId) {
|
|
1722
|
+
const card = this.byCallId(sessionId, callId);
|
|
1723
|
+
if (card === void 0) return false;
|
|
1724
|
+
card.reveal();
|
|
1725
|
+
return true;
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Show one settled tool call's recorded answers as a read-only card. A call
|
|
1729
|
+
* that somehow still holds a card is shown as it stands, so a live request is
|
|
1730
|
+
* never replaced by a stale copy of itself.
|
|
1731
|
+
* @param sessionId - Session the tool call belongs to.
|
|
1732
|
+
* @param callId - `ask_user_question` call whose record to show, as its
|
|
1733
|
+
* transcript row spells it.
|
|
1734
|
+
* @param record - the call's questions and recorded answers.
|
|
1735
|
+
* @returns true; a record always produces a card.
|
|
1736
|
+
*/
|
|
1737
|
+
review(sessionId, callId, record) {
|
|
1738
|
+
(this.byCallId(sessionId, callId) ?? this.#create(new PendingQuestion(sessionId, record.questions, brandString(callId), () => this.keysFor(sessionId), record.answers))).reveal();
|
|
1739
|
+
return true;
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Hand every live waterfall back at plugin teardown. The pending-interaction
|
|
1743
|
+
* registry only drains what it currently holds, and a hidden card is not in
|
|
1744
|
+
* it, so its Host request would wait for its own abort instead.
|
|
1745
|
+
*/
|
|
1746
|
+
dispose() {
|
|
1747
|
+
for (const card of this.values()) {
|
|
1748
|
+
card.pending.delegate();
|
|
1749
|
+
card.remove();
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
/** Present one forwarded request through its card until the waterfall settles. */
|
|
1754
|
+
async function answerQuestion(ctx, owner, request, next, cards) {
|
|
1755
|
+
const sessionId = ctx.sessions.scopeOf(owner);
|
|
1756
|
+
if (sessionId === void 0) return next();
|
|
1757
|
+
const callId = request.wait?.callId;
|
|
1758
|
+
const card = cards.ensure(sessionId, request.questions, callId);
|
|
1759
|
+
const claimLifetime = new AbortController();
|
|
1760
|
+
const claimSignal = request.signal === void 0 ? claimLifetime.signal : AbortSignal.any([claimLifetime.signal, request.signal]);
|
|
1761
|
+
let claim;
|
|
1762
|
+
let claimEnded;
|
|
1763
|
+
let delegateRequest;
|
|
1764
|
+
const releaseClaim = request.wait?.timed === true && callId !== void 0 ? ctx.effect(() => {
|
|
1765
|
+
claim = ctx.remote.userQuestions.attachWait(sessionId, callId, claimSignal);
|
|
1766
|
+
return async () => {
|
|
1767
|
+
delegateRequest?.();
|
|
1768
|
+
claimLifetime.abort();
|
|
1769
|
+
claim?.dispose();
|
|
1770
|
+
if (claimEnded !== void 0) await Promise.allSettled([claimEnded]);
|
|
1771
|
+
};
|
|
1772
|
+
}, "ui-user-questions: foreground claim") : void 0;
|
|
1773
|
+
const completed = Promise.withResolvers();
|
|
1774
|
+
const finishRequest = card.trackRequest(completed.promise);
|
|
1775
|
+
try {
|
|
1776
|
+
const iterator = claim?.[Symbol.asyncIterator]();
|
|
1777
|
+
const opening = iterator === void 0 ? void 0 : await iterator.next();
|
|
1778
|
+
if (opening?.done === true) return await next();
|
|
1779
|
+
const waterfall = createWaterfallRequest(opening === void 0 ? void 0 : Date.now() + opening.value.remainingMs, claimSignal, (channel) => {
|
|
1780
|
+
card.pending.detachWaterfall(channel);
|
|
1781
|
+
});
|
|
1782
|
+
if (claimSignal.aborted) return await waterfall.result;
|
|
1783
|
+
delegateRequest = () => {
|
|
1784
|
+
waterfall.channel.delegate();
|
|
1785
|
+
};
|
|
1786
|
+
card.pending.attachWaterfall(waterfall.channel);
|
|
1787
|
+
if (iterator !== void 0) claimEnded = (async () => {
|
|
1788
|
+
await iterator.next();
|
|
1789
|
+
throw new Error("the foreground question wait ended");
|
|
1790
|
+
})();
|
|
1791
|
+
try {
|
|
1792
|
+
return await (claimEnded === void 0 ? waterfall.result : Promise.race([waterfall.result, claimEnded]));
|
|
1793
|
+
} catch (error) {
|
|
1794
|
+
if (waterfall.isDelegation(error)) {
|
|
1795
|
+
await releaseClaim?.();
|
|
1796
|
+
return await next();
|
|
1797
|
+
}
|
|
1798
|
+
throw error;
|
|
1799
|
+
}
|
|
1800
|
+
} finally {
|
|
1801
|
+
if (releaseClaim === void 0) claimLifetime.abort();
|
|
1802
|
+
else if (claimEnded === void 0) await releaseClaim();
|
|
1803
|
+
else Promise.allSettled([claimEnded]).then(() => {
|
|
1804
|
+
releaseClaim();
|
|
1805
|
+
});
|
|
1806
|
+
if (callId === void 0) card.remove();
|
|
1807
|
+
completed.resolve();
|
|
1808
|
+
finishRequest();
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Mirror answerable calls onto cards. A submitted reply in the durable Inbox
|
|
1813
|
+
* removes the editable card until the reply is admitted or discarded.
|
|
1814
|
+
*/
|
|
1815
|
+
function publishContinuedQuestions(ctx, cards) {
|
|
1816
|
+
const sessions = ctx.sessions;
|
|
1817
|
+
const stopProjections = /* @__PURE__ */ new Map();
|
|
1818
|
+
const unwrap = (result) => {
|
|
1819
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
1820
|
+
return result.value;
|
|
1821
|
+
};
|
|
1822
|
+
const rpcFor = (sessionId, callId) => ({ answer: async (answer) => unwrap(await ctx.remote.userQuestions.answer(sessionId, callId, answer)) });
|
|
1823
|
+
const reconcile = () => {
|
|
1824
|
+
const snapshot = sessions.list.getSnapshot();
|
|
1825
|
+
const bound = new Map(Object.values(snapshot.byId).flatMap((summary) => {
|
|
1826
|
+
const binding = sessions.binding(summary.id);
|
|
1827
|
+
return binding === void 0 ? [] : [[summary.id, binding]];
|
|
1828
|
+
}));
|
|
1829
|
+
for (const [sessionId, stop] of stopProjections) {
|
|
1830
|
+
if (bound.has(sessionId)) continue;
|
|
1831
|
+
stop();
|
|
1832
|
+
stopProjections.delete(sessionId);
|
|
1833
|
+
}
|
|
1834
|
+
for (const [sessionId, binding] of bound) {
|
|
1835
|
+
if (stopProjections.has(sessionId)) continue;
|
|
1836
|
+
const stopQuestions = binding.session.projections.faceOf("userQuestions").subscribe(reconcile);
|
|
1837
|
+
const stopInbox = binding.session.projections.faceOf("inbox").subscribe(reconcile);
|
|
1838
|
+
stopProjections.set(sessionId, () => {
|
|
1839
|
+
stopQuestions();
|
|
1840
|
+
stopInbox();
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1844
|
+
for (const [sessionId, binding] of bound) {
|
|
1845
|
+
const projected = binding.session.projections.faceOf("userQuestions").getSnapshot();
|
|
1846
|
+
const inbox = binding.session.projections.faceOf("inbox").getSnapshot();
|
|
1847
|
+
const queued = /* @__PURE__ */ new Set();
|
|
1848
|
+
for (const message of [...inbox?.["next-step"] ?? [], ...inbox?.["next-turn"] ?? []]) {
|
|
1849
|
+
if (typeof message !== "object" || message === null || Array.isArray(message)) continue;
|
|
1850
|
+
const source = message.source;
|
|
1851
|
+
if (typeof source !== "object" || source === null || Array.isArray(source)) continue;
|
|
1852
|
+
if (source.kind === "user-question-reply" && typeof source.callId === "string") queued.add(source.callId);
|
|
1853
|
+
}
|
|
1854
|
+
for (const row of projected?.active ?? []) {
|
|
1855
|
+
if (row.state === "continued" && queued.has(row.callId)) continue;
|
|
1856
|
+
rows.set(PendingQuestion.keyOf(sessionId, row.callId), {
|
|
1857
|
+
sessionId,
|
|
1858
|
+
row
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
for (const { sessionId, row } of rows.values()) {
|
|
1863
|
+
if (row.state === "continued") {
|
|
1864
|
+
const card = cards.ensure(sessionId, row.questions, row.callId);
|
|
1865
|
+
card.pending.attachRpc(rpcFor(sessionId, row.callId));
|
|
1866
|
+
card.pending.setState("continued");
|
|
1867
|
+
continue;
|
|
1868
|
+
}
|
|
1869
|
+
cards.byCallId(sessionId, row.callId)?.pending.setState("open");
|
|
1870
|
+
}
|
|
1871
|
+
for (const card of cards.values()) {
|
|
1872
|
+
if (card.pending.callId === void 0 || card.pending.review !== void 0 || rows.has(card.pending.key) || card.hasRequest() || card.pending.hasWaterfall()) continue;
|
|
1873
|
+
card.remove();
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1876
|
+
reconcile();
|
|
1877
|
+
const stopList = sessions.list.subscribe(reconcile);
|
|
1878
|
+
return () => {
|
|
1879
|
+
stopList();
|
|
1880
|
+
for (const stop of stopProjections.values()) stop();
|
|
1881
|
+
stopProjections.clear();
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Client plugin body: register the `question` dictionaries, the question
|
|
1886
|
+
* composer into the composer chain, and the late-reply conversation node.
|
|
1887
|
+
* Zero business face — data and verbs live on the matched carrier; t rides
|
|
1888
|
+
* the standard locale seat.
|
|
1889
|
+
* @param ctx - client root context.
|
|
1890
|
+
*/
|
|
1891
|
+
function apply(ctx) {
|
|
1892
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
1893
|
+
zh,
|
|
1894
|
+
en
|
|
1895
|
+
}), "ui-user-questions: dictionaries");
|
|
1896
|
+
const questionDraftStore = createQuestionDraftStore();
|
|
1897
|
+
const cards = new QuestionCards(ctx.uiSession.registerPendingInteraction((pending) => pending.kind === "plan-review" ? 2 : 1));
|
|
1898
|
+
ctx.effect(() => () => {
|
|
1899
|
+
cards.dispose();
|
|
1900
|
+
}, "ui-user-questions: cards");
|
|
1901
|
+
const disposePanels = ctx.reflect.provide("userQuestionPanels", {
|
|
1902
|
+
reveal: (sessionId, callId) => cards.reveal(sessionId, callId),
|
|
1903
|
+
review: (sessionId, callId, record) => cards.review(sessionId, callId, record)
|
|
1904
|
+
});
|
|
1905
|
+
ctx.effect(() => disposePanels, "ui-user-questions: answer panels");
|
|
1906
|
+
ctx.effect(() => publishContinuedQuestions(ctx, cards), "ui-user-questions: continued questions");
|
|
1907
|
+
ctx.slots.inject("conversation.composer", () => ctx.slots.register({
|
|
1908
|
+
name: "conversation.composer",
|
|
1909
|
+
select: ({ pendingInteraction }) => pendingInteraction instanceof PendingQuestion ? pendingInteraction : null,
|
|
1910
|
+
locale: NS,
|
|
1911
|
+
store: questionDraftStore,
|
|
1912
|
+
inject: () => ({ keyedHooks: { questionCard: (key) => cards.source(key) } }),
|
|
1913
|
+
children: { "conversation.plan-review.actions": {
|
|
1914
|
+
kind: "list",
|
|
1915
|
+
scope: "session"
|
|
1916
|
+
} }
|
|
1917
|
+
}, QuestionComposer));
|
|
1918
|
+
ctx.uiConversation.events.register(questionReplyDefinition);
|
|
1919
|
+
ctx.slots.inject("conversation.chat.node", () => ctx.slots.register({
|
|
1920
|
+
name: "conversation.chat.node",
|
|
1921
|
+
key: "question-reply",
|
|
1922
|
+
locale: NS
|
|
1923
|
+
}, QuestionReplyView));
|
|
1924
|
+
ctx.remote.$on("user-questions/request", function(request, next) {
|
|
1925
|
+
return answerQuestion(ctx, this, request, next, cards);
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
//#endregion
|
|
1929
|
+
exports.apply = apply;
|
|
1930
|
+
exports.inject = inject;
|
|
1931
|
+
return module.exports;
|
|
1932
|
+
}
|
|
1933
|
+
});
|
|
1934
|
+
|
|
1935
|
+
//# sourceMappingURL=client.js.map
|