@datalayer/agent-runtimes 1.3.28 → 1.3.30
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/AgentNode.js +14 -33
- package/lib/chat/ChatFloating.d.ts +12 -2
- package/lib/chat/ChatFloating.js +196 -25
- package/lib/chat/assistant/AssistantStage.d.ts +70 -0
- package/lib/chat/assistant/AssistantStage.js +192 -0
- package/lib/chat/assistant/SpeechBalloon.d.ts +26 -0
- package/lib/chat/assistant/SpeechBalloon.js +46 -0
- package/lib/chat/assistant/SpriteCharacter.d.ts +26 -0
- package/lib/chat/assistant/SpriteCharacter.js +73 -0
- package/lib/chat/assistant/characters.d.ts +30 -0
- package/lib/chat/assistant/characters.js +41 -0
- package/lib/chat/assistant/formats/acs.d.ts +88 -0
- package/lib/chat/assistant/formats/acs.js +493 -0
- package/lib/chat/assistant/formats/agentCompression.d.ts +5 -0
- package/lib/chat/assistant/formats/agentCompression.js +92 -0
- package/lib/chat/assistant/formats/blobs.d.ts +6 -0
- package/lib/chat/assistant/formats/blobs.js +52 -0
- package/lib/chat/assistant/formats/clippy.d.ts +21 -0
- package/lib/chat/assistant/formats/clippy.js +167 -0
- package/lib/chat/assistant/formats/index.d.ts +13 -0
- package/lib/chat/assistant/formats/index.js +15 -0
- package/lib/chat/assistant/formats/objectLiteral.d.ts +23 -0
- package/lib/chat/assistant/formats/objectLiteral.js +233 -0
- package/lib/chat/assistant/formats/stateAnimations.d.ts +16 -0
- package/lib/chat/assistant/formats/stateAnimations.js +115 -0
- package/lib/chat/assistant/formats/types.d.ts +56 -0
- package/lib/chat/assistant/formats/types.js +11 -0
- package/lib/chat/assistant/state.d.ts +62 -0
- package/lib/chat/assistant/state.js +116 -0
- package/lib/chat/base/ChatBase.js +41 -32
- package/lib/chat/base/modelChoice.d.ts +13 -0
- package/lib/chat/base/modelChoice.js +13 -0
- package/lib/chat/header/ChatViewModeToggle.js +6 -3
- package/lib/{loop/plugins/chat → chat/presence}/Presence.js +1 -1
- package/lib/{loop/plugins/chat → chat/presence}/presenceStatus.d.ts +7 -1
- package/lib/{loop/plugins/chat → chat/presence}/presenceStatus.js +18 -0
- package/lib/chat/viewModes.d.ts +6 -0
- package/lib/chat/viewModes.js +10 -1
- package/lib/examples/ChatAssistantExample.d.ts +8 -0
- package/lib/examples/ChatAssistantExample.js +59 -0
- package/lib/examples/example-selector.js +1 -0
- package/lib/examples/main.js +3 -0
- package/lib/loop/apps/AppRenderer.d.ts +34 -2
- package/lib/loop/apps/AppRenderer.js +96 -29
- package/lib/loop/apps/agent.d.ts +15 -0
- package/lib/loop/apps/agent.js +42 -0
- package/lib/loop/apps/appspec.d.ts +5 -1
- package/lib/loop/apps/appspec.js +21 -1
- package/lib/loop/apps/checks.js +3 -1
- package/lib/loop/apps/index.d.ts +1 -0
- package/lib/loop/apps/index.js +1 -0
- package/lib/loop/core/index.d.ts +23 -0
- package/lib/loop/core/index.js +1 -0
- package/lib/loop/embed/AppEmbed.d.ts +42 -0
- package/lib/loop/embed/AppEmbed.js +205 -0
- package/lib/loop/embed/datalayer-app.d.ts +1 -0
- package/lib/loop/embed/datalayer-app.js +16 -0
- package/lib/loop/embed/element.d.ts +57 -0
- package/lib/loop/embed/element.js +405 -0
- package/lib/loop/embed/embedConfig.d.ts +120 -0
- package/lib/loop/embed/embedConfig.js +148 -0
- package/lib/loop/embed/embedTheme.d.ts +24 -0
- package/lib/loop/embed/embedTheme.js +83 -0
- package/lib/loop/index.d.ts +6 -2
- package/lib/loop/index.js +8 -2
- package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +21 -3
- package/lib/loop/plugins/a2ui-surface/InlineSurface.js +34 -6
- package/lib/loop/plugins/agent-capacity/index.d.ts +4 -0
- package/lib/loop/plugins/agent-capacity/index.js +3 -1
- package/lib/loop/plugins/agents/switchable.d.ts +7 -0
- package/lib/loop/plugins/agents/switchable.js +91 -88
- package/lib/loop/plugins/app-page/AppPage.d.ts +16 -0
- package/lib/loop/plugins/app-page/AppPage.js +53 -0
- package/lib/loop/plugins/app-page/appPage.d.ts +180 -0
- package/lib/loop/plugins/app-page/appPage.js +443 -0
- package/lib/loop/plugins/app-page/index.d.ts +8 -0
- package/lib/loop/plugins/app-page/index.js +34 -0
- package/lib/loop/plugins/assistant-characters/index.d.ts +30 -0
- package/lib/loop/plugins/assistant-characters/index.js +41 -0
- package/lib/loop/plugins/chat/ChatView.js +50 -39
- package/lib/loop/plugins/chat/index.d.ts +1 -1
- package/lib/loop/plugins/page-layout/SplitLayout.d.ts +31 -0
- package/lib/loop/plugins/page-layout/SplitLayout.js +129 -0
- package/lib/loop/plugins/page-layout/index.d.ts +13 -0
- package/lib/loop/plugins/page-layout/index.js +15 -2
- package/lib/loop/presets.d.ts +12 -0
- package/lib/loop/presets.js +5 -3
- package/lib/specs/actions.js +14 -0
- package/lib/specs/agents/agents.js +4 -0
- package/lib/specs/apps.d.ts +13 -1
- package/lib/specs/apps.js +2715 -373
- package/lib/specs/appspecSchema.js +21 -3
- package/lib/types/agentspecs.d.ts +21 -1
- package/lib/types/chat.d.ts +1 -1
- package/lib/types/config.d.ts +12 -0
- package/package.json +2 -1
- package/scripts/codegen/generate_agents.py +50 -2
- package/scripts/codegen/generate_apps.py +36 -6
- /package/lib/{loop/plugins/chat → chat/presence}/Presence.d.ts +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the floating assistant acts out (LOOP T-22): the presence of the
|
|
3
|
+
* application (T-08), and three moments of its own — greeting when it
|
|
4
|
+
* arrives, speaking while an answer streams in, goodbye when it is sent away.
|
|
5
|
+
*
|
|
6
|
+
* @module chat/assistant/state
|
|
7
|
+
*/
|
|
8
|
+
import type { PresenceState } from '../presence/presenceStatus';
|
|
9
|
+
/** The states a character has an animation for. */
|
|
10
|
+
export type AssistantState = 'idle' | 'thinking' | 'working' | 'waiting' | 'greeting' | 'speaking' | 'goodbye';
|
|
11
|
+
/** What a character is doing, beyond what the application is doing. */
|
|
12
|
+
export interface AssistantMoment {
|
|
13
|
+
/** It has just appeared, and says hello once. */
|
|
14
|
+
arriving?: boolean;
|
|
15
|
+
/** It is being sent away. */
|
|
16
|
+
leaving?: boolean;
|
|
17
|
+
/** The answer is arriving: words are being written. */
|
|
18
|
+
speaking?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The state to act, from the application's presence and the moment.
|
|
22
|
+
*
|
|
23
|
+
* Leaving and arriving win, being short; then waiting for the person, which
|
|
24
|
+
* must never be missed; then speaking over thinking, since words arriving
|
|
25
|
+
* are what the person watches; then the presence as it is.
|
|
26
|
+
*/
|
|
27
|
+
export declare function assistantStateOf(presence: PresenceState, moment?: AssistantMoment): AssistantState;
|
|
28
|
+
/**
|
|
29
|
+
* Whether words are arriving: the newest item of the conversation is the
|
|
30
|
+
* assistant's own message, not a tool call. Read with `busy`, it is speaking.
|
|
31
|
+
*/
|
|
32
|
+
export declare function newestIsAnswer(items: readonly unknown[]): boolean;
|
|
33
|
+
/** A word from the agent, for the character's balloon. */
|
|
34
|
+
export interface AssistantSaying {
|
|
35
|
+
/** The message it comes from: a new id is a new thing said. */
|
|
36
|
+
id: string;
|
|
37
|
+
/** Its text, short enough for a balloon. */
|
|
38
|
+
text: string;
|
|
39
|
+
/** Whether there was more than the balloon holds. */
|
|
40
|
+
more: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** How much of a message the balloon says before *Open the conversation*. */
|
|
43
|
+
export declare const SAYING_LIMIT = 220;
|
|
44
|
+
/**
|
|
45
|
+
* The newest thing the agent said, as the Office Assistant said it: in the
|
|
46
|
+
* balloon, while the conversation is closed (T-23). Only its own messages
|
|
47
|
+
* with text — not a tool call, not the person's words; markdown is read as
|
|
48
|
+
* plain words, and a long message is cut at a word with `more` set.
|
|
49
|
+
*/
|
|
50
|
+
export declare function latestSaying(items: readonly unknown[]): AssistantSaying | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* How long the assistant is away, once sent (T-27): for the page — the
|
|
53
|
+
* popup's button calls it back — for the session, or for good; the header's
|
|
54
|
+
* *Floating assistant* always calls it back.
|
|
55
|
+
*/
|
|
56
|
+
export type AssistantAway = 'none' | 'page' | 'session' | 'always';
|
|
57
|
+
/** Where the choice is kept: the session's storage, or for good. */
|
|
58
|
+
export declare const ASSISTANT_AWAY_KEY = "datalayer-assistant-away";
|
|
59
|
+
/** The away a page starts with, from what was kept. Storage may be refused. */
|
|
60
|
+
export declare function keptAway(): AssistantAway;
|
|
61
|
+
/** Keep the choice where it lasts as long as it says; `none` forgets it. */
|
|
62
|
+
export declare function keepAway(away: AssistantAway): void;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The state to act, from the application's presence and the moment.
|
|
7
|
+
*
|
|
8
|
+
* Leaving and arriving win, being short; then waiting for the person, which
|
|
9
|
+
* must never be missed; then speaking over thinking, since words arriving
|
|
10
|
+
* are what the person watches; then the presence as it is.
|
|
11
|
+
*/
|
|
12
|
+
export function assistantStateOf(presence, moment = {}) {
|
|
13
|
+
if (moment.leaving) {
|
|
14
|
+
return 'goodbye';
|
|
15
|
+
}
|
|
16
|
+
if (moment.arriving) {
|
|
17
|
+
return 'greeting';
|
|
18
|
+
}
|
|
19
|
+
if (presence === 'waiting') {
|
|
20
|
+
return 'waiting';
|
|
21
|
+
}
|
|
22
|
+
if (moment.speaking && presence !== 'idle') {
|
|
23
|
+
return 'speaking';
|
|
24
|
+
}
|
|
25
|
+
return presence;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether words are arriving: the newest item of the conversation is the
|
|
29
|
+
* assistant's own message, not a tool call. Read with `busy`, it is speaking.
|
|
30
|
+
*/
|
|
31
|
+
export function newestIsAnswer(items) {
|
|
32
|
+
const newest = items[items.length - 1];
|
|
33
|
+
return (!!newest &&
|
|
34
|
+
newest.role === 'assistant' &&
|
|
35
|
+
typeof newest.toolName !== 'string');
|
|
36
|
+
}
|
|
37
|
+
/** How much of a message the balloon says before *Open the conversation*. */
|
|
38
|
+
export const SAYING_LIMIT = 220;
|
|
39
|
+
/**
|
|
40
|
+
* The newest thing the agent said, as the Office Assistant said it: in the
|
|
41
|
+
* balloon, while the conversation is closed (T-23). Only its own messages
|
|
42
|
+
* with text — not a tool call, not the person's words; markdown is read as
|
|
43
|
+
* plain words, and a long message is cut at a word with `more` set.
|
|
44
|
+
*/
|
|
45
|
+
export function latestSaying(items) {
|
|
46
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
47
|
+
const item = items[index];
|
|
48
|
+
if (item.role !== 'assistant' || typeof item.toolName === 'string') {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const raw = typeof item.content === 'string'
|
|
52
|
+
? item.content
|
|
53
|
+
: Array.isArray(item.content)
|
|
54
|
+
? item.content
|
|
55
|
+
.map(part => part &&
|
|
56
|
+
typeof part === 'object' &&
|
|
57
|
+
part.type === 'text'
|
|
58
|
+
? String(part.text ?? '')
|
|
59
|
+
: '')
|
|
60
|
+
.join(' ')
|
|
61
|
+
: '';
|
|
62
|
+
const plain = raw
|
|
63
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
64
|
+
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
65
|
+
.replace(/[*_`#>]+/g, '')
|
|
66
|
+
.replace(/\s+/g, ' ')
|
|
67
|
+
.trim();
|
|
68
|
+
if (!plain) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (plain.length <= SAYING_LIMIT) {
|
|
72
|
+
return { id: String(item.id ?? index), text: plain, more: false };
|
|
73
|
+
}
|
|
74
|
+
const cut = plain.slice(0, SAYING_LIMIT);
|
|
75
|
+
const atWord = cut.slice(0, Math.max(cut.lastIndexOf(' '), SAYING_LIMIT * 0.6));
|
|
76
|
+
return {
|
|
77
|
+
id: String(item.id ?? index),
|
|
78
|
+
text: `${atWord.trimEnd()}…`,
|
|
79
|
+
more: true,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
/** Where the choice is kept: the session's storage, or for good. */
|
|
85
|
+
export const ASSISTANT_AWAY_KEY = 'datalayer-assistant-away';
|
|
86
|
+
/** The away a page starts with, from what was kept. Storage may be refused. */
|
|
87
|
+
export function keptAway() {
|
|
88
|
+
try {
|
|
89
|
+
if (window.localStorage.getItem(ASSISTANT_AWAY_KEY) === 'always') {
|
|
90
|
+
return 'always';
|
|
91
|
+
}
|
|
92
|
+
if (window.sessionStorage.getItem(ASSISTANT_AWAY_KEY) === 'session') {
|
|
93
|
+
return 'session';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// A page without storage keeps the assistant for the page only.
|
|
98
|
+
}
|
|
99
|
+
return 'none';
|
|
100
|
+
}
|
|
101
|
+
/** Keep the choice where it lasts as long as it says; `none` forgets it. */
|
|
102
|
+
export function keepAway(away) {
|
|
103
|
+
try {
|
|
104
|
+
window.localStorage.removeItem(ASSISTANT_AWAY_KEY);
|
|
105
|
+
window.sessionStorage.removeItem(ASSISTANT_AWAY_KEY);
|
|
106
|
+
if (away === 'always') {
|
|
107
|
+
window.localStorage.setItem(ASSISTANT_AWAY_KEY, 'always');
|
|
108
|
+
}
|
|
109
|
+
else if (away === 'session') {
|
|
110
|
+
window.sessionStorage.setItem(ASSISTANT_AWAY_KEY, 'session');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Without storage the choice holds for the page.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -48,7 +48,7 @@ import { InputPrompt } from '../prompt/InputPrompt';
|
|
|
48
48
|
import { isFloatingChatViewMode } from '../viewModes';
|
|
49
49
|
import { ToolApprovalBanner, ToolApprovalDialog, } from '../tools';
|
|
50
50
|
import { EphemeralNotebook } from '../notebook/EphemeralNotebook';
|
|
51
|
-
import { initialModelId, isOffered,
|
|
51
|
+
import { initialModelId, isOffered, offeredModels as offeredModelsFor, } from './modelChoice';
|
|
52
52
|
// EphemeralDocument statically imports `@datalayer/jupyter-lexical` (which
|
|
53
53
|
// initialises Lumino-backed nodes on load). Lazy-load it so notebook-only chats
|
|
54
54
|
// never pull lexical into the bundle or trigger its module side effects.
|
|
@@ -1380,37 +1380,46 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
1380
1380
|
: undefined;
|
|
1381
1381
|
const offeredModels = useMemo(() => {
|
|
1382
1382
|
// Only what can actually be called — see `modelChoice` for the rule.
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1383
|
+
// The runtime's answer, when it gave one, is the whole list.
|
|
1384
|
+
// A runtime that was asked and did not answer offers nothing: the
|
|
1385
|
+
// agentspecs catalogue below is not its answer.
|
|
1386
|
+
const fromConfig = availableModels ||
|
|
1387
|
+
configQuery.data?.models ||
|
|
1388
|
+
(configQuery.isError ? [] : undefined);
|
|
1389
|
+
return offeredModelsFor(fromConfig, () => {
|
|
1390
|
+
/*
|
|
1391
|
+
* The catalogue, filtered to what is worth offering.
|
|
1392
|
+
*
|
|
1393
|
+
* A server tells the chat which models it can reach and that answer wins.
|
|
1394
|
+
* Without one — an in-page agent, or a server that has not answered yet —
|
|
1395
|
+
* this used to offer the single model the protocol was built with, which
|
|
1396
|
+
* is a menu with nothing to choose.
|
|
1397
|
+
*
|
|
1398
|
+
* `available` is the filter, not the whole catalogue: twenty-six models,
|
|
1399
|
+
* most of them superseded, is a list nobody reads. The specs say which
|
|
1400
|
+
* four are current.
|
|
1401
|
+
*/
|
|
1402
|
+
const catalogued = Object.values(AI_MODEL_CATALOGUE)
|
|
1403
|
+
// A typed-judgment model answers typed questions, not a chat.
|
|
1404
|
+
.filter(model => model.available && isChatModel(model))
|
|
1405
|
+
.map(model => ({
|
|
1406
|
+
id: model.id,
|
|
1407
|
+
name: model.name,
|
|
1408
|
+
provider: model.provider,
|
|
1409
|
+
}));
|
|
1410
|
+
if (catalogued.length > 0) {
|
|
1411
|
+
return catalogued;
|
|
1412
|
+
}
|
|
1413
|
+
return browserModel
|
|
1414
|
+
? [{ id: browserModel, name: browserModel, provider: 'inference' }]
|
|
1415
|
+
: [];
|
|
1416
|
+
});
|
|
1417
|
+
}, [
|
|
1418
|
+
availableModels,
|
|
1419
|
+
configQuery.data?.models,
|
|
1420
|
+
configQuery.isError,
|
|
1421
|
+
browserModel,
|
|
1422
|
+
]);
|
|
1414
1423
|
/*
|
|
1415
1424
|
* Who `@` may address.
|
|
1416
1425
|
*
|
|
@@ -26,6 +26,17 @@ export declare const isUsable: (model: ModelConfig) => boolean;
|
|
|
26
26
|
* while the rows and their reasons say which key is missing.
|
|
27
27
|
*/
|
|
28
28
|
export declare function usableModels(models: ModelConfig[]): ModelConfig[];
|
|
29
|
+
/**
|
|
30
|
+
* The models a chat offers, given what its runtime answered.
|
|
31
|
+
*
|
|
32
|
+
* The runtime's answer is the list: an agent's own models (its `model` and
|
|
33
|
+
* `model_additionals`), each flagged with whether its inference serves it.
|
|
34
|
+
* When the runtime answered, nothing else is added — not even when its list
|
|
35
|
+
* is empty, which says the agent has nothing to switch to. `fallback` is
|
|
36
|
+
* asked only when there is no runtime to answer (an in-page agent) or it
|
|
37
|
+
* has not answered yet.
|
|
38
|
+
*/
|
|
39
|
+
export declare function offeredModels(fromRuntime: ModelConfig[] | undefined, fallback: () => ModelConfig[]): ModelConfig[];
|
|
29
40
|
/** Whether `id` is on offer and usable — the selection may stand. */
|
|
30
41
|
export declare const isOffered: (offered: ModelConfig[], id?: string) => boolean;
|
|
31
42
|
/**
|
|
@@ -59,4 +70,6 @@ export type ServerCatalogueModel = {
|
|
|
59
70
|
*/
|
|
60
71
|
export declare function readServerCatalogue(payload: {
|
|
61
72
|
models?: ServerCatalogueModel[];
|
|
73
|
+
source?: string;
|
|
74
|
+
note?: string;
|
|
62
75
|
} | null | undefined): ModelConfig[];
|
|
@@ -14,6 +14,19 @@ export function usableModels(models) {
|
|
|
14
14
|
const usable = models.filter(isUsable);
|
|
15
15
|
return usable.length > 0 ? usable : models;
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* The models a chat offers, given what its runtime answered.
|
|
19
|
+
*
|
|
20
|
+
* The runtime's answer is the list: an agent's own models (its `model` and
|
|
21
|
+
* `model_additionals`), each flagged with whether its inference serves it.
|
|
22
|
+
* When the runtime answered, nothing else is added — not even when its list
|
|
23
|
+
* is empty, which says the agent has nothing to switch to. `fallback` is
|
|
24
|
+
* asked only when there is no runtime to answer (an in-page agent) or it
|
|
25
|
+
* has not answered yet.
|
|
26
|
+
*/
|
|
27
|
+
export function offeredModels(fromRuntime, fallback) {
|
|
28
|
+
return usableModels(fromRuntime ?? fallback());
|
|
29
|
+
}
|
|
17
30
|
/** Whether `id` is on offer and usable — the selection may stand. */
|
|
18
31
|
export const isOffered = (offered, id) => Boolean(id) && offered.some(m => m.id === id && isUsable(m));
|
|
19
32
|
/**
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Tooltip } from '@primer/react';
|
|
3
3
|
import { Box } from '@datalayer/primer-addons';
|
|
4
|
-
import { CommentDiscussionIcon, DeviceMobileIcon, GrabberIcon, SidebarExpandIcon, } from '@primer/octicons-react';
|
|
5
|
-
import { CHAT_VIEW_MODES, SIDEBAR_NEEDS_MOUNT_POINT } from '../viewModes';
|
|
4
|
+
import { CommentDiscussionIcon, DeviceMobileIcon, GrabberIcon, PaperclipIcon, SidebarExpandIcon, } from '@primer/octicons-react';
|
|
5
|
+
import { ASSISTANT_NEEDS_A_FLOATING_CHAT, CHAT_VIEW_MODES, SIDEBAR_NEEDS_MOUNT_POINT, } from '../viewModes';
|
|
6
6
|
/** The drawing of each mode. */
|
|
7
7
|
export const VIEW_MODE_ICONS = {
|
|
8
8
|
floating: CommentDiscussionIcon,
|
|
9
9
|
'floating-small': DeviceMobileIcon,
|
|
10
10
|
'floating-draggable': GrabberIcon,
|
|
11
|
+
assistant: PaperclipIcon,
|
|
11
12
|
sidebar: SidebarExpandIcon,
|
|
12
13
|
};
|
|
13
14
|
export function ChatViewModeToggle({ value, onChange, disabledModes = [], tooltipDirection = 'n', }) {
|
|
@@ -26,7 +27,9 @@ export function ChatViewModeToggle({ value, onChange, disabledModes = [], toolti
|
|
|
26
27
|
const disabled = disabledModes.includes(mode);
|
|
27
28
|
return (_jsx(Tooltip, { text: disabled && mode === 'sidebar'
|
|
28
29
|
? `${label} — ${SIDEBAR_NEEDS_MOUNT_POINT}`
|
|
29
|
-
:
|
|
30
|
+
: disabled && mode === 'assistant'
|
|
31
|
+
? `${label} — ${ASSISTANT_NEEDS_A_FLOATING_CHAT}`
|
|
32
|
+
: label, direction: tooltipDirection, children: _jsx(Box, { as: "button", type: "button", "aria-label": label, "aria-pressed": selected, "aria-disabled": disabled || undefined, onClick: () => {
|
|
30
33
|
if (!disabled) {
|
|
31
34
|
onChange(mode);
|
|
32
35
|
}
|
|
@@ -10,7 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
* still under `prefers-reduced-motion`, and the line changes at the theme's
|
|
11
11
|
* status pace (T-10).
|
|
12
12
|
*
|
|
13
|
-
* @module
|
|
13
|
+
* @module chat/presence/Presence
|
|
14
14
|
*/
|
|
15
15
|
import { Box, Text } from '@primer/react';
|
|
16
16
|
import { PRESENCE_LINES } from './presenceStatus';
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* What an application is doing, in plain words (LOOP T-08): the one line of
|
|
3
3
|
* status beside its face.
|
|
4
4
|
*
|
|
5
|
-
* @module
|
|
5
|
+
* @module chat/presence/presenceStatus
|
|
6
6
|
*/
|
|
7
7
|
/** The states a person is told about. Paused waits for a pause to exist. */
|
|
8
8
|
export type PresenceState = 'idle' | 'thinking' | 'working' | 'waiting';
|
|
@@ -24,3 +24,9 @@ export interface PresenceTool {
|
|
|
24
24
|
* leave one open, and the application is not working then.
|
|
25
25
|
*/
|
|
26
26
|
export declare function presenceState(busy: boolean, tool?: PresenceTool): PresenceState;
|
|
27
|
+
/**
|
|
28
|
+
* The newest tool call of the conversation: whether it has not returned, and
|
|
29
|
+
* whether it waits on a person's approval. A tool call is an item with a
|
|
30
|
+
* tool name and a call id; it has returned once it is complete or failed.
|
|
31
|
+
*/
|
|
32
|
+
export declare function presenceToolOf(items: readonly unknown[]): PresenceTool;
|
|
@@ -26,3 +26,21 @@ export function presenceState(busy, tool) {
|
|
|
26
26
|
}
|
|
27
27
|
return tool?.open ? 'working' : 'thinking';
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The newest tool call of the conversation: whether it has not returned, and
|
|
31
|
+
* whether it waits on a person's approval. A tool call is an item with a
|
|
32
|
+
* tool name and a call id; it has returned once it is complete or failed.
|
|
33
|
+
*/
|
|
34
|
+
export function presenceToolOf(items) {
|
|
35
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
36
|
+
const item = items[index];
|
|
37
|
+
if (typeof item.toolName === 'string' && item.toolCallId) {
|
|
38
|
+
const open = item.status !== 'complete' && item.status !== 'error';
|
|
39
|
+
const pendingApproval = open &&
|
|
40
|
+
item.result
|
|
41
|
+
?.pending_approval === true;
|
|
42
|
+
return { open, pendingApproval };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { open: false, pendingApproval: false };
|
|
46
|
+
}
|
package/lib/chat/viewModes.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export type ChatViewModeOption = {
|
|
|
23
23
|
};
|
|
24
24
|
/** The modes, in the order the toggle shows them: floating first, docked last. */
|
|
25
25
|
export declare const CHAT_VIEW_MODES: readonly ChatViewModeOption[];
|
|
26
|
+
/**
|
|
27
|
+
* Why the assistant is greyed out, for its tooltip: it is drawn by the
|
|
28
|
+
* floating chat (`ChatFloating`); a layout that places the chat itself, as
|
|
29
|
+
* the page layout does, has no character to show (LOOP T-21).
|
|
30
|
+
*/
|
|
31
|
+
export declare const ASSISTANT_NEEDS_A_FLOATING_CHAT = "The assistant is drawn by the floating chat, not by this layout";
|
|
26
32
|
/** Why the sidebar option is greyed out, for its tooltip. */
|
|
27
33
|
export declare const SIDEBAR_NEEDS_MOUNT_POINT = "No mount point for a sidebar on this page";
|
|
28
34
|
/** Whether a mode floats over the page rather than docking into it. */
|
package/lib/chat/viewModes.js
CHANGED
|
@@ -7,15 +7,24 @@ export const CHAT_VIEW_MODES = [
|
|
|
7
7
|
{ mode: 'floating', label: 'Full-height popup' },
|
|
8
8
|
{ mode: 'floating-small', label: 'Floating popup' },
|
|
9
9
|
{ mode: 'floating-draggable', label: 'Floating draggable' },
|
|
10
|
+
// A character on the page that speaks in a balloon (LOOP T-21).
|
|
11
|
+
{ mode: 'assistant', label: 'Floating assistant' },
|
|
10
12
|
{ mode: 'sidebar', label: 'Sidebar panel' },
|
|
11
13
|
];
|
|
14
|
+
/**
|
|
15
|
+
* Why the assistant is greyed out, for its tooltip: it is drawn by the
|
|
16
|
+
* floating chat (`ChatFloating`); a layout that places the chat itself, as
|
|
17
|
+
* the page layout does, has no character to show (LOOP T-21).
|
|
18
|
+
*/
|
|
19
|
+
export const ASSISTANT_NEEDS_A_FLOATING_CHAT = 'The assistant is drawn by the floating chat, not by this layout';
|
|
12
20
|
/** Why the sidebar option is greyed out, for its tooltip. */
|
|
13
21
|
export const SIDEBAR_NEEDS_MOUNT_POINT = 'No mount point for a sidebar on this page';
|
|
14
22
|
/** Whether a mode floats over the page rather than docking into it. */
|
|
15
23
|
export function isFloatingChatViewMode(mode) {
|
|
16
24
|
return (mode === 'floating' ||
|
|
17
25
|
mode === 'floating-small' ||
|
|
18
|
-
mode === 'floating-draggable'
|
|
26
|
+
mode === 'floating-draggable' ||
|
|
27
|
+
mode === 'assistant');
|
|
19
28
|
}
|
|
20
29
|
/**
|
|
21
30
|
* Where a host said the sidebar goes.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The floating assistant (LOOP T-21 to T-27): the chat as a character on the
|
|
3
|
+
* page, after the Office Assistant — Datalayer's own characters, chosen
|
|
4
|
+
* here, acting out what the agent does and speaking in a balloon.
|
|
5
|
+
*/
|
|
6
|
+
import React from 'react';
|
|
7
|
+
declare const ChatAssistantExample: React.FC;
|
|
8
|
+
export default ChatAssistantExample;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The floating assistant (LOOP T-21 to T-27): the chat as a character on the
|
|
8
|
+
* page, after the Office Assistant — Datalayer's own characters, chosen
|
|
9
|
+
* here, acting out what the agent does and speaking in a balloon.
|
|
10
|
+
*/
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
import { Button, Heading, Text } from '@primer/react';
|
|
13
|
+
import { Box } from '@datalayer/primer-addons';
|
|
14
|
+
import { ThemedProvider } from './utils/themedProvider';
|
|
15
|
+
import { ChatFloating } from '../chat';
|
|
16
|
+
import { ASSISTANT_CHARACTERS } from '../chat/assistant/characters';
|
|
17
|
+
import { readAcsCharacter, readClippyCharacter, } from '../chat/assistant/formats';
|
|
18
|
+
/**
|
|
19
|
+
* A character file a person picked, read in the page (T-26): one `.acs`, or
|
|
20
|
+
* a clippy.js character's `agent.js` and `map.png` (and its sounds file).
|
|
21
|
+
*/
|
|
22
|
+
async function readPicked(files) {
|
|
23
|
+
const acs = files.find(file => file.name.toLowerCase().endsWith('.acs'));
|
|
24
|
+
if (acs) {
|
|
25
|
+
return readAcsCharacter(await acs.arrayBuffer());
|
|
26
|
+
}
|
|
27
|
+
const agentJs = files.find(file => file.name.toLowerCase() === 'agent.js');
|
|
28
|
+
const mapPng = files.find(file => /\.(png|gif|webp|jpe?g)$/i.test(file.name));
|
|
29
|
+
const soundsJs = files.find(file => /^sounds-.*\.js$/i.test(file.name));
|
|
30
|
+
if (!agentJs || !mapPng) {
|
|
31
|
+
throw new Error('Pick one .acs file, or a clippy.js character: its agent.js and its map image (and a sounds file if you have one).');
|
|
32
|
+
}
|
|
33
|
+
return readClippyCharacter({
|
|
34
|
+
agentJs: await agentJs.text(),
|
|
35
|
+
mapPng,
|
|
36
|
+
soundsJs: soundsJs ? await soundsJs.text() : undefined,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const ChatAssistantExample = () => {
|
|
40
|
+
const [character, setCharacter] = useState(ASSISTANT_CHARACTERS[0].id);
|
|
41
|
+
const [loadError, setLoadError] = useState();
|
|
42
|
+
return (_jsx(ThemedProvider, { children: _jsxs(Box, { sx: { minHeight: '100vh', bg: 'canvas.default', p: 4 }, children: [_jsxs(Box, { sx: { maxWidth: 720, mx: 'auto' }, children: [_jsx(Heading, { as: "h1", sx: { mb: 2 }, children: "Floating assistant" }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', mb: 3 }, children: "The chat as a character on the page. It greets you, acts out what the agent is doing, and says the agent\u2019s words in a balloon while the conversation is closed. Click it to talk, drag it to move it, hover it to send it away." }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: ASSISTANT_CHARACTERS.map(option => (_jsx(Button, { variant: option.id === character ? 'primary' : 'default', onClick: () => {
|
|
43
|
+
setCharacter(option.id);
|
|
44
|
+
setLoadError(undefined);
|
|
45
|
+
}, "aria-pressed": option.id === character, children: option.name }, option.id))) }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "A character you bring" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["The characters of Microsoft Office \u2014 Clippy, Merlin, Links and the others \u2014 are Microsoft\u2019s. Load only a character file you have the right to use: a Microsoft Agent ", _jsx("code", { children: ".acs" }), " file, or a clippy.js character\u2019s ", _jsx("code", { children: "agent.js" }), " and map image. It is read in this page and sent nowhere."] }), _jsx("input", { type: "file", multiple: true, accept: ".acs,.js,.png,.gif,.webp,.jpg,.jpeg", "aria-label": "Character files", onChange: async (event) => {
|
|
46
|
+
const files = Array.from(event.target.files ?? []);
|
|
47
|
+
if (!files.length) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
setCharacter(await readPicked(files));
|
|
52
|
+
setLoadError(undefined);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
setLoadError(error instanceof Error ? error.message : String(error));
|
|
56
|
+
}
|
|
57
|
+
} }), loadError && (_jsx(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mt: 2 }, children: loadError })), typeof character !== 'string' && (_jsxs(Text, { as: "p", sx: { mt: 2 }, children: ["Playing ", character.name, "."] }))] })] }), _jsx(ChatFloating, { defaultViewMode: "assistant", assistantCharacter: character, protocol: "vercel-ai", endpoint: "http://127.0.0.1:8765/api/v1/vercel-ai/assistant", title: "Assistant", description: "Hello! Ask me anything about this page.", position: "bottom-right", useStore: false }, typeof character === 'string' ? character : character.sprite)] }) }));
|
|
58
|
+
};
|
|
59
|
+
export default ChatAssistantExample;
|
|
@@ -87,6 +87,7 @@ export const EXAMPLE_ENTRIES = [
|
|
|
87
87
|
makeEntry('DocumentPageAgent', () => import('./DocumentPageAgent'), 'A document as a page: floating prompt on top, an agent writing in it.'),
|
|
88
88
|
makeEntry('DecksAgent', () => import('./DecksAgent'), 'A deck beside the chat: the decks plugin in a Loop, and an agent that writes presentations as data.'),
|
|
89
89
|
makeEntry('CellExample', () => import('./CellExample'), 'Simple cell example.'),
|
|
90
|
+
makeEntry('ChatAssistantExample', () => import('./ChatAssistantExample'), 'The floating assistant: the chat as a character on the page.'),
|
|
90
91
|
makeEntry('ChatCustomExample', () => import('./ChatCustomExample'), 'Custom chat experience composition example.'),
|
|
91
92
|
makeEntry('ChatExample', () => import('./ChatExample'), 'Baseline chat integration example.'),
|
|
92
93
|
makeEntry('ChatStandaloneExample', () => import('./ChatStandaloneExample'), 'Standalone chat component usage example.'),
|
package/lib/examples/main.js
CHANGED
|
@@ -77,6 +77,9 @@ const ANONYMOUS_EXAMPLES = new Set([
|
|
|
77
77
|
'DocumentPageAgent',
|
|
78
78
|
// The decks plugin in a Loop; the agent and the decks both live in the page.
|
|
79
79
|
'DecksAgent',
|
|
80
|
+
// The floating assistant: the character, its balloon and its motions need
|
|
81
|
+
// no runtime; a conversation does, and says so when there is none.
|
|
82
|
+
'ChatAssistantExample',
|
|
80
83
|
]);
|
|
81
84
|
const getExampleGroup = (id) => {
|
|
82
85
|
if (id === 'AgentspecsExample' ||
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ReactorPlugin } from '@datalayer/reactor';
|
|
2
2
|
import type { AppSpec } from '../../types/agentspecs';
|
|
3
3
|
import { type LoopEmbedProps } from '../embed/LoopEmbed';
|
|
4
|
-
import type {
|
|
4
|
+
import type { LoopPresetOptions } from '../presets';
|
|
5
|
+
import type { PresenceState } from '../../chat/presence/presenceStatus';
|
|
5
6
|
/** The id of an agent or a Cog, without its version. */
|
|
6
7
|
export declare const agentIdOf: (app: Pick<AppSpec, "agent" | "team">) => string;
|
|
7
8
|
/**
|
|
@@ -37,7 +38,38 @@ export type AppRendererProps = Omit<LoopEmbedProps, 'agentId'> & {
|
|
|
37
38
|
* stable function, such as a state setter.
|
|
38
39
|
*/
|
|
39
40
|
onPresence?: (state: PresenceState) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Draw the application in a window (the `window-frame` plugin), its title
|
|
43
|
+
* the application's face and name. A host's own `frameTitle` wins.
|
|
44
|
+
*/
|
|
45
|
+
frame?: boolean;
|
|
40
46
|
};
|
|
47
|
+
/** What `interface.layout` sets on the workspace. */
|
|
48
|
+
export type AppLayoutOptions = Pick<LoopPresetOptions, 'editors' | 'showViewSelector' | 'defaultEditor' | 'pageLayout' | 'pageLayoutArrangement' | 'pageLayoutPrompt' | 'pageLayoutPromptAnchor' | 'pageLayoutTurnPanelFooter'>;
|
|
49
|
+
/**
|
|
50
|
+
* The workspace an application's layout asks for (LOOP T-07).
|
|
51
|
+
*
|
|
52
|
+
* - `chat`: the conversation alone — no editor, no page, no strip.
|
|
53
|
+
* - `page`: the page layout — its page (or, without one, the editors) on the
|
|
54
|
+
* sheet; the composer a card over it, starting at the bottom, whose display
|
|
55
|
+
* modes put it in the conversation panel beside, over or in the corner of
|
|
56
|
+
* the page; the current turn under the composer, its copy and dismiss only.
|
|
57
|
+
* - `split`: the page layout's split — the conversation on the left with its
|
|
58
|
+
* composer, the page on the right, a hairline between them to drag.
|
|
59
|
+
*
|
|
60
|
+
* With a page, the page is the one editor, opened, without a strip to swap
|
|
61
|
+
* it; without one (a decision, whose page is the Studio's), the editors stay.
|
|
62
|
+
*/
|
|
63
|
+
export declare function appLayoutOptions(app: Pick<AppSpec, 'kind' | 'interface' | 'agent'>): AppLayoutOptions;
|
|
64
|
+
/**
|
|
65
|
+
* What an application's agent is created with on a Datalayer runtime: under
|
|
66
|
+
* the application's id, over AG-UI, with the application's own document —
|
|
67
|
+
* so that the runtime registers it and decides every tool call by its rules
|
|
68
|
+
* (LOOP R-03, R-05), as `loop apps run --cloud` does. One payload for every
|
|
69
|
+
* place it runs: the workspace (`AppRenderer`) and the embed's floating
|
|
70
|
+
* modes (`AppEmbed`).
|
|
71
|
+
*/
|
|
72
|
+
export declare function appDatalayerCreatePayload(app: AppSpec, instance?: AppInstance): Record<string, unknown>;
|
|
41
73
|
/**
|
|
42
74
|
* An application in a LOOP workspace: its agent, its starters, its layout.
|
|
43
75
|
*
|
|
@@ -47,5 +79,5 @@ export type AppRendererProps = Omit<LoopEmbedProps, 'agentId'> & {
|
|
|
47
79
|
/** No host plugins: one array, so that it never reads as a change. */
|
|
48
80
|
/** The agentspec a runtime is allocated with before an application's agent is created on it. */
|
|
49
81
|
export declare const DATALAYER_BOOTSTRAP_AGENTSPEC = "example-simple";
|
|
50
|
-
export declare function AppRenderer({ app, plugins, instance, onPresence, ...embed }: AppRendererProps): React.JSX.Element;
|
|
82
|
+
export declare function AppRenderer({ app, plugins, instance, onPresence, frame, ...embed }: AppRendererProps): React.JSX.Element;
|
|
51
83
|
export default AppRenderer;
|