@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.
Files changed (99) hide show
  1. package/README.md +126 -21
  2. package/dist/AgentAbout.js +6 -2
  3. package/dist/AgentAbout.js.map +1 -1
  4. package/dist/AgentChat.d.ts +25 -3
  5. package/dist/AgentChat.js +302 -28
  6. package/dist/AgentChat.js.map +1 -1
  7. package/dist/AgentSpace.d.ts +25 -4
  8. package/dist/AgentSpace.js +55 -11
  9. package/dist/AgentSpace.js.map +1 -1
  10. package/dist/Threads.d.ts +73 -0
  11. package/dist/Threads.js +140 -0
  12. package/dist/Threads.js.map +1 -0
  13. package/dist/View.d.ts +48 -0
  14. package/dist/View.js +101 -0
  15. package/dist/View.js.map +1 -0
  16. package/dist/automation/marks.js +7 -3
  17. package/dist/automation/marks.js.map +1 -1
  18. package/dist/automation/messages.d.ts +4 -0
  19. package/dist/automation/messages.js +5 -1
  20. package/dist/automation/messages.js.map +1 -1
  21. package/dist/automation/when.js +8 -0
  22. package/dist/automation/when.js.map +1 -1
  23. package/dist/blocks.d.ts +83 -2
  24. package/dist/client.d.ts +23 -5
  25. package/dist/client.js +8 -2
  26. package/dist/client.js.map +1 -1
  27. package/dist/contract.d.ts +9 -4
  28. package/dist/es.d.ts +2 -0
  29. package/dist/es.js +34 -3
  30. package/dist/es.js.map +1 -1
  31. package/dist/frames.d.ts +7 -1
  32. package/dist/frames.js.map +1 -1
  33. package/dist/index.d.ts +14 -7
  34. package/dist/index.js +11 -4
  35. package/dist/kit/Agenda.d.ts +25 -0
  36. package/dist/kit/Agenda.js +119 -0
  37. package/dist/kit/Agenda.js.map +1 -0
  38. package/dist/kit/Combobox.js +1 -1
  39. package/dist/kit/DateField.js +1 -1
  40. package/dist/kit/Kanban.d.ts +17 -0
  41. package/dist/kit/Kanban.js +157 -0
  42. package/dist/kit/Kanban.js.map +1 -0
  43. package/dist/kit/TimeField.js +1 -1
  44. package/dist/kit/Timeline.d.ts +14 -0
  45. package/dist/kit/Timeline.js +71 -0
  46. package/dist/kit/Timeline.js.map +1 -0
  47. package/dist/kit/context.d.ts +1 -1
  48. package/dist/kit/context.js.map +1 -1
  49. package/dist/kit/messages.d.ts +22 -0
  50. package/dist/kit/messages.js +10 -0
  51. package/dist/kit/messages.js.map +1 -1
  52. package/dist/surface/Form.js +1 -1
  53. package/dist/surface/Placed.d.ts +20 -0
  54. package/dist/surface/Placed.js +70 -0
  55. package/dist/surface/Placed.js.map +1 -0
  56. package/dist/surface/Surface.d.ts +4 -2
  57. package/dist/surface/Surface.js +78 -2
  58. package/dist/surface/Surface.js.map +1 -1
  59. package/dist/surface/places.d.ts +16 -0
  60. package/dist/surface/places.js +15 -0
  61. package/dist/surface/places.js.map +1 -0
  62. package/dist/surface/scope.d.ts +17 -1
  63. package/dist/surface/scope.js +1 -0
  64. package/dist/surface/scope.js.map +1 -1
  65. package/dist/types.d.ts +23 -0
  66. package/dist/types.js.map +1 -1
  67. package/dist/useConversation.d.ts +2 -0
  68. package/dist/useConversation.js +16 -0
  69. package/dist/useConversation.js.map +1 -1
  70. package/dist/widget.js +17 -16
  71. package/package.json +1 -1
  72. package/src/AgentAbout.tsx +4 -0
  73. package/src/AgentChat.tsx +417 -34
  74. package/src/AgentSpace.tsx +106 -14
  75. package/src/Threads.tsx +218 -0
  76. package/src/View.tsx +117 -0
  77. package/src/automation/marks.tsx +7 -0
  78. package/src/automation/messages.ts +8 -0
  79. package/src/automation/when.ts +4 -0
  80. package/src/blocks.ts +100 -2
  81. package/src/client.ts +42 -10
  82. package/src/contract.ts +404 -29
  83. package/src/es.ts +30 -0
  84. package/src/frames.ts +3 -1
  85. package/src/index.ts +36 -3
  86. package/src/kit/Agenda.tsx +161 -0
  87. package/src/kit/Kanban.tsx +182 -0
  88. package/src/kit/Timeline.tsx +58 -0
  89. package/src/kit/context.tsx +45 -1
  90. package/src/kit/messages.ts +31 -0
  91. package/src/styles.css +825 -66
  92. package/src/surface/Placed.tsx +98 -0
  93. package/src/surface/Surface.tsx +92 -1
  94. package/src/surface/places.ts +19 -0
  95. package/src/surface/scope.ts +24 -1
  96. package/src/types.ts +24 -0
  97. package/src/useConversation.ts +10 -0
  98. package/src/widget/element.tsx +54 -7
  99. package/src/widget/widget.css +13 -0
@@ -17,8 +17,16 @@ 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 { SurfaceComponents, SurfacePart } from "./surface/scope";
20
+ import type { SurfacePlaces } from "./surface/places";
21
+ import type { HostBlocks, SurfaceComponents, SurfacePart } from "./surface/scope";
21
22
  import { agentTint, type AgentTint } from "./tint";
23
+ import {
24
+ THREAD_MESSAGES,
25
+ Threads,
26
+ useUnread,
27
+ type ThreadMessages,
28
+ type ThreadPart,
29
+ } from "./Threads";
22
30
  import { useAgentFace } from "./useAgentFace";
23
31
  import { useConversation, type LiveConversation, type ToolHandler } from "./useConversation";
24
32
  import type { VoiceCallMessages, VoiceCallPart } from "./VoiceCall";
@@ -50,6 +58,8 @@ export interface SpaceMessages {
50
58
  voice: Partial<VoiceCallMessages>;
51
59
  /** What the person's tasks say (`AgentTasks`'s words). */
52
60
  tasks: TaskMessages;
61
+ /** What the person's threads say (`Threads`'s words). */
62
+ threads: ThreadMessages;
53
63
  }
54
64
 
55
65
  export const SPACE_MESSAGES: SpaceMessages = {
@@ -63,6 +73,7 @@ export const SPACE_MESSAGES: SpaceMessages = {
63
73
  chat: {},
64
74
  voice: {},
65
75
  tasks: TASK_MESSAGES,
76
+ threads: THREAD_MESSAGES,
66
77
  };
67
78
 
68
79
  /** The space's parts, by the class each carries; `classNames` adds a host's own under the same keys. */
@@ -86,6 +97,9 @@ export type SpacePart =
86
97
  | "sc-space__caption--agent"
87
98
  | "sc-space__status"
88
99
  | "sc-space__tasks"
100
+ | "sc-space__bar"
101
+ | "sc-space__threads"
102
+ | "sc-space__dot"
89
103
  | ModePart;
90
104
 
91
105
  /**
@@ -123,13 +137,31 @@ export interface AgentSpaceProps {
123
137
  classNames?:
124
138
  | Partial<
125
139
  Record<
126
- SpacePart | ChatPart | SurfacePart | KitPart | BrowserPart | VoiceCallPart | TaskPart,
140
+ | SpacePart
141
+ | ChatPart
142
+ | SurfacePart
143
+ | KitPart
144
+ | BrowserPart
145
+ | VoiceCallPart
146
+ | TaskPart
147
+ | ThreadPart,
127
148
  string
128
149
  >
129
150
  >
130
151
  | undefined;
131
152
  /** The host's own drawing of a block type, for every surface the agent shows. */
132
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;
133
165
  /** The language of the surfaces' calendars, days, times and numbers; English by default. */
134
166
  locale?: KitLocale | undefined;
135
167
  /** A few things the person might say first, offered while nothing has been said. */
@@ -147,6 +179,12 @@ export interface AgentSpaceProps {
147
179
  * while there are any, which opens them. On by default; the business's own space never has it.
148
180
  */
149
181
  tasks?: boolean | undefined;
182
+ /**
183
+ * Whether a person sees their threads with the agent (`Threads`): a line over the chat, a dot on
184
+ * it while another waits on them, which lists them by title with what is unread, to go to one
185
+ * or start another. On by default; the business's own space never has it.
186
+ */
187
+ threads?: boolean | undefined;
150
188
  }
151
189
 
152
190
  /**
@@ -174,6 +212,8 @@ export function AgentSpace({
174
212
  messages,
175
213
  classNames,
176
214
  components,
215
+ blocks,
216
+ surfaces,
177
217
  locale,
178
218
  suggestions,
179
219
  onTool,
@@ -181,10 +221,13 @@ export function AgentSpace({
181
221
  orb,
182
222
  onClose,
183
223
  tasks: showsTasks = true,
224
+ threads: showsThreads = true,
184
225
  }: AgentSpaceProps) {
185
226
  const words = { ...SPACE_MESSAGES, ...messages };
186
227
  const theirs = useTasks(client, agentId, showsTasks && client.who === "person");
187
228
  const [listing, setListing] = useState(false);
229
+ const threads = useUnread(client, agentId, showsThreads && client.who === "person");
230
+ const [browsing, setBrowsing] = useState(false);
188
231
  const { face, loading } = useAgentFace(client, agentId);
189
232
  const opened = useOpened(client, agentId, conversationId, onConversation);
190
233
  const [mode, setMode] = useState<SpaceMode>("text");
@@ -250,6 +293,30 @@ export function AgentSpace({
250
293
  </p>
251
294
  );
252
295
  }
296
+ if (browsing && threads.threads !== null) {
297
+ const went = (next: string) => {
298
+ opened.go(next);
299
+ setBrowsing(false);
300
+ void threads.refresh();
301
+ };
302
+ return (
303
+ <Threads
304
+ threads={threads.threads}
305
+ current={id}
306
+ onSelect={went}
307
+ onNew={() => {
308
+ void client.conversations.start(agentId).then((started) => {
309
+ went(started.id);
310
+ });
311
+ }}
312
+ onBack={() => {
313
+ setBrowsing(false);
314
+ }}
315
+ messages={words.threads}
316
+ classNames={classNames}
317
+ />
318
+ );
319
+ }
253
320
  if (listing && theirs.tasks !== null) {
254
321
  return (
255
322
  <AgentTasks
@@ -320,20 +387,43 @@ export function AgentSpace({
320
387
  );
321
388
  }
322
389
  const count = theirs.tasks?.length ?? 0;
390
+ // Another thread with something for them: the line to the threads carries a dot.
391
+ const news = (threads.threads ?? []).some(
392
+ (thread) =>
393
+ thread.id !== id && ((thread.unread ?? 0) > 0 || thread.waiting_on_person === true),
394
+ );
323
395
  return (
324
396
  <>
325
- {count > 0 && (
326
- <button
327
- type="button"
328
- part="tasks-open"
329
- className={classes(classNames, "sc-space__tasks")}
330
- onClick={() => {
331
- setListing(true);
332
- void theirs.refresh();
333
- }}
334
- >
335
- {words.tasks.open(count)}
336
- </button>
397
+ {(threads.threads !== null || count > 0) && (
398
+ <div className={classes(classNames, "sc-space__bar")}>
399
+ {threads.threads !== null && (
400
+ <button
401
+ type="button"
402
+ part="threads-open"
403
+ className={classes(classNames, "sc-space__threads")}
404
+ onClick={() => {
405
+ setBrowsing(true);
406
+ void threads.refresh();
407
+ }}
408
+ >
409
+ {words.threads.open}
410
+ {news && <span aria-hidden className={classes(classNames, "sc-space__dot")} />}
411
+ </button>
412
+ )}
413
+ {count > 0 && (
414
+ <button
415
+ type="button"
416
+ part="tasks-open"
417
+ className={classes(classNames, "sc-space__tasks")}
418
+ onClick={() => {
419
+ setListing(true);
420
+ void theirs.refresh();
421
+ }}
422
+ >
423
+ {words.tasks.open(count)}
424
+ </button>
425
+ )}
426
+ </div>
337
427
  )}
338
428
  <AgentChat
339
429
  client={client}
@@ -342,6 +432,8 @@ export function AgentSpace({
342
432
  messages={words.chat}
343
433
  classNames={classNames}
344
434
  components={components}
435
+ blocks={blocks}
436
+ surfaces={surfaces}
345
437
  locale={locale}
346
438
  suggestions={suggestions}
347
439
  steps={steps}
@@ -0,0 +1,218 @@
1
+ import { ArrowLeftIcon, PlusIcon } from "@phosphor-icons/react";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+
4
+ import { AgentChat, type AgentChatProps } from "./AgentChat";
5
+ import { classes } from "./classes";
6
+ import type { Client } from "./client";
7
+ import { Button } from "./kit/Button";
8
+ import type { Conversation as Thread } from "./types";
9
+
10
+ /** What the person's threads say; a host gives its own, in its language. */
11
+ export interface ThreadMessages {
12
+ /** Over the chat, to open the list; and the list's title. */
13
+ open: string;
14
+ title: string;
15
+ back: string;
16
+ /** Starts another conversation with the agent. */
17
+ new: string;
18
+ /** A thread nothing names yet. */
19
+ untitled: string;
20
+ /** Beside a thread: how many of the agent's messages are unread; that it waits on them. */
21
+ unread: (count: number) => string;
22
+ waiting: string;
23
+ empty: string;
24
+ }
25
+
26
+ export const THREAD_MESSAGES: ThreadMessages = {
27
+ open: "Conversations",
28
+ title: "Your conversations",
29
+ back: "Back to the conversation",
30
+ new: "New conversation",
31
+ untitled: "New conversation",
32
+ unread: (count) => (count === 1 ? "1 unread message" : `${count} unread messages`),
33
+ waiting: "Waiting for you",
34
+ empty: "Nothing here yet.",
35
+ };
36
+
37
+ export type ThreadPart =
38
+ | "sc-threads"
39
+ | "sc-threads__head"
40
+ | "sc-threads__title"
41
+ | "sc-threads__list"
42
+ | "sc-threads__item"
43
+ | "sc-threads__item--current"
44
+ | "sc-threads__name"
45
+ | "sc-threads__badge"
46
+ | "sc-threads__dot"
47
+ | "sc-threads__empty";
48
+
49
+ /** How often the person's threads are read again, for what is unread. */
50
+ export const THREADS_POLL_MS = 20_000;
51
+
52
+ /** The person's threads with an agent, and what of them waits on the person. */
53
+ export interface Unread {
54
+ /** Newest first; null until read, and whenever they can't be. */
55
+ threads: Thread[] | null;
56
+ /** Every unread message of the agent's, across the threads. */
57
+ unread: number;
58
+ /** Whether a thread waits on the person (a question, their yes). */
59
+ waiting: boolean;
60
+ refresh: () => Promise<void>;
61
+ }
62
+
63
+ /**
64
+ * The threads the person the client speaks for has with the agent, read now, again every
65
+ * `THREADS_POLL_MS`, when the page comes back into sight, and on `refresh`: what draws a dot on
66
+ * a launcher, a count beside a tab. `enabled` off reads nothing.
67
+ */
68
+ export function useUnread(client: Client, agentId: string, enabled = true): Unread {
69
+ const [threads, setThreads] = useState<Thread[] | null>(null);
70
+ const live = useRef(true);
71
+ const refresh = useCallback(async () => {
72
+ try {
73
+ const read = await client.conversations.list(agentId);
74
+ if (live.current) setThreads(read);
75
+ } catch {
76
+ if (live.current) setThreads(null);
77
+ }
78
+ }, [client, agentId]);
79
+ useEffect(() => {
80
+ live.current = true;
81
+ if (!enabled) return undefined;
82
+ void refresh();
83
+ const timer = setInterval(() => void refresh(), THREADS_POLL_MS);
84
+ const shown = () => {
85
+ if (document.visibilityState === "visible") void refresh();
86
+ };
87
+ document.addEventListener("visibilitychange", shown);
88
+ return () => {
89
+ live.current = false;
90
+ clearInterval(timer);
91
+ document.removeEventListener("visibilitychange", shown);
92
+ };
93
+ }, [enabled, refresh]);
94
+ const read = enabled ? threads : null;
95
+ return {
96
+ threads: read,
97
+ unread: (read ?? []).reduce((sum, thread) => sum + (thread.unread ?? 0), 0),
98
+ waiting: (read ?? []).some((thread) => thread.waiting_on_person === true),
99
+ refresh,
100
+ };
101
+ }
102
+
103
+ export interface ThreadsProps {
104
+ /** The threads, newest first (`useUnread`'s). */
105
+ threads: readonly Thread[];
106
+ /** The one open now, marked. */
107
+ current?: string | null | undefined;
108
+ onSelect: (id: string) => void;
109
+ /** Starts another; without it, no button for it. */
110
+ onNew?: (() => void) | undefined;
111
+ /** The way back to the open conversation; without it, none. */
112
+ onBack?: (() => void) | undefined;
113
+ messages?: Partial<ThreadMessages> | undefined;
114
+ classNames?: Partial<Record<ThreadPart, string>> | undefined;
115
+ }
116
+
117
+ /**
118
+ * The person's threads with the agent: each by its title, how many of the agent's messages in
119
+ * it are unread, a dot when it waits on them; the open one marked; another to start.
120
+ */
121
+ export function Threads({
122
+ threads,
123
+ current = null,
124
+ onSelect,
125
+ onNew,
126
+ onBack,
127
+ messages,
128
+ classNames,
129
+ }: ThreadsProps) {
130
+ const words = { ...THREAD_MESSAGES, ...messages };
131
+ return (
132
+ <section aria-label={words.title} part="threads" className={classes(classNames, "sc-threads")}>
133
+ <header className={classes(classNames, "sc-threads__head")}>
134
+ {onBack && (
135
+ <Button
136
+ tone="ghost"
137
+ size="sm"
138
+ icon={ArrowLeftIcon}
139
+ aria-label={words.back}
140
+ title={words.back}
141
+ onClick={onBack}
142
+ />
143
+ )}
144
+ <h3 className={classes(classNames, "sc-threads__title")}>{words.title}</h3>
145
+ {onNew && (
146
+ <Button tone="ghost" size="sm" icon={PlusIcon} onClick={onNew}>
147
+ {words.new}
148
+ </Button>
149
+ )}
150
+ </header>
151
+ {threads.length === 0 ? (
152
+ <p className={classes(classNames, "sc-threads__empty")}>{words.empty}</p>
153
+ ) : (
154
+ <ul className={classes(classNames, "sc-threads__list")}>
155
+ {threads.map((thread) => {
156
+ const unread = thread.unread ?? 0;
157
+ const open = thread.id === current;
158
+ return (
159
+ <li key={thread.id}>
160
+ <button
161
+ type="button"
162
+ aria-current={open || undefined}
163
+ className={classes(
164
+ classNames,
165
+ "sc-threads__item",
166
+ ...(open ? (["sc-threads__item--current"] as const) : []),
167
+ )}
168
+ onClick={() => {
169
+ onSelect(thread.id);
170
+ }}
171
+ >
172
+ <span className={classes(classNames, "sc-threads__name")}>
173
+ {thread.title ?? words.untitled}
174
+ </span>
175
+ {unread > 0 ? (
176
+ <span
177
+ aria-label={words.unread(unread)}
178
+ className={classes(classNames, "sc-threads__badge")}
179
+ >
180
+ {unread}
181
+ </span>
182
+ ) : (
183
+ thread.waiting_on_person === true && (
184
+ <span
185
+ role="img"
186
+ aria-label={words.waiting}
187
+ className={classes(classNames, "sc-threads__dot")}
188
+ />
189
+ )
190
+ )}
191
+ </button>
192
+ </li>
193
+ );
194
+ })}
195
+ </ul>
196
+ )}
197
+ </section>
198
+ );
199
+ }
200
+
201
+ /**
202
+ * A conversation between a person and one agent, as the API reads it (`types.ts`): one name for
203
+ * the shape and for the component that shows one.
204
+ */
205
+ export type Conversation = Thread;
206
+
207
+ export type ConversationProps = Omit<AgentChatProps, "conversationId"> & {
208
+ /** The conversation to show, by its id. */
209
+ id: string;
210
+ };
211
+
212
+ /**
213
+ * One conversation, for a host that lays out its own threads: the chat (`AgentChat`), which reads
214
+ * what comes as it shows it on a person's page, so their `unread` follows.
215
+ */
216
+ export function Conversation({ id, ...chat }: ConversationProps) {
217
+ return <AgentChat conversationId={id} {...chat} />;
218
+ }
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
+ }
@@ -1,4 +1,5 @@
1
1
  import {
2
+ ArrowBendDownRightIcon,
2
3
  BellIcon,
3
4
  BrainIcon,
4
5
  ChatCircleIcon,
@@ -8,7 +9,9 @@ import {
8
9
  FileTextIcon,
9
10
  FlaskIcon,
10
11
  GlobeIcon,
12
+ HandTapIcon,
11
13
  LightningIcon,
14
+ PlugsConnectedIcon,
12
15
  SparkleIcon,
13
16
  TableIcon,
14
17
  WebhooksLogoIcon,
@@ -52,6 +55,8 @@ const KINDS: Readonly<Record<Exclude<Trigger["kind"], "event">, Icon>> = {
52
55
  schedule: ClockIcon,
53
56
  webhook: WebhooksLogoIcon,
54
57
  platform: ChatCircleIcon,
58
+ manual: HandTapIcon,
59
+ after: ArrowBendDownRightIcon,
55
60
  };
56
61
 
57
62
  /** What starts an automation, as a mark: a clock, a webhook, a message, the app whose event it
@@ -79,6 +84,8 @@ const BY: Readonly<Record<Exclude<RunBy, "event">, Icon>> = {
79
84
  platform: ChatCircleIcon,
80
85
  agent: ChatCircleIcon,
81
86
  test: FlaskIcon,
87
+ api: PlugsConnectedIcon,
88
+ after: ArrowBendDownRightIcon,
82
89
  };
83
90
 
84
91
  export function ByMark({
@@ -68,6 +68,10 @@ export interface AutomationMessages {
68
68
  event: (app: string | null, event: string | null) => string;
69
69
  /** One of the platform's events: a contact's message stored on a channel. */
70
70
  platform: string;
71
+ /** Only started by hand: the agent, the business's app, a button. */
72
+ manual: string;
73
+ /** When another of the agent's automations ends well, by its name. */
74
+ after: (automation: string | null) => string;
71
75
  /** When it runs next, after when it runs: the day, and the time when the line doesn't say it. */
72
76
  next: (day: DayRef, time: string | null) => string;
73
77
  /** How it stands, on its pill. */
@@ -127,6 +131,8 @@ export const AUTOMATION_MESSAGES: AutomationMessages = {
127
131
  custom: "On a schedule of its own",
128
132
  webhook: "When another system calls it",
129
133
  platform: "When a message arrives",
134
+ manual: "When someone starts it",
135
+ after: (automation) => (automation ? `After “${automation}”` : "After another automation"),
130
136
  event: (app, event) =>
131
137
  event ? `When ${event} in ${app ?? "an app"}` : `On an event in ${app ?? "an app"}`,
132
138
  next: (day, time) => {
@@ -177,6 +183,8 @@ export const AUTOMATION_MESSAGES: AutomationMessages = {
177
183
  platform: "a message",
178
184
  agent: "you asked in the chat",
179
185
  test: "test",
186
+ api: "your app",
187
+ after: "after another",
180
188
  })[by],
181
189
  statuses: {
182
190
  running: "Running",
@@ -35,6 +35,10 @@ export function whenOf(trigger: Trigger, words: AutomationMessages, code: string
35
35
  return { text: words.webhook, timed: false };
36
36
  case "platform":
37
37
  return { text: words.platform, timed: false };
38
+ case "manual":
39
+ return { text: words.manual, timed: false };
40
+ case "after":
41
+ return { text: words.after(trigger.after ?? null), timed: false };
38
42
  case "event":
39
43
  return {
40
44
  text: words.event(trigger.app_title ?? trigger.app, trigger.event_title),