@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/src/AgentSpace.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
<
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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}
|
package/src/Threads.tsx
ADDED
|
@@ -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
|
+
}
|
package/src/automation/marks.tsx
CHANGED
|
@@ -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",
|
package/src/automation/when.ts
CHANGED
|
@@ -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),
|