@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.
Files changed (71) hide show
  1. package/README.md +79 -0
  2. package/dist/AgentChat.d.ts +19 -3
  3. package/dist/AgentChat.js +149 -27
  4. package/dist/AgentChat.js.map +1 -1
  5. package/dist/AgentSpace.d.ts +14 -2
  6. package/dist/AgentSpace.js +3 -1
  7. package/dist/AgentSpace.js.map +1 -1
  8. package/dist/View.d.ts +48 -0
  9. package/dist/View.js +101 -0
  10. package/dist/View.js.map +1 -0
  11. package/dist/blocks.d.ts +83 -2
  12. package/dist/client.d.ts +9 -1
  13. package/dist/client.js +1 -0
  14. package/dist/client.js.map +1 -1
  15. package/dist/es.js +12 -0
  16. package/dist/es.js.map +1 -1
  17. package/dist/index.d.ts +11 -5
  18. package/dist/index.js +7 -1
  19. package/dist/kit/Agenda.d.ts +25 -0
  20. package/dist/kit/Agenda.js +119 -0
  21. package/dist/kit/Agenda.js.map +1 -0
  22. package/dist/kit/Combobox.js +1 -1
  23. package/dist/kit/DateField.js +1 -1
  24. package/dist/kit/Kanban.d.ts +17 -0
  25. package/dist/kit/Kanban.js +157 -0
  26. package/dist/kit/Kanban.js.map +1 -0
  27. package/dist/kit/TimeField.js +1 -1
  28. package/dist/kit/Timeline.d.ts +14 -0
  29. package/dist/kit/Timeline.js +71 -0
  30. package/dist/kit/Timeline.js.map +1 -0
  31. package/dist/kit/context.d.ts +1 -1
  32. package/dist/kit/context.js.map +1 -1
  33. package/dist/kit/messages.d.ts +22 -0
  34. package/dist/kit/messages.js +10 -0
  35. package/dist/kit/messages.js.map +1 -1
  36. package/dist/surface/Placed.d.ts +20 -0
  37. package/dist/surface/Placed.js +70 -0
  38. package/dist/surface/Placed.js.map +1 -0
  39. package/dist/surface/Surface.d.ts +4 -2
  40. package/dist/surface/Surface.js +78 -2
  41. package/dist/surface/Surface.js.map +1 -1
  42. package/dist/surface/places.d.ts +16 -0
  43. package/dist/surface/places.js +15 -0
  44. package/dist/surface/places.js.map +1 -0
  45. package/dist/surface/scope.d.ts +17 -1
  46. package/dist/surface/scope.js +1 -0
  47. package/dist/surface/scope.js.map +1 -1
  48. package/dist/types.d.ts +12 -0
  49. package/dist/types.js.map +1 -1
  50. package/dist/widget.js +17 -17
  51. package/package.json +1 -1
  52. package/src/AgentChat.tsx +215 -34
  53. package/src/AgentSpace.tsx +17 -1
  54. package/src/View.tsx +117 -0
  55. package/src/blocks.ts +100 -2
  56. package/src/client.ts +16 -0
  57. package/src/contract.ts +166 -5
  58. package/src/es.ts +9 -0
  59. package/src/index.ts +27 -2
  60. package/src/kit/Agenda.tsx +161 -0
  61. package/src/kit/Kanban.tsx +182 -0
  62. package/src/kit/Timeline.tsx +58 -0
  63. package/src/kit/context.tsx +45 -1
  64. package/src/kit/messages.ts +31 -0
  65. package/src/styles.css +568 -0
  66. package/src/surface/Placed.tsx +98 -0
  67. package/src/surface/Surface.tsx +92 -1
  68. package/src/surface/places.ts +19 -0
  69. package/src/surface/scope.ts +24 -1
  70. package/src/types.ts +13 -0
  71. package/src/widget/element.tsx +41 -3
@@ -5,10 +5,13 @@ import { Automation } from "../automation/Automation";
5
5
  import type {
6
6
  AutomationBlock,
7
7
  Block,
8
+ BoardBlock,
8
9
  ButtonBlock,
10
+ CalendarBlock,
9
11
  ChoiceBlock,
10
12
  ConnectBlock,
11
13
  FieldsBlock,
14
+ HostBlock,
12
15
  ImageBlock,
13
16
  ListBlock,
14
17
  Surface as Shown,
@@ -18,6 +21,8 @@ import { classes } from "../classes";
18
21
  import { Button } from "../kit/Button";
19
22
  import { formatSize } from "../about";
20
23
  import { baseName } from "../files";
24
+ import { Agenda } from "../kit/Agenda";
25
+ import { Kanban } from "../kit/Kanban";
21
26
  import { Card } from "../kit/Card";
22
27
  import { Chart } from "../kit/Chart";
23
28
  import { KitProvider, useKit, useParts } from "../kit/context";
@@ -29,6 +34,7 @@ import type { FileHost } from "../kit/uploads";
29
34
  import { AudioPlayer } from "../kit/AudioPlayer";
30
35
  import { Note } from "../kit/Note";
31
36
  import { Stat } from "../kit/Stat";
37
+ import { Timeline } from "../kit/Timeline";
32
38
  import type { Attachment, FileBlock } from "../types";
33
39
  import { FormView } from "./Form";
34
40
  import { Markdown } from "./Markdown";
@@ -36,6 +42,8 @@ import {
36
42
  ScopeContext,
37
43
  useScope,
38
44
  useSurfaceParts,
45
+ type HostBlockProps,
46
+ type HostBlocks,
39
47
  type Scope,
40
48
  type SurfaceClassNames,
41
49
  type SurfaceComponents,
@@ -50,6 +58,8 @@ export interface SurfaceProps {
50
58
  /** What the person already did on it: the option picked, the button pressed, the form sent. */
51
59
  answered?: SurfaceAction | undefined;
52
60
  components?: SurfaceComponents | undefined;
61
+ /** The host's own blocks, by the name each was declared with. */
62
+ blocks?: HostBlocks | undefined;
53
63
  classNames?: SurfaceClassNames;
54
64
  /** The words its controls say, in the host's language; English by default. */
55
65
  messages?: Partial<KitMessages> | undefined;
@@ -80,6 +90,10 @@ const BLOCKS: ReadonlySet<string> = new Set([
80
90
  "button",
81
91
  "choice",
82
92
  "form",
93
+ "calendar",
94
+ "timeline",
95
+ "board",
96
+ "host",
83
97
  ]);
84
98
 
85
99
  /** Whether every block down the tree, and every field of its forms, is one this renderer draws. */
@@ -116,12 +130,13 @@ export function Surface({
116
130
  active = true,
117
131
  answered,
118
132
  components,
133
+ blocks,
119
134
  classNames,
120
135
  messages,
121
136
  locale,
122
137
  files,
123
138
  }: SurfaceProps) {
124
- const scope: Scope = { active, answered, onAction, components, classNames, files };
139
+ const scope: Scope = { active, answered, onAction, components, blocks, classNames, files };
125
140
  return (
126
141
  <KitProvider classNames={classNames} messages={messages} locale={locale}>
127
142
  <ScopeContext value={scope}>
@@ -227,9 +242,85 @@ function Draw({ block }: { block: Block }) {
227
242
  return <Choice block={block} />;
228
243
  case "form":
229
244
  return <FormView block={block} />;
245
+ case "calendar":
246
+ return <CalendarShown block={block} />;
247
+ case "timeline":
248
+ return <Timeline items={block.items} />;
249
+ case "board":
250
+ return <BoardShown block={block} />;
251
+ case "host":
252
+ return <Hosted block={block} />;
230
253
  }
231
254
  }
232
255
 
256
+ /** A calendar; with its action, pressing an event sends it (`{event}`). */
257
+ function CalendarShown({ block }: { block: CalendarBlock }) {
258
+ const { active, onAction } = useScope();
259
+ const { action } = block;
260
+ return (
261
+ <Agenda
262
+ span={block.span}
263
+ start={block.start}
264
+ events={block.events}
265
+ disabled={!active}
266
+ onEvent={
267
+ action === undefined
268
+ ? undefined
269
+ : (event) => {
270
+ onAction({ name: action, data: { event: event.id }, label: event.title });
271
+ }
272
+ }
273
+ />
274
+ );
275
+ }
276
+
277
+ /** A board; with its action, a card moved to another column sends it (`{card, column}`). */
278
+ function BoardShown({ block }: { block: BoardBlock }) {
279
+ const { active, onAction } = useScope();
280
+ const { messages } = useKit();
281
+ const { action } = block;
282
+ return (
283
+ <Kanban
284
+ columns={block.columns}
285
+ disabled={!active}
286
+ onMove={
287
+ action === undefined
288
+ ? undefined
289
+ : (card, column) => {
290
+ onAction({
291
+ name: action,
292
+ data: { card: card.id, column: column.id },
293
+ label: messages.board.moved(card.title, column.title),
294
+ });
295
+ }
296
+ }
297
+ />
298
+ );
299
+ }
300
+
301
+ /**
302
+ * One of the host's own blocks, drawn by the component it registered under the block's name; a
303
+ * name it gave none for, or a component that fails as it draws, leaves the block's words in a
304
+ * quiet card in its place, and the rest of the surface draws.
305
+ */
306
+ function Hosted({ block }: { block: HostBlock }) {
307
+ const { blocks, active, onAction } = useScope();
308
+ const part = useSurfaceParts();
309
+ const twin = (
310
+ <div part="host" className={part("sc-surface__host")} data-block={block.name}>
311
+ <Markdown text={block.text} />
312
+ </div>
313
+ );
314
+ // Registered under this name, for these props: the API checked them against its schema.
315
+ const Own = blocks?.[block.name] as ComponentType<HostBlockProps> | undefined;
316
+ if (Own === undefined) return twin;
317
+ return (
318
+ <Whole twin={twin}>
319
+ <Own props={block.props} text={block.text} active={active} onAction={onAction} />
320
+ </Whole>
321
+ );
322
+ }
323
+
233
324
  /**
234
325
  * A file the agent put in front of the person, compact: its kind's mark, its title (its name
235
326
  * under it when the title is another), its size at the right; pressed, it opens whole in the
@@ -0,0 +1,19 @@
1
+ import type { Place, Surface } from "../blocks";
2
+
3
+ /**
4
+ * Where a host lets the agent put what it shows besides the conversation: a panel beside it, a
5
+ * modal over the page. A place it doesn't allow shows in the conversation.
6
+ */
7
+ export interface SurfacePlaces {
8
+ panel?: boolean | undefined;
9
+ modal?: boolean | undefined;
10
+ }
11
+
12
+ /** Both, unless the host says otherwise. */
13
+ export const EVERY_PLACE: SurfacePlaces = { panel: true, modal: true };
14
+
15
+ /** Where a surface shows in this page: the place the agent chose when the host allows it. */
16
+ export function placeOf(surface: Surface, places: SurfacePlaces = EVERY_PLACE): Place {
17
+ const where = surface.where ?? "chat";
18
+ return where !== "chat" && places[where] === true ? where : "chat";
19
+ }
@@ -30,7 +30,8 @@ export type SurfacePart =
30
30
  | "sc-surface__foot"
31
31
  | "sc-surface__sent"
32
32
  | "sc-surface__connect"
33
- | "sc-surface__connect--done";
33
+ | "sc-surface__connect--done"
34
+ | "sc-surface__host";
34
35
 
35
36
  export type SurfaceClassNames = Partial<Record<SurfacePart | KitPart, string>> | undefined;
36
37
 
@@ -52,6 +53,25 @@ export type BlockProps<Type extends Block["type"]> = Omit<
52
53
  /** The host's own drawing of a block type, in place of the package's: `{ table: TheirTable }`. */
53
54
  export type SurfaceComponents = { [Type in Block["type"]]?: ComponentType<BlockProps<Type>> };
54
55
 
56
+ /**
57
+ * What a host's own block receives (`HostBlocks`): its `props`, as the API checked them against
58
+ * the schema the host declared; `text`, the same in words; whether the surface still takes
59
+ * input, and `onAction` to send what the person did with it, as any block does.
60
+ */
61
+ export interface HostBlockProps<Props = Record<string, unknown>> {
62
+ props: Props;
63
+ text: string;
64
+ active: boolean;
65
+ onAction: (action: SurfaceAction) => void;
66
+ }
67
+
68
+ /**
69
+ * The host's own blocks, by the name it declared each with (`PUT /v1/blocks/{name}`):
70
+ * `{ "demoprep.deal-card": DealCard }`. A block of a name it doesn't give shows its words.
71
+ */
72
+ // `never`: a component typed for its own props (`HostBlockProps<Deal>`) takes its place.
73
+ export type HostBlocks = Readonly<Record<string, ComponentType<HostBlockProps<never>>>>;
74
+
55
75
  /** What every block of one surface shares: whether it takes input, what was done on it. */
56
76
  export interface Scope {
57
77
  active: boolean;
@@ -59,6 +79,8 @@ export interface Scope {
59
79
  answered: SurfaceAction | undefined;
60
80
  onAction: (action: SurfaceAction) => void;
61
81
  components: SurfaceComponents | undefined;
82
+ /** The host's own blocks, by name. */
83
+ blocks: HostBlocks | undefined;
62
84
  classNames: SurfaceClassNames;
63
85
  /** Where its file fields' files go and are found; none: they take none. */
64
86
  files: FileHost | undefined;
@@ -69,6 +91,7 @@ export const ScopeContext = createContext<Scope>({
69
91
  answered: undefined,
70
92
  onAction: () => undefined,
71
93
  components: undefined,
94
+ blocks: undefined,
72
95
  classNames: undefined,
73
96
  files: undefined,
74
97
  });
package/src/types.ts CHANGED
@@ -496,6 +496,19 @@ export interface FileBlock {
496
496
  file: Attachment | null;
497
497
  }
498
498
 
499
+ /**
500
+ * An agent's view rendered for one contact (`GET /views/{name}/render`): its blocks as the kit
501
+ * draws them, filled from the rows when it was read, and `text`, the same in words.
502
+ */
503
+ export interface ViewRendered {
504
+ name: string;
505
+ title: string;
506
+ contact_id: string;
507
+ blocks: Surface["blocks"];
508
+ text: string;
509
+ rendered_at: string;
510
+ }
511
+
499
512
  /** Who holds the conversation's browser: the agent drives it, or the person does. */
500
513
  export type BrowserControl = "agent" | "person";
501
514
 
@@ -8,6 +8,8 @@ import { AgentSpace, SPACE_MESSAGES } from "../AgentSpace";
8
8
  import { useUnread } from "../Threads";
9
9
  import { createClient } from "../client";
10
10
  import type { Credentials } from "../credentials";
11
+ import type { SurfacePlaces } from "../surface/places";
12
+ import type { HostBlocks } from "../surface/scope";
11
13
  import { SPACE_MESSAGES_ES } from "../es";
12
14
  import { es } from "../locale";
13
15
  import styles from "../styles.css?inline";
@@ -32,6 +34,7 @@ const ATTRIBUTES = [
32
34
  "unstyled",
33
35
  "shadow",
34
36
  "contained",
37
+ "surfaces",
35
38
  ] as const;
36
39
 
37
40
  type Attribute = (typeof ATTRIBUTES)[number];
@@ -59,6 +62,11 @@ export interface WidgetSettings {
59
62
  steps: ChatSteps | undefined;
60
63
  /** Floating within the element's own box (`contained`), not at the page's corner: a preview. */
61
64
  contained: boolean;
65
+ /**
66
+ * Where the agent may put what it shows besides the conversation (`surfaces`: `"panel modal"`,
67
+ * either, or `""` for neither); both unless the page says.
68
+ */
69
+ surfaces: SurfacePlaces | undefined;
62
70
  }
63
71
 
64
72
  /** The launcher's words, by language. */
@@ -109,9 +117,17 @@ export function settingsOf(element: Element): WidgetSettings | null {
109
117
  header: on(element, "header"),
110
118
  steps: steps === "shown" || steps === "hidden" ? steps : undefined,
111
119
  contained: on(element, "contained"),
120
+ surfaces: placesOf(element.getAttribute("surfaces")),
112
121
  };
113
122
  }
114
123
 
124
+ /** The places a `surfaces` attribute names (`"panel modal"`); none given: the default, both. */
125
+ export function placesOf(named: string | null): SurfacePlaces | undefined {
126
+ if (named === null) return undefined;
127
+ const places = new Set(named.trim().toLowerCase().split(/\s+/));
128
+ return { panel: places.has("panel"), modal: places.has("modal") };
129
+ }
130
+
115
131
  /**
116
132
  * A look as the widget's own stylesheet: its tokens on the host (`:host` in its shadow root, the
117
133
  * element itself when drawn into the page), where the page's own rules for the element still
@@ -242,6 +258,8 @@ function stylePage() {
242
258
  export interface WidgetProps {
243
259
  settings: WidgetSettings & { agent: string };
244
260
  look?: WidgetLook | null | undefined;
261
+ /** The page's own blocks, by name (the element's `blocks`). */
262
+ blocks?: HostBlocks | undefined;
245
263
  }
246
264
 
247
265
  /**
@@ -249,8 +267,8 @@ export interface WidgetProps {
249
267
  * page's corner (the left one if its look says so), or (`inline`) in the page's flow at the
250
268
  * element's size. The conversation it opens is kept in the page's storage for the next visit.
251
269
  */
252
- export function Widget({ settings, look = null }: WidgetProps) {
253
- const { baseUrl, agent, credential, language, mode, steps, contained } = settings;
270
+ export function Widget({ settings, look = null, blocks }: WidgetProps) {
271
+ const { baseUrl, agent, credential, language, mode, steps, contained, surfaces } = settings;
254
272
  const signedAs = credential.token ?? credential.publishableKey ?? "cookie";
255
273
  // One client while the page says the same; a new token (the host's, renewed) makes another.
256
274
  const client = useMemo(() => createClient({ baseUrl, ...credential }), [baseUrl, signedAs]);
@@ -277,6 +295,8 @@ export function Widget({ settings, look = null }: WidgetProps) {
277
295
  layout={floating ? "compact" : "full"}
278
296
  header={header}
279
297
  steps={steps}
298
+ surfaces={surfaces}
299
+ blocks={blocks}
280
300
  messages={language === "es" ? SPACE_MESSAGES_ES : SPACE_MESSAGES}
281
301
  locale={language === "es" ? es : undefined}
282
302
  onClose={
@@ -346,6 +366,8 @@ export class SignalCoreChat extends HTMLElement {
346
366
  #lookSheet: HTMLStyleElement | null = null;
347
367
  /** The look set on the element, over its channel's. */
348
368
  #look: WidgetLook | null = null;
369
+ /** The page's own blocks, by name. */
370
+ #blocks: HostBlocks | undefined = undefined;
349
371
  /** The web channel its publishable key belongs to, as read for that key (null while read). */
350
372
  #channel: { key: string; config: WidgetConfig | null } | null = null;
351
373
  /** The page's type and ink as last given to the widget. */
@@ -372,6 +394,20 @@ export class SignalCoreChat extends HTMLElement {
372
394
  this.#draw();
373
395
  }
374
396
 
397
+ /**
398
+ * The page's own blocks, by the name each was declared with (`PUT /v1/blocks/{name}`), set as
399
+ * a property: `chat.blocks = { "demoprep.deal-card": DealCard }`, React components the
400
+ * widget's React draws. One it doesn't give shows its words.
401
+ */
402
+ get blocks(): HostBlocks | undefined {
403
+ return this.#blocks;
404
+ }
405
+
406
+ set blocks(value: HostBlocks | undefined) {
407
+ this.#blocks = value;
408
+ this.#draw();
409
+ }
410
+
375
411
  connectedCallback() {
376
412
  this.#mount ??= this.#build();
377
413
  this.#root = createRoot(this.#mount);
@@ -530,7 +566,9 @@ export class SignalCoreChat extends HTMLElement {
530
566
  }
531
567
  const agent = settings?.agent ?? config?.agent_id ?? null;
532
568
  this.#root.render(
533
- settings && agent && <Widget settings={{ ...settings, agent }} look={this.#worn} />,
569
+ settings && agent && (
570
+ <Widget settings={{ ...settings, agent }} look={this.#worn} blocks={this.#blocks} />
571
+ ),
534
572
  );
535
573
  }
536
574
  }