@signalcore/ui 1.0.0-beta.3 → 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 +79 -0
- package/dist/AgentChat.d.ts +19 -3
- package/dist/AgentChat.js +149 -27
- package/dist/AgentChat.js.map +1 -1
- package/dist/AgentSpace.d.ts +14 -2
- package/dist/AgentSpace.js +3 -1
- package/dist/AgentSpace.js.map +1 -1
- package/dist/View.d.ts +48 -0
- package/dist/View.js +101 -0
- package/dist/View.js.map +1 -0
- package/dist/blocks.d.ts +83 -2
- package/dist/client.d.ts +9 -1
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -1
- package/dist/es.js +12 -0
- package/dist/es.js.map +1 -1
- package/dist/index.d.ts +11 -5
- package/dist/index.js +7 -1
- 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/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 +12 -0
- package/dist/types.js.map +1 -1
- package/dist/widget.js +17 -17
- package/package.json +1 -1
- package/src/AgentChat.tsx +215 -34
- package/src/AgentSpace.tsx +17 -1
- package/src/View.tsx +117 -0
- package/src/blocks.ts +100 -2
- package/src/client.ts +16 -0
- package/src/contract.ts +166 -5
- package/src/es.ts +9 -0
- package/src/index.ts +27 -2
- 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 +568 -0
- 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 +13 -0
- package/src/widget/element.tsx +41 -3
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/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,
|
|
@@ -49,7 +51,9 @@ import { KIT_MESSAGES, type KitLocale, type KitMessages } from "./kit/messages";
|
|
|
49
51
|
import type { FileHost } from "./kit/uploads";
|
|
50
52
|
import { stepGlyph } from "./stepGlyphs";
|
|
51
53
|
import { Markdown } from "./surface/Markdown";
|
|
52
|
-
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";
|
|
53
57
|
import { framesItself, Surface, usePlayed } from "./surface/Surface";
|
|
54
58
|
import type { SurfaceAction } from "./surface/values";
|
|
55
59
|
import type {
|
|
@@ -138,6 +142,13 @@ export interface ChatMessages {
|
|
|
138
142
|
confirming: string;
|
|
139
143
|
/** The line across the log where the agent handed the conversation to a person. */
|
|
140
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;
|
|
141
152
|
/**
|
|
142
153
|
* The line across the log where who answers the conversation changed (`handovers`), before
|
|
143
154
|
* why: to whom (`holder`), and who changed it (`by`).
|
|
@@ -209,6 +220,8 @@ export const CHAT_MESSAGES: ChatMessages = {
|
|
|
209
220
|
declineAll: "Decline all",
|
|
210
221
|
confirming: "Waiting for the business to confirm",
|
|
211
222
|
handedOff: "Handed to a person",
|
|
223
|
+
opened: (name, title) => `${name} opened ${title}`,
|
|
224
|
+
closePlaced: "Close",
|
|
212
225
|
handover: (holder, by, name) => {
|
|
213
226
|
const automation = by.kind === "automation" ? ` by “${by.title}”` : "";
|
|
214
227
|
switch (holder) {
|
|
@@ -247,6 +260,7 @@ export type ChatPart =
|
|
|
247
260
|
| "sc-chat"
|
|
248
261
|
| "sc-chat--builder"
|
|
249
262
|
| "sc-chat--console"
|
|
263
|
+
| "sc-chat--placed"
|
|
250
264
|
| "sc-chat__log"
|
|
251
265
|
| "sc-chat__log--channel"
|
|
252
266
|
| "sc-chat__message"
|
|
@@ -276,6 +290,8 @@ export type ChatPart =
|
|
|
276
290
|
| "sc-chat__screen"
|
|
277
291
|
| "sc-chat__screen--bare"
|
|
278
292
|
| "sc-chat__handoff"
|
|
293
|
+
| "sc-chat__placed"
|
|
294
|
+
| "sc-chat__placed--open"
|
|
279
295
|
| "sc-chat__handover"
|
|
280
296
|
| "sc-chat__decision"
|
|
281
297
|
| "sc-chat__message--heard"
|
|
@@ -300,7 +316,7 @@ export type ChatPart =
|
|
|
300
316
|
| "sc-chat__stop";
|
|
301
317
|
|
|
302
318
|
type ClassNames =
|
|
303
|
-
Partial<Record<ChatPart | SurfacePart | KitPart | BrowserPart, string>> | undefined;
|
|
319
|
+
Partial<Record<ChatPart | SurfacePart | KitPart | BrowserPart | PlacedPart, string>> | undefined;
|
|
304
320
|
|
|
305
321
|
/**
|
|
306
322
|
* How the chat looks. `agent` is an agent's conversation. `builder` is the builder's, v2's: the
|
|
@@ -344,6 +360,13 @@ export interface AgentChatProps {
|
|
|
344
360
|
classNames?: ClassNames;
|
|
345
361
|
/** The host's own drawing of a block type, for every surface the agent shows. */
|
|
346
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;
|
|
347
370
|
/** The language of the surfaces' calendars, days, times and numbers; English by default. */
|
|
348
371
|
locale?: KitLocale | undefined;
|
|
349
372
|
/** An error's code, from the socket or `load` for a history that could not be read; the host words it. */
|
|
@@ -415,6 +438,86 @@ export interface AgentChatProps {
|
|
|
415
438
|
onTool?: ToolHandler | undefined;
|
|
416
439
|
}
|
|
417
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
|
+
|
|
418
521
|
/** How close to the foot, in pixels, still counts as being at it (v2's). */
|
|
419
522
|
const NEAR_FOOT = 80;
|
|
420
523
|
|
|
@@ -449,6 +552,8 @@ function Chat({
|
|
|
449
552
|
messages,
|
|
450
553
|
classNames,
|
|
451
554
|
components,
|
|
555
|
+
blocks,
|
|
556
|
+
surfaces,
|
|
452
557
|
locale,
|
|
453
558
|
onError,
|
|
454
559
|
actions,
|
|
@@ -491,6 +596,11 @@ function Chat({
|
|
|
491
596
|
const { pending } = live;
|
|
492
597
|
// A conversation on a channel: its messages say who said them.
|
|
493
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);
|
|
494
604
|
|
|
495
605
|
// The builder stops on Esc from anywhere on the page, as v2's did; an agent's chat, from inside it.
|
|
496
606
|
const stop = useRef(live.stop);
|
|
@@ -510,6 +620,8 @@ function Chat({
|
|
|
510
620
|
|
|
511
621
|
const looks: ChatPart[] = builder ? ["sc-chat--builder"] : [];
|
|
512
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");
|
|
513
625
|
const stepsShown = steps === "shown";
|
|
514
626
|
|
|
515
627
|
return (
|
|
@@ -571,6 +683,10 @@ function Chat({
|
|
|
571
683
|
words={words}
|
|
572
684
|
classNames={classNames}
|
|
573
685
|
components={components}
|
|
686
|
+
blocks={blocks}
|
|
687
|
+
places={places}
|
|
688
|
+
opened={opened}
|
|
689
|
+
onOpen={setOpened}
|
|
574
690
|
locale={locale}
|
|
575
691
|
following={following}
|
|
576
692
|
took={took}
|
|
@@ -582,6 +698,35 @@ function Chat({
|
|
|
582
698
|
steps={stepsShown}
|
|
583
699
|
/>
|
|
584
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
|
+
)}
|
|
585
730
|
{/* The browser the agent drives, over the composer while the conversation has one. */}
|
|
586
731
|
<BrowserView live={live} name={name} messages={words.browser} classNames={classNames} />
|
|
587
732
|
{live.pending && (
|
|
@@ -765,8 +910,17 @@ function Log({
|
|
|
765
910
|
handovers,
|
|
766
911
|
who,
|
|
767
912
|
steps,
|
|
913
|
+
blocks,
|
|
914
|
+
places,
|
|
915
|
+
opened,
|
|
916
|
+
onOpen,
|
|
768
917
|
}: PartProps & {
|
|
769
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;
|
|
770
924
|
locale: KitLocale | undefined;
|
|
771
925
|
following: RefObject<boolean>;
|
|
772
926
|
took: number | null;
|
|
@@ -828,16 +982,7 @@ function Log({
|
|
|
828
982
|
};
|
|
829
983
|
}, [following]);
|
|
830
984
|
|
|
831
|
-
|
|
832
|
-
// agent showed it since the person last spoke; older ones are read, not used. On a channel the
|
|
833
|
-
// person answered it there: here it only reads.
|
|
834
|
-
const idle = !onChannel && !pending && awaiting === null;
|
|
835
|
-
const lastSaid = messages.findLastIndex((message) => message.role === "user");
|
|
836
|
-
// What the person did on each surface, by the surface's id: a surface draws its answer.
|
|
837
|
-
const answers = new Map<string, Action>();
|
|
838
|
-
for (const message of messages) {
|
|
839
|
-
if (message.action) answers.set(message.action.surface, message.action);
|
|
840
|
-
}
|
|
985
|
+
const { answers, current } = surfacesOf(live, onChannel);
|
|
841
986
|
// Drawn, who answers says where the agent handed it over; otherwise its `escalate` step does.
|
|
842
987
|
const handed = handovers === undefined ? handoffs(messages) : NO_HANDOFFS;
|
|
843
988
|
const placed = handoversAt(messages, handovers ?? []);
|
|
@@ -894,12 +1039,16 @@ function Log({
|
|
|
894
1039
|
message={message}
|
|
895
1040
|
from={carried.current?.id === message.id ? carried.current.from : undefined}
|
|
896
1041
|
awaiting={onChannel ? null : awaiting}
|
|
897
|
-
current={
|
|
1042
|
+
current={current(index)}
|
|
898
1043
|
answers={answers}
|
|
899
1044
|
name={name}
|
|
900
1045
|
words={words}
|
|
901
1046
|
classNames={classNames}
|
|
902
1047
|
components={components}
|
|
1048
|
+
blocks={blocks}
|
|
1049
|
+
places={places}
|
|
1050
|
+
opened={opened}
|
|
1051
|
+
onOpen={onOpen}
|
|
903
1052
|
locale={locale}
|
|
904
1053
|
onAction={live.act}
|
|
905
1054
|
onActAll={live.actAll}
|
|
@@ -1253,7 +1402,15 @@ function Reply({
|
|
|
1253
1402
|
steps,
|
|
1254
1403
|
onChannel,
|
|
1255
1404
|
onActAll,
|
|
1405
|
+
blocks,
|
|
1406
|
+
places,
|
|
1407
|
+
opened,
|
|
1408
|
+
onOpen,
|
|
1256
1409
|
}: DrawProps & {
|
|
1410
|
+
blocks: HostBlocks | undefined;
|
|
1411
|
+
places: SurfacePlaces | undefined;
|
|
1412
|
+
opened: string | null;
|
|
1413
|
+
onOpen: (id: string) => void;
|
|
1257
1414
|
message: Message;
|
|
1258
1415
|
/** Several things done at once: a yes or no on each call it waits on, from one card. */
|
|
1259
1416
|
onActAll: (actions: readonly Action[]) => void;
|
|
@@ -1336,32 +1493,56 @@ function Reply({
|
|
|
1336
1493
|
))}
|
|
1337
1494
|
</ol>
|
|
1338
1495
|
)}
|
|
1339
|
-
{message.steps.map(
|
|
1340
|
-
(step)
|
|
1341
|
-
|
|
1342
|
-
|
|
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
|
|
1343
1503
|
key={step.id}
|
|
1344
|
-
|
|
1504
|
+
type="button"
|
|
1505
|
+
part="placed"
|
|
1506
|
+
aria-expanded={opened === step.id}
|
|
1507
|
+
onClick={() => {
|
|
1508
|
+
onOpen(step.id);
|
|
1509
|
+
}}
|
|
1345
1510
|
className={classes(
|
|
1346
1511
|
classNames,
|
|
1347
|
-
"sc-
|
|
1348
|
-
...(
|
|
1512
|
+
"sc-chat__placed",
|
|
1513
|
+
...(opened === step.id ? (["sc-chat__placed--open"] as const) : []),
|
|
1349
1514
|
)}
|
|
1350
1515
|
>
|
|
1351
|
-
<
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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
|
+
})}
|
|
1365
1546
|
{asked.length > 1 && (
|
|
1366
1547
|
<Approvals
|
|
1367
1548
|
key={asked.map((step) => step.id).join()}
|
package/src/AgentSpace.tsx
CHANGED
|
@@ -17,7 +17,8 @@ import { Button } from "./kit/Button";
|
|
|
17
17
|
import { KitProvider, type KitPart } from "./kit/context";
|
|
18
18
|
import type { KitLocale } from "./kit/messages";
|
|
19
19
|
import { Modes, type ModeMessages, type ModePart, type SpaceMode } from "./Modes";
|
|
20
|
-
import type {
|
|
20
|
+
import type { SurfacePlaces } from "./surface/places";
|
|
21
|
+
import type { HostBlocks, SurfaceComponents, SurfacePart } from "./surface/scope";
|
|
21
22
|
import { agentTint, type AgentTint } from "./tint";
|
|
22
23
|
import {
|
|
23
24
|
THREAD_MESSAGES,
|
|
@@ -150,6 +151,17 @@ export interface AgentSpaceProps {
|
|
|
150
151
|
| undefined;
|
|
151
152
|
/** The host's own drawing of a block type, for every surface the agent shows. */
|
|
152
153
|
components?: SurfaceComponents | undefined;
|
|
154
|
+
/**
|
|
155
|
+
* The host's own blocks, by the name each was declared with (`PUT /v1/blocks/{name}`):
|
|
156
|
+
* `{ "demoprep.deal-card": DealCard }`. One it doesn't give shows its words.
|
|
157
|
+
*/
|
|
158
|
+
blocks?: HostBlocks | undefined;
|
|
159
|
+
/**
|
|
160
|
+
* Where the agent may put what it shows besides the conversation: `panel`, beside it (over it
|
|
161
|
+
* in a narrow space), and `modal`, over the page; both by default. A place left out shows in
|
|
162
|
+
* the conversation: `{ panel: true, modal: false }`.
|
|
163
|
+
*/
|
|
164
|
+
surfaces?: SurfacePlaces | undefined;
|
|
153
165
|
/** The language of the surfaces' calendars, days, times and numbers; English by default. */
|
|
154
166
|
locale?: KitLocale | undefined;
|
|
155
167
|
/** A few things the person might say first, offered while nothing has been said. */
|
|
@@ -200,6 +212,8 @@ export function AgentSpace({
|
|
|
200
212
|
messages,
|
|
201
213
|
classNames,
|
|
202
214
|
components,
|
|
215
|
+
blocks,
|
|
216
|
+
surfaces,
|
|
203
217
|
locale,
|
|
204
218
|
suggestions,
|
|
205
219
|
onTool,
|
|
@@ -418,6 +432,8 @@ export function AgentSpace({
|
|
|
418
432
|
messages={words.chat}
|
|
419
433
|
classNames={classNames}
|
|
420
434
|
components={components}
|
|
435
|
+
blocks={blocks}
|
|
436
|
+
surfaces={surfaces}
|
|
421
437
|
locale={locale}
|
|
422
438
|
suggestions={suggestions}
|
|
423
439
|
steps={steps}
|
package/src/View.tsx
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { classes } from "./classes";
|
|
4
|
+
import { ApiError, type Client } from "./client";
|
|
5
|
+
import type { KitLocale, KitMessages } from "./kit/messages";
|
|
6
|
+
import type { HostBlocks, SurfaceClassNames, SurfaceComponents } from "./surface/scope";
|
|
7
|
+
import { Surface } from "./surface/Surface";
|
|
8
|
+
import type { SurfaceAction } from "./surface/values";
|
|
9
|
+
import type { ViewRendered } from "./types";
|
|
10
|
+
|
|
11
|
+
/** How often a view is read again while it is shown: the figures move as the rows do. */
|
|
12
|
+
export const VIEW_POLL_MS = 30_000;
|
|
13
|
+
|
|
14
|
+
/** A view as it was last read: the view once it came, why it failed, or that it isn't there. */
|
|
15
|
+
export interface ViewRead {
|
|
16
|
+
view: ViewRendered | null;
|
|
17
|
+
problem: unknown;
|
|
18
|
+
/** No such view, or not one for this person (404). */
|
|
19
|
+
gone: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* An agent's view (`name`), rendered for the person the page speaks for (their token: their
|
|
24
|
+
* own) or, for the business, for the contact it names: read when shown and again every
|
|
25
|
+
* `VIEW_POLL_MS`, what was read staying while the next read is on its way.
|
|
26
|
+
*/
|
|
27
|
+
export function useView(client: Client, name: string, contact?: string): ViewRead {
|
|
28
|
+
const key = `${name}\n${contact ?? ""}`;
|
|
29
|
+
const [read, setRead] = useState<ViewRead & { key: string }>({
|
|
30
|
+
key,
|
|
31
|
+
view: null,
|
|
32
|
+
problem: null,
|
|
33
|
+
gone: false,
|
|
34
|
+
});
|
|
35
|
+
const latest = useRef({ client, name, contact });
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
latest.current = { client, name, contact };
|
|
38
|
+
});
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
let stopped = false;
|
|
41
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
42
|
+
const load = async () => {
|
|
43
|
+
const now = latest.current;
|
|
44
|
+
try {
|
|
45
|
+
const view = await now.client.views.render(now.name, now.contact);
|
|
46
|
+
if (stopped) return;
|
|
47
|
+
setRead({ key, view, problem: null, gone: false });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (stopped) return;
|
|
50
|
+
const gone = error instanceof ApiError && error.status === 404;
|
|
51
|
+
setRead((last) => ({ ...last, key, problem: gone ? null : error, gone }));
|
|
52
|
+
}
|
|
53
|
+
if (!stopped) timer = setTimeout(() => void load(), VIEW_POLL_MS);
|
|
54
|
+
};
|
|
55
|
+
void load();
|
|
56
|
+
return () => {
|
|
57
|
+
stopped = true;
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
};
|
|
60
|
+
}, [client, key]);
|
|
61
|
+
// Another view's read is not this one's: until its own comes, nothing is shown.
|
|
62
|
+
return read.key === key ? read : { view: null, problem: null, gone: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ViewProps {
|
|
66
|
+
client: Client;
|
|
67
|
+
/** The view's name, as its agent or the business wrote it (`proxima_reunion`). */
|
|
68
|
+
name: string;
|
|
69
|
+
/** For the business: the contact it is rendered for (their id). A person's token: their own. */
|
|
70
|
+
contact?: string | undefined;
|
|
71
|
+
/** The host's own blocks, by the name each was declared with. */
|
|
72
|
+
blocks?: HostBlocks | undefined;
|
|
73
|
+
/** The host's own drawing of a block type. */
|
|
74
|
+
components?: SurfaceComponents | undefined;
|
|
75
|
+
classNames?: SurfaceClassNames;
|
|
76
|
+
/** The words of its blocks, in the host's language; English by default. */
|
|
77
|
+
messages?: Partial<KitMessages> | undefined;
|
|
78
|
+
/** The language of its days, times and numbers; English by default. */
|
|
79
|
+
locale?: KitLocale | undefined;
|
|
80
|
+
/** What the person does on a host's block that sends something; without it, they only read. */
|
|
81
|
+
onAction?: ((action: SurfaceAction) => void) | undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* An agent's view where the host's page puts it (a rep's next meeting on its home, their week
|
|
86
|
+
* beside a record), drawn with the kit's blocks in the host's style, live: read again every half
|
|
87
|
+
* minute, so it shows what the rows hold now, never a copy. While it is first read, an empty
|
|
88
|
+
* box holds its place; a view that isn't there (or isn't the person's) draws nothing.
|
|
89
|
+
*/
|
|
90
|
+
export function View({
|
|
91
|
+
client,
|
|
92
|
+
name,
|
|
93
|
+
contact,
|
|
94
|
+
blocks,
|
|
95
|
+
components,
|
|
96
|
+
classNames,
|
|
97
|
+
messages,
|
|
98
|
+
locale,
|
|
99
|
+
onAction,
|
|
100
|
+
}: ViewProps) {
|
|
101
|
+
const { view } = useView(client, name, contact);
|
|
102
|
+
if (view === null) {
|
|
103
|
+
return <div aria-busy="true" className={`sc ${classes(classNames, "sc-surface")}`} />;
|
|
104
|
+
}
|
|
105
|
+
return (
|
|
106
|
+
<Surface
|
|
107
|
+
surface={{ blocks: view.blocks, text: view.text }}
|
|
108
|
+
active={onAction !== undefined}
|
|
109
|
+
onAction={onAction ?? (() => undefined)}
|
|
110
|
+
blocks={blocks}
|
|
111
|
+
components={components}
|
|
112
|
+
classNames={classNames}
|
|
113
|
+
messages={messages}
|
|
114
|
+
locale={locale}
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
}
|
package/src/blocks.ts
CHANGED
|
@@ -124,6 +124,88 @@ export interface ConnectBlock {
|
|
|
124
124
|
connection: ConnectView | null;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
// --- Time and work ------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* One thing on a calendar: its title at its time ("YYYY-MM-DDTHH:MM" in the person's time; a day
|
|
131
|
+
* alone, "YYYY-MM-DD", is all of it), until `end`, with a line of detail and a page to open.
|
|
132
|
+
*/
|
|
133
|
+
export interface CalendarEvent {
|
|
134
|
+
id: string;
|
|
135
|
+
title: string;
|
|
136
|
+
start: string;
|
|
137
|
+
end?: string;
|
|
138
|
+
detail?: string;
|
|
139
|
+
/** https only. */
|
|
140
|
+
url?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A day's events, or a week's from `start` ("YYYY-MM-DD"), in their order. With `action`,
|
|
145
|
+
* pressing one sends it with `{event: id}`; one with `url` opens its page instead.
|
|
146
|
+
*/
|
|
147
|
+
export interface CalendarBlock {
|
|
148
|
+
type: "calendar";
|
|
149
|
+
span: "day" | "week";
|
|
150
|
+
start: string;
|
|
151
|
+
events: CalendarEvent[];
|
|
152
|
+
action?: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** One thing on a timeline: when, what, a line about it, where it stands in a word, its mark. */
|
|
156
|
+
export interface TimelineItem {
|
|
157
|
+
id?: string;
|
|
158
|
+
at: string;
|
|
159
|
+
title: string;
|
|
160
|
+
text?: string;
|
|
161
|
+
status?: string;
|
|
162
|
+
tone?: Tone;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Dated things, one under another, the newest first. */
|
|
166
|
+
export interface TimelineBlock {
|
|
167
|
+
type: "timeline";
|
|
168
|
+
items: TimelineItem[];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface BoardCard {
|
|
172
|
+
id: string;
|
|
173
|
+
title: string;
|
|
174
|
+
subtitle?: string;
|
|
175
|
+
/** At its foot: a price, a day, who. */
|
|
176
|
+
meta?: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface BoardColumn {
|
|
180
|
+
id: string;
|
|
181
|
+
title: string;
|
|
182
|
+
cards: BoardCard[];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Cards in columns, a column per stage. With `action`, the person moves a card to another column,
|
|
187
|
+
* which sends `action` with `{card, column}`.
|
|
188
|
+
*/
|
|
189
|
+
export interface BoardBlock {
|
|
190
|
+
type: "board";
|
|
191
|
+
columns: BoardColumn[];
|
|
192
|
+
action?: string;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// --- The host's own -----------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* One of the host's own blocks, by the name it declared (`PUT /v1/blocks/{name}`): its `props`,
|
|
199
|
+
* checked by the API against the schema the host declared, and `text`, the same in words, which a
|
|
200
|
+
* page that doesn't draw it shows instead.
|
|
201
|
+
*/
|
|
202
|
+
export interface HostBlock {
|
|
203
|
+
type: "host";
|
|
204
|
+
name: string;
|
|
205
|
+
props: Record<string, unknown>;
|
|
206
|
+
text: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
127
209
|
// --- Action -------------------------------------------------------------------------------
|
|
128
210
|
|
|
129
211
|
/** One thing the person can do: pressing it sends `action`, or, with `url`, opens that page. */
|
|
@@ -265,10 +347,26 @@ export type Block =
|
|
|
265
347
|
| ConnectBlock
|
|
266
348
|
| ButtonBlock
|
|
267
349
|
| ChoiceBlock
|
|
268
|
-
| FormBlock
|
|
350
|
+
| FormBlock
|
|
351
|
+
| CalendarBlock
|
|
352
|
+
| TimelineBlock
|
|
353
|
+
| BoardBlock
|
|
354
|
+
| HostBlock;
|
|
269
355
|
|
|
270
|
-
/**
|
|
356
|
+
/**
|
|
357
|
+
* Where a surface shows in a page: in the conversation, in a panel beside it, or in a modal over
|
|
358
|
+
* it. A page that doesn't allow the place shows it in the conversation.
|
|
359
|
+
*/
|
|
360
|
+
export type Place = "chat" | "panel" | "modal";
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* What the agent shows: blocks, and the words a channel without a screen gets instead; where it
|
|
364
|
+
* shows and, in a panel or a modal, its title, which the conversation's line to open it says.
|
|
365
|
+
*/
|
|
271
366
|
export interface Surface {
|
|
272
367
|
blocks: Block[];
|
|
273
368
|
text: string;
|
|
369
|
+
/** None: the conversation. */
|
|
370
|
+
where?: Place;
|
|
371
|
+
title?: string;
|
|
274
372
|
}
|