@datalayer/agent-runtimes 1.3.80 → 1.3.82
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/lib/chat/assistant/AssistantStage.d.ts +9 -4
- package/lib/chat/assistant/AssistantStage.js +18 -6
- package/lib/chat/assistant/SpeechBalloon.js +16 -1
- package/lib/chat/assistant/SpriteCharacter.d.ts +25 -3
- package/lib/chat/assistant/SpriteCharacter.js +68 -3
- package/lib/chat/assistant/formats/acf.d.ts +56 -0
- package/lib/chat/assistant/formats/acf.js +317 -0
- package/lib/chat/assistant/formats/acs.d.ts +85 -7
- package/lib/chat/assistant/formats/acs.js +111 -39
- package/lib/chat/assistant/formats/files.d.ts +6 -2
- package/lib/chat/assistant/formats/files.js +18 -4
- package/lib/chat/assistant/formats/index.d.ts +6 -3
- package/lib/chat/assistant/formats/index.js +4 -2
- package/lib/chat/assistant/formats/types.d.ts +14 -0
- package/lib/chat/assistant/state.d.ts +2 -0
- package/lib/chat/assistant/state.js +9 -0
- package/lib/chat/base/ChatBase.js +47 -4
- package/lib/chat/base/loopStep.d.ts +40 -0
- package/lib/chat/base/loopStep.js +73 -0
- package/lib/chat/indicators/McpStatusIndicator.d.ts +1 -1
- package/lib/chat/indicators/McpStatusIndicator.js +13 -6
- package/lib/chat/indicators/SkillsStatusIndicator.d.ts +1 -1
- package/lib/chat/indicators/SkillsStatusIndicator.js +7 -3
- package/lib/chat/prompt/InputPrompt.d.ts +4 -1
- package/lib/chat/prompt/InputPrompt.js +2 -2
- package/lib/chat/prompt/InputPromptBase.d.ts +4 -1
- package/lib/chat/prompt/InputPromptBase.js +2 -2
- package/lib/chat/prompt/InputPromptLexical.d.ts +7 -1
- package/lib/chat/prompt/InputPromptLexical.js +5 -3
- package/lib/components/teams/index.d.ts +1 -0
- package/lib/components/teams/index.js +1 -0
- package/lib/components/teams/usePeerSandbox.d.ts +13 -0
- package/lib/components/teams/usePeerSandbox.js +59 -0
- package/lib/examples/AgentA2ATeamExample.js +7 -2
- package/lib/loop/apps/AppComposer.d.ts +31 -0
- package/lib/loop/apps/AppComposer.js +67 -0
- package/lib/loop/apps/AppRenderer.d.ts +16 -5
- package/lib/loop/apps/AppRenderer.js +49 -16
- package/lib/loop/apps/appspec.d.ts +8 -1
- package/lib/loop/apps/appspec.js +119 -2
- package/lib/loop/apps/checks.js +161 -1
- package/lib/loop/apps/composer.d.ts +37 -0
- package/lib/loop/apps/composer.js +61 -0
- package/lib/loop/apps/hostTools.d.ts +17 -0
- package/lib/loop/apps/hostTools.js +20 -0
- package/lib/loop/apps/index.d.ts +2 -0
- package/lib/loop/apps/index.js +2 -0
- package/lib/loop/apps/rules.d.ts +22 -1
- package/lib/loop/apps/rules.js +49 -1
- package/lib/loop/core/index.d.ts +31 -0
- package/lib/loop/core/index.js +22 -0
- package/lib/loop/embed/AppEmbed.d.ts +100 -6
- package/lib/loop/embed/AppEmbed.js +172 -27
- package/lib/loop/embed/LoopEmbed.js +2 -0
- package/lib/loop/embed/element.js +58 -5
- package/lib/loop/embed/embedConfig.d.ts +10 -1
- package/lib/loop/embed/embedConfig.js +12 -1
- package/lib/loop/embed/embedSession.d.ts +79 -0
- package/lib/loop/embed/embedSession.js +136 -0
- package/lib/loop/embed/embedTheme.d.ts +12 -9
- package/lib/loop/embed/embedTheme.js +32 -22
- package/lib/loop/embed/hostBridge.d.ts +79 -0
- package/lib/loop/embed/hostBridge.js +104 -0
- package/lib/loop/index.d.ts +5 -3
- package/lib/loop/index.js +6 -3
- package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.d.ts +5 -2
- package/lib/loop/plugins/a2ui-surface/SurfaceToolResult.js +7 -1
- package/lib/loop/plugins/a2ui-surface/index.d.ts +1 -1
- package/lib/loop/plugins/a2ui-surface/index.js +4 -4
- package/lib/loop/plugins/a2ui-surface/toolResult.d.ts +27 -0
- package/lib/loop/plugins/a2ui-surface/toolResult.js +22 -0
- package/lib/loop/plugins/agent-capacity/index.d.ts +3 -1
- package/lib/loop/plugins/agent-capacity/index.js +2 -1
- package/lib/loop/plugins/chat/ChatView.js +31 -4
- package/lib/loop/plugins/chat/index.d.ts +6 -1
- package/lib/loop/presets.d.ts +3 -1
- package/lib/loop/presets.js +2 -1
- package/lib/loop/shell/LoopWorkspace.js +3 -0
- package/lib/protocols/BrowserAgentAdapter.d.ts +18 -0
- package/lib/protocols/BrowserAgentAdapter.js +35 -2
- package/lib/runtimes/__tests__/fixtures/accounting-agent-card.json +8 -0
- package/lib/runtimes/browser/a2aPeer.d.ts +11 -0
- package/lib/runtimes/browser/a2aPeer.js +15 -1
- package/lib/runtimes/browser/protocol.d.ts +3 -0
- package/lib/runtimes/browser/protocol.js +2 -1
- package/lib/specs/agents/agents.js +6 -25
- package/lib/specs/apps.js +50 -8
- package/lib/specs/appspecSchema.js +258 -0
- package/lib/types/agentspecs.d.ts +86 -0
- package/lib/types/chat.d.ts +29 -1
- package/package.json +1 -1
- package/scripts/codegen/generate_agents.py +4 -2
- package/scripts/codegen/generate_apps.py +7 -0
- package/scripts/codegen/generate_backend_tools.py +4 -12
- package/scripts/codegen/generate_cogs.py +3 -1
- package/scripts/codegen/generate_ui_plugins.py +22 -9
- package/scripts/voice/transcribe.mjs +29 -21
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import type { CSSProperties, JSX, ReactNode } from 'react';
|
|
2
2
|
import type { PluginRef } from '@datalayer/reactor';
|
|
3
|
-
import type { AppAccent, AppEmbedMode, AppSpec } from '../../types/agentspecs';
|
|
4
|
-
import {
|
|
3
|
+
import type { AppAccent, AppEmbedMode, AppSpec, AppThemeVariant } from '../../types/agentspecs';
|
|
4
|
+
import type { ResumedThread, ThemeOverrides } from '../../types/chat';
|
|
5
|
+
import type { AssistantCharacter } from '../../chat/assistant/characters';
|
|
6
|
+
import type { AssistantCharacterData } from '../../chat/assistant/formats/types';
|
|
7
|
+
import type { PresenceState } from '../../chat/presence/presenceStatus';
|
|
8
|
+
import { type AppInstance, type AppRendererProps } from '../apps/AppRenderer';
|
|
9
|
+
import type { ChatSaid } from '../plugins/chat';
|
|
5
10
|
import { type AssistantCharacterChosen } from '../plugins/assistant-characters';
|
|
6
11
|
import { type EmbedColorMode } from './embedConfig';
|
|
12
|
+
import { type AppEmbedHost } from './hostBridge';
|
|
7
13
|
export type AppEmbedProps = {
|
|
8
14
|
/** The application. */
|
|
9
15
|
app: AppSpec;
|
|
@@ -11,7 +17,10 @@ export type AppEmbedProps = {
|
|
|
11
17
|
mode?: AppEmbedMode;
|
|
12
18
|
/** The host's accent; the application's own by default. */
|
|
13
19
|
accent?: AppAccent;
|
|
14
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Light, dark, or the visitor's system: the host's, else the one the
|
|
22
|
+
* Appspec names (`interface.theme.mode`, T-30), else the system's.
|
|
23
|
+
*/
|
|
15
24
|
colorMode?: EmbedColorMode;
|
|
16
25
|
/** The host's face, a CSS font family; the theme's by default. */
|
|
17
26
|
font?: string;
|
|
@@ -44,6 +53,54 @@ export type AppEmbedProps = {
|
|
|
44
53
|
* element passes none.
|
|
45
54
|
*/
|
|
46
55
|
plugins?: PluginRef[];
|
|
56
|
+
/**
|
|
57
|
+
* What the page gives the application (LOOP D-10): the values it passes,
|
|
58
|
+
* its functions, and what it is told — read for the names the Appspec
|
|
59
|
+
* gives under `deployment.embedded.host`.
|
|
60
|
+
*/
|
|
61
|
+
host?: AppEmbedHost;
|
|
62
|
+
/**
|
|
63
|
+
* Keep the visit's session in the host's storage, and pick it up again
|
|
64
|
+
* after the page reloads (LOOP D-13); on by default. Off, the session
|
|
65
|
+
* lasts as long as the page.
|
|
66
|
+
*/
|
|
67
|
+
resume?: boolean;
|
|
68
|
+
};
|
|
69
|
+
/** What `useEmbedSession` gives the conversation. */
|
|
70
|
+
export type EmbedSession = {
|
|
71
|
+
/** Whether the kept session was asked for: nothing is drawn before. */
|
|
72
|
+
ready: boolean;
|
|
73
|
+
/** The thread the chat goes on with; none where no session is kept. */
|
|
74
|
+
thread?: ResumedThread;
|
|
75
|
+
/** Said once, when the session kept is gone: a new one started. */
|
|
76
|
+
said?: string;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* The visit's session, kept and picked up again (LOOP D-13): on the host's
|
|
80
|
+
* server with an embed token that names its visit, the session the chat
|
|
81
|
+
* opens is kept under the application and the visit — in the host's
|
|
82
|
+
* storage unless `resume` is off, in memory always — and, when the
|
|
83
|
+
* application is drawn again (the page reloaded, or a token renewed for the
|
|
84
|
+
* visit), its conversation is asked for and drawn again. One that is gone
|
|
85
|
+
* or refused is forgotten, said once, and a new one starts. Elsewhere —
|
|
86
|
+
* on a Datalayer runtime, without a token — nothing is kept.
|
|
87
|
+
*/
|
|
88
|
+
export declare function useEmbedSession({ app, serverUrl, embedToken, resume, }: {
|
|
89
|
+
app: Pick<AppSpec, 'id' | 'name'>;
|
|
90
|
+
serverUrl?: string;
|
|
91
|
+
embedToken?: string;
|
|
92
|
+
resume?: boolean;
|
|
93
|
+
}): EmbedSession;
|
|
94
|
+
/**
|
|
95
|
+
* The page and the application talking (LOOP D-10): the tools of the host
|
|
96
|
+
* the Appspec names, given to its agent through a chat-extras plugin made
|
|
97
|
+
* once — the page's newest values and functions read at each call — and
|
|
98
|
+
* `message` said to the page when an answer is done.
|
|
99
|
+
*/
|
|
100
|
+
export declare function useHostBridge(app: AppSpec, host: AppEmbedHost | undefined): {
|
|
101
|
+
plugins: PluginRef[];
|
|
102
|
+
onPresence: (state: PresenceState) => void;
|
|
103
|
+
onSaying: (said: ChatSaid) => void;
|
|
47
104
|
};
|
|
48
105
|
/**
|
|
49
106
|
* The character the assistant mode draws (T-24, D-07): the application's own
|
|
@@ -54,13 +111,50 @@ export type AppEmbedProps = {
|
|
|
54
111
|
export declare function embedAssistantCharacter(app: AppSpec, plugins?: PluginRef[]): AssistantCharacterChosen;
|
|
55
112
|
/** What a floating application says to a visitor Datalayer does not know. */
|
|
56
113
|
export declare const signedOutSentence: (app: Pick<AppSpec, "name">) => string;
|
|
57
|
-
|
|
58
|
-
|
|
114
|
+
export type AppFloatingProps = {
|
|
115
|
+
app: AppSpec;
|
|
116
|
+
view: 'floating-small' | 'panel' | 'assistant';
|
|
117
|
+
colorMode: 'light' | 'dark';
|
|
118
|
+
/** The theme it is drawn in (T-30); the page's when unsaid. */
|
|
119
|
+
themeVariant?: AppThemeVariant;
|
|
120
|
+
/** The accent and face inside the conversation. */
|
|
121
|
+
themeOverrides: ThemeOverrides;
|
|
122
|
+
/** The assistant's character, in the assistant mode. */
|
|
123
|
+
character?: AssistantCharacter | AssistantCharacterData;
|
|
124
|
+
/** The host's agent-runtimes server, when it names one. */
|
|
125
|
+
serverUrl?: string;
|
|
126
|
+
instance?: AppInstance;
|
|
127
|
+
/** The visit's embed token, on the host's server (R-20). */
|
|
128
|
+
embedToken?: string;
|
|
129
|
+
/**
|
|
130
|
+
* What else the host gives the application's `AppRenderer`: Datalayer's
|
|
131
|
+
* own page at an application's address (T-21) passes the UI plugins its
|
|
132
|
+
* organization turned off, the visitors' runtime and the one kept on.
|
|
133
|
+
*/
|
|
134
|
+
renderer?: Partial<Pick<AppRendererProps, 'pluginsOff' | 'datalayerVisitors' | 'datalayerKept'>>;
|
|
135
|
+
/** What the page gives the application (D-10). */
|
|
136
|
+
host?: AppEmbedHost;
|
|
137
|
+
/** The visit's session, picked up again after a reload (D-13). */
|
|
138
|
+
session?: EmbedSession;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* The bubble, the panel and the assistant: `ChatFloating`'s chrome in that
|
|
142
|
+
* mode — the button, its blink and its balloon, the panel at the edge, the
|
|
143
|
+
* character dragged about and sent away — holding the application as
|
|
144
|
+
* `AppRenderer` draws it inline (R-01): its kind's preset, its layout, its
|
|
145
|
+
* agent created with its spec and spoken to at its session endpoint (R-04).
|
|
146
|
+
* The workspace tells the chrome what the application is doing and what it
|
|
147
|
+
* last said (`onPresence`, `onSaying`).
|
|
148
|
+
*/
|
|
149
|
+
export declare function AppFloating({ app, view, colorMode, themeVariant, themeOverrides, character, serverUrl, instance, embedToken, renderer, host, session, }: AppFloatingProps): JSX.Element;
|
|
150
|
+
/** The application's theme (`loop` unless it names one), with its (or the host's) accent and face. */
|
|
151
|
+
export declare function EmbedThemed({ accent, colorMode, font, variant, children, style, }: {
|
|
59
152
|
accent: AppAccent;
|
|
60
153
|
colorMode: 'light' | 'dark';
|
|
61
154
|
font?: string;
|
|
155
|
+
variant: AppThemeVariant;
|
|
62
156
|
children: ReactNode;
|
|
63
157
|
style?: CSSProperties;
|
|
64
158
|
}): JSX.Element;
|
|
65
|
-
export declare function AppEmbed({ app, mode, accent, colorMode, font, serverUrl, instance, embedToken, height, ownPortal, plugins, }: AppEmbedProps): JSX.Element;
|
|
159
|
+
export declare function AppEmbed({ app, mode, accent, colorMode, font, serverUrl, instance, embedToken, height, ownPortal, plugins, host, resume, }: AppEmbedProps): JSX.Element;
|
|
66
160
|
export default AppEmbed;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/*
|
|
3
3
|
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
4
|
* Distributed under the terms of the Modified BSD License.
|
|
@@ -33,18 +33,145 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
33
33
|
* host may override with the face and the mode (D-11) — around the
|
|
34
34
|
* conversation and inside it, where the chat sets its theme again.
|
|
35
35
|
*
|
|
36
|
+
* On the host's server with the visit's embed token, its session is kept
|
|
37
|
+
* in the host's storage and picked up again after the page reloads (D-13,
|
|
38
|
+
* `useEmbedSession`): its conversation drawn again, and gone on with.
|
|
39
|
+
*
|
|
36
40
|
* @module loop/embed/AppEmbed
|
|
37
41
|
*/
|
|
38
|
-
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
42
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
39
43
|
import { Text, registerPortalRoot } from '@primer/react';
|
|
40
44
|
import { useIAMStore } from '@datalayer/core/lib/state/substates/IAMState';
|
|
41
45
|
import { FluentEmoji } from '@datalayer/core/lib/components/emoji';
|
|
42
|
-
import { DatalayerThemeProvider,
|
|
46
|
+
import { DatalayerThemeProvider, getThemeConfig, } from '@datalayer/primer-addons';
|
|
47
|
+
import { generateMessageId } from '../../types/messages';
|
|
43
48
|
import { ChatFloating } from '../../chat/ChatFloating';
|
|
44
|
-
import { AppRenderer } from '../apps/AppRenderer';
|
|
49
|
+
import { AppRenderer, } from '../apps/AppRenderer';
|
|
45
50
|
import { AssistantCharactersPlugin, assistantCharacterFor, assistantCharactersFrom, } from '../plugins/assistant-characters';
|
|
46
51
|
import { floatingViewOf } from './embedConfig';
|
|
52
|
+
import { createChatExtrasPlugin } from '../plugins/chat-extras';
|
|
53
|
+
import { hostFrontendTools } from './hostBridge';
|
|
47
54
|
import { embedThemeOverrides, embedThemeStyles } from './embedTheme';
|
|
55
|
+
import { embedSessionKey, reattachSession, sessionGoneSentence, sessionKeeper, visitOfToken, } from './embedSession';
|
|
56
|
+
/**
|
|
57
|
+
* The visit's session, kept and picked up again (LOOP D-13): on the host's
|
|
58
|
+
* server with an embed token that names its visit, the session the chat
|
|
59
|
+
* opens is kept under the application and the visit — in the host's
|
|
60
|
+
* storage unless `resume` is off, in memory always — and, when the
|
|
61
|
+
* application is drawn again (the page reloaded, or a token renewed for the
|
|
62
|
+
* visit), its conversation is asked for and drawn again. One that is gone
|
|
63
|
+
* or refused is forgotten, said once, and a new one starts. Elsewhere —
|
|
64
|
+
* on a Datalayer runtime, without a token — nothing is kept.
|
|
65
|
+
*/
|
|
66
|
+
export function useEmbedSession({ app, serverUrl, embedToken, resume = true, }) {
|
|
67
|
+
const visit = serverUrl && embedToken ? visitOfToken(embedToken) : undefined;
|
|
68
|
+
const key = visit ? embedSessionKey(app.id, visit) : undefined;
|
|
69
|
+
const keeper = useMemo(() => sessionKeeper({ persist: resume }), [resume]);
|
|
70
|
+
// What was found, for the token it was found with: a renewed token asks again.
|
|
71
|
+
const [found, setFound] = useState();
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!key || !serverUrl || !embedToken) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
let cancelled = false;
|
|
77
|
+
const onStarted = (uid) => {
|
|
78
|
+
keeper.keep(key, uid);
|
|
79
|
+
setFound(before => before?.said ? { ...before, said: undefined } : before);
|
|
80
|
+
};
|
|
81
|
+
const fresh = (said) => setFound({
|
|
82
|
+
token: embedToken,
|
|
83
|
+
thread: { id: generateMessageId(), messages: [], onStarted },
|
|
84
|
+
...(said ? { said } : {}),
|
|
85
|
+
});
|
|
86
|
+
const kept = keeper.read(key);
|
|
87
|
+
if (!kept) {
|
|
88
|
+
fresh();
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
void reattachSession({ serverUrl, uid: kept, token: embedToken }).then(reattached => {
|
|
92
|
+
if (cancelled) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (reattached.kind === 'resumed') {
|
|
96
|
+
setFound({
|
|
97
|
+
token: embedToken,
|
|
98
|
+
thread: { id: kept, messages: reattached.messages, onStarted },
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
keeper.forget(key);
|
|
103
|
+
fresh(sessionGoneSentence(app.name));
|
|
104
|
+
});
|
|
105
|
+
return () => {
|
|
106
|
+
cancelled = true;
|
|
107
|
+
};
|
|
108
|
+
// The application's name is only said.
|
|
109
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
110
|
+
}, [key, serverUrl, embedToken, keeper]);
|
|
111
|
+
if (!key) {
|
|
112
|
+
return { ready: true };
|
|
113
|
+
}
|
|
114
|
+
if (!found || found.token !== embedToken) {
|
|
115
|
+
return { ready: false };
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
ready: true,
|
|
119
|
+
thread: found.thread,
|
|
120
|
+
...(found.said ? { said: found.said } : {}),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** What the embed says once of its session, above the conversation. */
|
|
124
|
+
function SessionSaid({ said }) {
|
|
125
|
+
return said ? (_jsx(Text, { as: "p", role: "status", sx: { px: 3, py: 2, m: 0, fontSize: 1 }, children: said })) : null;
|
|
126
|
+
}
|
|
127
|
+
/** No plugins for the renderer: one array, so that it never reads as a change. */
|
|
128
|
+
const NO_HOST_PLUGINS = [];
|
|
129
|
+
/**
|
|
130
|
+
* The page and the application talking (LOOP D-10): the tools of the host
|
|
131
|
+
* the Appspec names, given to its agent through a chat-extras plugin made
|
|
132
|
+
* once — the page's newest values and functions read at each call — and
|
|
133
|
+
* `message` said to the page when an answer is done.
|
|
134
|
+
*/
|
|
135
|
+
export function useHostBridge(app, host) {
|
|
136
|
+
const hostRef = useRef(host);
|
|
137
|
+
hostRef.current = host;
|
|
138
|
+
const handle = useMemo(() => createChatExtrasPlugin(), []);
|
|
139
|
+
const bridge = app.deployment.embedded?.host;
|
|
140
|
+
const key = JSON.stringify([bridge ?? null, app.rules, app.connections]);
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
handle.setExtras({
|
|
143
|
+
frontendTools: hostFrontendTools(app, () => hostRef.current ?? {}),
|
|
144
|
+
});
|
|
145
|
+
// The tools change with what the Appspec names, and the rules that decide them.
|
|
146
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
147
|
+
}, [handle, key]);
|
|
148
|
+
const plugins = useMemo(() => (bridge ? [handle.plugin] : NO_HOST_PLUGINS), [bridge, handle]);
|
|
149
|
+
const said = useRef({ answering: false });
|
|
150
|
+
const before = useRef('idle');
|
|
151
|
+
const told = useRef('');
|
|
152
|
+
const onSaying = useCallback((next) => {
|
|
153
|
+
said.current = next;
|
|
154
|
+
}, []);
|
|
155
|
+
const onPresence = useCallback((state) => {
|
|
156
|
+
const was = before.current;
|
|
157
|
+
before.current = state;
|
|
158
|
+
const done = state === 'idle' || state === 'waiting';
|
|
159
|
+
const saying = said.current.saying;
|
|
160
|
+
if (done &&
|
|
161
|
+
was !== 'idle' &&
|
|
162
|
+
was !== 'waiting' &&
|
|
163
|
+
said.current.answering &&
|
|
164
|
+
saying &&
|
|
165
|
+
saying.id !== told.current) {
|
|
166
|
+
told.current = saying.id;
|
|
167
|
+
hostRef.current?.onEvent?.({
|
|
168
|
+
type: 'message',
|
|
169
|
+
detail: { id: saying.id, text: saying.markdown || saying.text },
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}, []);
|
|
173
|
+
return { plugins, onPresence, onSaying };
|
|
174
|
+
}
|
|
48
175
|
/** No host plugins: one array, so that it never reads as a change. */
|
|
49
176
|
const NO_PLUGINS = [];
|
|
50
177
|
/**
|
|
@@ -72,25 +199,35 @@ function Face({ emoji }) {
|
|
|
72
199
|
* The workspace tells the chrome what the application is doing and what it
|
|
73
200
|
* last said (`onPresence`, `onSaying`).
|
|
74
201
|
*/
|
|
75
|
-
function
|
|
202
|
+
export function AppFloating({ app, view, colorMode, themeVariant, themeOverrides, character, serverUrl, instance, embedToken, renderer, host, session, }) {
|
|
76
203
|
const [presence, setPresence] = useState('idle');
|
|
77
204
|
const [said, setSaid] = useState({ answering: false });
|
|
205
|
+
const bridge = useHostBridge(app, host);
|
|
206
|
+
const onPresence = useCallback((state) => {
|
|
207
|
+
setPresence(state);
|
|
208
|
+
bridge.onPresence(state);
|
|
209
|
+
}, [bridge.onPresence]);
|
|
210
|
+
const onSaying = useCallback((next) => {
|
|
211
|
+
setSaid(next);
|
|
212
|
+
bridge.onSaying(next);
|
|
213
|
+
}, [bridge.onSaying]);
|
|
78
214
|
const welcome = app.interface.welcome || app.description;
|
|
79
215
|
// A runtime is somebody's to pay for: on Datalayer, only for a visitor
|
|
80
216
|
// Datalayer knows — said, and nothing launched, for one it does not.
|
|
81
217
|
const token = useIAMStore(state => state.token);
|
|
82
|
-
|
|
218
|
+
// Signed out on the visitors' runtime (R-30) is somebody's too.
|
|
219
|
+
const signedOut = !serverUrl && !token && !renderer?.datalayerVisitors;
|
|
83
220
|
return (_jsx(ChatFloating, { defaultViewMode: view, ...(character ? { assistantCharacter: character } : {}), ...(app.interface.balloon
|
|
84
221
|
? { balloonDisplay: app.interface.balloon }
|
|
85
222
|
: {}), title: app.name, description: welcome, brandIcon: _jsx(Face, { emoji: app.emoji }), buttonIcon: _jsx(Face, { emoji: app.emoji }), buttonTooltip: `Talk to ${app.name}`, colorMode: colorMode, useStore: false, conversation: {
|
|
86
|
-
body: signedOut ? (_jsx(Text, { as: "p", role: "status", sx: { p: 3, m: 0, fontSize: 1 }, children: signedOutSentence(app) })) : (_jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), ...(serverUrl && embedToken ? { embedToken } : {}), instance: instance,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
223
|
+
body: signedOut ? (_jsx(Text, { as: "p", role: "status", sx: { p: 3, m: 0, fontSize: 1 }, children: signedOutSentence(app) })) : session && !session.ready ? null : (_jsxs(_Fragment, { children: [_jsx(SessionSaid, { said: session?.said }), _jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), ...(serverUrl && embedToken ? { embedToken } : {}), instance: instance,
|
|
224
|
+
// The host's accent and face over the application's own, inside
|
|
225
|
+
// its conversation too, in the embed's mode.
|
|
226
|
+
themeOverrides: themeOverrides, colorMode: colorMode, ...(themeVariant ? { themeVariant } : {}),
|
|
227
|
+
// The window draws its face, its name and close.
|
|
228
|
+
hideChatHeader: true,
|
|
229
|
+
// Mounted closed: the caret goes in when the window opens.
|
|
230
|
+
autoFocusPrompt: false, onPresence: onPresence, onSaying: onSaying, plugins: bridge.plugins, ...(session?.thread ? { thread: session.thread } : {}), ...renderer })] })),
|
|
94
231
|
presence,
|
|
95
232
|
saying: said.saying,
|
|
96
233
|
answering: said.answering,
|
|
@@ -127,9 +264,9 @@ function useSystemMode() {
|
|
|
127
264
|
}, [query]);
|
|
128
265
|
return dark ? 'dark' : 'light';
|
|
129
266
|
}
|
|
130
|
-
/** The `loop`
|
|
131
|
-
export function EmbedThemed({ accent, colorMode, font, children, style, }) {
|
|
132
|
-
const themeStyles = useMemo(() => embedThemeStyles({ accent, font }), [accent, font]);
|
|
267
|
+
/** The application's theme (`loop` unless it names one), with its (or the host's) accent and face. */
|
|
268
|
+
export function EmbedThemed({ accent, colorMode, font, variant, children, style, }) {
|
|
269
|
+
const themeStyles = useMemo(() => embedThemeStyles({ accent, font, variant }), [accent, font, variant]);
|
|
133
270
|
return (
|
|
134
271
|
/*
|
|
135
272
|
* Marked as themed above the provider, so that the provider counts
|
|
@@ -137,15 +274,21 @@ export function EmbedThemed({ accent, colorMode, font, children, style, }) {
|
|
|
137
274
|
* alone: it would otherwise write the theme's tokens, colour and face on
|
|
138
275
|
* the host's page (T-13). A wrapper of no box.
|
|
139
276
|
*/
|
|
140
|
-
_jsx("div", { "data-color-mode": colorMode, style: { display: 'contents' }, children: _jsx(DatalayerThemeProvider, { colorMode: colorMode, theme:
|
|
277
|
+
_jsx("div", { "data-color-mode": colorMode, style: { display: 'contents' }, children: _jsx(DatalayerThemeProvider, { colorMode: colorMode, theme: getThemeConfig(variant).primerTheme, themeStyles: themeStyles, baseStyles: style, children: children }) }));
|
|
141
278
|
}
|
|
142
|
-
export function AppEmbed({ app, mode, accent, colorMode
|
|
279
|
+
export function AppEmbed({ app, mode, accent, colorMode, font, serverUrl, instance, embedToken, height = 640, ownPortal = false, plugins = NO_PLUGINS, host, resume = true, }) {
|
|
280
|
+
const bridge = useHostBridge(app, host);
|
|
281
|
+
const session = useEmbedSession({ app, serverUrl, embedToken, resume });
|
|
143
282
|
const system = useSystemMode();
|
|
144
|
-
|
|
283
|
+
// The theme the application names (T-30), `loop` when none; its mode
|
|
284
|
+
// the host's, else the application's, else the visitor's system's.
|
|
285
|
+
const variant = app.interface.theme?.variant ?? 'loop';
|
|
286
|
+
const asked = colorMode ?? app.interface.theme?.mode ?? 'auto';
|
|
287
|
+
const resolvedMode = asked === 'auto' ? system : asked;
|
|
145
288
|
const shownAs = mode ?? app.deployment.embedded?.mode ?? 'inline';
|
|
146
289
|
const view = floatingViewOf(shownAs);
|
|
147
290
|
const worn = accent ?? app.interface.accent;
|
|
148
|
-
const themeOverrides = useMemo(() => embedThemeOverrides({ accent: worn, font }), [worn, font]);
|
|
291
|
+
const themeOverrides = useMemo(() => embedThemeOverrides({ accent: worn, font, variant }), [worn, font, variant]);
|
|
149
292
|
const named = app.interface.assistant;
|
|
150
293
|
const assistant = useMemo(() => view === 'assistant' ? embedAssistantCharacter(app, plugins) : undefined,
|
|
151
294
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -154,13 +297,15 @@ export function AppEmbed({ app, mode, accent, colorMode = 'auto', font, serverUr
|
|
|
154
297
|
return (_jsx("p", { role: "status", className: "datalayer-app-said", children: assistant.problem }));
|
|
155
298
|
}
|
|
156
299
|
const character = assistant?.character;
|
|
157
|
-
return (_jsxs(EmbedThemed, { accent: worn, colorMode: resolvedMode, font: font, style: view
|
|
300
|
+
return (_jsxs(EmbedThemed, { accent: worn, colorMode: resolvedMode, font: font, variant: variant, style: view
|
|
158
301
|
? // Floating: nothing in the page's flow, nothing painted behind.
|
|
159
302
|
{ backgroundColor: 'transparent' }
|
|
160
|
-
: { height, display: 'flex', flexDirection: 'column' }, children: [ownPortal ? _jsx(OwnPortalRoot, {}) : null, view ? (_jsx(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
303
|
+
: { height, display: 'flex', flexDirection: 'column' }, children: [ownPortal ? _jsx(OwnPortalRoot, {}) : null, view ? (_jsx(AppFloating, { app: app, view: view, serverUrl: serverUrl, instance: instance, embedToken: embedToken, colorMode: resolvedMode, themeVariant: variant, themeOverrides: themeOverrides, character: character, host: host, session: session })) : !session.ready ? null : (_jsxs(_Fragment, { children: [_jsx(SessionSaid, { said: session.said }), _jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), ...(serverUrl && embedToken ? { embedToken } : {}), instance: instance,
|
|
304
|
+
// The host's accent and face over the application's own, inside
|
|
305
|
+
// its conversation too, in the embed's mode: the host's or its
|
|
306
|
+
// visitor's system's, not a Datalayer setting.
|
|
307
|
+
themeOverrides: themeOverrides, colorMode: resolvedMode, themeVariant: variant,
|
|
308
|
+
// What the page gives it, and what it tells the page (D-10).
|
|
309
|
+
plugins: bridge.plugins, onPresence: bridge.onPresence, onSaying: bridge.onSaying, ...(session.thread ? { thread: session.thread } : {}) })] }))] }));
|
|
165
310
|
}
|
|
166
311
|
export default AppEmbed;
|
|
@@ -74,6 +74,8 @@ export function LoopEmbed({ agentId = '', showHeader = false, chatHeaderActions,
|
|
|
74
74
|
preset.presence?.welcome,
|
|
75
75
|
preset.presence?.paused,
|
|
76
76
|
preset.showTokenUsage,
|
|
77
|
+
// The thread a reloaded embed goes on with, by its id (D-13).
|
|
78
|
+
preset.thread?.id,
|
|
77
79
|
// By its content: an application's spec written inline is a new object each render.
|
|
78
80
|
JSON.stringify(preset.datalayerCreatePayload ?? null),
|
|
79
81
|
JSON.stringify(preset.datalayerVisitors ?? null),
|
|
@@ -29,16 +29,32 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
29
29
|
* drawn in it, so nothing of the application's styles reaches the page and
|
|
30
30
|
* nothing of the page's reaches the application.
|
|
31
31
|
*
|
|
32
|
+
* The page and the application talk (LOOP D-10), for the names its Appspec
|
|
33
|
+
* gives under `deployment.embedded.host`: the page sets `element.context`
|
|
34
|
+
* (`{ user, page, …its own }`), which its agent reads with `host_context`,
|
|
35
|
+
* and `element.functions` (`{ open_ticket: async args => … }`), which its
|
|
36
|
+
* agent calls as `host_open_ticket`, each as the rule naming it decides; the
|
|
37
|
+
* element raises `message` when the application has answered and `action`
|
|
38
|
+
* when it called a function of the page, beside `decision` and
|
|
39
|
+
* `token-expired`.
|
|
40
|
+
*
|
|
41
|
+
* On the host's server (`server`) with the visit's embed token, the
|
|
42
|
+
* session the application opens is kept in the page's `localStorage`, under
|
|
43
|
+
* the application and the visit its token names, and picked up again after
|
|
44
|
+
* the page reloads — its conversation drawn again (LOOP D-13); a token
|
|
45
|
+
* renewed for the visit redraws the application on the same session.
|
|
46
|
+
* `resume="false"` keeps nothing in the page's storage.
|
|
47
|
+
*
|
|
32
48
|
* @module loop/embed/element
|
|
33
49
|
*/
|
|
34
50
|
import { createRoot } from 'react-dom/client';
|
|
35
51
|
import { StyleSheetManager } from 'styled-components';
|
|
36
|
-
import {
|
|
52
|
+
import { getThemeConfig } from '@datalayer/primer-addons';
|
|
37
53
|
import { coreStore } from '@datalayer/core/lib/state/substates/CoreState';
|
|
38
54
|
import { parseAppspec } from '../apps/appspec';
|
|
39
55
|
import { readAppspecYaml } from '../apps/yaml';
|
|
40
56
|
import { AppEmbed, embedAssistantCharacter } from './AppEmbed';
|
|
41
|
-
import { EMBED_HOST_VARIABLES, EMBED_OBSERVED_ATTRIBUTES, EMBED_TAG, EmbedAttributeError, embedLookOf, inlineHeightOf, } from './embedConfig';
|
|
57
|
+
import { EMBED_HOST_VARIABLES, EMBED_OBSERVED_ATTRIBUTES, EMBED_TAG, EmbedAttributeError, embedLookOf, inlineHeightOf, resumeOf, } from './embedConfig';
|
|
42
58
|
import { embedShadowCss } from './embedTheme';
|
|
43
59
|
/** What the framed run page and the element say to each other. */
|
|
44
60
|
export const EMBED_MESSAGES = {
|
|
@@ -174,6 +190,36 @@ export function defineDatalayerAppElement(options = {}) {
|
|
|
174
190
|
frame = null;
|
|
175
191
|
source = null;
|
|
176
192
|
loading = 0;
|
|
193
|
+
passed = {};
|
|
194
|
+
/** One object, its functions replaced in place: the host below holds it. */
|
|
195
|
+
offered = {};
|
|
196
|
+
/** What the page gives the application, read at each call (D-10). */
|
|
197
|
+
host = {
|
|
198
|
+
context: () => this.passed,
|
|
199
|
+
functions: this.offered,
|
|
200
|
+
onEvent: event => this.dispatchEvent(new CustomEvent(event.type, {
|
|
201
|
+
detail: event.detail,
|
|
202
|
+
bubbles: true,
|
|
203
|
+
composed: true,
|
|
204
|
+
})),
|
|
205
|
+
};
|
|
206
|
+
/** The values the page passes: `user`, `page`, its own (D-10). */
|
|
207
|
+
get context() {
|
|
208
|
+
return this.passed;
|
|
209
|
+
}
|
|
210
|
+
set context(value) {
|
|
211
|
+
this.passed = { ...(value ?? {}) };
|
|
212
|
+
}
|
|
213
|
+
/** The page's functions the application may call, by name (D-10). */
|
|
214
|
+
get functions() {
|
|
215
|
+
return this.offered;
|
|
216
|
+
}
|
|
217
|
+
set functions(value) {
|
|
218
|
+
for (const name of Object.keys(this.offered)) {
|
|
219
|
+
delete this.offered[name];
|
|
220
|
+
}
|
|
221
|
+
Object.assign(this.offered, value ?? {});
|
|
222
|
+
}
|
|
177
223
|
constructor() {
|
|
178
224
|
super();
|
|
179
225
|
this.attachShadow({ mode: 'open' });
|
|
@@ -210,7 +256,7 @@ export function defineDatalayerAppElement(options = {}) {
|
|
|
210
256
|
return;
|
|
211
257
|
}
|
|
212
258
|
// How it looks is drawn again; what it is, read again.
|
|
213
|
-
if (['mode', 'accent', 'theme', 'font', 'height'].includes(name)) {
|
|
259
|
+
if (['mode', 'accent', 'theme', 'font', 'height', 'resume'].includes(name)) {
|
|
214
260
|
this.draw();
|
|
215
261
|
return;
|
|
216
262
|
}
|
|
@@ -355,8 +401,10 @@ export function defineDatalayerAppElement(options = {}) {
|
|
|
355
401
|
return;
|
|
356
402
|
}
|
|
357
403
|
let look;
|
|
404
|
+
let resume;
|
|
358
405
|
try {
|
|
359
406
|
look = embedLookOf(this.inputs(), source.app);
|
|
407
|
+
resume = resumeOf(this.getAttribute('resume'));
|
|
360
408
|
}
|
|
361
409
|
catch (error) {
|
|
362
410
|
this.say(error.message);
|
|
@@ -372,13 +420,18 @@ export function defineDatalayerAppElement(options = {}) {
|
|
|
372
420
|
}
|
|
373
421
|
}
|
|
374
422
|
this.setAttribute('data-embed-mode', look.mode);
|
|
375
|
-
|
|
423
|
+
// The theme's own stylesheet: `loop`'s, or the one the application
|
|
424
|
+
// names (T-30).
|
|
425
|
+
const shadow = this.skeleton(getThemeConfig(look.variant).themeStyles.css);
|
|
376
426
|
const mount = document.createElement('div');
|
|
377
427
|
mount.className = 'datalayer-app-root';
|
|
378
428
|
shadow.appendChild(mount);
|
|
379
429
|
this.root = createRoot(mount);
|
|
380
430
|
const server = this.getAttribute('server') || undefined;
|
|
381
|
-
this.root.render(_jsx(StyleSheetManager, { target: shadow, children: _jsx(AppEmbed, { app: source.app, mode: look.mode, accent: look.accent, colorMode: look.colorMode, font: look.font, serverUrl: server, ...(server && this.token ? { embedToken: this.token } : {}), height: inlineHeightOf(this.getAttribute('height')), ownPortal: true
|
|
431
|
+
this.root.render(_jsx(StyleSheetManager, { target: shadow, children: _jsx(AppEmbed, { app: source.app, mode: look.mode, accent: look.accent, colorMode: look.colorMode, font: look.font, serverUrl: server, ...(server && this.token ? { embedToken: this.token } : {}), height: inlineHeightOf(this.getAttribute('height')), ownPortal: true, host: this.host,
|
|
432
|
+
// The visit's session kept in the page's storage, and picked
|
|
433
|
+
// up again after a reload, unless the host says not (D-13).
|
|
434
|
+
resume: resume }) }));
|
|
382
435
|
}
|
|
383
436
|
onMessage(event) {
|
|
384
437
|
if (!this.frame || event.source !== this.frame.contentWindow) {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @module loop/embed/embedConfig
|
|
13
13
|
*/
|
|
14
|
-
import type { AppAccent, AppEmbedMode, AppSpec } from '../../types/agentspecs';
|
|
14
|
+
import type { AppAccent, AppEmbedMode, AppSpec, AppThemeVariant } from '../../types/agentspecs';
|
|
15
15
|
/** The element's name: the one a host writes. */
|
|
16
16
|
export declare const EMBED_TAG = "datalayer-app";
|
|
17
17
|
/** Where the script is served on a Datalayer origin; the stylesheet beside it. */
|
|
@@ -33,6 +33,7 @@ export declare const EMBED_ATTRIBUTES: {
|
|
|
33
33
|
readonly server: "An agent-runtimes server the application’s agent runs on; a Datalayer runtime by default.";
|
|
34
34
|
readonly api: "Datalayer’s API, where an application is read with its token; the platform’s by default.";
|
|
35
35
|
readonly height: "The height of an inline application, in pixels; 640 by default.";
|
|
36
|
+
readonly resume: "true or false: whether a visit’s conversation is picked up again after the page reloads, kept in the page’s storage; true by default.";
|
|
36
37
|
};
|
|
37
38
|
export type EmbedAttribute = keyof typeof EMBED_ATTRIBUTES;
|
|
38
39
|
/** Every attribute the element watches. */
|
|
@@ -86,6 +87,8 @@ export type EmbedLook = {
|
|
|
86
87
|
colorMode: EmbedColorMode;
|
|
87
88
|
/** The host's face; empty for the theme's own. */
|
|
88
89
|
font: string;
|
|
90
|
+
/** The theme the application names (T-30); `loop` when it names none. */
|
|
91
|
+
variant: AppThemeVariant;
|
|
89
92
|
};
|
|
90
93
|
/**
|
|
91
94
|
* What the element shows and how, from what the host wrote and what the
|
|
@@ -94,6 +97,12 @@ export type EmbedLook = {
|
|
|
94
97
|
* sentence (`EmbedAttributeError`), never quietly replaced.
|
|
95
98
|
*/
|
|
96
99
|
export declare function embedLookOf(inputs: EmbedInputs, app?: Pick<AppSpec, 'interface' | 'deployment'>): EmbedLook;
|
|
100
|
+
/**
|
|
101
|
+
* Whether a visit's conversation is picked up again after a reload (LOOP
|
|
102
|
+
* D-13): on unless the host writes `resume="false"`. Anything else is
|
|
103
|
+
* refused in a sentence.
|
|
104
|
+
*/
|
|
105
|
+
export declare function resumeOf(value: string | null | undefined): boolean;
|
|
97
106
|
/** The height of an inline application, in pixels. */
|
|
98
107
|
export declare function inlineHeightOf(value: string | null | undefined): number;
|
|
99
108
|
/** What a snippet says beside the application's id. */
|
|
@@ -24,6 +24,7 @@ export const EMBED_ATTRIBUTES = {
|
|
|
24
24
|
server: 'An agent-runtimes server the application’s agent runs on; a Datalayer runtime by default.',
|
|
25
25
|
api: 'Datalayer’s API, where an application is read with its token; the platform’s by default.',
|
|
26
26
|
height: 'The height of an inline application, in pixels; 640 by default.',
|
|
27
|
+
resume: 'true or false: whether a visit’s conversation is picked up again after the page reloads, kept in the page’s storage; true by default.',
|
|
27
28
|
};
|
|
28
29
|
/** Every attribute the element watches. */
|
|
29
30
|
export const EMBED_OBSERVED_ATTRIBUTES = Object.keys(EMBED_ATTRIBUTES);
|
|
@@ -105,9 +106,19 @@ export function embedLookOf(inputs, app) {
|
|
|
105
106
|
'green';
|
|
106
107
|
const colorMode = oneOf(attributes.theme, EMBED_COLOR_MODES, 'a theme') ??
|
|
107
108
|
oneOf(variables.theme, EMBED_COLOR_MODES, 'a theme') ??
|
|
109
|
+
app?.interface?.theme?.mode ??
|
|
108
110
|
'auto';
|
|
109
111
|
const font = checkedFont(attributes.font) || checkedFont(variables.font);
|
|
110
|
-
|
|
112
|
+
const variant = app?.interface?.theme?.variant ?? 'loop';
|
|
113
|
+
return { mode, accent, colorMode, font, variant };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Whether a visit's conversation is picked up again after a reload (LOOP
|
|
117
|
+
* D-13): on unless the host writes `resume="false"`. Anything else is
|
|
118
|
+
* refused in a sentence.
|
|
119
|
+
*/
|
|
120
|
+
export function resumeOf(value) {
|
|
121
|
+
return (oneOf(value, ['true', 'false'], 'a value of resume') !== 'false');
|
|
111
122
|
}
|
|
112
123
|
/** The height of an inline application, in pixels. */
|
|
113
124
|
export function inlineHeightOf(value) {
|