@signalcore/ui 1.0.0-beta.2 → 1.0.0-beta.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/README.md +126 -21
- package/dist/AgentAbout.js +6 -2
- package/dist/AgentAbout.js.map +1 -1
- package/dist/AgentChat.d.ts +25 -3
- package/dist/AgentChat.js +302 -28
- package/dist/AgentChat.js.map +1 -1
- package/dist/AgentSpace.d.ts +25 -4
- package/dist/AgentSpace.js +55 -11
- package/dist/AgentSpace.js.map +1 -1
- package/dist/Threads.d.ts +73 -0
- package/dist/Threads.js +140 -0
- package/dist/Threads.js.map +1 -0
- package/dist/View.d.ts +48 -0
- package/dist/View.js +101 -0
- package/dist/View.js.map +1 -0
- package/dist/automation/marks.js +7 -3
- package/dist/automation/marks.js.map +1 -1
- package/dist/automation/messages.d.ts +4 -0
- package/dist/automation/messages.js +5 -1
- package/dist/automation/messages.js.map +1 -1
- package/dist/automation/when.js +8 -0
- package/dist/automation/when.js.map +1 -1
- package/dist/blocks.d.ts +83 -2
- package/dist/client.d.ts +23 -5
- package/dist/client.js +8 -2
- package/dist/client.js.map +1 -1
- package/dist/contract.d.ts +9 -4
- package/dist/es.d.ts +2 -0
- package/dist/es.js +34 -3
- package/dist/es.js.map +1 -1
- package/dist/frames.d.ts +7 -1
- package/dist/frames.js.map +1 -1
- package/dist/index.d.ts +14 -7
- package/dist/index.js +11 -4
- package/dist/kit/Agenda.d.ts +25 -0
- package/dist/kit/Agenda.js +119 -0
- package/dist/kit/Agenda.js.map +1 -0
- package/dist/kit/Combobox.js +1 -1
- package/dist/kit/DateField.js +1 -1
- package/dist/kit/Kanban.d.ts +17 -0
- package/dist/kit/Kanban.js +157 -0
- package/dist/kit/Kanban.js.map +1 -0
- package/dist/kit/TimeField.js +1 -1
- package/dist/kit/Timeline.d.ts +14 -0
- package/dist/kit/Timeline.js +71 -0
- package/dist/kit/Timeline.js.map +1 -0
- package/dist/kit/context.d.ts +1 -1
- package/dist/kit/context.js.map +1 -1
- package/dist/kit/messages.d.ts +22 -0
- package/dist/kit/messages.js +10 -0
- package/dist/kit/messages.js.map +1 -1
- package/dist/surface/Form.js +1 -1
- package/dist/surface/Placed.d.ts +20 -0
- package/dist/surface/Placed.js +70 -0
- package/dist/surface/Placed.js.map +1 -0
- package/dist/surface/Surface.d.ts +4 -2
- package/dist/surface/Surface.js +78 -2
- package/dist/surface/Surface.js.map +1 -1
- package/dist/surface/places.d.ts +16 -0
- package/dist/surface/places.js +15 -0
- package/dist/surface/places.js.map +1 -0
- package/dist/surface/scope.d.ts +17 -1
- package/dist/surface/scope.js +1 -0
- package/dist/surface/scope.js.map +1 -1
- package/dist/types.d.ts +23 -0
- package/dist/types.js.map +1 -1
- package/dist/useConversation.d.ts +2 -0
- package/dist/useConversation.js +16 -0
- package/dist/useConversation.js.map +1 -1
- package/dist/widget.js +17 -16
- package/package.json +1 -1
- package/src/AgentAbout.tsx +4 -0
- package/src/AgentChat.tsx +417 -34
- package/src/AgentSpace.tsx +106 -14
- package/src/Threads.tsx +218 -0
- package/src/View.tsx +117 -0
- package/src/automation/marks.tsx +7 -0
- package/src/automation/messages.ts +8 -0
- package/src/automation/when.ts +4 -0
- package/src/blocks.ts +100 -2
- package/src/client.ts +42 -10
- package/src/contract.ts +404 -29
- package/src/es.ts +30 -0
- package/src/frames.ts +3 -1
- package/src/index.ts +36 -3
- package/src/kit/Agenda.tsx +161 -0
- package/src/kit/Kanban.tsx +182 -0
- package/src/kit/Timeline.tsx +58 -0
- package/src/kit/context.tsx +45 -1
- package/src/kit/messages.ts +31 -0
- package/src/styles.css +825 -66
- package/src/surface/Placed.tsx +98 -0
- package/src/surface/Surface.tsx +92 -1
- package/src/surface/places.ts +19 -0
- package/src/surface/scope.ts +24 -1
- package/src/types.ts +24 -0
- package/src/useConversation.ts +10 -0
- package/src/widget/element.tsx +54 -7
- package/src/widget/widget.css +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signalcore/ui",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.4",
|
|
4
4
|
"description": "SignalCore's components for the people an agent serves: the chat, the call, the agent's space, and the <signalcore-chat> widget.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"signalcore",
|
package/src/AgentAbout.tsx
CHANGED
|
@@ -155,6 +155,8 @@ export const ABOUT_MESSAGES: AboutMessages = {
|
|
|
155
155
|
webhook: "When called",
|
|
156
156
|
event: "On an event",
|
|
157
157
|
platform: "On a message",
|
|
158
|
+
manual: "When started",
|
|
159
|
+
after: "After another",
|
|
158
160
|
},
|
|
159
161
|
topics: {
|
|
160
162
|
apps: "Apps",
|
|
@@ -946,6 +948,8 @@ const KIND_ICONS: Readonly<Record<AutomationKind, Icon>> = {
|
|
|
946
948
|
webhook: "code",
|
|
947
949
|
event: "bell",
|
|
948
950
|
platform: "message",
|
|
951
|
+
manual: "hand",
|
|
952
|
+
after: "check",
|
|
949
953
|
};
|
|
950
954
|
|
|
951
955
|
/** An app's mark: the host's logo when it draws one, else the provider's, bare. */
|
package/src/AgentChat.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ArrowsOutSimpleIcon,
|
|
2
3
|
ArrowUpIcon,
|
|
3
4
|
CaretRightIcon,
|
|
4
5
|
CheckIcon,
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
MicrophoneIcon,
|
|
8
9
|
PaperclipIcon,
|
|
9
10
|
ShieldWarningIcon,
|
|
11
|
+
SidebarSimpleIcon,
|
|
10
12
|
StopIcon,
|
|
11
13
|
UploadSimpleIcon,
|
|
12
14
|
WarningCircleIcon,
|
|
@@ -40,13 +42,18 @@ import { useConversationFiles } from "./conversationFiles";
|
|
|
40
42
|
import { FILES_PER_MESSAGE } from "./frames";
|
|
41
43
|
import { actionMark } from "./marks";
|
|
42
44
|
import { AudioPlayer } from "./kit/AudioPlayer";
|
|
45
|
+
import { Button } from "./kit/Button";
|
|
43
46
|
import { KitProvider, type KitPart } from "./kit/context";
|
|
47
|
+
import { Field } from "./kit/Field";
|
|
48
|
+
import { Checkbox, NumberInput, TextArea, TextInput } from "./kit/Inputs";
|
|
44
49
|
import { carriesFiles, FileChips, UploadChips, useUploads, type Uploads } from "./kit/FileField";
|
|
45
50
|
import { KIT_MESSAGES, type KitLocale, type KitMessages } from "./kit/messages";
|
|
46
51
|
import type { FileHost } from "./kit/uploads";
|
|
47
52
|
import { stepGlyph } from "./stepGlyphs";
|
|
48
53
|
import { Markdown } from "./surface/Markdown";
|
|
49
|
-
import
|
|
54
|
+
import { Placed, type PlacedPart } from "./surface/Placed";
|
|
55
|
+
import { placeOf, type SurfacePlaces } from "./surface/places";
|
|
56
|
+
import type { HostBlocks, SurfaceComponents, SurfacePart } from "./surface/scope";
|
|
50
57
|
import { framesItself, Surface, usePlayed } from "./surface/Surface";
|
|
51
58
|
import type { SurfaceAction } from "./surface/values";
|
|
52
59
|
import type {
|
|
@@ -122,6 +129,12 @@ export interface ChatMessages {
|
|
|
122
129
|
inputs: Readonly<Record<string, string>>;
|
|
123
130
|
allow: string;
|
|
124
131
|
decline: string;
|
|
132
|
+
/** What heads the card of several actions that wait for the person's leave, by how many. */
|
|
133
|
+
approvals: (count: number) => string;
|
|
134
|
+
/** The card's one button, as many as are ticked: every one, some, or none (all declined). */
|
|
135
|
+
approveAll: string;
|
|
136
|
+
approveSome: (count: number) => string;
|
|
137
|
+
declineAll: string;
|
|
125
138
|
/**
|
|
126
139
|
* On a person's page, in place of the approval card: what the agent asked to do waits for the
|
|
127
140
|
* business's yes (`decides: "business"`), which its people give from their app.
|
|
@@ -129,6 +142,13 @@ export interface ChatMessages {
|
|
|
129
142
|
confirming: string;
|
|
130
143
|
/** The line across the log where the agent handed the conversation to a person. */
|
|
131
144
|
handedOff: string;
|
|
145
|
+
/**
|
|
146
|
+
* The line where the agent put what it showed in a panel or a modal, which opens it again:
|
|
147
|
+
* the agent's name and the surface's title ("Ray opened your week").
|
|
148
|
+
*/
|
|
149
|
+
opened: (name: string, title: string) => string;
|
|
150
|
+
/** The button that closes a panel or a modal. */
|
|
151
|
+
closePlaced: string;
|
|
132
152
|
/**
|
|
133
153
|
* The line across the log where who answers the conversation changed (`handovers`), before
|
|
134
154
|
* why: to whom (`holder`), and who changed it (`by`).
|
|
@@ -194,8 +214,14 @@ export const CHAT_MESSAGES: ChatMessages = {
|
|
|
194
214
|
inputs: {},
|
|
195
215
|
allow: "Allow",
|
|
196
216
|
decline: "Decline",
|
|
217
|
+
approvals: (count) => `${count} actions need your approval`,
|
|
218
|
+
approveAll: "Approve all",
|
|
219
|
+
approveSome: (count) => `Approve ${count}`,
|
|
220
|
+
declineAll: "Decline all",
|
|
197
221
|
confirming: "Waiting for the business to confirm",
|
|
198
222
|
handedOff: "Handed to a person",
|
|
223
|
+
opened: (name, title) => `${name} opened ${title}`,
|
|
224
|
+
closePlaced: "Close",
|
|
199
225
|
handover: (holder, by, name) => {
|
|
200
226
|
const automation = by.kind === "automation" ? ` by “${by.title}”` : "";
|
|
201
227
|
switch (holder) {
|
|
@@ -234,6 +260,7 @@ export type ChatPart =
|
|
|
234
260
|
| "sc-chat"
|
|
235
261
|
| "sc-chat--builder"
|
|
236
262
|
| "sc-chat--console"
|
|
263
|
+
| "sc-chat--placed"
|
|
237
264
|
| "sc-chat__log"
|
|
238
265
|
| "sc-chat__log--channel"
|
|
239
266
|
| "sc-chat__message"
|
|
@@ -256,10 +283,15 @@ export type ChatPart =
|
|
|
256
283
|
| "sc-chat__step--failed"
|
|
257
284
|
| "sc-chat__output"
|
|
258
285
|
| "sc-chat__request"
|
|
286
|
+
| "sc-chat__requests"
|
|
287
|
+
| "sc-chat__ask"
|
|
288
|
+
| "sc-chat__ask-fields"
|
|
259
289
|
| "sc-chat__confirming"
|
|
260
290
|
| "sc-chat__screen"
|
|
261
291
|
| "sc-chat__screen--bare"
|
|
262
292
|
| "sc-chat__handoff"
|
|
293
|
+
| "sc-chat__placed"
|
|
294
|
+
| "sc-chat__placed--open"
|
|
263
295
|
| "sc-chat__handover"
|
|
264
296
|
| "sc-chat__decision"
|
|
265
297
|
| "sc-chat__message--heard"
|
|
@@ -284,7 +316,7 @@ export type ChatPart =
|
|
|
284
316
|
| "sc-chat__stop";
|
|
285
317
|
|
|
286
318
|
type ClassNames =
|
|
287
|
-
Partial<Record<ChatPart | SurfacePart | KitPart | BrowserPart, string>> | undefined;
|
|
319
|
+
Partial<Record<ChatPart | SurfacePart | KitPart | BrowserPart | PlacedPart, string>> | undefined;
|
|
288
320
|
|
|
289
321
|
/**
|
|
290
322
|
* How the chat looks. `agent` is an agent's conversation. `builder` is the builder's, v2's: the
|
|
@@ -328,6 +360,13 @@ export interface AgentChatProps {
|
|
|
328
360
|
classNames?: ClassNames;
|
|
329
361
|
/** The host's own drawing of a block type, for every surface the agent shows. */
|
|
330
362
|
components?: SurfaceComponents | undefined;
|
|
363
|
+
/** The host's own blocks, by the name each was declared with (`PUT /v1/blocks/{name}`). */
|
|
364
|
+
blocks?: HostBlocks | undefined;
|
|
365
|
+
/**
|
|
366
|
+
* Where the agent may put what it shows besides the conversation: a panel beside it, a modal
|
|
367
|
+
* over the page (both by default). One the host doesn't allow shows in the conversation.
|
|
368
|
+
*/
|
|
369
|
+
surfaces?: SurfacePlaces | undefined;
|
|
331
370
|
/** The language of the surfaces' calendars, days, times and numbers; English by default. */
|
|
332
371
|
locale?: KitLocale | undefined;
|
|
333
372
|
/** An error's code, from the socket or `load` for a history that could not be read; the host words it. */
|
|
@@ -399,6 +438,86 @@ export interface AgentChatProps {
|
|
|
399
438
|
onTool?: ToolHandler | undefined;
|
|
400
439
|
}
|
|
401
440
|
|
|
441
|
+
/** Where nothing is put apart: a conversation on a channel. */
|
|
442
|
+
const NO_PLACES: SurfacePlaces = {};
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* What the person can still do on the conversation's surfaces: what they did on each, by its id
|
|
446
|
+
* (each draws its answer), and whether one takes input. A surface takes input while the turn
|
|
447
|
+
* waits on it or, once nothing runs or waits, when the agent showed it since the person last
|
|
448
|
+
* spoke; older ones are read, not used. On a channel the person answered there: here it reads.
|
|
449
|
+
*/
|
|
450
|
+
export function surfacesOf(
|
|
451
|
+
live: Pick<LiveConversation, "messages" | "pending" | "awaiting">,
|
|
452
|
+
onChannel: boolean,
|
|
453
|
+
): {
|
|
454
|
+
answers: ReadonlyMap<string, Action>;
|
|
455
|
+
/** Whether a reply at `index` of the messages is since the person last spoke, all settled. */
|
|
456
|
+
current: (index: number) => boolean;
|
|
457
|
+
/** Whether the surface `id`, of the reply at `index`, takes input. */
|
|
458
|
+
takes: (id: string, index: number) => boolean;
|
|
459
|
+
} {
|
|
460
|
+
const { messages, pending, awaiting } = live;
|
|
461
|
+
const idle = !onChannel && !pending && awaiting === null;
|
|
462
|
+
const lastSaid = messages.findLastIndex((message) => message.role === "user");
|
|
463
|
+
const answers = new Map<string, Action>();
|
|
464
|
+
for (const message of messages) {
|
|
465
|
+
if (message.action) answers.set(message.action.surface, message.action);
|
|
466
|
+
}
|
|
467
|
+
const current = (index: number) => idle && index > lastSaid;
|
|
468
|
+
return {
|
|
469
|
+
answers,
|
|
470
|
+
current,
|
|
471
|
+
takes: (id, index) => (!onChannel && id === awaiting) || current(index),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** A step that drew a surface, by its id: the surface, and the index of its reply. */
|
|
476
|
+
function placedStep(
|
|
477
|
+
messages: readonly Message[],
|
|
478
|
+
id: string,
|
|
479
|
+
): { step: Step; surface: Shown; index: number } | null {
|
|
480
|
+
for (const [index, message] of messages.entries()) {
|
|
481
|
+
const step = message.steps.find((each) => each.id === id);
|
|
482
|
+
if (step?.surface) return { step, surface: step.surface, index };
|
|
483
|
+
}
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* The surface open in a panel or a modal, by its step's id: each one the agent puts apart opens
|
|
489
|
+
* as it comes, and a conversation that opens waiting on one opens it; one read with the history
|
|
490
|
+
* waits for its line. Closing it, or opening another, is the person's.
|
|
491
|
+
*/
|
|
492
|
+
function useOpenedPlace(
|
|
493
|
+
live: Pick<LiveConversation, "messages" | "loading" | "awaiting">,
|
|
494
|
+
places: SurfacePlaces | undefined,
|
|
495
|
+
): [string | null, (id: string | null) => void] {
|
|
496
|
+
const [opened, setOpened] = useState<string | null>(null);
|
|
497
|
+
const ids = live.messages
|
|
498
|
+
.flatMap((message) => message.steps)
|
|
499
|
+
.filter((step) => step.surface && placeOf(step.surface, places) !== "chat")
|
|
500
|
+
.map((step) => step.id);
|
|
501
|
+
const seen = useRef<Set<string> | null>(null);
|
|
502
|
+
const key = ids.join(" ");
|
|
503
|
+
const { loading, awaiting } = live;
|
|
504
|
+
useEffect(() => {
|
|
505
|
+
if (loading) return;
|
|
506
|
+
const known = seen.current;
|
|
507
|
+
const placed = key === "" ? [] : key.split(" ");
|
|
508
|
+
if (known === null) {
|
|
509
|
+
seen.current = new Set(placed);
|
|
510
|
+
if (awaiting !== null && placed.includes(awaiting)) setOpened(awaiting);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const fresh = placed.filter((id) => !known.has(id));
|
|
514
|
+
for (const id of fresh) known.add(id);
|
|
515
|
+
const newest = fresh.at(-1);
|
|
516
|
+
if (newest !== undefined) setOpened(newest);
|
|
517
|
+
}, [key, loading, awaiting]);
|
|
518
|
+
return [opened, setOpened];
|
|
519
|
+
}
|
|
520
|
+
|
|
402
521
|
/** How close to the foot, in pixels, still counts as being at it (v2's). */
|
|
403
522
|
const NEAR_FOOT = 80;
|
|
404
523
|
|
|
@@ -433,6 +552,8 @@ function Chat({
|
|
|
433
552
|
messages,
|
|
434
553
|
classNames,
|
|
435
554
|
components,
|
|
555
|
+
blocks,
|
|
556
|
+
surfaces,
|
|
436
557
|
locale,
|
|
437
558
|
onError,
|
|
438
559
|
actions,
|
|
@@ -460,6 +581,7 @@ function Chat({
|
|
|
460
581
|
onTool: onTool ?? (() => Promise.reject(new Error(words.noTool))),
|
|
461
582
|
});
|
|
462
583
|
const files = useConversationFiles(client, conversationId, live.messages, onOpenFile);
|
|
584
|
+
useReadAsShown(client, conversationId, live.messages.at(-1)?.id);
|
|
463
585
|
// What the person attaches to their next message: picked, dropped on the chat, or pasted.
|
|
464
586
|
const uploads = useUploads(attachments ? files : undefined, { max: FILES_PER_MESSAGE });
|
|
465
587
|
// Files go with what is said, once the turn waits on nothing; to the host's `write`, at once.
|
|
@@ -474,6 +596,11 @@ function Chat({
|
|
|
474
596
|
const { pending } = live;
|
|
475
597
|
// A conversation on a channel: its messages say who said them.
|
|
476
598
|
const onChannel = live.messages.some((message) => message.author != null);
|
|
599
|
+
// What the agent put in a panel or a modal, where the host allows it (on a channel, nothing:
|
|
600
|
+
// it went there in the conversation); the one open now.
|
|
601
|
+
const places = onChannel ? NO_PLACES : surfaces;
|
|
602
|
+
const [opened, setOpened] = useOpenedPlace(live, places);
|
|
603
|
+
const open = opened === null ? null : placedStep(live.messages, opened);
|
|
477
604
|
|
|
478
605
|
// The builder stops on Esc from anywhere on the page, as v2's did; an agent's chat, from inside it.
|
|
479
606
|
const stop = useRef(live.stop);
|
|
@@ -493,6 +620,8 @@ function Chat({
|
|
|
493
620
|
|
|
494
621
|
const looks: ChatPart[] = builder ? ["sc-chat--builder"] : [];
|
|
495
622
|
if (look === "console") looks.push("sc-chat--console");
|
|
623
|
+
// A panel open makes room for itself at the chat's right.
|
|
624
|
+
if (open !== null && placeOf(open.surface, places) === "panel") looks.push("sc-chat--placed");
|
|
496
625
|
const stepsShown = steps === "shown";
|
|
497
626
|
|
|
498
627
|
return (
|
|
@@ -554,6 +683,10 @@ function Chat({
|
|
|
554
683
|
words={words}
|
|
555
684
|
classNames={classNames}
|
|
556
685
|
components={components}
|
|
686
|
+
blocks={blocks}
|
|
687
|
+
places={places}
|
|
688
|
+
opened={opened}
|
|
689
|
+
onOpen={setOpened}
|
|
557
690
|
locale={locale}
|
|
558
691
|
following={following}
|
|
559
692
|
took={took}
|
|
@@ -565,6 +698,35 @@ function Chat({
|
|
|
565
698
|
steps={stepsShown}
|
|
566
699
|
/>
|
|
567
700
|
)}
|
|
701
|
+
{open !== null && (
|
|
702
|
+
<Placed
|
|
703
|
+
key={open.step.id}
|
|
704
|
+
where={placeOf(open.surface, places) === "modal" ? "modal" : "panel"}
|
|
705
|
+
title={open.surface.title ?? ""}
|
|
706
|
+
closeLabel={words.closePlaced}
|
|
707
|
+
onClose={() => {
|
|
708
|
+
setOpened(null);
|
|
709
|
+
}}
|
|
710
|
+
classNames={classNames}
|
|
711
|
+
>
|
|
712
|
+
<Surface
|
|
713
|
+
surface={open.surface}
|
|
714
|
+
active={surfacesOf(live, onChannel).takes(open.step.id, open.index)}
|
|
715
|
+
answered={surfacesOf(live, onChannel).answers.get(open.step.id)}
|
|
716
|
+
components={components}
|
|
717
|
+
blocks={blocks}
|
|
718
|
+
classNames={classNames}
|
|
719
|
+
messages={words.kit}
|
|
720
|
+
locale={locale}
|
|
721
|
+
files={files}
|
|
722
|
+
onAction={(action) => {
|
|
723
|
+
live.act({ surface: open.step.id, ...action });
|
|
724
|
+
// A modal is for now: once used, the conversation goes on under it.
|
|
725
|
+
if (placeOf(open.surface, places) === "modal") setOpened(null);
|
|
726
|
+
}}
|
|
727
|
+
/>
|
|
728
|
+
</Placed>
|
|
729
|
+
)}
|
|
568
730
|
{/* The browser the agent drives, over the composer while the conversation has one. */}
|
|
569
731
|
<BrowserView live={live} name={name} messages={words.browser} classNames={classNames} />
|
|
570
732
|
{live.pending && (
|
|
@@ -748,8 +910,17 @@ function Log({
|
|
|
748
910
|
handovers,
|
|
749
911
|
who,
|
|
750
912
|
steps,
|
|
913
|
+
blocks,
|
|
914
|
+
places,
|
|
915
|
+
opened,
|
|
916
|
+
onOpen,
|
|
751
917
|
}: PartProps & {
|
|
752
918
|
components: SurfaceComponents | undefined;
|
|
919
|
+
blocks: HostBlocks | undefined;
|
|
920
|
+
places: SurfacePlaces | undefined;
|
|
921
|
+
/** The surface open in a panel or a modal, and the way to open one from its line. */
|
|
922
|
+
opened: string | null;
|
|
923
|
+
onOpen: (id: string) => void;
|
|
753
924
|
locale: KitLocale | undefined;
|
|
754
925
|
following: RefObject<boolean>;
|
|
755
926
|
took: number | null;
|
|
@@ -811,16 +982,7 @@ function Log({
|
|
|
811
982
|
};
|
|
812
983
|
}, [following]);
|
|
813
984
|
|
|
814
|
-
|
|
815
|
-
// agent showed it since the person last spoke; older ones are read, not used. On a channel the
|
|
816
|
-
// person answered it there: here it only reads.
|
|
817
|
-
const idle = !onChannel && !pending && awaiting === null;
|
|
818
|
-
const lastSaid = messages.findLastIndex((message) => message.role === "user");
|
|
819
|
-
// What the person did on each surface, by the surface's id: a surface draws its answer.
|
|
820
|
-
const answers = new Map<string, Action>();
|
|
821
|
-
for (const message of messages) {
|
|
822
|
-
if (message.action) answers.set(message.action.surface, message.action);
|
|
823
|
-
}
|
|
985
|
+
const { answers, current } = surfacesOf(live, onChannel);
|
|
824
986
|
// Drawn, who answers says where the agent handed it over; otherwise its `escalate` step does.
|
|
825
987
|
const handed = handovers === undefined ? handoffs(messages) : NO_HANDOFFS;
|
|
826
988
|
const placed = handoversAt(messages, handovers ?? []);
|
|
@@ -877,14 +1039,19 @@ function Log({
|
|
|
877
1039
|
message={message}
|
|
878
1040
|
from={carried.current?.id === message.id ? carried.current.from : undefined}
|
|
879
1041
|
awaiting={onChannel ? null : awaiting}
|
|
880
|
-
current={
|
|
1042
|
+
current={current(index)}
|
|
881
1043
|
answers={answers}
|
|
882
1044
|
name={name}
|
|
883
1045
|
words={words}
|
|
884
1046
|
classNames={classNames}
|
|
885
1047
|
components={components}
|
|
1048
|
+
blocks={blocks}
|
|
1049
|
+
places={places}
|
|
1050
|
+
opened={opened}
|
|
1051
|
+
onOpen={onOpen}
|
|
886
1052
|
locale={locale}
|
|
887
1053
|
onAction={live.act}
|
|
1054
|
+
onActAll={live.actAll}
|
|
888
1055
|
builder={builder}
|
|
889
1056
|
files={files}
|
|
890
1057
|
who={who}
|
|
@@ -1234,8 +1401,19 @@ function Reply({
|
|
|
1234
1401
|
who,
|
|
1235
1402
|
steps,
|
|
1236
1403
|
onChannel,
|
|
1404
|
+
onActAll,
|
|
1405
|
+
blocks,
|
|
1406
|
+
places,
|
|
1407
|
+
opened,
|
|
1408
|
+
onOpen,
|
|
1237
1409
|
}: DrawProps & {
|
|
1410
|
+
blocks: HostBlocks | undefined;
|
|
1411
|
+
places: SurfacePlaces | undefined;
|
|
1412
|
+
opened: string | null;
|
|
1413
|
+
onOpen: (id: string) => void;
|
|
1238
1414
|
message: Message;
|
|
1415
|
+
/** Several things done at once: a yes or no on each call it waits on, from one card. */
|
|
1416
|
+
onActAll: (actions: readonly Action[]) => void;
|
|
1239
1417
|
/** It was streaming: its words go on revealing from here. None: they show whole. */
|
|
1240
1418
|
from: number | undefined;
|
|
1241
1419
|
awaiting: string | null;
|
|
@@ -1251,6 +1429,11 @@ function Reply({
|
|
|
1251
1429
|
onChannel: boolean;
|
|
1252
1430
|
}) {
|
|
1253
1431
|
const text = useReveal(message.text, from ?? message.text.length);
|
|
1432
|
+
// The calls it waits on the leave of whoever this page speaks for: two or more, in one card.
|
|
1433
|
+
const asked = message.steps.filter(
|
|
1434
|
+
(step): step is Step & { request: Request } =>
|
|
1435
|
+
step.request !== null && !(step.request.decides === "business" && who === "person"),
|
|
1436
|
+
);
|
|
1254
1437
|
// On a channel, what the person there has got so far; elsewhere, the words as they come.
|
|
1255
1438
|
const { shown: said, typing } = onChannel
|
|
1256
1439
|
? channelParts(message, text)
|
|
@@ -1310,35 +1493,71 @@ function Reply({
|
|
|
1310
1493
|
))}
|
|
1311
1494
|
</ol>
|
|
1312
1495
|
)}
|
|
1313
|
-
{message.steps.map(
|
|
1314
|
-
(step)
|
|
1315
|
-
|
|
1316
|
-
|
|
1496
|
+
{message.steps.map((step) => {
|
|
1497
|
+
if (!step.surface) return null;
|
|
1498
|
+
const place = placeOf(step.surface, places);
|
|
1499
|
+
if (place !== "chat") {
|
|
1500
|
+
const Glyph = place === "panel" ? SidebarSimpleIcon : ArrowsOutSimpleIcon;
|
|
1501
|
+
return (
|
|
1502
|
+
<button
|
|
1317
1503
|
key={step.id}
|
|
1318
|
-
|
|
1504
|
+
type="button"
|
|
1505
|
+
part="placed"
|
|
1506
|
+
aria-expanded={opened === step.id}
|
|
1507
|
+
onClick={() => {
|
|
1508
|
+
onOpen(step.id);
|
|
1509
|
+
}}
|
|
1319
1510
|
className={classes(
|
|
1320
1511
|
classNames,
|
|
1321
|
-
"sc-
|
|
1322
|
-
...(
|
|
1512
|
+
"sc-chat__placed",
|
|
1513
|
+
...(opened === step.id ? (["sc-chat__placed--open"] as const) : []),
|
|
1323
1514
|
)}
|
|
1324
1515
|
>
|
|
1325
|
-
<
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1516
|
+
<Glyph aria-hidden />
|
|
1517
|
+
<span>{words.opened(name, step.surface.title ?? "")}</span>
|
|
1518
|
+
</button>
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
return (
|
|
1522
|
+
<div
|
|
1523
|
+
key={step.id}
|
|
1524
|
+
part="surface"
|
|
1525
|
+
className={classes(
|
|
1526
|
+
classNames,
|
|
1527
|
+
"sc-chat__screen",
|
|
1528
|
+
...(framesItself(step.surface) ? (["sc-chat__screen--bare"] as const) : []),
|
|
1529
|
+
)}
|
|
1530
|
+
>
|
|
1531
|
+
<Surface
|
|
1532
|
+
surface={step.surface}
|
|
1533
|
+
active={step.id === awaiting || current}
|
|
1534
|
+
answered={answers.get(step.id)}
|
|
1535
|
+
components={components}
|
|
1536
|
+
blocks={blocks}
|
|
1537
|
+
classNames={classNames}
|
|
1538
|
+
messages={words.kit}
|
|
1539
|
+
locale={locale}
|
|
1540
|
+
files={files}
|
|
1541
|
+
onAction={(action) => onAction({ surface: step.id, ...action })}
|
|
1542
|
+
/>
|
|
1543
|
+
</div>
|
|
1544
|
+
);
|
|
1545
|
+
})}
|
|
1546
|
+
{asked.length > 1 && (
|
|
1547
|
+
<Approvals
|
|
1548
|
+
key={asked.map((step) => step.id).join()}
|
|
1549
|
+
steps={asked}
|
|
1550
|
+
name={name}
|
|
1551
|
+
words={words}
|
|
1552
|
+
classNames={classNames}
|
|
1553
|
+
onActAll={onActAll}
|
|
1554
|
+
/>
|
|
1338
1555
|
)}
|
|
1339
1556
|
{message.steps.map((step) => {
|
|
1340
1557
|
const { request } = step;
|
|
1341
|
-
if (request === null)
|
|
1558
|
+
if (request === null || (asked.length > 1 && asked.some((one) => one.id === step.id))) {
|
|
1559
|
+
return null;
|
|
1560
|
+
}
|
|
1342
1561
|
if (request.decides === "business" && who === "person") {
|
|
1343
1562
|
return (
|
|
1344
1563
|
<p
|
|
@@ -1583,6 +1802,170 @@ function Approval({
|
|
|
1583
1802
|
);
|
|
1584
1803
|
}
|
|
1585
1804
|
|
|
1805
|
+
/**
|
|
1806
|
+
* Several actions a reply waits on the person's leave for, in one card: a box to tick for each
|
|
1807
|
+
* (all ticked to begin with), what each would take under it, editable while ticked, and one
|
|
1808
|
+
* button that sends a yes for each ticked (with what was changed) and a no for the rest, at once.
|
|
1809
|
+
*/
|
|
1810
|
+
function Approvals({
|
|
1811
|
+
steps,
|
|
1812
|
+
name,
|
|
1813
|
+
words,
|
|
1814
|
+
classNames,
|
|
1815
|
+
onActAll,
|
|
1816
|
+
}: {
|
|
1817
|
+
steps: readonly (Step & { request: Request })[];
|
|
1818
|
+
name: string;
|
|
1819
|
+
words: ChatMessages;
|
|
1820
|
+
classNames: ClassNames;
|
|
1821
|
+
onActAll: (actions: readonly Action[]) => void;
|
|
1822
|
+
}) {
|
|
1823
|
+
const [chosen, setChosen] = useState<ReadonlySet<string>>(
|
|
1824
|
+
() => new Set(steps.map((step) => step.id)),
|
|
1825
|
+
);
|
|
1826
|
+
const [edits, setEdits] = useState<Readonly<Record<string, Record<string, unknown>>>>({});
|
|
1827
|
+
const [sent, setSent] = useState(false);
|
|
1828
|
+
const inputOf = (step: Step & { request: Request }) => ({
|
|
1829
|
+
...step.request.input,
|
|
1830
|
+
...edits[step.id],
|
|
1831
|
+
});
|
|
1832
|
+
const send = () => {
|
|
1833
|
+
setSent(true);
|
|
1834
|
+
onActAll(
|
|
1835
|
+
steps.map((step) =>
|
|
1836
|
+
chosen.has(step.id)
|
|
1837
|
+
? {
|
|
1838
|
+
surface: step.id,
|
|
1839
|
+
name: "approve",
|
|
1840
|
+
data: step.id in edits ? { input: inputOf(step) } : {},
|
|
1841
|
+
label: words.allow,
|
|
1842
|
+
}
|
|
1843
|
+
: { surface: step.id, name: "decline", data: {}, label: words.decline },
|
|
1844
|
+
),
|
|
1845
|
+
);
|
|
1846
|
+
};
|
|
1847
|
+
const label =
|
|
1848
|
+
chosen.size === steps.length
|
|
1849
|
+
? words.approveAll
|
|
1850
|
+
: chosen.size === 0
|
|
1851
|
+
? words.declineAll
|
|
1852
|
+
: words.approveSome(chosen.size);
|
|
1853
|
+
return (
|
|
1854
|
+
<div part="approval" className={classes(classNames, "sc-chat__request", "sc-chat__requests")}>
|
|
1855
|
+
<header>
|
|
1856
|
+
<ShieldWarningIcon aria-hidden />
|
|
1857
|
+
{words.approvals(steps.length)}
|
|
1858
|
+
</header>
|
|
1859
|
+
<ul>
|
|
1860
|
+
{steps.map((step) => {
|
|
1861
|
+
const ticked = chosen.has(step.id);
|
|
1862
|
+
return (
|
|
1863
|
+
<li key={step.id} className={classes(classNames, "sc-chat__ask")}>
|
|
1864
|
+
<Checkbox
|
|
1865
|
+
value={ticked}
|
|
1866
|
+
disabled={sent}
|
|
1867
|
+
onChange={(on) => {
|
|
1868
|
+
setChosen((last) => {
|
|
1869
|
+
const next = new Set(last);
|
|
1870
|
+
if (on) next.add(step.id);
|
|
1871
|
+
else next.delete(step.id);
|
|
1872
|
+
return next;
|
|
1873
|
+
});
|
|
1874
|
+
}}
|
|
1875
|
+
>
|
|
1876
|
+
{words.request(name, step.request.action)}
|
|
1877
|
+
</Checkbox>
|
|
1878
|
+
{ticked && (
|
|
1879
|
+
<div className={classes(classNames, "sc-chat__ask-fields")}>
|
|
1880
|
+
{Object.entries(inputOf(step)).map(([field, value]) => (
|
|
1881
|
+
<InputValue
|
|
1882
|
+
key={field}
|
|
1883
|
+
label={words.inputs[field] ?? field}
|
|
1884
|
+
value={value}
|
|
1885
|
+
disabled={sent}
|
|
1886
|
+
onChange={(changed) => {
|
|
1887
|
+
setEdits((last) => ({
|
|
1888
|
+
...last,
|
|
1889
|
+
[step.id]: { ...last[step.id], [field]: changed },
|
|
1890
|
+
}));
|
|
1891
|
+
}}
|
|
1892
|
+
/>
|
|
1893
|
+
))}
|
|
1894
|
+
</div>
|
|
1895
|
+
)}
|
|
1896
|
+
</li>
|
|
1897
|
+
);
|
|
1898
|
+
})}
|
|
1899
|
+
</ul>
|
|
1900
|
+
<footer>
|
|
1901
|
+
<Button tone="primary" disabled={sent} onClick={send}>
|
|
1902
|
+
{label}
|
|
1903
|
+
</Button>
|
|
1904
|
+
</footer>
|
|
1905
|
+
</div>
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
/**
|
|
1910
|
+
* One field of an action's input, editable as what it holds: words, a number, a yes or no;
|
|
1911
|
+
* anything else (a list, an object) shown as it is.
|
|
1912
|
+
*/
|
|
1913
|
+
function InputValue({
|
|
1914
|
+
label,
|
|
1915
|
+
value,
|
|
1916
|
+
disabled,
|
|
1917
|
+
onChange,
|
|
1918
|
+
}: {
|
|
1919
|
+
label: string;
|
|
1920
|
+
value: unknown;
|
|
1921
|
+
disabled: boolean;
|
|
1922
|
+
onChange: (value: unknown) => void;
|
|
1923
|
+
}) {
|
|
1924
|
+
if (typeof value === "boolean") {
|
|
1925
|
+
return (
|
|
1926
|
+
<Checkbox value={value} disabled={disabled} onChange={onChange}>
|
|
1927
|
+
{label}
|
|
1928
|
+
</Checkbox>
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
return (
|
|
1932
|
+
<Field label={label}>
|
|
1933
|
+
{(ids) =>
|
|
1934
|
+
typeof value === "number" ? (
|
|
1935
|
+
<NumberInput {...ids} value={value} disabled={disabled} onChange={onChange} />
|
|
1936
|
+
) : typeof value === "string" ? (
|
|
1937
|
+
value.length > 80 || value.includes("\n") ? (
|
|
1938
|
+
<TextArea {...ids} value={value} disabled={disabled} onChange={onChange} />
|
|
1939
|
+
) : (
|
|
1940
|
+
<TextInput {...ids} value={value} disabled={disabled} onChange={onChange} />
|
|
1941
|
+
)
|
|
1942
|
+
) : (
|
|
1943
|
+
<TextInput {...ids} value={JSON.stringify(value)} disabled onChange={() => undefined} />
|
|
1944
|
+
)
|
|
1945
|
+
}
|
|
1946
|
+
</Field>
|
|
1947
|
+
);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
/**
|
|
1951
|
+
* On a person's page, what came is read as the chat shows it: each newest message while the page
|
|
1952
|
+
* is in sight, and once it comes back into sight. Their threads' `unread` follows.
|
|
1953
|
+
*/
|
|
1954
|
+
function useReadAsShown(client: Client, conversationId: string, newest: string | undefined) {
|
|
1955
|
+
useEffect(() => {
|
|
1956
|
+
if (client.who !== "person" || newest === undefined) return undefined;
|
|
1957
|
+
const read = () => {
|
|
1958
|
+
if (document.visibilityState === "hidden") return;
|
|
1959
|
+
client.conversations.read(conversationId).catch(() => undefined);
|
|
1960
|
+
};
|
|
1961
|
+
read();
|
|
1962
|
+
document.addEventListener("visibilitychange", read);
|
|
1963
|
+
return () => {
|
|
1964
|
+
document.removeEventListener("visibilitychange", read);
|
|
1965
|
+
};
|
|
1966
|
+
}, [client, conversationId, newest]);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1586
1969
|
/**
|
|
1587
1970
|
* Where the person writes: a box that grows a few lines with the text. Enter sends, Shift+Enter
|
|
1588
1971
|
* breaks the line; nothing goes while a reply is on its way or the socket is closed, but the
|