pi-roundtable 0.19.2 → 0.21.0
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/CHANGELOG.md +26 -0
- package/README.md +1 -1
- package/README.zh-TW.md +1 -1
- package/docs/plugins.md +51 -5
- package/package.json +2 -1
- package/src/core/discord/card-audience.ts +47 -0
- package/src/core/discord/owner-card-answers.ts +180 -0
- package/src/core/discord/owner-card-asks.ts +149 -0
- package/src/core/discord/owner-card-questionnaire.ts +281 -0
- package/src/core/discord/owner-card-rows.ts +40 -18
- package/src/core/discord/owner-card-types.ts +54 -5
- package/src/core/discord/owner-cards-fixture.ts +37 -3
- package/src/core/discord/owner-cards.ts +62 -133
- package/src/core/discord/utf16.ts +11 -0
- package/src/core/holds.ts +15 -2
- package/src/core/i18n/cards.ts +32 -2
- package/src/core/interactions/prompts.ts +43 -22
- package/src/core/interactions/questionnaire.ts +213 -0
- package/src/core/runtime/ask-questions.ts +93 -0
- package/src/core/runtime/extensions/ask-user-arguments.ts +52 -0
- package/src/core/runtime/extensions/ask-user-text.ts +146 -0
- package/src/core/runtime/extensions/ask-user.ts +87 -81
- package/src/core/runtime/extensions/card-ask.ts +3 -0
- package/src/core/runtime/extensions/confirmation-gate.ts +2 -0
- package/src/core/runtime/extensions/gate-refusals.ts +2 -0
- package/src/core/runtime/extensions/hold-lookups.ts +16 -4
- package/src/core/runtime/prompt-slot.ts +21 -1
- package/src/core/testing/surface-contract-questionnaire.ts +192 -0
- package/src/core/testing/surface-contract-support.ts +29 -1
- package/src/core/testing/surface-contract.ts +46 -20
- package/src/index.ts +11 -0
|
@@ -5,30 +5,24 @@ import type { ChannelKey } from "../domain/conversation.ts";
|
|
|
5
5
|
import { messages } from "../i18n/index.ts";
|
|
6
6
|
import type {
|
|
7
7
|
Approval,
|
|
8
|
-
OwnerAnswer,
|
|
9
|
-
OwnerQuestion,
|
|
10
8
|
PromptScope,
|
|
11
9
|
Prompts,
|
|
12
|
-
PromptWait,
|
|
13
10
|
} from "../interactions/prompts.ts";
|
|
14
11
|
import type { Logger } from "../log.ts";
|
|
15
|
-
import {
|
|
16
|
-
type Audience,
|
|
17
|
-
type CardAudienceOptions,
|
|
18
|
-
CardAudiences,
|
|
19
|
-
} from "./card-audience.ts";
|
|
12
|
+
import { type CardAudienceOptions, CardAudiences } from "./card-audience.ts";
|
|
20
13
|
import { CardCopies, copyReply, offeredCopy } from "./card-copies.ts";
|
|
21
14
|
import { DiscordOwners, discordUser } from "./discord-owners.ts";
|
|
22
15
|
import type { InteractionModule } from "./interaction-module.ts";
|
|
23
16
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
answerDeferred,
|
|
18
|
+
answerForm,
|
|
19
|
+
answerSingle,
|
|
20
|
+
showPreviews,
|
|
21
|
+
} from "./owner-card-answers.ts";
|
|
22
|
+
import { askPrompts } from "./owner-card-asks.ts";
|
|
23
|
+
import {
|
|
26
24
|
approvalRows,
|
|
27
|
-
askRows,
|
|
28
25
|
CARD_PREFIX,
|
|
29
|
-
OTHER,
|
|
30
|
-
type Rows,
|
|
31
|
-
TEXT_FIELD,
|
|
32
26
|
withdrawnFooter,
|
|
33
27
|
} from "./owner-card-rows.ts";
|
|
34
28
|
import type {
|
|
@@ -36,10 +30,11 @@ import type {
|
|
|
36
30
|
CardChannel,
|
|
37
31
|
CardMessage,
|
|
38
32
|
CardPayload,
|
|
33
|
+
CardSpec,
|
|
39
34
|
LateTurn,
|
|
40
35
|
OpenCard,
|
|
41
36
|
} from "./owner-card-types.ts";
|
|
42
|
-
import { ownerPanel
|
|
37
|
+
import { ownerPanel } from "./owner-panel.ts";
|
|
43
38
|
|
|
44
39
|
export type {
|
|
45
40
|
CardChannel,
|
|
@@ -47,6 +42,7 @@ export type {
|
|
|
47
42
|
LateTurn,
|
|
48
43
|
} from "./owner-card-types.ts";
|
|
49
44
|
export { CARD_PREFIX };
|
|
45
|
+
|
|
50
46
|
/** How long a turn that can be resumed waits on its card before it goes on without the answer. */
|
|
51
47
|
export const CARD_GRACE_MS = 2 * 60_000;
|
|
52
48
|
|
|
@@ -120,6 +116,12 @@ export class OwnerCards implements InteractionModule {
|
|
|
120
116
|
prompts(channel: ChannelKey, scope?: PromptScope): Prompts | undefined {
|
|
121
117
|
const channelId = parseChannelKey(channel).id;
|
|
122
118
|
return {
|
|
119
|
+
...askPrompts({
|
|
120
|
+
channelId,
|
|
121
|
+
scope,
|
|
122
|
+
audiences: this.#audiences,
|
|
123
|
+
post: (c, s, u, x, card) => this.#post(c, s, u, x, card),
|
|
124
|
+
}),
|
|
123
125
|
confirm: async (
|
|
124
126
|
title,
|
|
125
127
|
message,
|
|
@@ -139,6 +141,7 @@ export class OwnerCards implements InteractionModule {
|
|
|
139
141
|
scope,
|
|
140
142
|
minTier,
|
|
141
143
|
options?.approvers,
|
|
144
|
+
options?.speakerOnly === true,
|
|
142
145
|
);
|
|
143
146
|
// A private conversation's call above its person's tier is no one's to approve.
|
|
144
147
|
if (!audience) return "unavailable";
|
|
@@ -177,37 +180,6 @@ export class OwnerCards implements InteractionModule {
|
|
|
177
180
|
},
|
|
178
181
|
);
|
|
179
182
|
},
|
|
180
|
-
ask: async (title, question, signal, wait) => {
|
|
181
|
-
const audience = await this.#audiences.question(scope);
|
|
182
|
-
if (!audience) return undefined;
|
|
183
|
-
return this.#post<OwnerAnswer | "pending" | undefined>(
|
|
184
|
-
channelId,
|
|
185
|
-
signal,
|
|
186
|
-
undefined,
|
|
187
|
-
undefined,
|
|
188
|
-
{
|
|
189
|
-
audience,
|
|
190
|
-
...(wait
|
|
191
|
-
? {
|
|
192
|
-
wait: {
|
|
193
|
-
pending: "pending",
|
|
194
|
-
late: (value, by) =>
|
|
195
|
-
wait.late({
|
|
196
|
-
kind: "question",
|
|
197
|
-
answer: value as OwnerAnswer,
|
|
198
|
-
by: by.user.name,
|
|
199
|
-
}),
|
|
200
|
-
},
|
|
201
|
-
}
|
|
202
|
-
: {}),
|
|
203
|
-
title,
|
|
204
|
-
sections: [question.question],
|
|
205
|
-
question,
|
|
206
|
-
rows: (id, disabled) => askRows(id, question, disabled),
|
|
207
|
-
footer: messages().cardQuestionFooter,
|
|
208
|
-
},
|
|
209
|
-
);
|
|
210
|
-
},
|
|
211
183
|
};
|
|
212
184
|
}
|
|
213
185
|
|
|
@@ -220,28 +192,7 @@ export class OwnerCards implements InteractionModule {
|
|
|
220
192
|
signal: AbortSignal | undefined,
|
|
221
193
|
unposted: T,
|
|
222
194
|
cancelled: T,
|
|
223
|
-
card:
|
|
224
|
-
audience: Audience;
|
|
225
|
-
title: string;
|
|
226
|
-
sections: string[];
|
|
227
|
-
/** The text behind the card's copy button, kept under the card's id. */
|
|
228
|
-
copy?: string;
|
|
229
|
-
/** Closes the card, also after its turn stopped waiting, when it aborts; see `ApprovalOptions.supersede`. */
|
|
230
|
-
supersede?: AbortSignal;
|
|
231
|
-
question?: OwnerQuestion;
|
|
232
|
-
rows(id: string, disabled: boolean): Rows;
|
|
233
|
-
footer: string;
|
|
234
|
-
wait?: {
|
|
235
|
-
pending: T;
|
|
236
|
-
late(
|
|
237
|
-
value: unknown,
|
|
238
|
-
by: Answerer,
|
|
239
|
-
prompt: string,
|
|
240
|
-
): ReturnType<PromptWait["late"]>;
|
|
241
|
-
};
|
|
242
|
-
/** Hears who approved the card, before it resolves approved in its turn. */
|
|
243
|
-
approved?(by: Answerer, prompt: string): void;
|
|
244
|
-
},
|
|
195
|
+
card: CardSpec<T>,
|
|
245
196
|
): Promise<T> {
|
|
246
197
|
const { logger, graceMs = CARD_GRACE_MS, resume } = this.#options;
|
|
247
198
|
if (signal?.aborted || card.supersede?.aborted)
|
|
@@ -279,12 +230,13 @@ export class OwnerCards implements InteractionModule {
|
|
|
279
230
|
};
|
|
280
231
|
const view = (footer: string, disabled = true) =>
|
|
281
232
|
this.#panel(
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
233
|
+
card.build?.(id, footer, disabled) ??
|
|
234
|
+
ownerPanel({
|
|
235
|
+
title: card.title,
|
|
236
|
+
sections,
|
|
237
|
+
rows: card.rows?.(id, disabled) ?? [],
|
|
238
|
+
footer,
|
|
239
|
+
}),
|
|
288
240
|
open.mention === true,
|
|
289
241
|
card.audience.mentions,
|
|
290
242
|
);
|
|
@@ -297,20 +249,26 @@ export class OwnerCards implements InteractionModule {
|
|
|
297
249
|
close(withdrawnFooter(card.supersede));
|
|
298
250
|
if (!late) resolve(cancelled);
|
|
299
251
|
};
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
252
|
+
const arm = () =>
|
|
253
|
+
wait
|
|
254
|
+
? setTimeout(() => {
|
|
255
|
+
if (!this.#open.has(id)) return;
|
|
256
|
+
late = true;
|
|
257
|
+
signal?.removeEventListener("abort", abort);
|
|
258
|
+
resolve(wait.pending);
|
|
259
|
+
}, graceMs)
|
|
260
|
+
: undefined;
|
|
261
|
+
let timer = arm();
|
|
308
262
|
signal?.addEventListener("abort", abort, { once: true });
|
|
309
263
|
card.supersede?.addEventListener("abort", withdraw, { once: true });
|
|
310
264
|
const open: OpenCard = {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
265
|
+
view,
|
|
266
|
+
isOpen: () => this.#open.has(id),
|
|
267
|
+
touch: () => {
|
|
268
|
+
if (late || !this.#open.has(id)) return;
|
|
269
|
+
clearTimeout(timer);
|
|
270
|
+
timer = arm();
|
|
271
|
+
},
|
|
314
272
|
audience: card.audience,
|
|
315
273
|
end: (value, by) => {
|
|
316
274
|
// Withdrawn or answered already: this answer changes nothing.
|
|
@@ -336,6 +294,8 @@ export class OwnerCards implements InteractionModule {
|
|
|
336
294
|
return true;
|
|
337
295
|
},
|
|
338
296
|
...(card.question ? { question: card.question } : {}),
|
|
297
|
+
...(card.form ? { form: card.form } : {}),
|
|
298
|
+
...(card.previews ? { previews: card.previews } : {}),
|
|
339
299
|
};
|
|
340
300
|
this.#open.set(id, open);
|
|
341
301
|
this.#options
|
|
@@ -358,11 +318,10 @@ export class OwnerCards implements InteractionModule {
|
|
|
358
318
|
|
|
359
319
|
/** The card's panel; in a thread it leads with the mention of who may answer. */
|
|
360
320
|
#panel(
|
|
361
|
-
|
|
321
|
+
panel: CardPayload,
|
|
362
322
|
mention: boolean,
|
|
363
323
|
userIds: readonly string[],
|
|
364
324
|
): CardPayload {
|
|
365
|
-
const panel = ownerPanel(content);
|
|
366
325
|
if (!mention) return panel;
|
|
367
326
|
return {
|
|
368
327
|
...panel,
|
|
@@ -386,7 +345,7 @@ export class OwnerCards implements InteractionModule {
|
|
|
386
345
|
!interaction.customId.startsWith(CARD_PREFIX)
|
|
387
346
|
)
|
|
388
347
|
return false;
|
|
389
|
-
const [id = "", action = ""] = interaction.customId
|
|
348
|
+
const [id = "", action = "", index = ""] = interaction.customId
|
|
390
349
|
.slice(CARD_PREFIX.length)
|
|
391
350
|
.split(":");
|
|
392
351
|
// Copying answers nothing: anyone who sees the card may copy, as often as they like, also
|
|
@@ -408,6 +367,10 @@ export class OwnerCards implements InteractionModule {
|
|
|
408
367
|
});
|
|
409
368
|
return true;
|
|
410
369
|
}
|
|
370
|
+
if (action === "previews") {
|
|
371
|
+
await showPreviews(card, interaction);
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
411
374
|
const user = discordUser(interaction);
|
|
412
375
|
const by: Answerer = {
|
|
413
376
|
user,
|
|
@@ -415,6 +378,14 @@ export class OwnerCards implements InteractionModule {
|
|
|
415
378
|
...(interaction.guildId ? { guildId: interaction.guildId } : {}),
|
|
416
379
|
isDirect: !interaction.inGuild(),
|
|
417
380
|
};
|
|
381
|
+
// A menu pick or Submit is deferred before the audience is looked up, which may take a while.
|
|
382
|
+
if (
|
|
383
|
+
card.form &&
|
|
384
|
+
(action === "qpick" || action === "submit" || action === "qtext")
|
|
385
|
+
) {
|
|
386
|
+
await answerDeferred(card, action, Number(index), interaction, by);
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
418
389
|
if (!(await card.audience.allows(user))) {
|
|
419
390
|
await interaction.reply({
|
|
420
391
|
content: card.audience.refusal,
|
|
@@ -422,17 +393,6 @@ export class OwnerCards implements InteractionModule {
|
|
|
422
393
|
});
|
|
423
394
|
return true;
|
|
424
395
|
}
|
|
425
|
-
const closed = (outcome: string) =>
|
|
426
|
-
this.#panel(
|
|
427
|
-
{
|
|
428
|
-
title: card.title,
|
|
429
|
-
sections: card.sections,
|
|
430
|
-
rows: card.rows(true),
|
|
431
|
-
footer: outcome,
|
|
432
|
-
},
|
|
433
|
-
card.mention === true,
|
|
434
|
-
card.audience.mentions,
|
|
435
|
-
);
|
|
436
396
|
if (interaction.isButton() && (action === "yes" || action === "no")) {
|
|
437
397
|
const approved = action === "yes";
|
|
438
398
|
const approver = approved
|
|
@@ -457,43 +417,12 @@ export class OwnerCards implements InteractionModule {
|
|
|
457
417
|
return true;
|
|
458
418
|
}
|
|
459
419
|
await interaction.update(
|
|
460
|
-
|
|
420
|
+
card.view(approved ? messages().cardApproved : messages().cardDeclined),
|
|
461
421
|
);
|
|
462
422
|
return true;
|
|
463
423
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if (interaction.isButton() && action === "write") {
|
|
467
|
-
card.picked = [];
|
|
468
|
-
await interaction.showModal(answerModal(id));
|
|
469
|
-
return true;
|
|
470
|
-
}
|
|
471
|
-
if (interaction.isStringSelectMenu() && action === "pick") {
|
|
472
|
-
const values = interaction.values;
|
|
473
|
-
const choices = question.options.flatMap((option, i) =>
|
|
474
|
-
values.includes(String(i)) ? [option.label] : [],
|
|
475
|
-
);
|
|
476
|
-
if (values.includes(OTHER)) {
|
|
477
|
-
card.picked = choices;
|
|
478
|
-
await interaction.showModal(answerModal(id));
|
|
479
|
-
return true;
|
|
480
|
-
}
|
|
481
|
-
const answer = { choices };
|
|
482
|
-
card.end(answer, by);
|
|
483
|
-
await interaction.update(closed(answeredLine(answer)));
|
|
484
|
-
return true;
|
|
485
|
-
}
|
|
486
|
-
if (interaction.isModalSubmit() && action === "text") {
|
|
487
|
-
const answer = {
|
|
488
|
-
choices: card.picked ?? [],
|
|
489
|
-
text: interaction.fields.getTextInputValue(TEXT_FIELD),
|
|
490
|
-
};
|
|
491
|
-
card.end(answer, by);
|
|
492
|
-
if (interaction.isFromMessage())
|
|
493
|
-
await interaction.update(closed(answeredLine(answer)));
|
|
494
|
-
else await interaction.deferUpdate();
|
|
495
|
-
return true;
|
|
496
|
-
}
|
|
424
|
+
if (card.form) await answerForm(card, id, action, interaction);
|
|
425
|
+
else await answerSingle(card, id, action, interaction, by);
|
|
497
426
|
return true;
|
|
498
427
|
}
|
|
499
428
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `text` cut to at most `max` UTF-16 units, the count discord.js validates, without splitting a
|
|
3
|
+
* surrogate pair; with `ellipsis` the cut ends in `…` inside the limit.
|
|
4
|
+
*/
|
|
5
|
+
export function clip(text: string, max: number, ellipsis = false): string {
|
|
6
|
+
if (text.length <= max) return text;
|
|
7
|
+
let end = ellipsis ? max - 1 : max;
|
|
8
|
+
const last = text.charCodeAt(end - 1);
|
|
9
|
+
if (last >= 0xd800 && last <= 0xdbff) end -= 1;
|
|
10
|
+
return ellipsis ? `${text.slice(0, end)}…` : text.slice(0, end);
|
|
11
|
+
}
|
package/src/core/holds.ts
CHANGED
|
@@ -19,8 +19,20 @@ export interface HoldContext {
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* What a hold that runs in a turn answers with: the description of the call, or the description
|
|
22
|
-
* as `text` with a `copy` the owner's approval card offers beside it (see `ApprovalDetails.copy`)
|
|
23
|
-
*
|
|
22
|
+
* as `text` with a `copy` the owner's approval card offers beside it (see `ApprovalDetails.copy`),
|
|
23
|
+
* a `supersede` signal to withdraw the call by, and `approval: "speaker"` to make the card the
|
|
24
|
+
* speaker's alone.
|
|
25
|
+
*
|
|
26
|
+
* With `approval: "speaker"` only the speaker whose turn made the call may answer its card, on
|
|
27
|
+
* any of their identities the surface knows and at the call's tier; no owner may answer it as an
|
|
28
|
+
* owner (an owner who is the speaker answers as the speaker). A speaker below the call's tier, a
|
|
29
|
+
* turn with no speaker, or a surface that cannot tell who the speaker is gets `unavailable` at
|
|
30
|
+
* once and the call stays held. Combined with approvers the card is still the speaker's alone,
|
|
31
|
+
* and the runtime accepts their press only when the approvers name them or they are an owner;
|
|
32
|
+
* otherwise the call stays held. It governs the card only: a message that confirms the held call
|
|
33
|
+
* is judged as for any call, so a tool that must act on the speaker's press alone checks
|
|
34
|
+
* `ToolTurn.approvedPrompt` and `ToolTurn.approvedBy` in `run`. Anything but `"speaker"` leaves
|
|
35
|
+
* the card as every card is.
|
|
24
36
|
*
|
|
25
37
|
* Aborting `supersede` withdraws the call the hold held, when the plugin no longer wants it
|
|
26
38
|
* (a newer request replaced it, its subject is gone): its approval card is closed, also after
|
|
@@ -39,6 +51,7 @@ export type HoldDescription =
|
|
|
39
51
|
text: string;
|
|
40
52
|
copy?: NonNullable<ApprovalDetails["copy"]>;
|
|
41
53
|
supersede?: AbortSignal;
|
|
54
|
+
approval?: "speaker";
|
|
42
55
|
};
|
|
43
56
|
|
|
44
57
|
/**
|
package/src/core/i18n/cards.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { CatalogContext } from "./types.ts";
|
|
2
2
|
|
|
3
|
+
/** `text` cut to at most 300 UTF-16 units without splitting a surrogate pair. */
|
|
4
|
+
function clip(text: string, max: number): string {
|
|
5
|
+
if (text.length <= max) return text;
|
|
6
|
+
const last = text.charCodeAt(max - 1);
|
|
7
|
+
return text.slice(0, last >= 0xd800 && last <= 0xdbff ? max - 1 : max);
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
/** The owner's approval and question cards. */
|
|
4
11
|
export function cardsEn(ctx: CatalogContext) {
|
|
5
12
|
return {
|
|
@@ -36,7 +43,19 @@ export function cardsEn(ctx: CatalogContext) {
|
|
|
36
43
|
cardModalField: "Your answer",
|
|
37
44
|
cardQuote: (text: string) => `“${text}”`,
|
|
38
45
|
cardAnswered: (parts: readonly string[]) =>
|
|
39
|
-
`Answered: ${parts.join(", ")
|
|
46
|
+
`Answered: ${clip(parts.join(", "), 300)}`,
|
|
47
|
+
cardPreviewsFile:
|
|
48
|
+
"The previews contain code blocks, so they are attached as a file.",
|
|
49
|
+
cardSubmitLabel: "Submit",
|
|
50
|
+
cardWriteLabel: "Write answers / note…",
|
|
51
|
+
cardPreviewsLabel: "Previews",
|
|
52
|
+
cardQuestionName: (n: number) => `Question ${n}`,
|
|
53
|
+
cardNoteField: "Note on all answers",
|
|
54
|
+
cardNothingYet: "Choose or write at least one answer first.",
|
|
55
|
+
cardQuestionnaireFooter: (answered: number, total: number) =>
|
|
56
|
+
`${answered} of ${total} answered; submit when you are ready.`,
|
|
57
|
+
cardUnanswered: "not answered",
|
|
58
|
+
cardNoteLine: (note: string) => `Note: ${note}`,
|
|
40
59
|
};
|
|
41
60
|
}
|
|
42
61
|
|
|
@@ -71,6 +90,17 @@ export function cardsZhTW(ctx: CatalogContext): ReturnType<typeof cardsEn> {
|
|
|
71
90
|
cardModalField: "你的回答",
|
|
72
91
|
cardQuote: (text: string) => `「${text}」`,
|
|
73
92
|
cardAnswered: (parts: readonly string[]) =>
|
|
74
|
-
`已回答:${parts.join("、")
|
|
93
|
+
`已回答:${clip(parts.join("、"), 300)}`,
|
|
94
|
+
cardPreviewsFile: "預覽裡有程式碼區塊,所以用檔案附上。",
|
|
95
|
+
cardSubmitLabel: "送出",
|
|
96
|
+
cardWriteLabel: "寫答案/備註…",
|
|
97
|
+
cardPreviewsLabel: "預覽",
|
|
98
|
+
cardQuestionName: (n: number) => `問題 ${n}`,
|
|
99
|
+
cardNoteField: "給全部答案的備註",
|
|
100
|
+
cardNothingYet: "請先選或寫至少一個答案。",
|
|
101
|
+
cardQuestionnaireFooter: (answered: number, total: number) =>
|
|
102
|
+
`已回答 ${answered}/${total} 題;準備好再送出。`,
|
|
103
|
+
cardUnanswered: "未回答",
|
|
104
|
+
cardNoteLine: (note: string) => `備註:${note}`,
|
|
75
105
|
};
|
|
76
106
|
}
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import type { ConversationVisibility } from "../conversations/conversation-registry.ts";
|
|
2
2
|
import type { TurnRequest } from "../domain/ports.ts";
|
|
3
3
|
import type { Speaker, Tier } from "../speakers.ts";
|
|
4
|
+
import type {
|
|
5
|
+
AskSupport,
|
|
6
|
+
OwnerAnswer,
|
|
7
|
+
OwnerQuestion,
|
|
8
|
+
Questionnaire,
|
|
9
|
+
QuestionnaireAnswer,
|
|
10
|
+
} from "./questionnaire.ts";
|
|
11
|
+
|
|
12
|
+
export type {
|
|
13
|
+
AskOption,
|
|
14
|
+
AskSupport,
|
|
15
|
+
OwnerAnswer,
|
|
16
|
+
OwnerQuestion,
|
|
17
|
+
Questionnaire,
|
|
18
|
+
QuestionnaireAnswer,
|
|
19
|
+
} from "./questionnaire.ts";
|
|
4
20
|
|
|
5
21
|
/**
|
|
6
22
|
* How a prompt ended: answered yes or no; `expired` when the card was shown and no one answered
|
|
@@ -79,6 +95,16 @@ export interface ApprovalOptions {
|
|
|
79
95
|
* conversation an approval it does not name is not accepted for a call that names approvers.
|
|
80
96
|
*/
|
|
81
97
|
approvers?: readonly string[];
|
|
98
|
+
/**
|
|
99
|
+
* When true the card is the scope's speaker's alone: they answer it on any of their identities
|
|
100
|
+
* the surface knows, at the call's `minTier` checked again when they answer, and the owners do
|
|
101
|
+
* not, whatever the scope escalates to. Without a speaker, below the tier, or on a surface that
|
|
102
|
+
* cannot tell, it resolves `unavailable` at once. With `approvers`, the speaker's press is
|
|
103
|
+
* accepted only if they are named (the runtime refuses the rest). A surface that cannot honour
|
|
104
|
+
* it must resolve `unavailable` rather than show the card to the owners. Absent or false leaves
|
|
105
|
+
* the card as every card is.
|
|
106
|
+
*/
|
|
107
|
+
speakerOnly?: boolean;
|
|
82
108
|
/**
|
|
83
109
|
* Called with who approved the call, before the approval resolves `approved`; not called for a
|
|
84
110
|
* refusal, or by a surface that does not know who answered.
|
|
@@ -135,6 +161,7 @@ export type LateAnswer = { by: string } & (
|
|
|
135
161
|
prompt?: string;
|
|
136
162
|
}
|
|
137
163
|
| { kind: "question"; answer: OwnerAnswer }
|
|
164
|
+
| { kind: "questionnaire"; answer: QuestionnaireAnswer }
|
|
138
165
|
);
|
|
139
166
|
|
|
140
167
|
/**
|
|
@@ -148,28 +175,6 @@ export interface PromptWait {
|
|
|
148
175
|
late(answer: LateAnswer): string | undefined;
|
|
149
176
|
}
|
|
150
177
|
|
|
151
|
-
export interface AskOption {
|
|
152
|
-
label: string;
|
|
153
|
-
description?: string;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export interface OwnerQuestion {
|
|
157
|
-
question: string;
|
|
158
|
-
/** Up to 25; none means a free-text answer. */
|
|
159
|
-
options: readonly AskOption[];
|
|
160
|
-
/** More than one option may be chosen. */
|
|
161
|
-
multi: boolean;
|
|
162
|
-
/** They may write an answer of their own besides the options. */
|
|
163
|
-
allowOther: boolean;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export interface OwnerAnswer {
|
|
167
|
-
/** The chosen options' labels, in the order they were offered. */
|
|
168
|
-
choices: string[];
|
|
169
|
-
/** What they wrote themselves, if anything. */
|
|
170
|
-
text?: string;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
178
|
/**
|
|
174
179
|
* Who may answer a running turn's prompts: the person whose turn asks, and, in a conversation
|
|
175
180
|
* others share, the owners. The runtime makes it from the turn's speaker and the conversation's
|
|
@@ -235,6 +240,22 @@ export interface Prompts {
|
|
|
235
240
|
signal?: AbortSignal,
|
|
236
241
|
wait?: PromptWait,
|
|
237
242
|
): Promise<OwnerAnswer | "pending" | undefined>;
|
|
243
|
+
/**
|
|
244
|
+
* What `questionnaire` can show; absent means the 0.19 contract (one question, no previews,
|
|
245
|
+
* no note), and `ask` is the only way to ask.
|
|
246
|
+
*/
|
|
247
|
+
asks?: AskSupport;
|
|
248
|
+
/**
|
|
249
|
+
* Asks several questions on one card, shaped for `asks` (see `shapeFor`); the answer is
|
|
250
|
+
* index-aligned with the questions. Results are as for `ask`; a late answer is a
|
|
251
|
+
* `questionnaire` `LateAnswer`. Present only on a surface that declares `asks`.
|
|
252
|
+
*/
|
|
253
|
+
questionnaire?(
|
|
254
|
+
title: string,
|
|
255
|
+
questionnaire: Questionnaire,
|
|
256
|
+
signal?: AbortSignal,
|
|
257
|
+
wait?: PromptWait,
|
|
258
|
+
): Promise<QuestionnaireAnswer | "pending" | undefined>;
|
|
238
259
|
}
|
|
239
260
|
|
|
240
261
|
/** @deprecated Since 0.9 the prompts are `Prompts`, answered by those their `PromptScope` names; this name goes away in 1.0. */
|