@signalcore/ui 1.0.0-beta.1 → 1.0.0-beta.3
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 +55 -22
- package/dist/AgentAbout.js +11 -4
- package/dist/AgentAbout.js.map +1 -1
- package/dist/AgentChat.d.ts +7 -1
- package/dist/AgentChat.js +154 -2
- package/dist/AgentChat.js.map +1 -1
- package/dist/AgentSpace.d.ts +12 -3
- package/dist/AgentSpace.js +53 -11
- package/dist/AgentSpace.js.map +1 -1
- package/dist/AgentTasks.js +2 -1
- package/dist/AgentTasks.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/VoiceCall.js +1 -1
- package/dist/VoiceCall.js.map +1 -1
- package/dist/about.d.ts +34 -144
- package/dist/about.js +6 -8
- package/dist/about.js.map +1 -1
- package/dist/automation/cron.d.ts +1 -1
- package/dist/automation/cron.js.map +1 -1
- package/dist/automation/marks.js +13 -5
- package/dist/automation/marks.js.map +1 -1
- package/dist/automation/messages.d.ts +6 -0
- package/dist/automation/messages.js +7 -1
- package/dist/automation/messages.js.map +1 -1
- package/dist/automation/types.d.ts +25 -128
- package/dist/automation/types.js +36 -0
- package/dist/automation/types.js.map +1 -0
- package/dist/automation/when.d.ts +1 -1
- package/dist/automation/when.js +13 -1
- package/dist/automation/when.js.map +1 -1
- package/dist/blocks.d.ts +23 -1
- package/dist/browser.js +2 -2
- package/dist/browser.js.map +1 -1
- package/dist/call.d.ts +1 -1
- package/dist/call.js.map +1 -1
- package/dist/client.d.ts +14 -4
- package/dist/client.js +7 -2
- package/dist/client.js.map +1 -1
- package/dist/contract.d.ts +267 -0
- package/dist/credentials.js.map +1 -1
- package/dist/dev.js +1 -1
- package/dist/dev.js.map +1 -1
- package/dist/es.d.ts +2 -0
- package/dist/es.js +31 -4
- 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 +6 -5
- package/dist/index.js +6 -4
- package/dist/kit/AudioPlayer.js +1 -1
- package/dist/kit/AudioPlayer.js.map +1 -1
- package/dist/kit/Choices.js +1 -1
- package/dist/kit/Choices.js.map +1 -1
- package/dist/kit/FileField.js +1 -1
- package/dist/kit/FileField.js.map +1 -1
- package/dist/kit/Popover.js.map +1 -1
- package/dist/kit/Tile.d.ts +1 -1
- package/dist/kit/Tile.js +1 -1
- package/dist/kit/Tile.js.map +1 -1
- package/dist/kit/messages.d.ts +13 -0
- package/dist/kit/messages.js +7 -1
- package/dist/kit/messages.js.map +1 -1
- package/dist/kit/uploads.d.ts +2 -1
- package/dist/kit/uploads.js +1 -1
- package/dist/kit/uploads.js.map +1 -1
- package/dist/kit/waveform.js.map +1 -1
- package/dist/markdown.js.map +1 -1
- package/dist/marks.js +2 -3
- package/dist/marks.js.map +1 -1
- package/dist/surface/Form.js +1 -1
- package/dist/surface/Markdown.js +1 -1
- package/dist/surface/Markdown.js.map +1 -1
- package/dist/surface/Surface.js +43 -4
- package/dist/surface/Surface.js.map +1 -1
- package/dist/surface/scope.d.ts +1 -1
- package/dist/surface/scope.js.map +1 -1
- package/dist/surface/values.js +1 -1
- package/dist/surface/values.js.map +1 -1
- package/dist/tint.d.ts +1 -1
- package/dist/tint.js +1 -1
- package/dist/tint.js.map +1 -1
- package/dist/types.d.ts +11 -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/usePolled.js +1 -1
- package/dist/usePolled.js.map +1 -1
- package/dist/voice/webgl.js +1 -1
- package/dist/voice/webgl.js.map +1 -1
- package/dist/widget.js +17 -16
- package/package.json +1 -1
- package/src/AgentAbout.tsx +14 -3
- package/src/AgentChat.tsx +203 -1
- package/src/AgentSpace.tsx +89 -13
- package/src/AgentTasks.tsx +2 -2
- package/src/Threads.tsx +218 -0
- package/src/VoiceCall.tsx +1 -1
- package/src/about.ts +42 -144
- package/src/automation/cron.ts +1 -1
- package/src/automation/marks.tsx +13 -2
- package/src/automation/messages.ts +12 -0
- package/src/automation/types.ts +58 -118
- package/src/automation/when.ts +7 -1
- package/src/blocks.ts +25 -0
- package/src/browser.ts +4 -4
- package/src/call.ts +1 -1
- package/src/client.ts +26 -10
- package/src/contract.ts +1934 -0
- package/src/credentials.ts +2 -2
- package/src/dev.ts +1 -1
- package/src/es.ts +29 -0
- package/src/frames.ts +3 -1
- package/src/index.ts +10 -1
- package/src/kit/AudioPlayer.tsx +1 -1
- package/src/kit/Choices.tsx +1 -1
- package/src/kit/FileField.tsx +1 -1
- package/src/kit/Popover.tsx +1 -1
- package/src/kit/Tile.tsx +1 -1
- package/src/kit/messages.ts +20 -0
- package/src/kit/uploads.ts +2 -2
- package/src/kit/waveform.ts +1 -1
- package/src/markdown.ts +1 -1
- package/src/marks.tsx +3 -123
- package/src/styles.css +206 -0
- package/src/surface/Markdown.tsx +1 -1
- package/src/surface/Surface.tsx +53 -4
- package/src/surface/scope.ts +3 -1
- package/src/surface/values.ts +5 -5
- package/src/tint.ts +1 -1
- package/src/types.ts +11 -0
- package/src/useConversation.ts +10 -0
- package/src/usePolled.ts +2 -2
- package/src/voice/webgl.ts +1 -1
- package/src/widget/element.tsx +13 -4
- package/src/widget/widget.css +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signalcore/ui",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.3",
|
|
4
4
|
"description": "SignalCore's components for the people an agent serves: the chat, the call, the agent's space, and the <signalcore-chat> widget.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"signalcore",
|
package/src/AgentAbout.tsx
CHANGED
|
@@ -150,7 +150,14 @@ export const ABOUT_MESSAGES: AboutMessages = {
|
|
|
150
150
|
does: "What it does on its own",
|
|
151
151
|
noAutomations: "Nothing on its own yet: it answers when someone writes to it.",
|
|
152
152
|
paused: "Paused",
|
|
153
|
-
kinds: {
|
|
153
|
+
kinds: {
|
|
154
|
+
schedule: "At a time",
|
|
155
|
+
webhook: "When called",
|
|
156
|
+
event: "On an event",
|
|
157
|
+
platform: "On a message",
|
|
158
|
+
manual: "When started",
|
|
159
|
+
after: "After another",
|
|
160
|
+
},
|
|
154
161
|
topics: {
|
|
155
162
|
apps: "Apps",
|
|
156
163
|
tools: "Tools",
|
|
@@ -934,11 +941,15 @@ function Gone({ classNames, children }: { classNames: ClassNames; children: Reac
|
|
|
934
941
|
return <p className={classes(classNames, "sc-about__empty")}>{children}</p>;
|
|
935
942
|
}
|
|
936
943
|
|
|
937
|
-
/** An automation's mark says its kind: a time, a call from another system, an event
|
|
944
|
+
/** An automation's mark says its kind: a time, a call from another system, an event, a
|
|
945
|
+
* message. */
|
|
938
946
|
const KIND_ICONS: Readonly<Record<AutomationKind, Icon>> = {
|
|
939
947
|
schedule: "clock",
|
|
940
948
|
webhook: "code",
|
|
941
949
|
event: "bell",
|
|
950
|
+
platform: "message",
|
|
951
|
+
manual: "hand",
|
|
952
|
+
after: "check",
|
|
942
953
|
};
|
|
943
954
|
|
|
944
955
|
/** An app's mark: the host's logo when it draws one, else the provider's, bare. */
|
|
@@ -1080,7 +1091,7 @@ function Automations({
|
|
|
1080
1091
|
<Row
|
|
1081
1092
|
key={automation.id}
|
|
1082
1093
|
mark={
|
|
1083
|
-
automation.provider === undefined ? (
|
|
1094
|
+
automation.provider === undefined || automation.provider === null ? (
|
|
1084
1095
|
<Mark icon={GLYPHS[KIND_ICONS[automation.kind]]} />
|
|
1085
1096
|
) : (
|
|
1086
1097
|
appMark(
|
package/src/AgentChat.tsx
CHANGED
|
@@ -40,7 +40,10 @@ import { useConversationFiles } from "./conversationFiles";
|
|
|
40
40
|
import { FILES_PER_MESSAGE } from "./frames";
|
|
41
41
|
import { actionMark } from "./marks";
|
|
42
42
|
import { AudioPlayer } from "./kit/AudioPlayer";
|
|
43
|
+
import { Button } from "./kit/Button";
|
|
43
44
|
import { KitProvider, type KitPart } from "./kit/context";
|
|
45
|
+
import { Field } from "./kit/Field";
|
|
46
|
+
import { Checkbox, NumberInput, TextArea, TextInput } from "./kit/Inputs";
|
|
44
47
|
import { carriesFiles, FileChips, UploadChips, useUploads, type Uploads } from "./kit/FileField";
|
|
45
48
|
import { KIT_MESSAGES, type KitLocale, type KitMessages } from "./kit/messages";
|
|
46
49
|
import type { FileHost } from "./kit/uploads";
|
|
@@ -122,6 +125,12 @@ export interface ChatMessages {
|
|
|
122
125
|
inputs: Readonly<Record<string, string>>;
|
|
123
126
|
allow: string;
|
|
124
127
|
decline: string;
|
|
128
|
+
/** What heads the card of several actions that wait for the person's leave, by how many. */
|
|
129
|
+
approvals: (count: number) => string;
|
|
130
|
+
/** The card's one button, as many as are ticked: every one, some, or none (all declined). */
|
|
131
|
+
approveAll: string;
|
|
132
|
+
approveSome: (count: number) => string;
|
|
133
|
+
declineAll: string;
|
|
125
134
|
/**
|
|
126
135
|
* On a person's page, in place of the approval card: what the agent asked to do waits for the
|
|
127
136
|
* business's yes (`decides: "business"`), which its people give from their app.
|
|
@@ -194,6 +203,10 @@ export const CHAT_MESSAGES: ChatMessages = {
|
|
|
194
203
|
inputs: {},
|
|
195
204
|
allow: "Allow",
|
|
196
205
|
decline: "Decline",
|
|
206
|
+
approvals: (count) => `${count} actions need your approval`,
|
|
207
|
+
approveAll: "Approve all",
|
|
208
|
+
approveSome: (count) => `Approve ${count}`,
|
|
209
|
+
declineAll: "Decline all",
|
|
197
210
|
confirming: "Waiting for the business to confirm",
|
|
198
211
|
handedOff: "Handed to a person",
|
|
199
212
|
handover: (holder, by, name) => {
|
|
@@ -256,6 +269,9 @@ export type ChatPart =
|
|
|
256
269
|
| "sc-chat__step--failed"
|
|
257
270
|
| "sc-chat__output"
|
|
258
271
|
| "sc-chat__request"
|
|
272
|
+
| "sc-chat__requests"
|
|
273
|
+
| "sc-chat__ask"
|
|
274
|
+
| "sc-chat__ask-fields"
|
|
259
275
|
| "sc-chat__confirming"
|
|
260
276
|
| "sc-chat__screen"
|
|
261
277
|
| "sc-chat__screen--bare"
|
|
@@ -460,6 +476,7 @@ function Chat({
|
|
|
460
476
|
onTool: onTool ?? (() => Promise.reject(new Error(words.noTool))),
|
|
461
477
|
});
|
|
462
478
|
const files = useConversationFiles(client, conversationId, live.messages, onOpenFile);
|
|
479
|
+
useReadAsShown(client, conversationId, live.messages.at(-1)?.id);
|
|
463
480
|
// What the person attaches to their next message: picked, dropped on the chat, or pasted.
|
|
464
481
|
const uploads = useUploads(attachments ? files : undefined, { max: FILES_PER_MESSAGE });
|
|
465
482
|
// Files go with what is said, once the turn waits on nothing; to the host's `write`, at once.
|
|
@@ -885,6 +902,7 @@ function Log({
|
|
|
885
902
|
components={components}
|
|
886
903
|
locale={locale}
|
|
887
904
|
onAction={live.act}
|
|
905
|
+
onActAll={live.actAll}
|
|
888
906
|
builder={builder}
|
|
889
907
|
files={files}
|
|
890
908
|
who={who}
|
|
@@ -1234,8 +1252,11 @@ function Reply({
|
|
|
1234
1252
|
who,
|
|
1235
1253
|
steps,
|
|
1236
1254
|
onChannel,
|
|
1255
|
+
onActAll,
|
|
1237
1256
|
}: DrawProps & {
|
|
1238
1257
|
message: Message;
|
|
1258
|
+
/** Several things done at once: a yes or no on each call it waits on, from one card. */
|
|
1259
|
+
onActAll: (actions: readonly Action[]) => void;
|
|
1239
1260
|
/** It was streaming: its words go on revealing from here. None: they show whole. */
|
|
1240
1261
|
from: number | undefined;
|
|
1241
1262
|
awaiting: string | null;
|
|
@@ -1251,6 +1272,11 @@ function Reply({
|
|
|
1251
1272
|
onChannel: boolean;
|
|
1252
1273
|
}) {
|
|
1253
1274
|
const text = useReveal(message.text, from ?? message.text.length);
|
|
1275
|
+
// The calls it waits on the leave of whoever this page speaks for: two or more, in one card.
|
|
1276
|
+
const asked = message.steps.filter(
|
|
1277
|
+
(step): step is Step & { request: Request } =>
|
|
1278
|
+
step.request !== null && !(step.request.decides === "business" && who === "person"),
|
|
1279
|
+
);
|
|
1254
1280
|
// On a channel, what the person there has got so far; elsewhere, the words as they come.
|
|
1255
1281
|
const { shown: said, typing } = onChannel
|
|
1256
1282
|
? channelParts(message, text)
|
|
@@ -1336,9 +1362,21 @@ function Reply({
|
|
|
1336
1362
|
</div>
|
|
1337
1363
|
),
|
|
1338
1364
|
)}
|
|
1365
|
+
{asked.length > 1 && (
|
|
1366
|
+
<Approvals
|
|
1367
|
+
key={asked.map((step) => step.id).join()}
|
|
1368
|
+
steps={asked}
|
|
1369
|
+
name={name}
|
|
1370
|
+
words={words}
|
|
1371
|
+
classNames={classNames}
|
|
1372
|
+
onActAll={onActAll}
|
|
1373
|
+
/>
|
|
1374
|
+
)}
|
|
1339
1375
|
{message.steps.map((step) => {
|
|
1340
1376
|
const { request } = step;
|
|
1341
|
-
if (request === null)
|
|
1377
|
+
if (request === null || (asked.length > 1 && asked.some((one) => one.id === step.id))) {
|
|
1378
|
+
return null;
|
|
1379
|
+
}
|
|
1342
1380
|
if (request.decides === "business" && who === "person") {
|
|
1343
1381
|
return (
|
|
1344
1382
|
<p
|
|
@@ -1583,6 +1621,170 @@ function Approval({
|
|
|
1583
1621
|
);
|
|
1584
1622
|
}
|
|
1585
1623
|
|
|
1624
|
+
/**
|
|
1625
|
+
* Several actions a reply waits on the person's leave for, in one card: a box to tick for each
|
|
1626
|
+
* (all ticked to begin with), what each would take under it, editable while ticked, and one
|
|
1627
|
+
* button that sends a yes for each ticked (with what was changed) and a no for the rest, at once.
|
|
1628
|
+
*/
|
|
1629
|
+
function Approvals({
|
|
1630
|
+
steps,
|
|
1631
|
+
name,
|
|
1632
|
+
words,
|
|
1633
|
+
classNames,
|
|
1634
|
+
onActAll,
|
|
1635
|
+
}: {
|
|
1636
|
+
steps: readonly (Step & { request: Request })[];
|
|
1637
|
+
name: string;
|
|
1638
|
+
words: ChatMessages;
|
|
1639
|
+
classNames: ClassNames;
|
|
1640
|
+
onActAll: (actions: readonly Action[]) => void;
|
|
1641
|
+
}) {
|
|
1642
|
+
const [chosen, setChosen] = useState<ReadonlySet<string>>(
|
|
1643
|
+
() => new Set(steps.map((step) => step.id)),
|
|
1644
|
+
);
|
|
1645
|
+
const [edits, setEdits] = useState<Readonly<Record<string, Record<string, unknown>>>>({});
|
|
1646
|
+
const [sent, setSent] = useState(false);
|
|
1647
|
+
const inputOf = (step: Step & { request: Request }) => ({
|
|
1648
|
+
...step.request.input,
|
|
1649
|
+
...edits[step.id],
|
|
1650
|
+
});
|
|
1651
|
+
const send = () => {
|
|
1652
|
+
setSent(true);
|
|
1653
|
+
onActAll(
|
|
1654
|
+
steps.map((step) =>
|
|
1655
|
+
chosen.has(step.id)
|
|
1656
|
+
? {
|
|
1657
|
+
surface: step.id,
|
|
1658
|
+
name: "approve",
|
|
1659
|
+
data: step.id in edits ? { input: inputOf(step) } : {},
|
|
1660
|
+
label: words.allow,
|
|
1661
|
+
}
|
|
1662
|
+
: { surface: step.id, name: "decline", data: {}, label: words.decline },
|
|
1663
|
+
),
|
|
1664
|
+
);
|
|
1665
|
+
};
|
|
1666
|
+
const label =
|
|
1667
|
+
chosen.size === steps.length
|
|
1668
|
+
? words.approveAll
|
|
1669
|
+
: chosen.size === 0
|
|
1670
|
+
? words.declineAll
|
|
1671
|
+
: words.approveSome(chosen.size);
|
|
1672
|
+
return (
|
|
1673
|
+
<div part="approval" className={classes(classNames, "sc-chat__request", "sc-chat__requests")}>
|
|
1674
|
+
<header>
|
|
1675
|
+
<ShieldWarningIcon aria-hidden />
|
|
1676
|
+
{words.approvals(steps.length)}
|
|
1677
|
+
</header>
|
|
1678
|
+
<ul>
|
|
1679
|
+
{steps.map((step) => {
|
|
1680
|
+
const ticked = chosen.has(step.id);
|
|
1681
|
+
return (
|
|
1682
|
+
<li key={step.id} className={classes(classNames, "sc-chat__ask")}>
|
|
1683
|
+
<Checkbox
|
|
1684
|
+
value={ticked}
|
|
1685
|
+
disabled={sent}
|
|
1686
|
+
onChange={(on) => {
|
|
1687
|
+
setChosen((last) => {
|
|
1688
|
+
const next = new Set(last);
|
|
1689
|
+
if (on) next.add(step.id);
|
|
1690
|
+
else next.delete(step.id);
|
|
1691
|
+
return next;
|
|
1692
|
+
});
|
|
1693
|
+
}}
|
|
1694
|
+
>
|
|
1695
|
+
{words.request(name, step.request.action)}
|
|
1696
|
+
</Checkbox>
|
|
1697
|
+
{ticked && (
|
|
1698
|
+
<div className={classes(classNames, "sc-chat__ask-fields")}>
|
|
1699
|
+
{Object.entries(inputOf(step)).map(([field, value]) => (
|
|
1700
|
+
<InputValue
|
|
1701
|
+
key={field}
|
|
1702
|
+
label={words.inputs[field] ?? field}
|
|
1703
|
+
value={value}
|
|
1704
|
+
disabled={sent}
|
|
1705
|
+
onChange={(changed) => {
|
|
1706
|
+
setEdits((last) => ({
|
|
1707
|
+
...last,
|
|
1708
|
+
[step.id]: { ...last[step.id], [field]: changed },
|
|
1709
|
+
}));
|
|
1710
|
+
}}
|
|
1711
|
+
/>
|
|
1712
|
+
))}
|
|
1713
|
+
</div>
|
|
1714
|
+
)}
|
|
1715
|
+
</li>
|
|
1716
|
+
);
|
|
1717
|
+
})}
|
|
1718
|
+
</ul>
|
|
1719
|
+
<footer>
|
|
1720
|
+
<Button tone="primary" disabled={sent} onClick={send}>
|
|
1721
|
+
{label}
|
|
1722
|
+
</Button>
|
|
1723
|
+
</footer>
|
|
1724
|
+
</div>
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
/**
|
|
1729
|
+
* One field of an action's input, editable as what it holds: words, a number, a yes or no;
|
|
1730
|
+
* anything else (a list, an object) shown as it is.
|
|
1731
|
+
*/
|
|
1732
|
+
function InputValue({
|
|
1733
|
+
label,
|
|
1734
|
+
value,
|
|
1735
|
+
disabled,
|
|
1736
|
+
onChange,
|
|
1737
|
+
}: {
|
|
1738
|
+
label: string;
|
|
1739
|
+
value: unknown;
|
|
1740
|
+
disabled: boolean;
|
|
1741
|
+
onChange: (value: unknown) => void;
|
|
1742
|
+
}) {
|
|
1743
|
+
if (typeof value === "boolean") {
|
|
1744
|
+
return (
|
|
1745
|
+
<Checkbox value={value} disabled={disabled} onChange={onChange}>
|
|
1746
|
+
{label}
|
|
1747
|
+
</Checkbox>
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
return (
|
|
1751
|
+
<Field label={label}>
|
|
1752
|
+
{(ids) =>
|
|
1753
|
+
typeof value === "number" ? (
|
|
1754
|
+
<NumberInput {...ids} value={value} disabled={disabled} onChange={onChange} />
|
|
1755
|
+
) : typeof value === "string" ? (
|
|
1756
|
+
value.length > 80 || value.includes("\n") ? (
|
|
1757
|
+
<TextArea {...ids} value={value} disabled={disabled} onChange={onChange} />
|
|
1758
|
+
) : (
|
|
1759
|
+
<TextInput {...ids} value={value} disabled={disabled} onChange={onChange} />
|
|
1760
|
+
)
|
|
1761
|
+
) : (
|
|
1762
|
+
<TextInput {...ids} value={JSON.stringify(value)} disabled onChange={() => undefined} />
|
|
1763
|
+
)
|
|
1764
|
+
}
|
|
1765
|
+
</Field>
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
/**
|
|
1770
|
+
* On a person's page, what came is read as the chat shows it: each newest message while the page
|
|
1771
|
+
* is in sight, and once it comes back into sight. Their threads' `unread` follows.
|
|
1772
|
+
*/
|
|
1773
|
+
function useReadAsShown(client: Client, conversationId: string, newest: string | undefined) {
|
|
1774
|
+
useEffect(() => {
|
|
1775
|
+
if (client.who !== "person" || newest === undefined) return undefined;
|
|
1776
|
+
const read = () => {
|
|
1777
|
+
if (document.visibilityState === "hidden") return;
|
|
1778
|
+
client.conversations.read(conversationId).catch(() => undefined);
|
|
1779
|
+
};
|
|
1780
|
+
read();
|
|
1781
|
+
document.addEventListener("visibilitychange", read);
|
|
1782
|
+
return () => {
|
|
1783
|
+
document.removeEventListener("visibilitychange", read);
|
|
1784
|
+
};
|
|
1785
|
+
}, [client, conversationId, newest]);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1586
1788
|
/**
|
|
1587
1789
|
* Where the person writes: a box that grows a few lines with the text. Enter sends, Shift+Enter
|
|
1588
1790
|
* breaks the line; nothing goes while a reply is on its way or the socket is closed, but the
|
package/src/AgentSpace.tsx
CHANGED
|
@@ -19,6 +19,13 @@ import type { KitLocale } from "./kit/messages";
|
|
|
19
19
|
import { Modes, type ModeMessages, type ModePart, type SpaceMode } from "./Modes";
|
|
20
20
|
import type { SurfaceComponents, SurfacePart } from "./surface/scope";
|
|
21
21
|
import { agentTint, type AgentTint } from "./tint";
|
|
22
|
+
import {
|
|
23
|
+
THREAD_MESSAGES,
|
|
24
|
+
Threads,
|
|
25
|
+
useUnread,
|
|
26
|
+
type ThreadMessages,
|
|
27
|
+
type ThreadPart,
|
|
28
|
+
} from "./Threads";
|
|
22
29
|
import { useAgentFace } from "./useAgentFace";
|
|
23
30
|
import { useConversation, type LiveConversation, type ToolHandler } from "./useConversation";
|
|
24
31
|
import type { VoiceCallMessages, VoiceCallPart } from "./VoiceCall";
|
|
@@ -50,6 +57,8 @@ export interface SpaceMessages {
|
|
|
50
57
|
voice: Partial<VoiceCallMessages>;
|
|
51
58
|
/** What the person's tasks say (`AgentTasks`'s words). */
|
|
52
59
|
tasks: TaskMessages;
|
|
60
|
+
/** What the person's threads say (`Threads`'s words). */
|
|
61
|
+
threads: ThreadMessages;
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
export const SPACE_MESSAGES: SpaceMessages = {
|
|
@@ -63,6 +72,7 @@ export const SPACE_MESSAGES: SpaceMessages = {
|
|
|
63
72
|
chat: {},
|
|
64
73
|
voice: {},
|
|
65
74
|
tasks: TASK_MESSAGES,
|
|
75
|
+
threads: THREAD_MESSAGES,
|
|
66
76
|
};
|
|
67
77
|
|
|
68
78
|
/** The space's parts, by the class each carries; `classNames` adds a host's own under the same keys. */
|
|
@@ -86,6 +96,9 @@ export type SpacePart =
|
|
|
86
96
|
| "sc-space__caption--agent"
|
|
87
97
|
| "sc-space__status"
|
|
88
98
|
| "sc-space__tasks"
|
|
99
|
+
| "sc-space__bar"
|
|
100
|
+
| "sc-space__threads"
|
|
101
|
+
| "sc-space__dot"
|
|
89
102
|
| ModePart;
|
|
90
103
|
|
|
91
104
|
/**
|
|
@@ -123,7 +136,14 @@ export interface AgentSpaceProps {
|
|
|
123
136
|
classNames?:
|
|
124
137
|
| Partial<
|
|
125
138
|
Record<
|
|
126
|
-
|
|
139
|
+
| SpacePart
|
|
140
|
+
| ChatPart
|
|
141
|
+
| SurfacePart
|
|
142
|
+
| KitPart
|
|
143
|
+
| BrowserPart
|
|
144
|
+
| VoiceCallPart
|
|
145
|
+
| TaskPart
|
|
146
|
+
| ThreadPart,
|
|
127
147
|
string
|
|
128
148
|
>
|
|
129
149
|
>
|
|
@@ -147,6 +167,12 @@ export interface AgentSpaceProps {
|
|
|
147
167
|
* while there are any, which opens them. On by default; the business's own space never has it.
|
|
148
168
|
*/
|
|
149
169
|
tasks?: boolean | undefined;
|
|
170
|
+
/**
|
|
171
|
+
* Whether a person sees their threads with the agent (`Threads`): a line over the chat, a dot on
|
|
172
|
+
* it while another waits on them, which lists them by title with what is unread, to go to one
|
|
173
|
+
* or start another. On by default; the business's own space never has it.
|
|
174
|
+
*/
|
|
175
|
+
threads?: boolean | undefined;
|
|
150
176
|
}
|
|
151
177
|
|
|
152
178
|
/**
|
|
@@ -181,10 +207,13 @@ export function AgentSpace({
|
|
|
181
207
|
orb,
|
|
182
208
|
onClose,
|
|
183
209
|
tasks: showsTasks = true,
|
|
210
|
+
threads: showsThreads = true,
|
|
184
211
|
}: AgentSpaceProps) {
|
|
185
212
|
const words = { ...SPACE_MESSAGES, ...messages };
|
|
186
213
|
const theirs = useTasks(client, agentId, showsTasks && client.who === "person");
|
|
187
214
|
const [listing, setListing] = useState(false);
|
|
215
|
+
const threads = useUnread(client, agentId, showsThreads && client.who === "person");
|
|
216
|
+
const [browsing, setBrowsing] = useState(false);
|
|
188
217
|
const { face, loading } = useAgentFace(client, agentId);
|
|
189
218
|
const opened = useOpened(client, agentId, conversationId, onConversation);
|
|
190
219
|
const [mode, setMode] = useState<SpaceMode>("text");
|
|
@@ -250,6 +279,30 @@ export function AgentSpace({
|
|
|
250
279
|
</p>
|
|
251
280
|
);
|
|
252
281
|
}
|
|
282
|
+
if (browsing && threads.threads !== null) {
|
|
283
|
+
const went = (next: string) => {
|
|
284
|
+
opened.go(next);
|
|
285
|
+
setBrowsing(false);
|
|
286
|
+
void threads.refresh();
|
|
287
|
+
};
|
|
288
|
+
return (
|
|
289
|
+
<Threads
|
|
290
|
+
threads={threads.threads}
|
|
291
|
+
current={id}
|
|
292
|
+
onSelect={went}
|
|
293
|
+
onNew={() => {
|
|
294
|
+
void client.conversations.start(agentId).then((started) => {
|
|
295
|
+
went(started.id);
|
|
296
|
+
});
|
|
297
|
+
}}
|
|
298
|
+
onBack={() => {
|
|
299
|
+
setBrowsing(false);
|
|
300
|
+
}}
|
|
301
|
+
messages={words.threads}
|
|
302
|
+
classNames={classNames}
|
|
303
|
+
/>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
253
306
|
if (listing && theirs.tasks !== null) {
|
|
254
307
|
return (
|
|
255
308
|
<AgentTasks
|
|
@@ -320,20 +373,43 @@ export function AgentSpace({
|
|
|
320
373
|
);
|
|
321
374
|
}
|
|
322
375
|
const count = theirs.tasks?.length ?? 0;
|
|
376
|
+
// Another thread with something for them: the line to the threads carries a dot.
|
|
377
|
+
const news = (threads.threads ?? []).some(
|
|
378
|
+
(thread) =>
|
|
379
|
+
thread.id !== id && ((thread.unread ?? 0) > 0 || thread.waiting_on_person === true),
|
|
380
|
+
);
|
|
323
381
|
return (
|
|
324
382
|
<>
|
|
325
|
-
{count > 0 && (
|
|
326
|
-
<
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
383
|
+
{(threads.threads !== null || count > 0) && (
|
|
384
|
+
<div className={classes(classNames, "sc-space__bar")}>
|
|
385
|
+
{threads.threads !== null && (
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
part="threads-open"
|
|
389
|
+
className={classes(classNames, "sc-space__threads")}
|
|
390
|
+
onClick={() => {
|
|
391
|
+
setBrowsing(true);
|
|
392
|
+
void threads.refresh();
|
|
393
|
+
}}
|
|
394
|
+
>
|
|
395
|
+
{words.threads.open}
|
|
396
|
+
{news && <span aria-hidden className={classes(classNames, "sc-space__dot")} />}
|
|
397
|
+
</button>
|
|
398
|
+
)}
|
|
399
|
+
{count > 0 && (
|
|
400
|
+
<button
|
|
401
|
+
type="button"
|
|
402
|
+
part="tasks-open"
|
|
403
|
+
className={classes(classNames, "sc-space__tasks")}
|
|
404
|
+
onClick={() => {
|
|
405
|
+
setListing(true);
|
|
406
|
+
void theirs.refresh();
|
|
407
|
+
}}
|
|
408
|
+
>
|
|
409
|
+
{words.tasks.open(count)}
|
|
410
|
+
</button>
|
|
411
|
+
)}
|
|
412
|
+
</div>
|
|
337
413
|
)}
|
|
338
414
|
<AgentChat
|
|
339
415
|
client={client}
|
package/src/AgentTasks.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { ArrowLeftIcon, TrashIcon } from "@phosphor-icons/react";
|
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
3
|
|
|
4
4
|
import { Automation } from "./automation/Automation";
|
|
5
|
-
import type
|
|
5
|
+
import { taskCard, type Task } from "./automation/types";
|
|
6
6
|
import { classes } from "./classes";
|
|
7
7
|
import type { Client } from "./client";
|
|
8
8
|
import { Button } from "./kit/Button";
|
|
@@ -139,7 +139,7 @@ export function AgentTasks({
|
|
|
139
139
|
{tasks.map((task) => (
|
|
140
140
|
<Automation
|
|
141
141
|
key={task.id}
|
|
142
|
-
automation={task}
|
|
142
|
+
automation={taskCard(task)}
|
|
143
143
|
runs={task.runs}
|
|
144
144
|
switching={busy === task.id}
|
|
145
145
|
onSwitch={(on) => void change(task, () => client.tasks.switch(agentId, task.id, on))}
|