@signalcore/ui 1.0.0-beta.3 → 1.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -0
- package/dist/AgentChat.d.ts +19 -3
- package/dist/AgentChat.js +149 -27
- package/dist/AgentChat.js.map +1 -1
- package/dist/AgentSpace.d.ts +14 -2
- package/dist/AgentSpace.js +3 -1
- package/dist/AgentSpace.js.map +1 -1
- package/dist/View.d.ts +48 -0
- package/dist/View.js +101 -0
- package/dist/View.js.map +1 -0
- package/dist/blocks.d.ts +83 -2
- package/dist/client.d.ts +9 -1
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -1
- package/dist/es.js +12 -0
- package/dist/es.js.map +1 -1
- package/dist/index.d.ts +11 -5
- package/dist/index.js +7 -1
- package/dist/kit/Agenda.d.ts +25 -0
- package/dist/kit/Agenda.js +119 -0
- package/dist/kit/Agenda.js.map +1 -0
- package/dist/kit/Combobox.js +1 -1
- package/dist/kit/DateField.js +1 -1
- package/dist/kit/Kanban.d.ts +17 -0
- package/dist/kit/Kanban.js +157 -0
- package/dist/kit/Kanban.js.map +1 -0
- package/dist/kit/TimeField.js +1 -1
- package/dist/kit/Timeline.d.ts +14 -0
- package/dist/kit/Timeline.js +71 -0
- package/dist/kit/Timeline.js.map +1 -0
- package/dist/kit/context.d.ts +1 -1
- package/dist/kit/context.js.map +1 -1
- package/dist/kit/messages.d.ts +22 -0
- package/dist/kit/messages.js +10 -0
- package/dist/kit/messages.js.map +1 -1
- package/dist/surface/Placed.d.ts +20 -0
- package/dist/surface/Placed.js +70 -0
- package/dist/surface/Placed.js.map +1 -0
- package/dist/surface/Surface.d.ts +4 -2
- package/dist/surface/Surface.js +78 -2
- package/dist/surface/Surface.js.map +1 -1
- package/dist/surface/places.d.ts +16 -0
- package/dist/surface/places.js +15 -0
- package/dist/surface/places.js.map +1 -0
- package/dist/surface/scope.d.ts +17 -1
- package/dist/surface/scope.js +1 -0
- package/dist/surface/scope.js.map +1 -1
- package/dist/types.d.ts +12 -0
- package/dist/types.js.map +1 -1
- package/dist/widget.js +17 -17
- package/package.json +1 -1
- package/src/AgentChat.tsx +215 -34
- package/src/AgentSpace.tsx +17 -1
- package/src/View.tsx +117 -0
- package/src/blocks.ts +100 -2
- package/src/client.ts +16 -0
- package/src/contract.ts +166 -5
- package/src/es.ts +9 -0
- package/src/index.ts +27 -2
- package/src/kit/Agenda.tsx +161 -0
- package/src/kit/Kanban.tsx +182 -0
- package/src/kit/Timeline.tsx +58 -0
- package/src/kit/context.tsx +45 -1
- package/src/kit/messages.ts +31 -0
- package/src/styles.css +568 -0
- package/src/surface/Placed.tsx +98 -0
- package/src/surface/Surface.tsx +92 -1
- package/src/surface/places.ts +19 -0
- package/src/surface/scope.ts +24 -1
- package/src/types.ts +13 -0
- package/src/widget/element.tsx +41 -3
package/src/surface/Surface.tsx
CHANGED
|
@@ -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
|
+
}
|
package/src/surface/scope.ts
CHANGED
|
@@ -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
|
|
package/src/widget/element.tsx
CHANGED
|
@@ -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 &&
|
|
569
|
+
settings && agent && (
|
|
570
|
+
<Widget settings={{ ...settings, agent }} look={this.#worn} blocks={this.#blocks} />
|
|
571
|
+
),
|
|
534
572
|
);
|
|
535
573
|
}
|
|
536
574
|
}
|