@datalayer/agent-runtimes 1.3.29 → 1.3.32
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.js +40 -8
- package/lib/chat/assistant/AssistantStage.d.ts +26 -7
- package/lib/chat/assistant/AssistantStage.js +115 -8
- package/lib/chat/assistant/SpeechBalloon.js +7 -3
- package/lib/chat/assistant/characters.d.ts +32 -1
- package/lib/chat/assistant/characters.js +180 -15
- package/lib/chat/assistant/state.d.ts +28 -0
- package/lib/chat/assistant/state.js +31 -0
- package/lib/chat/base/ChatBase.js +74 -50
- package/lib/chat/base/modelChoice.d.ts +40 -1
- package/lib/chat/base/modelChoice.js +42 -15
- package/lib/chat/prompt/InputPrompt.d.ts +7 -1
- package/lib/chat/prompt/InputPrompt.js +2 -2
- package/lib/chat/prompt/InputPromptBase.js +1 -1
- package/lib/chat/prompt/menus/ModelSelector.d.ts +13 -1
- package/lib/chat/prompt/menus/ModelSelector.js +17 -5
- package/lib/chat/useViewportDrag.d.ts +9 -1
- package/lib/chat/useViewportDrag.js +4 -4
- package/lib/components/a2ui/index.d.ts +2 -0
- package/lib/components/a2ui/index.js +2 -0
- package/lib/components/a2ui/visibility.d.ts +15 -0
- package/lib/components/a2ui/visibility.js +24 -0
- package/lib/components/a2ui/visibleWhen.d.ts +17 -0
- package/lib/components/a2ui/visibleWhen.js +54 -0
- package/lib/examples/ChatAssistantExample.d.ts +4 -0
- package/lib/examples/ChatAssistantExample.js +26 -5
- package/lib/examples/utils/owlCharacterPlugin.d.ts +7 -0
- package/lib/examples/utils/owlCharacterPlugin.js +44 -0
- package/lib/loop/apps/AppFeedback.d.ts +27 -0
- package/lib/loop/apps/AppFeedback.js +78 -0
- package/lib/loop/apps/AppRenderer.d.ts +33 -1
- package/lib/loop/apps/AppRenderer.js +111 -29
- package/lib/loop/apps/agent.d.ts +15 -0
- package/lib/loop/apps/agent.js +42 -0
- package/lib/loop/apps/appspec.js +6 -3
- package/lib/loop/apps/checks.d.ts +10 -2
- package/lib/loop/apps/checks.js +21 -10
- package/lib/loop/apps/feedback.d.ts +51 -0
- package/lib/loop/apps/feedback.js +61 -0
- package/lib/loop/apps/index.d.ts +3 -0
- package/lib/loop/apps/index.js +3 -0
- package/lib/loop/apps/rules.d.ts +14 -0
- package/lib/loop/apps/rules.js +24 -0
- package/lib/loop/core/canvasBlocks.d.ts +16 -0
- package/lib/loop/core/canvasBlocks.js +13 -0
- package/lib/loop/core/index.d.ts +15 -0
- package/lib/loop/core/index.js +3 -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/A2uiView.js +2 -3
- package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +23 -3
- package/lib/loop/plugins/a2ui-surface/InlineSurface.js +36 -9
- 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/appPageModel.d.ts +180 -0
- package/lib/loop/plugins/app-page/appPageModel.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/canvas-blocks/index.d.ts +63 -0
- package/lib/loop/plugins/canvas-blocks/index.js +118 -0
- package/lib/loop/plugins/chat/ChatView.js +52 -26
- package/lib/loop/plugins/chat/turnState.d.ts +5 -2
- package/lib/loop/plugins/chat/turnState.js +2 -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 +10 -1
- package/lib/loop/presets.d.ts +12 -0
- package/lib/loop/presets.js +5 -3
- package/lib/specs/actions.js +9 -0
- package/lib/specs/agents/agents.js +20 -4
- package/lib/specs/apps.d.ts +10 -1
- package/lib/specs/apps.js +1779 -154
- package/lib/specs/appspecSchema.js +14 -8
- package/lib/specs/models.d.ts +1 -1
- package/lib/specs/models.js +8 -8
- package/lib/specs/tools.d.ts +1 -0
- package/lib/specs/tools.js +19 -0
- package/lib/stories/loop/LoopReference.stories.d.ts +21 -0
- package/lib/stories/loop/LoopReference.stories.js +31 -0
- package/lib/stories/loop/ReferenceScreens.d.ts +53 -0
- package/lib/stories/loop/ReferenceScreens.js +256 -0
- package/lib/stories/loop/pictures-main.d.ts +1 -0
- package/lib/stories/loop/pictures-main.js +64 -0
- package/lib/types/agentspecs.d.ts +17 -1
- package/lib/types/chat.d.ts +5 -0
- package/lib/types/config.d.ts +20 -0
- package/package.json +10 -6
- package/scripts/check-cloudflare-models.py +2 -2
- package/scripts/codegen/generate_agents.py +50 -2
- package/scripts/codegen/generate_apps.py +38 -7
- package/scripts/codegen/generate_models.py +4 -4
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An application's page, as data: what it publishes to its surface by path,
|
|
3
|
+
* what it takes from it, and what a button on it does (LOOP R-01, R-01b,
|
|
4
|
+
* C-04).
|
|
5
|
+
*
|
|
6
|
+
* A chat, a widget or a worker runs as its conversation. Its page is its
|
|
7
|
+
* A2UI surface (`interface.surface`) drawn beside that conversation, fed from
|
|
8
|
+
* it: the question last asked, the answer as it streams, where the turn
|
|
9
|
+
* stands. What a block writes — an input's `value` — stays in the surface's
|
|
10
|
+
* data model at a path the application takes, and what a button does reaches
|
|
11
|
+
* the application through the chat's own channels: a message sent, the
|
|
12
|
+
* answer stopped, the conversation started over. Nothing here is a server
|
|
13
|
+
* API; a setting reaches the application in the words of the message it is
|
|
14
|
+
* sent with, until the session API carries a settings change (R-04).
|
|
15
|
+
*
|
|
16
|
+
* A decision is not here: its page is drawn by the Studio's run page, with
|
|
17
|
+
* its own paths (`INTERFACE_PATHS`), until it moves onto the workspace (R-02).
|
|
18
|
+
*
|
|
19
|
+
* Pure: the plugin (`./index`) and its view (`./AppPage`) do the drawing.
|
|
20
|
+
*
|
|
21
|
+
* @module loop/plugins/app-page/appPageModel
|
|
22
|
+
*/
|
|
23
|
+
import type { AppSettingSpec, AppSpec } from '../../../types/agentspecs';
|
|
24
|
+
import type { ChatTurnSnapshot } from '../../core';
|
|
25
|
+
/** The id of the editor surface an application's page is drawn in. */
|
|
26
|
+
export declare const APP_PAGE_SURFACE = "app-page";
|
|
27
|
+
/** The id of the A2UI surface itself, inside it. */
|
|
28
|
+
export declare const APP_PAGE_SURFACE_ID = "app";
|
|
29
|
+
/** The kinds that run as their conversation and may have a page drawn by it. */
|
|
30
|
+
export type AppPageKind = 'chat' | 'widget' | 'worker';
|
|
31
|
+
/**
|
|
32
|
+
* One thing an application publishes to its page, or takes from it, by
|
|
33
|
+
* path. The shape of the Studio's `InterfacePath`, so that the Canvas reads
|
|
34
|
+
* this list as it reads a decision's.
|
|
35
|
+
*/
|
|
36
|
+
export type AppPagePath = {
|
|
37
|
+
path: string;
|
|
38
|
+
/** What it is, for a composer. */
|
|
39
|
+
meaning: string;
|
|
40
|
+
/** What it is, for a person choosing it on the Canvas. */
|
|
41
|
+
words: string;
|
|
42
|
+
list?: boolean;
|
|
43
|
+
};
|
|
44
|
+
/** What a Button on the page may do: the Studio's `InterfaceAction`. */
|
|
45
|
+
export type AppPageAction = {
|
|
46
|
+
name: string;
|
|
47
|
+
meaning: string;
|
|
48
|
+
words: string;
|
|
49
|
+
/** The keys its context may carry. */
|
|
50
|
+
context: string[];
|
|
51
|
+
/** Each key, in words. */
|
|
52
|
+
asks: Record<string, string>;
|
|
53
|
+
};
|
|
54
|
+
/** What one kind gives its page and takes from it. */
|
|
55
|
+
export type AppKindPaths = {
|
|
56
|
+
/** What a block may show. */
|
|
57
|
+
publishes: AppPagePath[];
|
|
58
|
+
/** Where what a block writes is read. */
|
|
59
|
+
accepts: AppPagePath[];
|
|
60
|
+
/** What a Button may do. */
|
|
61
|
+
actions: AppPageAction[];
|
|
62
|
+
/**
|
|
63
|
+
* Whether each of the application's settings (`interface.settings`) is
|
|
64
|
+
* also published and taken, at `/inputs/<id>` — see {@link appKindPaths}.
|
|
65
|
+
*/
|
|
66
|
+
inputs: boolean;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* What each kind publishes to its page and takes from it, by path — the
|
|
70
|
+
* least that is real today, read from the conversation it runs as.
|
|
71
|
+
*/
|
|
72
|
+
export declare const APP_KIND_PATHS: Record<AppPageKind, AppKindPaths>;
|
|
73
|
+
/** Whether a kind runs as its conversation and has its paths here. */
|
|
74
|
+
export declare const isAppPageKind: (kind: string) => kind is AppPageKind;
|
|
75
|
+
/** The path a setting, an input of the page, is kept at. */
|
|
76
|
+
export declare const inputPath: (id: string) => string;
|
|
77
|
+
/**
|
|
78
|
+
* What an application publishes and takes, with its own settings: the kind's
|
|
79
|
+
* list, and for a kind that has inputs each setting at `/inputs/<id>`, both
|
|
80
|
+
* shown and written. Empty lists for a decision, or a kind not known.
|
|
81
|
+
*/
|
|
82
|
+
export declare function appKindPaths(app: Pick<AppSpec, 'kind' | 'interface'>): AppKindPaths;
|
|
83
|
+
/**
|
|
84
|
+
* Whether the workspace draws the application's page: a chat, a widget or a
|
|
85
|
+
* worker whose layout has one (`page`, `split`). A `chat` layout is the
|
|
86
|
+
* conversation alone: a surface composed for it is not drawn, which
|
|
87
|
+
* `surfaceUnshown` says in a sentence.
|
|
88
|
+
*/
|
|
89
|
+
export declare function hasAppPage(app: Pick<AppSpec, 'kind' | 'interface' | 'agent'>): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Why a composed surface is not drawn, or `null` when it is (or there is
|
|
92
|
+
* none): the application's layout is `chat`, the conversation alone.
|
|
93
|
+
*/
|
|
94
|
+
export declare function surfaceUnshown(app: Pick<AppSpec, 'kind' | 'interface' | 'agent'>): string | null;
|
|
95
|
+
/** A setting's first value, as its input holds it. */
|
|
96
|
+
export declare function initialInput(setting: AppSettingSpec): unknown;
|
|
97
|
+
/** What the conversation publishes to the page, by path, now. */
|
|
98
|
+
export declare function appPageData(app: Pick<AppSpec, 'kind' | 'name' | 'goal'>, turn: ChatTurnSnapshot): Record<string, unknown>;
|
|
99
|
+
/** The page's data model when it is drawn: what is published, the inputs, the draft. */
|
|
100
|
+
export declare function appPageInitialData(app: Pick<AppSpec, 'kind' | 'name' | 'goal' | 'interface'>): Record<string, unknown>;
|
|
101
|
+
/**
|
|
102
|
+
* The inputs written on the page, in words, one per line: each setting by
|
|
103
|
+
* its label, then any other value a block wrote under `/inputs` by its id.
|
|
104
|
+
*/
|
|
105
|
+
export declare function inputsInWords(app: Pick<AppSpec, 'interface'>, inputs: unknown): string;
|
|
106
|
+
/** What a button asked, as the surface hands it over. */
|
|
107
|
+
export type AppPageEvent = {
|
|
108
|
+
name: string;
|
|
109
|
+
context?: Record<string, unknown>;
|
|
110
|
+
};
|
|
111
|
+
/** What the page does about a button: at most one of these. */
|
|
112
|
+
export type AppPageOutcome = {
|
|
113
|
+
/** A message for the application, sent as the person's next turn. */
|
|
114
|
+
send: string;
|
|
115
|
+
/** The inputs in words as sent, to tell a change next time. */
|
|
116
|
+
inputs: string;
|
|
117
|
+
/** Paths to empty once it is sent. */
|
|
118
|
+
clear: string[];
|
|
119
|
+
} | {
|
|
120
|
+
stop: true;
|
|
121
|
+
} | {
|
|
122
|
+
newChat: true;
|
|
123
|
+
} | {
|
|
124
|
+
refused: string;
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* What a button pressed on the page does.
|
|
128
|
+
*
|
|
129
|
+
* `read` gives what is in the surface's data model at a path; `lastInputs`
|
|
130
|
+
* is the inputs in words as last sent, so that a chat's message carries its
|
|
131
|
+
* settings only when they changed.
|
|
132
|
+
*/
|
|
133
|
+
export declare function appPageAction(app: Pick<AppSpec, 'kind' | 'name' | 'interface'>, event: AppPageEvent, read: (path: string) => unknown, lastInputs?: string): AppPageOutcome;
|
|
134
|
+
/** A Text, a Button and its label: the parts the default pages are made of. */
|
|
135
|
+
type Component = {
|
|
136
|
+
id: string;
|
|
137
|
+
component: string;
|
|
138
|
+
[key: string]: unknown;
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* The page drawn when the application has a page layout and no surface
|
|
142
|
+
* composed: what its kind publishes, its settings as inputs, and the
|
|
143
|
+
* buttons its kind handles that the conversation does not already give.
|
|
144
|
+
*/
|
|
145
|
+
export declare function defaultAppSurface(app: Pick<AppSpec, 'kind' | 'interface'>): Component[];
|
|
146
|
+
/**
|
|
147
|
+
* The A2UI messages that draw the application's page: its surface composed,
|
|
148
|
+
* or the default page of its kind, fed its first data.
|
|
149
|
+
*/
|
|
150
|
+
export declare function appPageMessages(app: Pick<AppSpec, 'kind' | 'name' | 'goal' | 'interface'>, catalogId: string): ({
|
|
151
|
+
version: "v0.9";
|
|
152
|
+
createSurface: {
|
|
153
|
+
surfaceId: string;
|
|
154
|
+
catalogId: string;
|
|
155
|
+
};
|
|
156
|
+
updateComponents?: undefined;
|
|
157
|
+
updateDataModel?: undefined;
|
|
158
|
+
} | {
|
|
159
|
+
version: "v0.9";
|
|
160
|
+
updateComponents: {
|
|
161
|
+
surfaceId: string;
|
|
162
|
+
components: {
|
|
163
|
+
[property: string]: unknown;
|
|
164
|
+
id: string;
|
|
165
|
+
component: string;
|
|
166
|
+
}[];
|
|
167
|
+
};
|
|
168
|
+
createSurface?: undefined;
|
|
169
|
+
updateDataModel?: undefined;
|
|
170
|
+
} | {
|
|
171
|
+
version: "v0.9";
|
|
172
|
+
updateDataModel: {
|
|
173
|
+
surfaceId: string;
|
|
174
|
+
path: string;
|
|
175
|
+
value: Record<string, unknown>;
|
|
176
|
+
};
|
|
177
|
+
createSurface?: undefined;
|
|
178
|
+
updateComponents?: undefined;
|
|
179
|
+
})[];
|
|
180
|
+
export {};
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/** The id of the editor surface an application's page is drawn in. */
|
|
6
|
+
export const APP_PAGE_SURFACE = 'app-page';
|
|
7
|
+
/** The id of the A2UI surface itself, inside it. */
|
|
8
|
+
export const APP_PAGE_SURFACE_ID = 'app';
|
|
9
|
+
const APP = {
|
|
10
|
+
path: '/app',
|
|
11
|
+
meaning: "The application's name.",
|
|
12
|
+
words: 'Its name',
|
|
13
|
+
};
|
|
14
|
+
const STATUS = {
|
|
15
|
+
path: '/status',
|
|
16
|
+
meaning: 'Where its answer stands, in a word: Ready, Thinking, Answering, Answered or Stopped.',
|
|
17
|
+
words: 'Where its answer stands',
|
|
18
|
+
};
|
|
19
|
+
const DRAFT = {
|
|
20
|
+
path: '/draft',
|
|
21
|
+
meaning: 'A message written on the page, which a Send button sends.',
|
|
22
|
+
words: 'A message to send',
|
|
23
|
+
};
|
|
24
|
+
const SEND = {
|
|
25
|
+
name: 'send',
|
|
26
|
+
meaning: 'Send a message to the application: the context’s message, or what is written at /draft.',
|
|
27
|
+
words: 'Send the message',
|
|
28
|
+
context: ['message'],
|
|
29
|
+
asks: { message: 'The message it sends' },
|
|
30
|
+
};
|
|
31
|
+
const STOP = {
|
|
32
|
+
name: 'stop',
|
|
33
|
+
meaning: 'Stop the answer being written.',
|
|
34
|
+
words: 'Stop the answer',
|
|
35
|
+
context: [],
|
|
36
|
+
asks: {},
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* What each kind publishes to its page and takes from it, by path — the
|
|
40
|
+
* least that is real today, read from the conversation it runs as.
|
|
41
|
+
*/
|
|
42
|
+
export const APP_KIND_PATHS = {
|
|
43
|
+
chat: {
|
|
44
|
+
publishes: [
|
|
45
|
+
APP,
|
|
46
|
+
{
|
|
47
|
+
path: '/question',
|
|
48
|
+
meaning: 'The question last asked.',
|
|
49
|
+
words: 'The question last asked',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
path: '/answer',
|
|
53
|
+
meaning: 'Its answer to it, as it is written.',
|
|
54
|
+
words: 'Its answer',
|
|
55
|
+
},
|
|
56
|
+
STATUS,
|
|
57
|
+
],
|
|
58
|
+
accepts: [DRAFT],
|
|
59
|
+
actions: [
|
|
60
|
+
SEND,
|
|
61
|
+
STOP,
|
|
62
|
+
{
|
|
63
|
+
name: 'new',
|
|
64
|
+
meaning: 'Start the conversation over.',
|
|
65
|
+
words: 'Start over',
|
|
66
|
+
context: [],
|
|
67
|
+
asks: {},
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
inputs: true,
|
|
71
|
+
},
|
|
72
|
+
widget: {
|
|
73
|
+
publishes: [
|
|
74
|
+
APP,
|
|
75
|
+
{
|
|
76
|
+
path: '/output',
|
|
77
|
+
meaning: 'What it gave for the inputs last run, as it is written.',
|
|
78
|
+
words: 'What it gave',
|
|
79
|
+
},
|
|
80
|
+
STATUS,
|
|
81
|
+
],
|
|
82
|
+
accepts: [],
|
|
83
|
+
actions: [
|
|
84
|
+
{
|
|
85
|
+
name: 'run',
|
|
86
|
+
meaning: 'Run it on its inputs: every value written under /inputs is sent, with the context’s message when there is one.',
|
|
87
|
+
words: 'Run it on its inputs',
|
|
88
|
+
context: ['message'],
|
|
89
|
+
asks: { message: 'What it says before the inputs' },
|
|
90
|
+
},
|
|
91
|
+
STOP,
|
|
92
|
+
],
|
|
93
|
+
inputs: true,
|
|
94
|
+
},
|
|
95
|
+
worker: {
|
|
96
|
+
publishes: [
|
|
97
|
+
APP,
|
|
98
|
+
{
|
|
99
|
+
path: '/goal',
|
|
100
|
+
meaning: 'What it works toward.',
|
|
101
|
+
words: 'What it works toward',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
path: '/activity',
|
|
105
|
+
meaning: 'What it is doing right now, in a short line, or nothing.',
|
|
106
|
+
words: 'What it is doing now',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
path: '/report',
|
|
110
|
+
meaning: 'What it last said, as it is written.',
|
|
111
|
+
words: 'What it last said',
|
|
112
|
+
},
|
|
113
|
+
STATUS,
|
|
114
|
+
],
|
|
115
|
+
accepts: [DRAFT],
|
|
116
|
+
actions: [SEND, STOP],
|
|
117
|
+
inputs: false,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
/** Whether a kind runs as its conversation and has its paths here. */
|
|
121
|
+
export const isAppPageKind = (kind) => Object.prototype.hasOwnProperty.call(APP_KIND_PATHS, kind);
|
|
122
|
+
/** The path a setting, an input of the page, is kept at. */
|
|
123
|
+
export const inputPath = (id) => `/inputs/${id}`;
|
|
124
|
+
/**
|
|
125
|
+
* What an application publishes and takes, with its own settings: the kind's
|
|
126
|
+
* list, and for a kind that has inputs each setting at `/inputs/<id>`, both
|
|
127
|
+
* shown and written. Empty lists for a decision, or a kind not known.
|
|
128
|
+
*/
|
|
129
|
+
export function appKindPaths(app) {
|
|
130
|
+
if (!isAppPageKind(app.kind)) {
|
|
131
|
+
return { publishes: [], accepts: [], actions: [], inputs: false };
|
|
132
|
+
}
|
|
133
|
+
const kind = APP_KIND_PATHS[app.kind];
|
|
134
|
+
if (!kind.inputs) {
|
|
135
|
+
return kind;
|
|
136
|
+
}
|
|
137
|
+
const inputs = app.interface.settings.map(setting => ({
|
|
138
|
+
path: inputPath(setting.id),
|
|
139
|
+
meaning: `The setting “${setting.label || setting.id}”.`,
|
|
140
|
+
words: setting.label || setting.id,
|
|
141
|
+
}));
|
|
142
|
+
return {
|
|
143
|
+
...kind,
|
|
144
|
+
publishes: [...kind.publishes, ...inputs],
|
|
145
|
+
accepts: [...kind.accepts, ...inputs],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/** The components its surface has, or none. */
|
|
149
|
+
const surfaceComponents = (app) => app.interface.surface?.components ?? [];
|
|
150
|
+
/**
|
|
151
|
+
* Whether the workspace draws the application's page: a chat, a widget or a
|
|
152
|
+
* worker whose layout has one (`page`, `split`). A `chat` layout is the
|
|
153
|
+
* conversation alone: a surface composed for it is not drawn, which
|
|
154
|
+
* `surfaceUnshown` says in a sentence.
|
|
155
|
+
*/
|
|
156
|
+
export function hasAppPage(app) {
|
|
157
|
+
if (!app.agent || !isAppPageKind(app.kind)) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
return app.interface.layout !== 'chat';
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Why a composed surface is not drawn, or `null` when it is (or there is
|
|
164
|
+
* none): the application's layout is `chat`, the conversation alone.
|
|
165
|
+
*/
|
|
166
|
+
export function surfaceUnshown(app) {
|
|
167
|
+
if (!app.agent ||
|
|
168
|
+
!isAppPageKind(app.kind) ||
|
|
169
|
+
app.interface.layout !== 'chat' ||
|
|
170
|
+
surfaceComponents(app).length === 0) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return 'Its page is composed but its layout is chat, the conversation alone: choose page or split to show it.';
|
|
174
|
+
}
|
|
175
|
+
const STATUS_WORDS = {
|
|
176
|
+
idle: 'Ready',
|
|
177
|
+
thinking: 'Thinking',
|
|
178
|
+
streaming: 'Answering',
|
|
179
|
+
done: 'Answered',
|
|
180
|
+
error: 'Stopped',
|
|
181
|
+
};
|
|
182
|
+
/** A setting's first value, as its input holds it. */
|
|
183
|
+
export function initialInput(setting) {
|
|
184
|
+
switch (setting.type) {
|
|
185
|
+
case 'toggle':
|
|
186
|
+
return Boolean(setting.default ?? false);
|
|
187
|
+
case 'select':
|
|
188
|
+
// A ChoicePicker holds a list of the values chosen.
|
|
189
|
+
return setting.default === undefined ? [] : [String(setting.default)];
|
|
190
|
+
case 'slider':
|
|
191
|
+
return Number(setting.default ?? setting.min ?? 0);
|
|
192
|
+
case 'number':
|
|
193
|
+
return setting.max === undefined
|
|
194
|
+
? String(setting.default ?? '')
|
|
195
|
+
: Number(setting.default ?? setting.min ?? 0);
|
|
196
|
+
default:
|
|
197
|
+
return String(setting.default ?? '');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/** What the conversation publishes to the page, by path, now. */
|
|
201
|
+
export function appPageData(app, turn) {
|
|
202
|
+
const data = {
|
|
203
|
+
'/app': app.name,
|
|
204
|
+
'/status': STATUS_WORDS[turn.status] ?? '',
|
|
205
|
+
};
|
|
206
|
+
switch (app.kind) {
|
|
207
|
+
case 'chat':
|
|
208
|
+
data['/question'] = turn.user ?? '';
|
|
209
|
+
data['/answer'] = turn.assistant ?? '';
|
|
210
|
+
break;
|
|
211
|
+
case 'widget':
|
|
212
|
+
data['/output'] = turn.assistant ?? '';
|
|
213
|
+
break;
|
|
214
|
+
case 'worker':
|
|
215
|
+
data['/goal'] = app.goal;
|
|
216
|
+
data['/activity'] = turn.activity ?? '';
|
|
217
|
+
data['/report'] = turn.assistant ?? '';
|
|
218
|
+
break;
|
|
219
|
+
default:
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
return data;
|
|
223
|
+
}
|
|
224
|
+
/** The page's data model when it is drawn: what is published, the inputs, the draft. */
|
|
225
|
+
export function appPageInitialData(app) {
|
|
226
|
+
const tree = {};
|
|
227
|
+
for (const [path, value] of Object.entries(appPageData(app, { id: 0, status: 'idle' }))) {
|
|
228
|
+
tree[path.slice(1)] = value;
|
|
229
|
+
}
|
|
230
|
+
const paths = appKindPaths(app);
|
|
231
|
+
if (paths.inputs) {
|
|
232
|
+
tree.inputs = Object.fromEntries(app.interface.settings.map(setting => [
|
|
233
|
+
setting.id,
|
|
234
|
+
initialInput(setting),
|
|
235
|
+
]));
|
|
236
|
+
}
|
|
237
|
+
if (paths.accepts.some(entry => entry.path === DRAFT.path)) {
|
|
238
|
+
tree.draft = '';
|
|
239
|
+
}
|
|
240
|
+
return tree;
|
|
241
|
+
}
|
|
242
|
+
/** One input in words: `Plan: Team`; nothing for an empty one. */
|
|
243
|
+
function inputWords(label, value) {
|
|
244
|
+
const shown = Array.isArray(value)
|
|
245
|
+
? value.map(String).join(', ')
|
|
246
|
+
: typeof value === 'boolean'
|
|
247
|
+
? value
|
|
248
|
+
? 'yes'
|
|
249
|
+
: 'no'
|
|
250
|
+
: value === undefined || value === null
|
|
251
|
+
? ''
|
|
252
|
+
: String(value);
|
|
253
|
+
return shown.trim() ? `${label}: ${shown}` : '';
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The inputs written on the page, in words, one per line: each setting by
|
|
257
|
+
* its label, then any other value a block wrote under `/inputs` by its id.
|
|
258
|
+
*/
|
|
259
|
+
export function inputsInWords(app, inputs) {
|
|
260
|
+
const values = inputs && typeof inputs === 'object'
|
|
261
|
+
? inputs
|
|
262
|
+
: {};
|
|
263
|
+
const lines = [];
|
|
264
|
+
const seen = new Set();
|
|
265
|
+
for (const setting of app.interface.settings) {
|
|
266
|
+
seen.add(setting.id);
|
|
267
|
+
lines.push(inputWords(setting.label || setting.id, values[setting.id]));
|
|
268
|
+
}
|
|
269
|
+
for (const [id, value] of Object.entries(values)) {
|
|
270
|
+
if (!seen.has(id)) {
|
|
271
|
+
lines.push(inputWords(id, value));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return lines.filter(Boolean).join('\n');
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* What a button pressed on the page does.
|
|
278
|
+
*
|
|
279
|
+
* `read` gives what is in the surface's data model at a path; `lastInputs`
|
|
280
|
+
* is the inputs in words as last sent, so that a chat's message carries its
|
|
281
|
+
* settings only when they changed.
|
|
282
|
+
*/
|
|
283
|
+
export function appPageAction(app, event, read, lastInputs = '') {
|
|
284
|
+
const paths = appKindPaths(app);
|
|
285
|
+
if (!paths.actions.some(action => action.name === event.name)) {
|
|
286
|
+
const offered = paths.actions.map(action => action.name).join(', ');
|
|
287
|
+
return {
|
|
288
|
+
refused: offered
|
|
289
|
+
? `“${event.name}” is not something ${app.name} does: it does ${offered}.`
|
|
290
|
+
: `${app.name} handles no button.`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
const given = event.context?.message;
|
|
294
|
+
const message = typeof given === 'string' ? given.trim() : '';
|
|
295
|
+
const inputs = paths.inputs ? inputsInWords(app, read('/inputs')) : '';
|
|
296
|
+
switch (event.name) {
|
|
297
|
+
case 'stop':
|
|
298
|
+
return { stop: true };
|
|
299
|
+
case 'new':
|
|
300
|
+
return { newChat: true };
|
|
301
|
+
case 'run': {
|
|
302
|
+
const text = [message, inputs].filter(Boolean).join('\n');
|
|
303
|
+
return text
|
|
304
|
+
? { send: text, inputs, clear: [] }
|
|
305
|
+
: { refused: `${app.name} has no inputs to run on: set one first.` };
|
|
306
|
+
}
|
|
307
|
+
default: {
|
|
308
|
+
const draft = read(DRAFT.path);
|
|
309
|
+
const written = message || (typeof draft === 'string' ? draft.trim() : '');
|
|
310
|
+
if (!written) {
|
|
311
|
+
return { refused: 'Nothing to send: write a message first.' };
|
|
312
|
+
}
|
|
313
|
+
const changed = inputs && inputs !== lastInputs;
|
|
314
|
+
return {
|
|
315
|
+
send: changed ? `${written}\n\n${inputs}` : written,
|
|
316
|
+
inputs,
|
|
317
|
+
clear: message ? [] : [DRAFT.path],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const text = (id, path, variant) => ({
|
|
323
|
+
id,
|
|
324
|
+
component: 'Text',
|
|
325
|
+
text: { path },
|
|
326
|
+
...(variant ? { variant } : {}),
|
|
327
|
+
});
|
|
328
|
+
const button = (id, label, name, primary = false) => [
|
|
329
|
+
{
|
|
330
|
+
id,
|
|
331
|
+
component: 'Button',
|
|
332
|
+
child: `${id}-label`,
|
|
333
|
+
...(primary ? { variant: 'primary' } : {}),
|
|
334
|
+
action: { event: { name } },
|
|
335
|
+
},
|
|
336
|
+
{ id: `${id}-label`, component: 'Text', text: label },
|
|
337
|
+
];
|
|
338
|
+
/** A setting as the input of the catalog that holds its value. */
|
|
339
|
+
function settingInput(setting) {
|
|
340
|
+
const base = {
|
|
341
|
+
id: `input-${setting.id}`,
|
|
342
|
+
label: setting.label || setting.id,
|
|
343
|
+
value: { path: inputPath(setting.id) },
|
|
344
|
+
};
|
|
345
|
+
switch (setting.type) {
|
|
346
|
+
case 'toggle':
|
|
347
|
+
return { ...base, component: 'CheckBox' };
|
|
348
|
+
case 'select':
|
|
349
|
+
return {
|
|
350
|
+
...base,
|
|
351
|
+
component: 'ChoicePicker',
|
|
352
|
+
options: setting.options.map(option => ({
|
|
353
|
+
label: option,
|
|
354
|
+
value: option,
|
|
355
|
+
})),
|
|
356
|
+
};
|
|
357
|
+
case 'slider':
|
|
358
|
+
return {
|
|
359
|
+
...base,
|
|
360
|
+
component: 'Slider',
|
|
361
|
+
min: setting.min ?? 0,
|
|
362
|
+
max: setting.max ?? 100,
|
|
363
|
+
};
|
|
364
|
+
case 'number':
|
|
365
|
+
return setting.max === undefined
|
|
366
|
+
? { ...base, component: 'TextField', variant: 'number' }
|
|
367
|
+
: {
|
|
368
|
+
...base,
|
|
369
|
+
component: 'Slider',
|
|
370
|
+
min: setting.min ?? 0,
|
|
371
|
+
max: setting.max,
|
|
372
|
+
};
|
|
373
|
+
default:
|
|
374
|
+
return { ...base, component: 'TextField' };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* The page drawn when the application has a page layout and no surface
|
|
379
|
+
* composed: what its kind publishes, its settings as inputs, and the
|
|
380
|
+
* buttons its kind handles that the conversation does not already give.
|
|
381
|
+
*/
|
|
382
|
+
export function defaultAppSurface(app) {
|
|
383
|
+
const inputs = appKindPaths(app).inputs
|
|
384
|
+
? app.interface.settings.map(settingInput)
|
|
385
|
+
: [];
|
|
386
|
+
const body = [];
|
|
387
|
+
switch (app.kind) {
|
|
388
|
+
case 'chat':
|
|
389
|
+
body.push(text('question', '/question', 'h3'), text('answer', '/answer'), text('status', '/status', 'caption'), ...inputs);
|
|
390
|
+
break;
|
|
391
|
+
case 'widget':
|
|
392
|
+
body.push(...inputs, { id: 'actions', component: 'Row', children: ['run', 'stop'] }, ...button('run', 'Run', 'run', true), ...button('stop', 'Stop', 'stop'), text('status', '/status', 'caption'), text('output', '/output'));
|
|
393
|
+
break;
|
|
394
|
+
case 'worker':
|
|
395
|
+
body.push(text('goal', '/goal', 'h3'), text('status', '/status', 'caption'), text('activity', '/activity', 'caption'), text('report', '/report'));
|
|
396
|
+
break;
|
|
397
|
+
default:
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
// The root's children: what is not inside a Row or a Button.
|
|
401
|
+
const inner = new Set(body.flatMap(component => [
|
|
402
|
+
...(Array.isArray(component.children) ? component.children : []),
|
|
403
|
+
component.child,
|
|
404
|
+
].filter((id) => typeof id === 'string')));
|
|
405
|
+
return [
|
|
406
|
+
{
|
|
407
|
+
id: 'root',
|
|
408
|
+
component: 'Column',
|
|
409
|
+
children: body
|
|
410
|
+
.map(component => component.id)
|
|
411
|
+
.filter(id => !inner.has(id)),
|
|
412
|
+
},
|
|
413
|
+
...body,
|
|
414
|
+
];
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* The A2UI messages that draw the application's page: its surface composed,
|
|
418
|
+
* or the default page of its kind, fed its first data.
|
|
419
|
+
*/
|
|
420
|
+
export function appPageMessages(app, catalogId) {
|
|
421
|
+
const composed = surfaceComponents(app);
|
|
422
|
+
return [
|
|
423
|
+
{
|
|
424
|
+
version: 'v0.9',
|
|
425
|
+
createSurface: { surfaceId: APP_PAGE_SURFACE_ID, catalogId },
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
version: 'v0.9',
|
|
429
|
+
updateComponents: {
|
|
430
|
+
surfaceId: APP_PAGE_SURFACE_ID,
|
|
431
|
+
components: composed.length > 0 ? composed : defaultAppSurface(app),
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
version: 'v0.9',
|
|
436
|
+
updateDataModel: {
|
|
437
|
+
surfaceId: APP_PAGE_SURFACE_ID,
|
|
438
|
+
path: '/',
|
|
439
|
+
value: appPageInitialData(app),
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
];
|
|
443
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ReactorPlugin } from '@datalayer/reactor';
|
|
2
|
+
import type { AppSpec } from '../../../types/agentspecs';
|
|
3
|
+
export declare const APP_PAGE_PLUGIN_NAME = "@datalayer/loop-plugin-app-page";
|
|
4
|
+
/** The plugin that draws an application's page beside its conversation. */
|
|
5
|
+
export declare function defineAppPagePlugin(app: AppSpec): ReactorPlugin<Record<string, never>, unknown, unknown>;
|
|
6
|
+
export { AppPage, type AppPageProps } from './AppPage';
|
|
7
|
+
export * from './appPageModel';
|
|
8
|
+
export default defineAppPagePlugin;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { BrowserIcon } from '@primer/octicons-react';
|
|
3
|
+
import { contribution, definePlugin, } from '@datalayer/reactor';
|
|
4
|
+
import { LoopEditorView } from '../../core';
|
|
5
|
+
import { AppPage } from './AppPage';
|
|
6
|
+
import { APP_PAGE_SURFACE } from './appPageModel';
|
|
7
|
+
export const APP_PAGE_PLUGIN_NAME = '@datalayer/loop-plugin-app-page';
|
|
8
|
+
/** The plugin that draws an application's page beside its conversation. */
|
|
9
|
+
export function defineAppPagePlugin(app) {
|
|
10
|
+
// One component per plugin: the editor keeps it, and the application with it.
|
|
11
|
+
function Page({ workspace }) {
|
|
12
|
+
return _jsx(AppPage, { app: app, workspace: workspace });
|
|
13
|
+
}
|
|
14
|
+
const plugin = definePlugin({
|
|
15
|
+
name: `${APP_PAGE_PLUGIN_NAME}-${app.id}`,
|
|
16
|
+
displayName: `${app.name}: its page`,
|
|
17
|
+
description: `The page of ${app.name}, drawn beside its conversation.`,
|
|
18
|
+
octicon: 'browser',
|
|
19
|
+
emoji: app.emoji || undefined,
|
|
20
|
+
contributes: [
|
|
21
|
+
contribution(LoopEditorView, {
|
|
22
|
+
surfaceId: APP_PAGE_SURFACE,
|
|
23
|
+
title: app.name,
|
|
24
|
+
icon: BrowserIcon,
|
|
25
|
+
order: -10,
|
|
26
|
+
load: async () => ({ default: Page }),
|
|
27
|
+
}, { id: APP_PAGE_SURFACE, order: -10 }),
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
return plugin;
|
|
31
|
+
}
|
|
32
|
+
export { AppPage } from './AppPage';
|
|
33
|
+
export * from './appPageModel';
|
|
34
|
+
export default defineAppPagePlugin;
|