@datalayer/agent-runtimes 1.3.34 → 1.3.36

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 (55) hide show
  1. package/lib/chat/ChatFloating.d.ts +8 -2
  2. package/lib/chat/ChatFloating.js +2 -2
  3. package/lib/chat/base/ChatBase.js +10 -6
  4. package/lib/chat/display/EmptyState.js +8 -1
  5. package/lib/chat/presence/Presence.d.ts +15 -1
  6. package/lib/chat/presence/Presence.js +0 -13
  7. package/lib/components/a2ui/datalayer/Chart.d.ts +132 -0
  8. package/lib/components/a2ui/datalayer/Chart.js +173 -0
  9. package/lib/components/a2ui/datalayer/Chat.d.ts +21 -0
  10. package/lib/components/a2ui/datalayer/Chat.js +116 -0
  11. package/lib/components/a2ui/datalayer/Evidence.d.ts +22 -0
  12. package/lib/components/a2ui/datalayer/Evidence.js +73 -0
  13. package/lib/components/a2ui/datalayer/FileUpload.d.ts +22 -0
  14. package/lib/components/a2ui/datalayer/FileUpload.js +107 -0
  15. package/lib/components/a2ui/datalayer/Form.d.ts +12 -0
  16. package/lib/components/a2ui/datalayer/Form.js +46 -0
  17. package/lib/components/a2ui/datalayer/Table.d.ts +20 -0
  18. package/lib/components/a2ui/datalayer/Table.js +103 -0
  19. package/lib/components/a2ui/datalayer/drawn.d.ts +10 -0
  20. package/lib/components/a2ui/datalayer/drawn.js +20 -0
  21. package/lib/components/a2ui/datalayer/implementation.d.ts +17 -0
  22. package/lib/components/a2ui/datalayer/implementation.js +10 -0
  23. package/lib/components/a2ui/datalayer/index.d.ts +23 -0
  24. package/lib/components/a2ui/datalayer/index.js +44 -0
  25. package/lib/components/a2ui/datalayer/parts.d.ts +32 -0
  26. package/lib/components/a2ui/datalayer/parts.js +44 -0
  27. package/lib/components/a2ui/datalayer/schema.d.ts +34 -0
  28. package/lib/components/a2ui/datalayer/schema.js +108 -0
  29. package/lib/components/a2ui/index.d.ts +1 -0
  30. package/lib/components/a2ui/index.js +1 -0
  31. package/lib/components/a2ui/visibleWhen.d.ts +4 -2
  32. package/lib/components/a2ui/visibleWhen.js +6 -3
  33. package/lib/loop/apps/AppRenderer.d.ts +8 -0
  34. package/lib/loop/apps/AppRenderer.js +21 -1
  35. package/lib/loop/apps/appspec.d.ts +9 -2
  36. package/lib/loop/apps/appspec.js +14 -8
  37. package/lib/loop/apps/checks.js +4 -3
  38. package/lib/loop/embed/AppEmbed.d.ts +16 -1
  39. package/lib/loop/embed/AppEmbed.js +38 -9
  40. package/lib/loop/embed/LoopEmbed.js +2 -0
  41. package/lib/loop/embed/element.js +10 -1
  42. package/lib/loop/embed/embedTheme.d.ts +12 -0
  43. package/lib/loop/embed/embedTheme.js +15 -0
  44. package/lib/loop/plugins/assistant/index.d.ts +38 -0
  45. package/lib/loop/plugins/assistant/index.js +140 -0
  46. package/lib/loop/plugins/assistant-characters/index.d.ts +35 -1
  47. package/lib/loop/plugins/assistant-characters/index.js +45 -2
  48. package/lib/loop/plugins/chat/ChatView.js +21 -9
  49. package/lib/loop/plugins/chat/index.d.ts +6 -0
  50. package/lib/loop/presets.d.ts +3 -0
  51. package/lib/loop/presets.js +2 -1
  52. package/lib/specs/appspecSchema.js +5 -8
  53. package/lib/types/agentspecs.d.ts +10 -4
  54. package/lib/types/chat.d.ts +11 -0
  55. package/package.json +2 -1
@@ -0,0 +1,108 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * The A2UI schema of a component of Datalayer's own, read from the catalog
7
+ * (LOOP C-13, C-18): each property its JSON Schema declares, typed as it
8
+ * says; each thing it shows or sends a binding into the data model — a path
9
+ * the application publishes, or a value given in place; its `action`, what
10
+ * it dispatches when a person acts on it, written as A2UI writes a Button's.
11
+ *
12
+ * A2UI's renderer reads a component's schema to tell its properties apart —
13
+ * a binding it resolves and writes back through, an action it dispatches, a
14
+ * plain value it hands over — so the schema is built with the zod A2UI reads
15
+ * (v3), from the catalog, and cannot drift from what the Canvas, the YAML
16
+ * and Python check a component against.
17
+ *
18
+ * @module components/a2ui/datalayer/schema
19
+ */
20
+ import { z } from 'zod/v3';
21
+ import { ActionSchema, DataBindingSchema } from '@a2ui/web_core/v0_9';
22
+ import { COMPONENT_CATALOGUE } from '../../../specs/uiPlugins';
23
+ /** The components of Datalayer's own the catalog has, by id. */
24
+ export const OWN_COMPONENT_IDS = Object.values(COMPONENT_CATALOGUE)
25
+ .filter(component => !component.standard)
26
+ .map(component => component.id);
27
+ /** A component of Datalayer's own, from the catalog; it throws for any other. */
28
+ export function ownComponent(id) {
29
+ const component = COMPONENT_CATALOGUE[id];
30
+ if (!component) {
31
+ throw new Error(`The catalog has no component ${id}.`);
32
+ }
33
+ if (component.standard || !component.properties || !component.bindings) {
34
+ throw new Error(`${id} is A2UI's own: its schema is the basic catalog's, not Datalayer's.`);
35
+ }
36
+ return component;
37
+ }
38
+ /**
39
+ * What a component shows or sends: a list, a record, words, a number, true
40
+ * or false given in place, or a path into the data model. A2UI resolves it
41
+ * — and gives the component a setter for it when it is a path.
42
+ */
43
+ const BindingSchema = z
44
+ .union([
45
+ z.array(z.any()),
46
+ z.record(z.string(), z.unknown()),
47
+ z.string(),
48
+ z.number(),
49
+ z.boolean(),
50
+ DataBindingSchema,
51
+ ])
52
+ .describe('REF:#/$defs/DynamicValue|Datalayer: what the component shows or sends — a path the application publishes or takes, or a value given in place.');
53
+ /** A property as its JSON Schema types it. */
54
+ function propertySchema(name, property) {
55
+ if (property.enum) {
56
+ return z.enum(property.enum);
57
+ }
58
+ switch (property.type) {
59
+ case 'string':
60
+ return z.string();
61
+ case 'integer':
62
+ case 'number':
63
+ return z.number();
64
+ case 'boolean':
65
+ return z.boolean();
66
+ case 'array':
67
+ return z.array(z.any());
68
+ case 'object':
69
+ return z.record(z.string(), z.unknown());
70
+ default:
71
+ throw new Error(`${name} is typed ${String(property.type)}, which a component's property cannot be.`);
72
+ }
73
+ }
74
+ /** The names a component shows and sends through, each once. */
75
+ export function bindingNames(component) {
76
+ const { shows, sends } = component.bindings;
77
+ return [...new Set([...shows, ...sends])];
78
+ }
79
+ /**
80
+ * The A2UI schema of a component of Datalayer's own: its properties as its
81
+ * JSON Schema has them, its bindings, its `action`, and the `weight` every
82
+ * basic component takes inside a Row or a Column. Strict, as the basic
83
+ * catalog's are: a key it does not declare refuses the tree.
84
+ */
85
+ export function ownComponentSchema(id) {
86
+ const component = ownComponent(id);
87
+ const json = component.properties;
88
+ const required = new Set(json.required ?? []);
89
+ const shape = {};
90
+ for (const [name, property] of Object.entries(json.properties ?? {})) {
91
+ const schema = propertySchema(name, property);
92
+ shape[name] = required.has(name) ? schema : schema.optional();
93
+ }
94
+ for (const name of bindingNames(component)) {
95
+ if (name in shape) {
96
+ throw new Error(`${id} both declares ${name} as a property and binds through it.`);
97
+ }
98
+ shape[name] = BindingSchema.optional();
99
+ }
100
+ shape.action = ActionSchema
101
+ .describe('REF:#/$defs/Action|What it dispatches when a person acts on it.')
102
+ .optional();
103
+ shape.weight = z
104
+ .number()
105
+ .describe('The relative weight of this component within a Row or Column.')
106
+ .optional();
107
+ return z.object(shape).strict();
108
+ }
@@ -3,3 +3,4 @@ export * from './A2uiSurfaceComposed';
3
3
  export * from './styles';
4
4
  export * from './visibleWhen';
5
5
  export * from './visibility';
6
+ export * from './datalayer';
@@ -7,3 +7,4 @@ export * from './A2uiSurfaceComposed';
7
7
  export * from './styles';
8
8
  export * from './visibleWhen';
9
9
  export * from './visibility';
10
+ export * from './datalayer';
@@ -11,7 +11,9 @@ export declare function nodeIsShown(props: Record<string, unknown>): boolean;
11
11
  /** A component, its schema declaring `visible_when`, drawn behind the guard. */
12
12
  export declare function withVisibleWhen(component: ReactComponentImplementation): ReactComponentImplementation;
13
13
  /**
14
- * The catalog Datalayer's renderer draws from: A2UI's basic catalog, under
15
- * its own id — the payloads name it — with `visible_when` on every component.
14
+ * The catalog Datalayer's renderer draws from: A2UI's basic catalog and
15
+ * Datalayer's own components (Table, Chart, File upload, Chat, Evidence,
16
+ * Form — `./datalayer`), under the basic catalog's id — the payloads name
17
+ * it — with `visible_when` on every component.
16
18
  */
17
19
  export declare const datalayerCatalog: Catalog<ReactComponentImplementation, import("@a2ui/web_core/v0_9").FunctionImplementation>;
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { basicCatalog, useSignalValue, } from '@a2ui/react/v0_9';
3
3
  import { Catalog, DynamicBooleanSchema, ResolvedBinding, isWritable, } from '@a2ui/web_core/v0_9';
4
4
  import { VISIBLE_WHEN, isShown } from './visibility';
5
+ import { OWN_COMPONENTS } from './datalayer';
5
6
  /**
6
7
  * Whether a node's resolved properties let it be drawn: no condition, or one
7
8
  * that holds. A2UI resolves a dynamic property to a binding whether the block
@@ -48,7 +49,9 @@ export function withVisibleWhen(component) {
48
49
  };
49
50
  }
50
51
  /**
51
- * The catalog Datalayer's renderer draws from: A2UI's basic catalog, under
52
- * its own id — the payloads name it — with `visible_when` on every component.
52
+ * The catalog Datalayer's renderer draws from: A2UI's basic catalog and
53
+ * Datalayer's own components (Table, Chart, File upload, Chat, Evidence,
54
+ * Form — `./datalayer`), under the basic catalog's id — the payloads name
55
+ * it — with `visible_when` on every component.
53
56
  */
54
- export const datalayerCatalog = new Catalog(basicCatalog.id, basicCatalog.protocolVersion, [...basicCatalog.components.values()].map(withVisibleWhen), [...basicCatalog.functions.values()], basicCatalog.themeSchema, basicCatalog.instructions);
57
+ export const datalayerCatalog = new Catalog(basicCatalog.id, basicCatalog.protocolVersion, [...basicCatalog.components.values(), ...OWN_COMPONENTS].map(withVisibleWhen), [...basicCatalog.functions.values()], basicCatalog.themeSchema, basicCatalog.instructions);
@@ -1,5 +1,6 @@
1
1
  import type { ReactorPlugin } from '@datalayer/reactor';
2
2
  import type { AppSpec } from '../../types/agentspecs';
3
+ import type { ThemeOverrides } from '../../types/chat';
3
4
  import { type LoopEmbedProps } from '../embed/LoopEmbed';
4
5
  import type { LoopPresetOptions } from '../presets';
5
6
  import type { PresenceState } from '../../chat/presence/presenceStatus';
@@ -79,5 +80,12 @@ export declare function appDatalayerCreatePayload(app: AppSpec, instance?: AppIn
79
80
  /** No host plugins: one array, so that it never reads as a change. */
80
81
  /** The agentspec a runtime is allocated with before an application's agent is created on it. */
81
82
  export declare const DATALAYER_BOOTSTRAP_AGENTSPEC = "example-simple";
83
+ /**
84
+ * The application's accent over the theme its conversation wears, in both
85
+ * modes (LOOP T-05): its bubbles and its one button in its own colour, and
86
+ * not in the theme's default — the chat sets its theme again inside it, so
87
+ * what the page around it set does not reach in.
88
+ */
89
+ export declare function appThemeOverrides(app: AppSpec): ThemeOverrides | undefined;
82
90
  export declare function AppRenderer({ app, plugins, instance, onPresence, frame, ...embed }: AppRendererProps): React.JSX.Element;
83
91
  export default AppRenderer;
@@ -34,6 +34,7 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
34
34
  * @module loop/apps/AppRenderer
35
35
  */
36
36
  import { useMemo } from 'react';
37
+ import { loopAccentStyles } from '@datalayer/primer-addons';
37
38
  import { defineAgentCapacityPlugin } from '../plugins/agent-capacity';
38
39
  import { APP_PAGE_SURFACE, defineAppPagePlugin, hasAppPage, } from '../plugins/app-page';
39
40
  import { LoopEmbed } from '../embed/LoopEmbed';
@@ -158,6 +159,21 @@ export function appDatalayerCreatePayload(app, instance) {
158
159
  /** The agentspec a runtime is allocated with before an application's agent is created on it. */
159
160
  export const DATALAYER_BOOTSTRAP_AGENTSPEC = 'example-simple';
160
161
  const NO_PLUGINS = [];
162
+ /**
163
+ * The application's accent over the theme its conversation wears, in both
164
+ * modes (LOOP T-05): its bubbles and its one button in its own colour, and
165
+ * not in the theme's default — the chat sets its theme again inside it, so
166
+ * what the page around it set does not reach in.
167
+ */
168
+ export function appThemeOverrides(app) {
169
+ const accent = app.interface?.accent;
170
+ return accent
171
+ ? {
172
+ light: loopAccentStyles(accent, 'light'),
173
+ dark: loopAccentStyles(accent, 'dark'),
174
+ }
175
+ : undefined;
176
+ }
161
177
  export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, frame = false, ...embed }) {
162
178
  /*
163
179
  * The application plugin, made once per application. `LoopEmbed` rebuilds
@@ -198,6 +214,10 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, f
198
214
  const datalayerCreatePayload = useMemo(() => (app.agent ? appDatalayerCreatePayload(app, instance) : undefined),
199
215
  // eslint-disable-next-line react-hooks/exhaustive-deps
200
216
  [source, instance?.appUid, instance?.deploymentUid, instance?.version]);
217
+ const accent = app.interface?.accent;
218
+ const themeOverrides = useMemo(() => appThemeOverrides(app),
219
+ // eslint-disable-next-line react-hooks/exhaustive-deps
220
+ [accent]);
201
221
  if (!appPlugin) {
202
222
  return (_jsxs("div", { role: "status", style: { padding: 16 }, children: ["\u201C", app.name, "\u201D is run by a team, which the workspace does not run as an application yet."] }));
203
223
  }
@@ -217,7 +237,7 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, f
217
237
  targetFixed: true,
218
238
  // Applications first (LOOP T-12): an application's conversation wears
219
239
  // the `loop` theme; a host may say otherwise.
220
- themeVariant: "loop",
240
+ themeVariant: "loop", themeOverrides: themeOverrides,
221
241
  // Its own face, name and welcome in the chat (T-08); no counters: a
222
242
  // person using an application is not asking about tokens.
223
243
  presence: {
@@ -33,8 +33,15 @@ export declare const APP_KINDS: AppKind[];
33
33
  export declare const APP_ACCENTS: AppAccent[];
34
34
  /** The four ways an application sits in another product's page (LOOP D-07). */
35
35
  export declare const APP_EMBED_MODES: AppEmbedMode[];
36
- /** The characters an Appspec may name for its floating assistant (LOOP T-24). */
37
- export declare const APP_ASSISTANT_CHARACTERS: AppAssistantCharacter[];
36
+ /**
37
+ * How an Appspec names the character of its floating assistant (LOOP T-24):
38
+ * the id a plugin contributes it under, `paperclip` or `acme-owl` — as
39
+ * agentspecs' `ASSISTANT_CHARACTER_ID`. Which ids exist is what the enabled
40
+ * plugins contribute (`assistantCharacterNamed`), not the spec's to say.
41
+ */
42
+ export declare const APP_ASSISTANT_CHARACTER_ID: RegExp;
43
+ /** Whether a value is shaped as a character id an Appspec may name. */
44
+ export declare function isAssistantCharacterId(value: unknown): value is AppAssistantCharacter;
38
45
  /** The layout a kind starts with. */
39
46
  export declare const DEFAULT_LAYOUTS: Record<AppKind, AppLayout>;
40
47
  /** What an application keeps of what it did, when it does not say. */
@@ -23,13 +23,19 @@ export const APP_EMBED_MODES = [
23
23
  'panel',
24
24
  'assistant',
25
25
  ];
26
- /** The characters an Appspec may name for its floating assistant (LOOP T-24). */
27
- export const APP_ASSISTANT_CHARACTERS = [
28
- 'paperclip',
29
- 'wizard',
30
- 'cat',
31
- 'eyes',
32
- ];
26
+ /**
27
+ * How an Appspec names the character of its floating assistant (LOOP T-24):
28
+ * the id a plugin contributes it under, `paperclip` or `acme-owl` — as
29
+ * agentspecs' `ASSISTANT_CHARACTER_ID`. Which ids exist is what the enabled
30
+ * plugins contribute (`assistantCharacterNamed`), not the spec's to say.
31
+ */
32
+ export const APP_ASSISTANT_CHARACTER_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
33
+ /** Whether a value is shaped as a character id an Appspec may name. */
34
+ export function isAssistantCharacterId(value) {
35
+ return (typeof value === 'string' &&
36
+ value.length <= 64 &&
37
+ APP_ASSISTANT_CHARACTER_ID.test(value));
38
+ }
33
39
  const ACTION_CLASS_NAMES = [
34
40
  'read',
35
41
  'write',
@@ -218,7 +224,7 @@ function parseInterface(data, kind) {
218
224
  if (isData(data.surface)) {
219
225
  parsed.surface = parseSurface(data.surface);
220
226
  }
221
- if (APP_ASSISTANT_CHARACTERS.includes(data.assistant)) {
227
+ if (isAssistantCharacterId(data.assistant)) {
222
228
  parsed.assistant = data.assistant;
223
229
  }
224
230
  return parsed;
@@ -39,7 +39,7 @@ import { getTeamSpec } from '../../specs/teams';
39
39
  import { getToolSpec } from '../../specs/tools';
40
40
  import { getTrack } from '../../specs/tracks';
41
41
  import { pluginsOffSetupNotes } from '../plugins/canvas-blocks';
42
- import { parseAppspec } from './appspec';
42
+ import { isAssistantCharacterId, parseAppspec } from './appspec';
43
43
  import { classesOf, splitRef, toolBehaviours } from './rules';
44
44
  export const NOT_READY = 'Not ready';
45
45
  export const NEEDS_ATTENTION = 'Needs attention';
@@ -378,7 +378,6 @@ const ENUMS = {
378
378
  ],
379
379
  visibility: ['private', 'invited', 'organization', 'link', 'public'],
380
380
  mode: ['inline', 'bubble', 'panel', 'assistant'],
381
- character: ['paperclip', 'wizard', 'cat', 'eyes'],
382
381
  trigger: ['schedule', 'event', 'once'],
383
382
  criterion: ['metric', 'noul', 'choice', 'score'],
384
383
  direction: ['higher', 'lower'],
@@ -506,7 +505,9 @@ export function documentShapeProblems(document) {
506
505
  mapping(d.interface, 'interface', ui => {
507
506
  oneOf(ui.layout, ENUMS.layout, 'interface.layout');
508
507
  oneOf(ui.accent, ENUMS.accent, 'interface.accent');
509
- oneOf(ui.assistant, ENUMS.character, 'interface.assistant');
508
+ if (ui.assistant !== undefined && !isAssistantCharacterId(ui.assistant)) {
509
+ at('interface.assistant', 'names a character by the id a plugin contributes it under, lowercase words joined by a hyphen: `paperclip`, `acme-owl`');
510
+ }
510
511
  text(ui.welcome, 'interface.welcome');
511
512
  texts(ui.components, 'interface.components');
512
513
  records(ui.starters, 'interface.starters', (item, where) => {
@@ -1,6 +1,8 @@
1
1
  import type { CSSProperties, JSX, ReactNode } from 'react';
2
+ import type { PluginRef } from '@datalayer/reactor';
2
3
  import type { AppAccent, AppEmbedMode, AppSpec } from '../../types/agentspecs';
3
4
  import { type AppInstance } from '../apps/AppRenderer';
5
+ import { type AssistantCharacterChosen } from '../plugins/assistant-characters';
4
6
  import { type EmbedColorMode } from './embedConfig';
5
7
  export type AppEmbedProps = {
6
8
  /** The application. */
@@ -29,7 +31,20 @@ export type AppEmbedProps = {
29
31
  * shadow root's styles stop at its edge. A React host leaves it off.
30
32
  */
31
33
  ownPortal?: boolean;
34
+ /**
35
+ * The host's plugins, for the assistant's character: what they contribute
36
+ * to `loop.assistant.character` is there beside Datalayer's four. The
37
+ * element passes none.
38
+ */
39
+ plugins?: PluginRef[];
32
40
  };
41
+ /**
42
+ * The character the assistant mode draws (T-24, D-07): the application's own
43
+ * (`interface.assistant`), else the paper clip, from what Datalayer's
44
+ * characters and the host's plugins contribute — as a page decides it, with
45
+ * no person's choice, since a visitor to another product has none here.
46
+ */
47
+ export declare function embedAssistantCharacter(app: AppSpec, plugins?: PluginRef[]): AssistantCharacterChosen;
33
48
  /** The `loop` theme, with the application's (or the host's) accent and face. */
34
49
  export declare function EmbedThemed({ accent, colorMode, font, children, style, }: {
35
50
  accent: AppAccent;
@@ -38,5 +53,5 @@ export declare function EmbedThemed({ accent, colorMode, font, children, style,
38
53
  children: ReactNode;
39
54
  style?: CSSProperties;
40
55
  }): JSX.Element;
41
- export declare function AppEmbed({ app, mode, accent, colorMode, font, serverUrl, instance, height, ownPortal, }: AppEmbedProps): JSX.Element;
56
+ export declare function AppEmbed({ app, mode, accent, colorMode, font, serverUrl, instance, height, ownPortal, plugins, }: AppEmbedProps): JSX.Element;
42
57
  export default AppEmbed;
@@ -17,7 +17,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
17
17
  * - **panel** — the conversation at the right edge of the page, at full
18
18
  * height;
19
19
  * - **assistant** — the application's character on the page, speaking in a
20
- * balloon (§6.9), the one its Appspec names (`interface.assistant`).
20
+ * balloon (§6.9): the one its Appspec names (`interface.assistant`), else
21
+ * the paper clip, looked up in what is contributed — Datalayer's four and
22
+ * the host's own plugins (T-24); an id nothing contributes is said, never
23
+ * replaced.
21
24
  *
22
25
  * The three floating modes are `ChatFloating`'s, given the application's
23
26
  * name, welcome, starters and accent, and its agent's endpoint — created
@@ -25,7 +28,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
25
28
  * rules there as it does inline.
26
29
  *
27
30
  * In the `loop` theme with the application's accent (T-12, T-05), which the
28
- * host may override with the face and the mode (D-11).
31
+ * host may override with the face and the mode (D-11) — around the
32
+ * conversation and inside it, where the chat sets its theme again.
29
33
  *
30
34
  * @module loop/embed/AppEmbed
31
35
  */
@@ -38,8 +42,20 @@ import { useAgentRuntimes } from '../../hooks/useAgentRuntimes';
38
42
  import { AppRenderer, DATALAYER_BOOTSTRAP_AGENTSPEC, appDatalayerCreatePayload, defineAppPlugin, } from '../apps/AppRenderer';
39
43
  import { LoopAgentBlueprint } from '../core';
40
44
  import { ensureServerAgent } from '../plugins/agents/switchable';
45
+ import { AssistantCharactersPlugin, assistantCharacterFor, assistantCharactersFrom, } from '../plugins/assistant-characters';
41
46
  import { floatingViewOf } from './embedConfig';
42
- import { embedThemeStyles } from './embedTheme';
47
+ import { embedThemeOverrides, embedThemeStyles } from './embedTheme';
48
+ /** No host plugins: one array, so that it never reads as a change. */
49
+ const NO_PLUGINS = [];
50
+ /**
51
+ * The character the assistant mode draws (T-24, D-07): the application's own
52
+ * (`interface.assistant`), else the paper clip, from what Datalayer's
53
+ * characters and the host's plugins contribute — as a page decides it, with
54
+ * no person's choice, since a visitor to another product has none here.
55
+ */
56
+ export function embedAssistantCharacter(app, plugins = NO_PLUGINS) {
57
+ return assistantCharacterFor(assistantCharactersFrom([AssistantCharactersPlugin, ...plugins]), { app: app.interface.assistant });
58
+ }
43
59
  /** What the application's agent is created with on an agent-runtimes server. */
44
60
  function serverCreatePayload(app) {
45
61
  const blueprint = (defineAppPlugin(app).contributes ?? []).find((item) => item.point === LoopAgentBlueprint);
@@ -132,14 +148,14 @@ function Face({ emoji }) {
132
148
  return (_jsx("span", { "aria-hidden": true, style: { fontSize: 18, lineHeight: 1 }, children: emoji }));
133
149
  }
134
150
  /** The bubble, the panel and the assistant: `ChatFloating` in that mode. */
135
- function FloatingChat({ app, view, colorMode, agent, }) {
151
+ function FloatingChat({ app, view, colorMode, themeOverrides, character, agent, }) {
136
152
  const welcome = app.interface.welcome || app.description;
137
153
  return (_jsx(ChatFloating
138
154
  // A new agent is a new conversation.
139
- , { defaultViewMode: view, assistantCharacter: app.interface.assistant ?? 'paperclip', protocol: agent.protocol, authToken: agent.protocol?.authToken, title: app.name, description: welcome, brandIcon: _jsx(Face, { emoji: app.emoji }), buttonIcon: _jsx(Face, { emoji: app.emoji }), buttonTooltip: `Talk to ${app.name}`, suggestions: app.interface.starters.map(starter => ({
155
+ , { defaultViewMode: view, ...(character ? { assistantCharacter: character } : {}), protocol: agent.protocol, authToken: agent.protocol?.authToken, title: app.name, description: welcome, brandIcon: _jsx(Face, { emoji: app.emoji }), buttonIcon: _jsx(Face, { emoji: app.emoji }), buttonTooltip: `Talk to ${app.name}`, suggestions: app.interface.starters.map(starter => ({
140
156
  title: starter.label,
141
157
  message: starter.message,
142
- })), themeVariant: "loop", colorMode: colorMode, showTokenUsage: false, showSettingsButton: false, showPoweredBy: false, useStore: false, launching: !agent.protocol && !agent.error, launchingMessage: agent.waiting, disabled: Boolean(agent.error), disableReason: agent.error }, agent.protocol?.endpoint ?? 'waiting'));
158
+ })), themeVariant: "loop", themeOverrides: themeOverrides, colorMode: colorMode, showTokenUsage: false, showSettingsButton: false, showPoweredBy: false, useStore: false, launching: !agent.protocol && !agent.error, launchingMessage: agent.waiting, disabled: Boolean(agent.error), disableReason: agent.error }, agent.protocol?.endpoint ?? 'waiting'));
143
159
  }
144
160
  /** Floating, its agent on the host's agent-runtimes server. */
145
161
  function ServerFloating({ serverUrl, ...props }) {
@@ -192,14 +208,27 @@ export function EmbedThemed({ accent, colorMode, font, children, style, }) {
192
208
  */
193
209
  _jsx("div", { "data-color-mode": colorMode, style: { display: 'contents' }, children: _jsx(DatalayerThemeProvider, { colorMode: colorMode, theme: loopTheme, themeStyles: themeStyles, baseStyles: style, children: children }) }));
194
210
  }
195
- export function AppEmbed({ app, mode, accent, colorMode = 'auto', font, serverUrl, instance, height = 640, ownPortal = false, }) {
211
+ export function AppEmbed({ app, mode, accent, colorMode = 'auto', font, serverUrl, instance, height = 640, ownPortal = false, plugins = NO_PLUGINS, }) {
196
212
  const system = useSystemMode();
197
213
  const resolvedMode = colorMode === 'auto' ? system : colorMode;
198
214
  const shownAs = mode ?? app.deployment.embedded?.mode ?? 'inline';
199
215
  const view = floatingViewOf(shownAs);
200
- return (_jsxs(EmbedThemed, { accent: accent ?? app.interface.accent, colorMode: resolvedMode, font: font, style: view
216
+ const worn = accent ?? app.interface.accent;
217
+ const themeOverrides = useMemo(() => embedThemeOverrides({ accent: worn, font }), [worn, font]);
218
+ const named = app.interface.assistant;
219
+ const assistant = useMemo(() => view === 'assistant' ? embedAssistantCharacter(app, plugins) : undefined,
220
+ // eslint-disable-next-line react-hooks/exhaustive-deps
221
+ [view, named, plugins]);
222
+ if (assistant && 'problem' in assistant) {
223
+ return (_jsx("p", { role: "status", className: "datalayer-app-said", children: assistant.problem }));
224
+ }
225
+ const character = assistant?.character;
226
+ return (_jsxs(EmbedThemed, { accent: worn, colorMode: resolvedMode, font: font, style: view
201
227
  ? // Floating: nothing in the page's flow, nothing painted behind.
202
228
  { backgroundColor: 'transparent' }
203
- : { height, display: 'flex', flexDirection: 'column' }, children: [ownPortal ? _jsx(OwnPortalRoot, {}) : null, view && serverUrl ? (_jsx(ServerFloating, { app: app, view: view, serverUrl: serverUrl, colorMode: resolvedMode })) : view ? (_jsx(DatalayerFloating, { app: app, view: view, instance: instance, colorMode: resolvedMode })) : (_jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), instance: instance }))] }));
229
+ : { height, display: 'flex', flexDirection: 'column' }, children: [ownPortal ? _jsx(OwnPortalRoot, {}) : null, view && serverUrl ? (_jsx(ServerFloating, { app: app, view: view, serverUrl: serverUrl, colorMode: resolvedMode, themeOverrides: themeOverrides, character: character })) : view ? (_jsx(DatalayerFloating, { app: app, view: view, instance: instance, colorMode: resolvedMode, themeOverrides: themeOverrides, character: character })) : (_jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), instance: instance,
230
+ // The host's accent and face over the application's own, inside
231
+ // its conversation too.
232
+ themeOverrides: themeOverrides }))] }));
204
233
  }
205
234
  export default AppEmbed;
@@ -66,6 +66,8 @@ export function LoopEmbed({ agentId = '', showHeader = false, chatHeaderActions,
66
66
  preset.datalayerAgentSpecId,
67
67
  preset.targetFixed,
68
68
  preset.themeVariant,
69
+ // By its content, as the payload below.
70
+ JSON.stringify(preset.themeOverrides ?? null),
69
71
  preset.presence?.name,
70
72
  preset.presence?.face,
71
73
  preset.presence?.welcome,
@@ -37,7 +37,7 @@ import { loopThemeStyles } from '@datalayer/primer-addons';
37
37
  import { coreStore } from '@datalayer/core/lib/state/substates/CoreState';
38
38
  import { parseAppspec } from '../apps/appspec';
39
39
  import { readAppspecYaml } from '../apps/yaml';
40
- import { AppEmbed } from './AppEmbed';
40
+ import { AppEmbed, embedAssistantCharacter } from './AppEmbed';
41
41
  import { EMBED_HOST_VARIABLES, EMBED_OBSERVED_ATTRIBUTES, EMBED_TAG, EmbedAttributeError, embedLookOf, inlineHeightOf, } from './embedConfig';
42
42
  import { embedShadowCss } from './embedTheme';
43
43
  /** What the framed run page and the element say to each other. */
@@ -362,6 +362,15 @@ export function defineDatalayerAppElement(options = {}) {
362
362
  this.say(error.message);
363
363
  return;
364
364
  }
365
+ // A character nothing contributes is said in place, as an attribute
366
+ // that is not one is (D-07, T-24).
367
+ if (look.mode === 'assistant') {
368
+ const chosen = embedAssistantCharacter(source.app);
369
+ if ('problem' in chosen) {
370
+ this.say(chosen.problem);
371
+ return;
372
+ }
373
+ }
365
374
  this.setAttribute('data-embed-mode', look.mode);
366
375
  const shadow = this.skeleton(loopThemeStyles.css);
367
376
  const mount = document.createElement('div');
@@ -1,5 +1,6 @@
1
1
  import { type ThemeStyles } from '@datalayer/primer-addons';
2
2
  import type { AppAccent } from '../../types/agentspecs';
3
+ import type { ThemeOverrides } from '../../types/chat';
3
4
  /**
4
5
  * The `loop` theme as the embed wears it: the application's accent over it
5
6
  * in both modes (T-05), and the host's face when it named one.
@@ -8,6 +9,17 @@ export declare function embedThemeStyles({ accent, font, }: {
8
9
  accent: AppAccent;
9
10
  font?: string;
10
11
  }): ThemeStyles;
12
+ /**
13
+ * The same accent and face laid over the conversation's own theme (T-05,
14
+ * D-11): the chat sets the `loop` theme again inside it, which would put the
15
+ * theme's mint and face back over what the embed set around it. The
16
+ * accent's properties in each mode, and, when the host named a face, every
17
+ * property of the theme that names its own.
18
+ */
19
+ export declare function embedThemeOverrides({ accent, font, }: {
20
+ accent: AppAccent;
21
+ font?: string;
22
+ }): ThemeOverrides;
11
23
  /**
12
24
  * The element's own stylesheet, written into its shadow root.
13
25
  *
@@ -30,6 +30,21 @@ export function embedThemeStyles({ accent, font = '', }) {
30
30
  dark: mode('dark'),
31
31
  };
32
32
  }
33
+ /**
34
+ * The same accent and face laid over the conversation's own theme (T-05,
35
+ * D-11): the chat sets the `loop` theme again inside it, which would put the
36
+ * theme's mint and face back over what the embed set around it. The
37
+ * accent's properties in each mode, and, when the host named a face, every
38
+ * property of the theme that names its own.
39
+ */
40
+ export function embedThemeOverrides({ accent, font = '', }) {
41
+ const mode = (which) => {
42
+ const theme = loopThemeStyles[which];
43
+ const faced = Object.fromEntries(Object.entries(withFace(theme, font)).filter(([name, value]) => typeof value === 'string' && value !== theme[name]));
44
+ return { ...faced, ...loopAccentStyles(accent, which) };
45
+ };
46
+ return { light: mode('light'), dark: mode('dark') };
47
+ }
33
48
  /**
34
49
  * The element's own stylesheet, written into its shadow root.
35
50
  *
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The floating assistant in the LOOP workspace (LOOP T-24): the character a
3
+ * page draws over its workspace, chosen from what the enabled plugins
4
+ * contribute to `loop.assistant.character` — the application's own
5
+ * (`interface.assistant`) when its Appspec names one, else the person's
6
+ * choice from their settings, else the paper clip. It acts out the chat's
7
+ * turn (T-22), says the agent's newest words in its balloon while the
8
+ * conversation is out of sight (T-23), opens the conversation when clicked,
9
+ * is dragged about and sent away as the chat's own assistant is (T-27).
10
+ *
11
+ * The workspace's chat is the conversation: on a page layout, its side panel
12
+ * is opened and closed; where the conversation is always on screen, a click
13
+ * takes the person to its composer.
14
+ *
15
+ * @module loop/plugins/assistant
16
+ */
17
+ import type { JSX } from 'react';
18
+ import { type AssistantState } from '../../../chat/assistant/state';
19
+ import { type ChatTurnSnapshot } from '../../core';
20
+ export declare const LOOP_ASSISTANT_PLUGIN_NAME = "@datalayer/loop-plugin-assistant";
21
+ export type LoopAssistantConfig = {
22
+ /** The character the application's Appspec names; it wins. */
23
+ app?: string;
24
+ /** The character the person chose in their settings. */
25
+ person?: string;
26
+ };
27
+ /**
28
+ * What the character acts out, from the workspace's chat turn (T-22): it
29
+ * thinks while the agent thinks, works while a tool runs (the turn's
30
+ * `activity`), speaks while the answer is written, and idles otherwise.
31
+ */
32
+ export declare function assistantStateOfTurn(turn: ChatTurnSnapshot, moment?: {
33
+ arriving?: boolean;
34
+ leaving?: boolean;
35
+ }): AssistantState;
36
+ export declare function LoopAssistant({ app, person, }: LoopAssistantConfig): JSX.Element | null;
37
+ export declare const LoopAssistantPlugin: import("@datalayer/reactor").ReactorPlugin<LoopAssistantConfig, unknown, unknown>;
38
+ export default LoopAssistantPlugin;