@datalayer/agent-runtimes 1.3.74 → 1.3.78
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/agents/AgentDetails.d.ts +16 -2
- package/lib/agents/AgentDetails.js +381 -272
- package/lib/chat/ChatFloating.d.ts +27 -1
- package/lib/chat/ChatFloating.js +97 -13
- package/lib/chat/assistant/AgentDetailsDialog.d.ts +21 -0
- package/lib/chat/assistant/AgentDetailsDialog.js +19 -0
- package/lib/chat/assistant/AssistantContextMenu.d.ts +67 -0
- package/lib/chat/assistant/AssistantContextMenu.js +68 -0
- package/lib/chat/assistant/AssistantStage.d.ts +144 -4
- package/lib/chat/assistant/AssistantStage.js +280 -10
- package/lib/chat/assistant/BalloonParts.d.ts +33 -5
- package/lib/chat/assistant/BalloonParts.js +66 -44
- package/lib/chat/assistant/BalloonVisual.d.ts +10 -2
- package/lib/chat/assistant/BalloonVisual.js +4 -3
- package/lib/chat/assistant/SandboxDetailsDialog.d.ts +44 -0
- package/lib/chat/assistant/SandboxDetailsDialog.js +214 -0
- package/lib/chat/assistant/SpeechBalloon.d.ts +54 -6
- package/lib/chat/assistant/SpeechBalloon.js +73 -22
- package/lib/chat/assistant/assistantDetails.d.ts +84 -0
- package/lib/chat/assistant/assistantDetails.js +140 -0
- package/lib/chat/assistant/formats/files.d.ts +23 -0
- package/lib/chat/assistant/formats/files.js +48 -0
- package/lib/chat/assistant/formats/index.d.ts +2 -0
- package/lib/chat/assistant/formats/index.js +1 -0
- package/lib/chat/assistant/state.d.ts +7 -14
- package/lib/chat/assistant/state.js +21 -20
- package/lib/chat/assistant/toolLine.d.ts +16 -0
- package/lib/chat/assistant/toolLine.js +46 -0
- package/lib/chat/base/ChatBase.js +71 -1
- package/lib/chat/base/loopMessage.d.ts +37 -0
- package/lib/chat/base/loopMessage.js +37 -0
- package/lib/chat/indicators/TypingDots.d.ts +13 -0
- package/lib/chat/indicators/TypingDots.js +21 -0
- package/lib/chat/messages/ChatMarkdown.d.ts +16 -0
- package/lib/chat/messages/ChatMarkdown.js +18 -0
- package/lib/chat/messages/ChatMessageList.d.ts +8 -1
- package/lib/chat/messages/ChatMessageList.js +25 -33
- package/lib/chat/styles/streamdownStyles.js +3 -1
- package/lib/chat/tools/ToolCallDisplay.d.ts +8 -1
- package/lib/chat/tools/ToolCallDisplay.js +8 -7
- package/lib/components/code-sandboxes/CodeSandboxCellVariablesDialog.js +5 -11
- package/lib/components/code-sandboxes/CodeSandboxVariables.d.ts +8 -0
- package/lib/components/code-sandboxes/CodeSandboxVariables.js +17 -11
- package/lib/components/inspector/AgentInspector.d.ts +59 -0
- package/lib/components/inspector/AgentInspector.js +220 -0
- package/lib/components/inspector/AgentInspectorDialog.d.ts +19 -0
- package/lib/components/inspector/AgentInspectorDialog.js +9 -0
- package/lib/components/inspector/JsonTree.d.ts +24 -0
- package/lib/components/inspector/JsonTree.js +89 -0
- package/lib/components/inspector/a2aInspect.d.ts +56 -0
- package/lib/components/inspector/a2aInspect.js +478 -0
- package/lib/components/inspector/agentInspector.d.ts +179 -0
- package/lib/components/inspector/agentInspector.js +267 -0
- package/lib/components/inspector/chatInspect.d.ts +38 -0
- package/lib/components/inspector/chatInspect.js +65 -0
- package/lib/components/inspector/classify.d.ts +19 -0
- package/lib/components/inspector/classify.js +42 -0
- package/lib/components/inspector/index.d.ts +14 -0
- package/lib/components/inspector/index.js +18 -0
- package/lib/components/teams/A2ATeamGraph.d.ts +69 -1
- package/lib/components/teams/A2ATeamGraph.js +277 -37
- package/lib/components/teams/NotebookPreviewView.js +9 -1
- package/lib/components/teams/TeamNotebook.d.ts +3 -1
- package/lib/components/teams/TeamNotebook.js +3 -3
- package/lib/components/teams/TeamNotebookView.js +15 -0
- package/lib/components/teams/jupyterThemeVariables.d.ts +9 -0
- package/lib/components/teams/jupyterThemeVariables.js +441 -0
- package/lib/components/teams/teamConnections.d.ts +9 -1
- package/lib/components/teams/teamConnections.js +20 -0
- package/lib/components/teams/teamNotebookKernel.d.ts +3 -0
- package/lib/components/teams/teamNotebookKernel.js +17 -0
- package/lib/components/teams/useA2ATeam.d.ts +20 -3
- package/lib/components/teams/useA2ATeam.js +108 -23
- package/lib/examples/AgentA2ATeamExample.d.ts +13 -0
- package/lib/examples/AgentA2ATeamExample.js +67 -13
- package/lib/examples/AssistantExample.js +65 -26
- package/lib/examples/AssistantGalleryExample.js +2 -1
- package/lib/examples/exampleGroups.d.ts +1 -1
- package/lib/examples/exampleGroups.js +1 -1
- package/lib/examples/utils/assistantGallery.d.ts +7 -6
- package/lib/examples/utils/assistantGallery.js +23 -8
- package/lib/jupyter/exec/Python.d.ts +0 -1
- package/lib/jupyter/exec/Python.js +0 -32
- package/lib/jupyter/exec/Snippets.d.ts +4 -12
- package/lib/jupyter/exec/Snippets.js +0 -8
- package/lib/loop/apps/AppFeedback.js +3 -1
- package/lib/loop/apps/AppKept.d.ts +22 -0
- package/lib/loop/apps/AppKept.js +36 -0
- package/lib/loop/apps/AppRenderer.d.ts +11 -1
- package/lib/loop/apps/AppRenderer.js +22 -4
- package/lib/loop/apps/checks.d.ts +7 -0
- package/lib/loop/apps/checks.js +39 -0
- package/lib/loop/apps/index.d.ts +3 -0
- package/lib/loop/apps/index.js +3 -0
- package/lib/loop/apps/kept.d.ts +30 -0
- package/lib/loop/apps/kept.js +52 -0
- package/lib/loop/apps/records.d.ts +12 -4
- package/lib/loop/apps/records.js +13 -5
- package/lib/loop/apps/saved.d.ts +30 -0
- package/lib/loop/apps/saved.js +40 -0
- package/lib/loop/core/index.d.ts +17 -0
- package/lib/loop/core/index.js +1 -0
- package/lib/loop/embed/AppEmbed.d.ts +8 -1
- package/lib/loop/embed/AppEmbed.js +4 -4
- package/lib/loop/embed/LoopEmbed.d.ts +6 -1
- package/lib/loop/embed/LoopEmbed.js +4 -2
- package/lib/loop/embed/element.js +1 -1
- package/lib/loop/plugins/agents/DatalayerAgentBridge.js +12 -6
- package/lib/loop/plugins/agents/browserService.d.ts +7 -1
- package/lib/loop/plugins/agents/browserService.js +4 -0
- package/lib/loop/plugins/agents/plugin.d.ts +16 -0
- package/lib/loop/plugins/agents/switchable.d.ts +3 -0
- package/lib/loop/plugins/agents/switchable.js +1 -0
- package/lib/loop/plugins/app-activity/index.js +3 -0
- package/lib/loop/plugins/app-computer/index.js +3 -0
- package/lib/loop/plugins/app-rules/AppRulesCard.d.ts +5 -0
- package/lib/loop/plugins/app-rules/AppRulesCard.js +25 -6
- package/lib/loop/plugins/app-rules/index.js +3 -0
- package/lib/loop/plugins/assistant/index.d.ts +7 -2
- package/lib/loop/plugins/assistant/index.js +58 -4
- package/lib/loop/plugins/assistant-characters/index.d.ts +7 -0
- package/lib/loop/plugins/assistant-characters/index.js +17 -0
- package/lib/loop/plugins/chat/ChatView.js +4 -1
- package/lib/loop/plugins/page-layout/SplitLayout.d.ts +2 -1
- package/lib/loop/plugins/page-layout/SplitLayout.js +4 -0
- package/lib/loop/presets.d.ts +4 -0
- package/lib/loop/presets.js +3 -1
- package/lib/loop/shell/LoopWorkspace.d.ts +7 -0
- package/lib/loop/shell/LoopWorkspace.js +5 -4
- package/lib/loop/shell/SidebarRail.d.ts +53 -0
- package/lib/loop/shell/SidebarRail.js +74 -0
- package/lib/loop/shell/index.d.ts +2 -0
- package/lib/loop/shell/index.js +2 -0
- package/lib/loop/shell/paneMotion.d.ts +21 -0
- package/lib/loop/shell/paneMotion.js +37 -0
- package/lib/runtimes/browser/a2aPeer.d.ts +4 -1
- package/lib/specs/apps.js +8 -5
- package/lib/specs/uiPlugins.js +529 -0
- package/lib/types/agentspecs.d.ts +5 -3
- package/lib/types/chat.d.ts +12 -0
- package/package.json +2 -2
- package/scripts/codegen/generate_jupyter_theme_variables.mjs +57 -0
- package/scripts/codegen/generate_ui_plugins.py +112 -2
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/** The statuses of a sandbox that has something to read. */
|
|
6
|
+
const LIVE = new Set(['running', 'idle', 'busy', 'starting']);
|
|
7
|
+
/** Whether a sandbox is up: one with no status is taken to be. */
|
|
8
|
+
export function sandboxIsLive(sandbox) {
|
|
9
|
+
return sandbox.status === undefined || LIVE.has(sandbox.status);
|
|
10
|
+
}
|
|
11
|
+
/** What the dialog says of a sandbox that is not running yet. */
|
|
12
|
+
export const NO_SANDBOX_YET = 'No sandbox is running yet: run a cell or ask the agent to execute code.';
|
|
13
|
+
/** What each kind is called. */
|
|
14
|
+
export const SANDBOX_KIND_LABELS = {
|
|
15
|
+
browser: 'Python in your browser (Pyodide)',
|
|
16
|
+
local: 'Local Jupyter server',
|
|
17
|
+
jupyter: 'Jupyter server',
|
|
18
|
+
cloud: 'Datalayer cloud runtime',
|
|
19
|
+
runtime: 'In the agent runtime',
|
|
20
|
+
};
|
|
21
|
+
/** Whether a URL is on this machine. */
|
|
22
|
+
export function isLocalUrl(url) {
|
|
23
|
+
if (!url) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const host = new URL(url).hostname;
|
|
28
|
+
return (host === 'localhost' ||
|
|
29
|
+
host === '127.0.0.1' ||
|
|
30
|
+
host === '::1' ||
|
|
31
|
+
host === '[::1]' ||
|
|
32
|
+
host.endsWith('.localhost'));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Query parameters that carry a secret. */
|
|
39
|
+
const SECRET_PARAMS = /^(token|access_token|api_key|apikey|key|auth|password|secret|signature|sig|jwt)$/i;
|
|
40
|
+
/** A URL fit to show: no credentials, no token in its query. */
|
|
41
|
+
export function displayUrl(url) {
|
|
42
|
+
if (!url) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const parsed = new URL(url);
|
|
47
|
+
parsed.username = '';
|
|
48
|
+
parsed.password = '';
|
|
49
|
+
for (const name of [...parsed.searchParams.keys()]) {
|
|
50
|
+
if (SECRET_PARAMS.test(name)) {
|
|
51
|
+
parsed.searchParams.delete(name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return parsed.toString();
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Not a URL: shown up to its query, which is where a token would be.
|
|
58
|
+
return url.split('?')[0];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const text = (value) => typeof value === 'string' && value ? value : undefined;
|
|
62
|
+
/**
|
|
63
|
+
* A sandbox from the status its server reports — the Loop's sandbox
|
|
64
|
+
* service, or the agent's codemode status — and what the host adds.
|
|
65
|
+
*/
|
|
66
|
+
export function assistantSandboxOf(status, options = {}) {
|
|
67
|
+
if (!status) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const variant = text(status.variant);
|
|
71
|
+
const url = text(status.jupyter_url) ?? text(status.agent_base_url);
|
|
72
|
+
const running = status.sandbox_running === true || status.jupyter_connected === true;
|
|
73
|
+
if (!variant && !running) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const kind = variant === 'datalayer'
|
|
77
|
+
? 'cloud'
|
|
78
|
+
: variant === 'jupyter-server' || variant === 'jupyter'
|
|
79
|
+
? isLocalUrl(text(status.jupyter_url))
|
|
80
|
+
? 'local'
|
|
81
|
+
: 'jupyter'
|
|
82
|
+
: 'runtime';
|
|
83
|
+
return {
|
|
84
|
+
kind,
|
|
85
|
+
status: options.state ?? (running ? 'running' : 'stopped'),
|
|
86
|
+
variant,
|
|
87
|
+
url,
|
|
88
|
+
token: text(status.jupyter_token),
|
|
89
|
+
kernelId: text(status.kernel_id),
|
|
90
|
+
agentId: options.agentId,
|
|
91
|
+
environment: text(status.environment_name),
|
|
92
|
+
serverUrl: options.serverUrl,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Run code in an agent's sandbox through its agent-runtimes server
|
|
97
|
+
* (`POST /api/v1/sandbox/execute`), whatever the variant: what a page with
|
|
98
|
+
* no kernel connection to the sandbox lists its variables with.
|
|
99
|
+
*/
|
|
100
|
+
export function sandboxExecuteOverHttp(serverUrl, agentId) {
|
|
101
|
+
const base = serverUrl.replace(/\/+$/, '').replace(/\/api\/v1$/, '');
|
|
102
|
+
return async (code) => {
|
|
103
|
+
const response = await fetch(`${base}/api/v1/sandbox/execute`, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: { 'content-type': 'application/json' },
|
|
106
|
+
body: JSON.stringify({ code, agent_id: agentId }),
|
|
107
|
+
});
|
|
108
|
+
if (!response.ok) {
|
|
109
|
+
return {
|
|
110
|
+
stdout: '',
|
|
111
|
+
error: `The sandbox did not run the code (HTTP ${response.status}).`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const payload = (await response.json());
|
|
115
|
+
return { stdout: payload.stdout ?? '', error: payload.error ?? undefined };
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** How long until a moment, for a reader: `1 h 20 min`, `never`. */
|
|
119
|
+
export function timeLeft(expiresAt, now = Date.now()) {
|
|
120
|
+
if (expiresAt === null) {
|
|
121
|
+
return 'never';
|
|
122
|
+
}
|
|
123
|
+
if (expiresAt === undefined || expiresAt === '') {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
const at = typeof expiresAt === 'number'
|
|
127
|
+
? expiresAt < 1e12
|
|
128
|
+
? expiresAt * 1000
|
|
129
|
+
: expiresAt
|
|
130
|
+
: Date.parse(expiresAt);
|
|
131
|
+
if (!Number.isFinite(at)) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const minutes = Math.round((at - now) / 60000);
|
|
135
|
+
if (minutes <= 0) {
|
|
136
|
+
return 'ended';
|
|
137
|
+
}
|
|
138
|
+
const hours = Math.floor(minutes / 60);
|
|
139
|
+
return hours ? `${hours} h ${minutes % 60} min` : `${minutes} min`;
|
|
140
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type AssistantCharacterData } from './types';
|
|
2
|
+
/** What a file picker for a character accepts. */
|
|
3
|
+
export declare const CHARACTER_FILES_ACCEPT = ".acs,.js,.png,.gif,.webp,.jpg,.jpeg";
|
|
4
|
+
/** The sentence for a pick that is not a character. */
|
|
5
|
+
export declare const CHARACTER_FILES_EXPECTED = "Pick one .acs file, or a clippy.js character: its agent.js and its map image (and a sounds file if you have one).";
|
|
6
|
+
/** A picked file, as much of `File` as the reader needs. */
|
|
7
|
+
export type PickedFile = Blob & {
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
/** Which of the picked files make the character, or why none do. */
|
|
11
|
+
export declare function characterFilesOf(files: ReadonlyArray<PickedFile>): {
|
|
12
|
+
kind: 'acs';
|
|
13
|
+
acs: PickedFile;
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'clippy';
|
|
16
|
+
agentJs: PickedFile;
|
|
17
|
+
map: PickedFile;
|
|
18
|
+
sounds?: PickedFile;
|
|
19
|
+
} | {
|
|
20
|
+
problem: string;
|
|
21
|
+
};
|
|
22
|
+
/** Read the picked files as a character; refuses in a sentence. */
|
|
23
|
+
export declare function readCharacterFiles(files: ReadonlyArray<PickedFile>): Promise<AssistantCharacterData>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The files a person picked, read as one character (LOOP T-26): one
|
|
7
|
+
* Microsoft Agent `.acs`, or a clippy.js character's `agent.js` and map
|
|
8
|
+
* image (and its sounds file, if there is one). Read in the page; nothing is
|
|
9
|
+
* sent anywhere.
|
|
10
|
+
*
|
|
11
|
+
* @module chat/assistant/formats/files
|
|
12
|
+
*/
|
|
13
|
+
import { readAcsCharacter } from './acs';
|
|
14
|
+
import { readClippyCharacter } from './clippy';
|
|
15
|
+
import { AssistantCharacterFormatError, } from './types';
|
|
16
|
+
/** What a file picker for a character accepts. */
|
|
17
|
+
export const CHARACTER_FILES_ACCEPT = '.acs,.js,.png,.gif,.webp,.jpg,.jpeg';
|
|
18
|
+
/** The sentence for a pick that is not a character. */
|
|
19
|
+
export const CHARACTER_FILES_EXPECTED = 'Pick one .acs file, or a clippy.js character: its agent.js and its map image (and a sounds file if you have one).';
|
|
20
|
+
/** Which of the picked files make the character, or why none do. */
|
|
21
|
+
export function characterFilesOf(files) {
|
|
22
|
+
const acs = files.find(file => file.name.toLowerCase().endsWith('.acs'));
|
|
23
|
+
if (acs) {
|
|
24
|
+
return { kind: 'acs', acs };
|
|
25
|
+
}
|
|
26
|
+
const agentJs = files.find(file => file.name.toLowerCase() === 'agent.js');
|
|
27
|
+
const map = files.find(file => /\.(png|gif|webp|jpe?g)$/i.test(file.name));
|
|
28
|
+
const sounds = files.find(file => /^sounds-.*\.js$/i.test(file.name));
|
|
29
|
+
if (!agentJs || !map) {
|
|
30
|
+
return { problem: CHARACTER_FILES_EXPECTED };
|
|
31
|
+
}
|
|
32
|
+
return { kind: 'clippy', agentJs, map, ...(sounds ? { sounds } : {}) };
|
|
33
|
+
}
|
|
34
|
+
/** Read the picked files as a character; refuses in a sentence. */
|
|
35
|
+
export async function readCharacterFiles(files) {
|
|
36
|
+
const picked = characterFilesOf(files);
|
|
37
|
+
if ('problem' in picked) {
|
|
38
|
+
throw new AssistantCharacterFormatError(picked.problem);
|
|
39
|
+
}
|
|
40
|
+
if (picked.kind === 'acs') {
|
|
41
|
+
return readAcsCharacter(await picked.acs.arrayBuffer());
|
|
42
|
+
}
|
|
43
|
+
return readClippyCharacter({
|
|
44
|
+
agentJs: await picked.agentJs.text(),
|
|
45
|
+
mapPng: picked.map,
|
|
46
|
+
soundsJs: picked.sounds ? await picked.sounds.text() : undefined,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -11,3 +11,5 @@ export { readAcsCharacter, parseAcs, decodeAcsImage, layoutAcsSprite, composeAcs
|
|
|
11
11
|
export type { AcsFile, AcsAnimation, AcsFrame, AcsImage } from './acs';
|
|
12
12
|
export { decompressAgentData } from './agentCompression';
|
|
13
13
|
export { stateAnimations } from './stateAnimations';
|
|
14
|
+
export { CHARACTER_FILES_ACCEPT, CHARACTER_FILES_EXPECTED, characterFilesOf, readCharacterFiles, } from './files';
|
|
15
|
+
export type { PickedFile } from './files';
|
|
@@ -13,3 +13,4 @@ export { readClippyCharacter, readClippyAgent, readClippySounds, } from './clipp
|
|
|
13
13
|
export { readAcsCharacter, parseAcs, decodeAcsImage, layoutAcsSprite, composeAcsCell, } from './acs';
|
|
14
14
|
export { decompressAgentData } from './agentCompression';
|
|
15
15
|
export { stateAnimations } from './stateAnimations';
|
|
16
|
+
export { CHARACTER_FILES_ACCEPT, CHARACTER_FILES_EXPECTED, characterFilesOf, readCharacterFiles, } from './files';
|
|
@@ -76,6 +76,11 @@ export interface AssistantSaying {
|
|
|
76
76
|
text: string;
|
|
77
77
|
/** Whether there was more than the balloon holds. */
|
|
78
78
|
more: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Its words as written, markdown and all: what the `current` balloon
|
|
81
|
+
* draws, through the chat's markdown.
|
|
82
|
+
*/
|
|
83
|
+
markdown: string;
|
|
79
84
|
}
|
|
80
85
|
/** How much of a message is kept as the agent's saying before it is cut. */
|
|
81
86
|
export declare const SAYING_LIMIT = 220;
|
|
@@ -84,20 +89,8 @@ export declare const SAYING_LIMIT = 220;
|
|
|
84
89
|
* code blocks left out, links and Markdown read as plain words.
|
|
85
90
|
*/
|
|
86
91
|
export declare function plainWordsOf(content: unknown): string;
|
|
87
|
-
/** A message
|
|
88
|
-
export
|
|
89
|
-
id: string;
|
|
90
|
-
/** Who said it: the person, or the agent. */
|
|
91
|
-
role: 'user' | 'assistant';
|
|
92
|
-
/** Its words, plain. */
|
|
93
|
-
text: string;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* The conversation as the `history` balloon lists it (T-23): the person's
|
|
97
|
-
* and the agent's messages with words, in order — not the tool calls, not
|
|
98
|
-
* an empty message — the same messages its header counts.
|
|
99
|
-
*/
|
|
100
|
-
export declare function balloonHistoryOf(items: readonly unknown[]): BalloonHistoryMessage[];
|
|
92
|
+
/** A message's text parts as written, markdown and all. */
|
|
93
|
+
export declare function markdownOf(content: unknown): string;
|
|
101
94
|
/**
|
|
102
95
|
* The newest thing the agent said, as the Office Assistant said it: in the
|
|
103
96
|
* balloon, while the conversation is closed (T-23). Only its own messages
|
|
@@ -75,25 +75,19 @@ export function plainWordsOf(content) {
|
|
|
75
75
|
.replace(/\s+/g, ' ')
|
|
76
76
|
.trim();
|
|
77
77
|
}
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const text = plainWordsOf(item.content);
|
|
92
|
-
if (text) {
|
|
93
|
-
history.push({ id: String(item.id ?? index), role: item.role, text });
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
return history;
|
|
78
|
+
/** A message's text parts as written, markdown and all. */
|
|
79
|
+
export function markdownOf(content) {
|
|
80
|
+
return typeof content === 'string'
|
|
81
|
+
? content
|
|
82
|
+
: Array.isArray(content)
|
|
83
|
+
? content
|
|
84
|
+
.map(part => part &&
|
|
85
|
+
typeof part === 'object' &&
|
|
86
|
+
part.type === 'text'
|
|
87
|
+
? String(part.text ?? '')
|
|
88
|
+
: '')
|
|
89
|
+
.join('\n\n')
|
|
90
|
+
: '';
|
|
97
91
|
}
|
|
98
92
|
/**
|
|
99
93
|
* The newest thing the agent said, as the Office Assistant said it: in the
|
|
@@ -111,8 +105,14 @@ export function latestSaying(items) {
|
|
|
111
105
|
if (!plain) {
|
|
112
106
|
continue;
|
|
113
107
|
}
|
|
108
|
+
const markdown = markdownOf(item.content);
|
|
114
109
|
if (plain.length <= SAYING_LIMIT) {
|
|
115
|
-
return {
|
|
110
|
+
return {
|
|
111
|
+
id: String(item.id ?? index),
|
|
112
|
+
text: plain,
|
|
113
|
+
more: false,
|
|
114
|
+
markdown,
|
|
115
|
+
};
|
|
116
116
|
}
|
|
117
117
|
const cut = plain.slice(0, SAYING_LIMIT);
|
|
118
118
|
const atWord = cut.slice(0, Math.max(cut.lastIndexOf(' '), SAYING_LIMIT * 0.6));
|
|
@@ -120,6 +120,7 @@ export function latestSaying(items) {
|
|
|
120
120
|
id: String(item.id ?? index),
|
|
121
121
|
text: `${atWord.trimEnd()}…`,
|
|
122
122
|
more: true,
|
|
123
|
+
markdown,
|
|
123
124
|
};
|
|
124
125
|
}
|
|
125
126
|
return undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DisplayItem, ToolCallMessage } from '../../types/chat';
|
|
1
2
|
/**
|
|
2
3
|
* How the balloon shows the conversation: `history`, every message,
|
|
3
4
|
* scrolled, the composer last; `current`, only what it says or does now, in
|
|
@@ -67,3 +68,18 @@ export declare function toolAnnouncement(line: BalloonToolLine | undefined, prev
|
|
|
67
68
|
* header counts them: the person's and the agent's, not the tool calls.
|
|
68
69
|
*/
|
|
69
70
|
export declare function conversationCount(items: readonly unknown[]): number;
|
|
71
|
+
/**
|
|
72
|
+
* A tool line as the chat holds it: a tool call (`ToolCallMessage`), drawn
|
|
73
|
+
* by the chat's own tool card in the balloon, its runtime's own words
|
|
74
|
+
* (`Asking Accounting…`) as its summary; or, when it names no tool — what
|
|
75
|
+
* the agent is doing, in its own words only — the agent's message saying
|
|
76
|
+
* so.
|
|
77
|
+
*/
|
|
78
|
+
export declare function displayItemOfLine(line: BalloonToolLine): DisplayItem;
|
|
79
|
+
/** A tool line as the chat's own tool call (`ToolCallMessage`). */
|
|
80
|
+
export declare function toolCallOfLine(line: BalloonToolLine): ToolCallMessage;
|
|
81
|
+
/**
|
|
82
|
+
* The conversation with the tool being called, when the conversation does
|
|
83
|
+
* not hold that call already: what the history balloon lists.
|
|
84
|
+
*/
|
|
85
|
+
export declare function withToolCall(items: readonly DisplayItem[], line: BalloonToolLine | undefined): readonly DisplayItem[];
|
|
@@ -116,3 +116,49 @@ export function conversationCount(items) {
|
|
|
116
116
|
typeof message.toolName !== 'string');
|
|
117
117
|
}).length;
|
|
118
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* A tool line as the chat holds it: a tool call (`ToolCallMessage`), drawn
|
|
121
|
+
* by the chat's own tool card in the balloon, its runtime's own words
|
|
122
|
+
* (`Asking Accounting…`) as its summary; or, when it names no tool — what
|
|
123
|
+
* the agent is doing, in its own words only — the agent's message saying
|
|
124
|
+
* so.
|
|
125
|
+
*/
|
|
126
|
+
export function displayItemOfLine(line) {
|
|
127
|
+
if (!line.tool && !line.name) {
|
|
128
|
+
return {
|
|
129
|
+
id: `balloon-activity:${line.id}`,
|
|
130
|
+
role: 'assistant',
|
|
131
|
+
content: line.words ?? '',
|
|
132
|
+
createdAt: new Date(0),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return toolCallOfLine(line);
|
|
136
|
+
}
|
|
137
|
+
/** A tool line as the chat's own tool call (`ToolCallMessage`). */
|
|
138
|
+
export function toolCallOfLine(line) {
|
|
139
|
+
return {
|
|
140
|
+
id: `balloon-tool:${line.id}`,
|
|
141
|
+
type: 'tool-call',
|
|
142
|
+
toolCallId: line.id,
|
|
143
|
+
toolName: line.tool || line.name,
|
|
144
|
+
args: {},
|
|
145
|
+
status: line.phase === 'failed'
|
|
146
|
+
? 'error'
|
|
147
|
+
: line.phase === 'done'
|
|
148
|
+
? 'complete'
|
|
149
|
+
: 'executing',
|
|
150
|
+
...(line.words ? { summary: line.words } : {}),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The conversation with the tool being called, when the conversation does
|
|
155
|
+
* not hold that call already: what the history balloon lists.
|
|
156
|
+
*/
|
|
157
|
+
export function withToolCall(items, line) {
|
|
158
|
+
if (!line ||
|
|
159
|
+
items.some(item => item.toolCallId === line.id ||
|
|
160
|
+
item.id === line.id)) {
|
|
161
|
+
return items;
|
|
162
|
+
}
|
|
163
|
+
return [...items, displayItemOfLine(line)];
|
|
164
|
+
}
|
|
@@ -58,6 +58,9 @@ const EphemeralDocument = React.lazy(() => import('../document/EphemeralDocument
|
|
|
58
58
|
default: m.EphemeralDocument,
|
|
59
59
|
})));
|
|
60
60
|
import { useNotebookTools } from '../../tools/adapters/agent-runtimes/notebookHooks';
|
|
61
|
+
import { useAgentInspectorSink } from '../../components/inspector/agentInspector';
|
|
62
|
+
import { chatInspectorRecorder, } from '../../components/inspector/chatInspect';
|
|
63
|
+
import { loopMessageChange, speakerOf, withLoopMessage } from './loopMessage';
|
|
61
64
|
// Tracks pending prompts already auto-sent for a given conversation scope.
|
|
62
65
|
// This prevents layout-driven unmount/remount cycles from re-sending prompts.
|
|
63
66
|
const sentPendingPromptKeys = new Set();
|
|
@@ -513,7 +516,7 @@ useStore: useStoreMode = true, protocol: protocolRaw, onSendMessage, onSendReady
|
|
|
513
516
|
// Extended props
|
|
514
517
|
brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, renderHeader, showTurnFooter = true, notebookToolSurfacesId, footerContent, welcome, trailingContent, voice, voiceSpeaking = false, onStopSpeaking, showInformation = false, onInformationClick, headerContent, children, borderRadius, backgroundColor, border, boxShadow, compact = false, placeholder, description = 'Start a conversation with the AI agent.', onStateUpdate, onNewChat, onClear, onMessagesChange, onItemsChange, onDisplayItemsChange, autoFocus = false, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, focusTrigger, frontendTools: frontendToolsProp, enableEphemeralNotebook = false, initialEphemeralNotebookOpen = true, onEphemeralNotebookOpenChange, enableEphemeralDocument = false, initialEphemeralSurfaceMode, onEphemeralSurfaceModeChange, collapsed = false, onExpandFromCollapsed, ephemeralNotebookToolbar, ephemeralNotebookToolbarExtraItems, ephemeralDocumentToolbarExtraItems, ephemeralNotebookCollaborationProvider, ephemeralNotebookCollaborationDocumentId, ephemeralDocumentCollaboration, ephemeralRuntimeOverride,
|
|
515
518
|
// Tool invocation hooks
|
|
516
|
-
onToolCallStart, onToolCallComplete,
|
|
519
|
+
onToolCallStart, onToolCallComplete, inspector: inspectorGiven,
|
|
517
520
|
// Identity/Authorization props
|
|
518
521
|
onAuthorizationRequired: _onAuthorizationRequired, connectedIdentities,
|
|
519
522
|
// Conversation persistence
|
|
@@ -1589,6 +1592,25 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
1589
1592
|
onToolCallStartRef.current = onToolCallStart;
|
|
1590
1593
|
const onToolCallCompleteRef = useRef(onToolCallComplete);
|
|
1591
1594
|
onToolCallCompleteRef.current = onToolCallComplete;
|
|
1595
|
+
// The Agent Inspector: the turns and tool calls, recorded as they happen.
|
|
1596
|
+
const inspectorFromContext = useAgentInspectorSink();
|
|
1597
|
+
const inspectorSink = inspectorGiven ?? inspectorFromContext;
|
|
1598
|
+
const inspectorActorRef = useRef('Agent');
|
|
1599
|
+
inspectorActorRef.current =
|
|
1600
|
+
(typeof title === 'string' && title) || activeAgentId || 'Agent';
|
|
1601
|
+
const recorderRef = useRef(null);
|
|
1602
|
+
if (inspectorSink && recorderRef.current?.sink !== inspectorSink) {
|
|
1603
|
+
recorderRef.current = {
|
|
1604
|
+
sink: inspectorSink,
|
|
1605
|
+
recorder: chatInspectorRecorder(inspectorSink, () => inspectorActorRef.current, {
|
|
1606
|
+
frontendTools: () => (frontendToolsRef.current ?? []).map(tool => tool.name),
|
|
1607
|
+
}),
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
else if (!inspectorSink) {
|
|
1611
|
+
recorderRef.current = null;
|
|
1612
|
+
}
|
|
1613
|
+
const inspectAnswerRef = useRef('');
|
|
1592
1614
|
const handleRespondRef = useRef(null);
|
|
1593
1615
|
// A decision made outside the chat — a host page's own approval banner —
|
|
1594
1616
|
// is answered exactly as the tool card's buttons answer it: through the
|
|
@@ -2415,6 +2437,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2415
2437
|
const sanitizedContent = typeof rawContent === 'string'
|
|
2416
2438
|
? sanitizeAssistantContent(rawContent)
|
|
2417
2439
|
: (rawContent ?? '');
|
|
2440
|
+
if (typeof sanitizedContent === 'string' && sanitizedContent) {
|
|
2441
|
+
inspectAnswerRef.current = sanitizedContent;
|
|
2442
|
+
}
|
|
2418
2443
|
setDisplayItems(prev => {
|
|
2419
2444
|
const newItems = [...prev];
|
|
2420
2445
|
const idx = newItems.findIndex(item => !isToolCallMessage(item) && item.id === currentId);
|
|
@@ -2520,6 +2545,11 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2520
2545
|
toolCallId,
|
|
2521
2546
|
args,
|
|
2522
2547
|
});
|
|
2548
|
+
recorderRef.current?.recorder.toolStarted({
|
|
2549
|
+
id: toolCallId,
|
|
2550
|
+
name: toolName,
|
|
2551
|
+
args,
|
|
2552
|
+
});
|
|
2523
2553
|
const frontendTool = frontendToolsRef.current?.find(t => t.name === toolName);
|
|
2524
2554
|
const toolHandler = frontendTool?.handler;
|
|
2525
2555
|
// Execute when the args are complete. The protocol says so
|
|
@@ -2604,6 +2634,13 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2604
2634
|
setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
|
|
2605
2635
|
? updatedToolCall
|
|
2606
2636
|
: item));
|
|
2637
|
+
recorderRef.current?.recorder.toolEnded(toolCallId, {
|
|
2638
|
+
result: event.toolResult.result,
|
|
2639
|
+
error: event.toolResult.error ??
|
|
2640
|
+
(updatedToolCall.status === 'error'
|
|
2641
|
+
? (executionError ?? 'failed')
|
|
2642
|
+
: undefined),
|
|
2643
|
+
});
|
|
2607
2644
|
// Fire post-hook for tool results
|
|
2608
2645
|
onToolCallCompleteRef.current?.({
|
|
2609
2646
|
toolName: existingToolCall.toolName,
|
|
@@ -2646,6 +2683,25 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2646
2683
|
}
|
|
2647
2684
|
}
|
|
2648
2685
|
break;
|
|
2686
|
+
case 'activity': {
|
|
2687
|
+
// An application's code changed a message it sent: its author
|
|
2688
|
+
// said, or the message taken away (LOOP P-15).
|
|
2689
|
+
const change = loopMessageChange(event.activity);
|
|
2690
|
+
if (!change)
|
|
2691
|
+
break;
|
|
2692
|
+
setDisplayItems(prev => withLoopMessage(prev, change));
|
|
2693
|
+
if (useStoreMode) {
|
|
2694
|
+
if ('removed' in change) {
|
|
2695
|
+
useChatStore.getState().deleteMessage(change.id);
|
|
2696
|
+
}
|
|
2697
|
+
else {
|
|
2698
|
+
useChatStore.getState().updateMessage(change.id, {
|
|
2699
|
+
speaker: speakerOf(change.author),
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
break;
|
|
2704
|
+
}
|
|
2649
2705
|
case 'done':
|
|
2650
2706
|
/*
|
|
2651
2707
|
* What the turn cost, when nobody else is counting.
|
|
@@ -2657,6 +2713,11 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2657
2713
|
* The bar and its ring simply never appeared for a browser agent.
|
|
2658
2714
|
*/
|
|
2659
2715
|
{
|
|
2716
|
+
recorderRef.current?.recorder.turnEnded({
|
|
2717
|
+
text: inspectAnswerRef.current || undefined,
|
|
2718
|
+
usage: event.usage,
|
|
2719
|
+
});
|
|
2720
|
+
inspectAnswerRef.current = '';
|
|
2660
2721
|
const turnInput = event.usage?.promptTokens ?? 0;
|
|
2661
2722
|
const turnOutput = event.usage?.completionTokens ?? 0;
|
|
2662
2723
|
// The parts sum to the total when the harness did not send one:
|
|
@@ -2710,6 +2771,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2710
2771
|
break;
|
|
2711
2772
|
case 'error':
|
|
2712
2773
|
console.error('[ChatBase] Protocol error:', event.error);
|
|
2774
|
+
recorderRef.current?.recorder.turnEnded({
|
|
2775
|
+
error: event.error?.message ?? 'error',
|
|
2776
|
+
});
|
|
2713
2777
|
if (event.error?.message &&
|
|
2714
2778
|
/exceeded maximum retries/i.test(event.error.message) &&
|
|
2715
2779
|
hideMessagesAfterToolUIRef.current) {
|
|
@@ -2777,6 +2841,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2777
2841
|
result,
|
|
2778
2842
|
status: 'complete',
|
|
2779
2843
|
};
|
|
2844
|
+
recorderRef.current?.recorder.toolEnded(toolCallId, { result });
|
|
2780
2845
|
toolCallsRef.current.set(toolCallId, completedToolCall);
|
|
2781
2846
|
setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
|
|
2782
2847
|
? completedToolCall
|
|
@@ -2789,6 +2854,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2789
2854
|
status: 'error',
|
|
2790
2855
|
error: err.message,
|
|
2791
2856
|
};
|
|
2857
|
+
recorderRef.current?.recorder.toolEnded(toolCallId, {
|
|
2858
|
+
error: err.message,
|
|
2859
|
+
});
|
|
2792
2860
|
toolCallsRef.current.set(toolCallId, errorToolCall);
|
|
2793
2861
|
setDisplayItems(prev => prev.map(item => isToolCallMessage(item) && item.toolCallId === toolCallId
|
|
2794
2862
|
? errorToolCall
|
|
@@ -2838,6 +2906,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2838
2906
|
if (!adapterRef.current && !onSendMessage)
|
|
2839
2907
|
return;
|
|
2840
2908
|
stoppedRef.current = false;
|
|
2909
|
+
inspectAnswerRef.current = '';
|
|
2910
|
+
recorderRef.current?.recorder.turnStarted(messageContent, selectedModel || undefined);
|
|
2841
2911
|
suppressAssistantTextForToolOnlyRef.current =
|
|
2842
2912
|
isToolCallOnlyPrompt(messageContent);
|
|
2843
2913
|
// Said, then sent: by the microphone at once, or from the composer it
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A message of an application's code that changed after it was sent
|
|
3
|
+
* (LOOP P-15).
|
|
4
|
+
*
|
|
5
|
+
* The session API says a message's author, or that it was taken away, with a
|
|
6
|
+
* `CUSTOM` event named `loop.message` — `{id, author}` or `{id, removed: true}`
|
|
7
|
+
* — which the AG-UI adapter hands on as an `activity`. A message whose text
|
|
8
|
+
* changed needs nothing here: it is written again under its id, and the chat
|
|
9
|
+
* draws it in place.
|
|
10
|
+
*/
|
|
11
|
+
import type { MessageSpeaker } from '../../types/messages';
|
|
12
|
+
/** The `CUSTOM` event's name. */
|
|
13
|
+
export declare const LOOP_MESSAGE = "loop.message";
|
|
14
|
+
/** What changed about a message already in the conversation. */
|
|
15
|
+
export type LoopMessageChange = {
|
|
16
|
+
id: string;
|
|
17
|
+
removed: true;
|
|
18
|
+
} | {
|
|
19
|
+
id: string;
|
|
20
|
+
author: string;
|
|
21
|
+
};
|
|
22
|
+
/** The change an activity says, or `null` when it says none. */
|
|
23
|
+
export declare function loopMessageChange(activity: {
|
|
24
|
+
type: string;
|
|
25
|
+
data: unknown;
|
|
26
|
+
} | undefined): LoopMessageChange | null;
|
|
27
|
+
/** Who a message is by, as the chat draws a named voice. */
|
|
28
|
+
export declare function speakerOf(author: string): MessageSpeaker;
|
|
29
|
+
/**
|
|
30
|
+
* The conversation with the change made: the message taken away, or given its
|
|
31
|
+
* author. Items that are not that message — tool calls among them — are left
|
|
32
|
+
* as they are.
|
|
33
|
+
*/
|
|
34
|
+
export declare function withLoopMessage<T extends {
|
|
35
|
+
id?: string;
|
|
36
|
+
speaker?: MessageSpeaker;
|
|
37
|
+
}>(items: T[], change: LoopMessageChange): T[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/** The `CUSTOM` event's name. */
|
|
6
|
+
export const LOOP_MESSAGE = 'loop.message';
|
|
7
|
+
/** The change an activity says, or `null` when it says none. */
|
|
8
|
+
export function loopMessageChange(activity) {
|
|
9
|
+
if (!activity || activity.type !== LOOP_MESSAGE)
|
|
10
|
+
return null;
|
|
11
|
+
const data = activity.data;
|
|
12
|
+
if (!data || typeof data.id !== 'string' || !data.id)
|
|
13
|
+
return null;
|
|
14
|
+
if (data.removed === true)
|
|
15
|
+
return { id: data.id, removed: true };
|
|
16
|
+
if (typeof data.author === 'string' && data.author.trim()) {
|
|
17
|
+
return { id: data.id, author: data.author.trim() };
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
/** Who a message is by, as the chat draws a named voice. */
|
|
22
|
+
export function speakerOf(author) {
|
|
23
|
+
return { id: `author:${author}`, name: author };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The conversation with the change made: the message taken away, or given its
|
|
27
|
+
* author. Items that are not that message — tool calls among them — are left
|
|
28
|
+
* as they are.
|
|
29
|
+
*/
|
|
30
|
+
export function withLoopMessage(items, change) {
|
|
31
|
+
if ('removed' in change) {
|
|
32
|
+
return items.filter(item => item.id !== change.id);
|
|
33
|
+
}
|
|
34
|
+
return items.map(item => item.id === change.id
|
|
35
|
+
? { ...item, speaker: speakerOf(change.author) }
|
|
36
|
+
: item);
|
|
37
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three pulsing dots: a speaker is still at work and has not written yet —
|
|
3
|
+
* the chat's waiting indicator, and the assistant's balloon's. Still, but
|
|
4
|
+
* shown, for a reader who asks for reduced motion. Hidden from a screen
|
|
5
|
+
* reader: the live region beside it says the state.
|
|
6
|
+
*
|
|
7
|
+
* @module chat/indicators/TypingDots
|
|
8
|
+
*/
|
|
9
|
+
import type { ReactElement } from 'react';
|
|
10
|
+
export declare function TypingDots({ size }: {
|
|
11
|
+
size?: number;
|
|
12
|
+
}): ReactElement;
|
|
13
|
+
export default TypingDots;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box } from '@datalayer/primer-addons';
|
|
3
|
+
export function TypingDots({ size = 8 }) {
|
|
4
|
+
return (_jsx(Box, { "aria-hidden": "true", "data-typing-dots": "", sx: { display: 'inline-flex', alignItems: 'center', gap: '4px' }, children: [0, 0.2, 0.4].map((delay, index) => (_jsx(Box, { sx: {
|
|
5
|
+
width: size,
|
|
6
|
+
height: size,
|
|
7
|
+
borderRadius: '50%',
|
|
8
|
+
bg: 'fg.muted',
|
|
9
|
+
animation: 'typingPulse 1.4s ease-in-out infinite',
|
|
10
|
+
animationDelay: `${delay}s`,
|
|
11
|
+
'@keyframes typingPulse': {
|
|
12
|
+
'0%, 60%, 100%': { transform: 'scale(0.6)', opacity: 0.4 },
|
|
13
|
+
'30%': { transform: 'scale(1)', opacity: 1 },
|
|
14
|
+
},
|
|
15
|
+
'@media (prefers-reduced-motion: reduce)': {
|
|
16
|
+
animation: 'none',
|
|
17
|
+
opacity: 0.7,
|
|
18
|
+
},
|
|
19
|
+
} }, index))) }));
|
|
20
|
+
}
|
|
21
|
+
export default TypingDots;
|