@datalayer/agent-runtimes 1.3.71 → 1.3.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/chat/ChatFloating.d.ts +16 -1
- package/lib/chat/ChatFloating.js +62 -17
- package/lib/chat/assistant/AssistantStage.d.ts +17 -2
- package/lib/chat/assistant/AssistantStage.js +3 -3
- package/lib/chat/assistant/BalloonParts.d.ts +55 -0
- package/lib/chat/assistant/BalloonParts.js +140 -0
- package/lib/chat/assistant/ConversationBalloon.d.ts +5 -0
- package/lib/chat/assistant/ConversationBalloon.js +5 -0
- package/lib/chat/assistant/SpeechBalloon.d.ts +23 -2
- package/lib/chat/assistant/SpeechBalloon.js +22 -8
- package/lib/chat/assistant/toolLine.d.ts +69 -0
- package/lib/chat/assistant/toolLine.js +118 -0
- package/lib/components/teams/A2ATeamGraph.d.ts +12 -0
- package/lib/components/teams/A2ATeamGraph.js +35 -8
- package/lib/components/teams/NotebookPreview.d.ts +39 -0
- package/lib/components/teams/NotebookPreview.js +68 -0
- package/lib/components/teams/NotebookPreviewView.d.ts +21 -0
- package/lib/components/teams/NotebookPreviewView.js +42 -0
- package/lib/components/teams/TeamNotebook.js +16 -1
- package/lib/components/teams/a2aTeamFlow.d.ts +5 -0
- package/lib/components/teams/a2aTeamFlow.js +11 -0
- package/lib/components/teams/index.d.ts +1 -0
- package/lib/components/teams/index.js +1 -0
- package/lib/components/teams/useA2ATeam.d.ts +8 -0
- package/lib/components/teams/useA2ATeam.js +38 -3
- package/lib/examples/AgentA2ATeamExample.d.ts +8 -4
- package/lib/examples/AgentA2ATeamExample.js +2 -2
- package/lib/examples/ChatAssistantExample.js +4 -2
- package/lib/examples/ChatAssistantGalleryExample.d.ts +5 -1
- package/lib/examples/ChatAssistantGalleryExample.js +40 -10
- package/lib/examples/utils/AssistantGalleryGrid.d.ts +14 -11
- package/lib/examples/utils/AssistantGalleryGrid.js +42 -1
- package/lib/examples/utils/assistantGallery.d.ts +49 -1
- package/lib/examples/utils/assistantGallery.js +52 -1
- package/lib/loop/apps/appspec.js +6 -0
- package/lib/loop/apps/checks.js +2 -0
- package/lib/loop/embed/AppEmbed.js +3 -1
- package/lib/loop/plugins/assistant/index.d.ts +14 -1
- package/lib/loop/plugins/assistant/index.js +39 -13
- package/lib/specs/appspecSchema.js +18 -0
- package/lib/stories/loop/ReferenceScreens.d.ts +1 -1
- package/lib/stories/loop/ReferenceScreens.js +20 -3
- package/lib/types/agentspecs.d.ts +8 -0
- package/package.json +1 -1
|
@@ -28,7 +28,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
28
28
|
import { stepCountIs, ToolLoopAgent } from 'ai';
|
|
29
29
|
import { createBrowserModel, } from '../../runtimes/browser/model';
|
|
30
30
|
import { a2aPeerTool, NOTEBOOK_MEDIA_TYPE, } from '../../runtimes/browser/a2aPeer';
|
|
31
|
-
import { callsAfter, flowAfter, pruneCalls, } from './a2aTeamFlow';
|
|
31
|
+
import { callsAfter, flowAfter, toolOwnName, pruneCalls, } from './a2aTeamFlow';
|
|
32
|
+
import { toolLineOfStep, } from '../../chat/assistant/toolLine';
|
|
32
33
|
const NO_CONNECTIONS = [];
|
|
33
34
|
export const AT_REST = {
|
|
34
35
|
state: 'idle',
|
|
@@ -116,6 +117,7 @@ export function useA2ATeam(options) {
|
|
|
116
117
|
state: 'greeting',
|
|
117
118
|
saying: 'On it. Let me read the books.',
|
|
118
119
|
insist: true,
|
|
120
|
+
tool: undefined,
|
|
119
121
|
}));
|
|
120
122
|
}
|
|
121
123
|
else if (event.phase === 'working') {
|
|
@@ -126,7 +128,12 @@ export function useA2ATeam(options) {
|
|
|
126
128
|
...prev,
|
|
127
129
|
state: 'working',
|
|
128
130
|
saying: event.note ? balloonLine(event.note) : prev.saying,
|
|
129
|
-
insist: Boolean(event.note) || prev.insist,
|
|
131
|
+
insist: Boolean(event.note) || Boolean(event.tool) || prev.insist,
|
|
132
|
+
// The tool it calls, in plain words, from the step it told; a
|
|
133
|
+
// step without a tool (its own words) puts the tool line away.
|
|
134
|
+
tool: event.tool
|
|
135
|
+
? toolLineOfStep(event.tool, toolOwnName(event.tool.name, peerConnections))
|
|
136
|
+
: undefined,
|
|
130
137
|
}));
|
|
131
138
|
}
|
|
132
139
|
else if (event.phase === 'answered') {
|
|
@@ -145,7 +152,12 @@ export function useA2ATeam(options) {
|
|
|
145
152
|
state: 'speaking',
|
|
146
153
|
saying: balloonLine(event.answer),
|
|
147
154
|
insist: true,
|
|
155
|
+
tool: undefined,
|
|
148
156
|
}));
|
|
157
|
+
// The entry was given a notebook: in its balloon, read-only.
|
|
158
|
+
if (given) {
|
|
159
|
+
setEntryPersona(prev => ({ ...prev, notebook: given }));
|
|
160
|
+
}
|
|
149
161
|
}
|
|
150
162
|
else {
|
|
151
163
|
setExchange(prev => [
|
|
@@ -157,6 +169,7 @@ export function useA2ATeam(options) {
|
|
|
157
169
|
state: 'idle',
|
|
158
170
|
saying: balloonLine(event.error),
|
|
159
171
|
insist: true,
|
|
172
|
+
tool: undefined,
|
|
160
173
|
}));
|
|
161
174
|
}
|
|
162
175
|
}, [entry.name, peerApp.id, peerApp.name, peerConnections]);
|
|
@@ -210,6 +223,14 @@ export function useA2ATeam(options) {
|
|
|
210
223
|
state: 'waiting',
|
|
211
224
|
saying: balloonLine(`${peerApp.name}, could you: ${request}`),
|
|
212
225
|
insist: true,
|
|
226
|
+
// Its one tool, in its own words: it asks its peer.
|
|
227
|
+
tool: {
|
|
228
|
+
id: part.toolCallId,
|
|
229
|
+
tool: askTool,
|
|
230
|
+
name: askTool,
|
|
231
|
+
phase: 'running',
|
|
232
|
+
words: `Asking ${peerApp.name}…`,
|
|
233
|
+
},
|
|
213
234
|
}));
|
|
214
235
|
}
|
|
215
236
|
else if (part.type === 'tool-result' && part.toolName === askTool) {
|
|
@@ -217,6 +238,7 @@ export function useA2ATeam(options) {
|
|
|
217
238
|
...prev,
|
|
218
239
|
state: 'thinking',
|
|
219
240
|
saying: 'Thanks!',
|
|
241
|
+
tool: undefined,
|
|
220
242
|
}));
|
|
221
243
|
// Accounting has handed its answer over: back at rest, its last
|
|
222
244
|
// words still in its balloon.
|
|
@@ -229,6 +251,16 @@ export function useA2ATeam(options) {
|
|
|
229
251
|
state: 'speaking',
|
|
230
252
|
saying: balloonLine(said),
|
|
231
253
|
insist: true,
|
|
254
|
+
tool: undefined,
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
else if (part.type === 'tool-error' && part.toolName === askTool) {
|
|
258
|
+
setEntryPersona(prev => ({
|
|
259
|
+
...prev,
|
|
260
|
+
state: 'thinking',
|
|
261
|
+
tool: prev.tool
|
|
262
|
+
? { ...prev.tool, phase: 'failed', words: undefined }
|
|
263
|
+
: undefined,
|
|
232
264
|
}));
|
|
233
265
|
}
|
|
234
266
|
else if (part.type === 'error') {
|
|
@@ -257,6 +289,7 @@ export function useA2ATeam(options) {
|
|
|
257
289
|
state: 'idle',
|
|
258
290
|
saying: balloonLine(`Something went wrong: ${message}`),
|
|
259
291
|
insist: true,
|
|
292
|
+
tool: undefined,
|
|
260
293
|
}));
|
|
261
294
|
setFlow('still');
|
|
262
295
|
callsNow.current = [];
|
|
@@ -266,7 +299,9 @@ export function useA2ATeam(options) {
|
|
|
266
299
|
abort.current = null;
|
|
267
300
|
setBusy(false);
|
|
268
301
|
// However the turn ended, neither member is still at work.
|
|
269
|
-
setPeerPersona(prev => prev.state === 'idle'
|
|
302
|
+
setPeerPersona(prev => prev.state === 'idle' && !prev.tool
|
|
303
|
+
? prev
|
|
304
|
+
: { ...prev, state: 'idle', tool: undefined });
|
|
270
305
|
}
|
|
271
306
|
}, [agent, busy, askTool, peerApp.name]);
|
|
272
307
|
const stop = useCallback(() => abort.current?.abort(), []);
|
|
@@ -14,14 +14,18 @@
|
|
|
14
14
|
* — the edge flows back — each in its own state and balloon. Under
|
|
15
15
|
* Accounting hangs Odoo, its connection (the odoo-accounting MCP server, from
|
|
16
16
|
* its Appspec): the edge to it flows while Accounting calls one of its tools,
|
|
17
|
-
* the tool's name on it.
|
|
18
|
-
*
|
|
17
|
+
* the tool's name on it. Each balloon shows only what its member says or
|
|
18
|
+
* does now (`current`): "Asking Accounting…" for Sales' one tool, "Using
|
|
19
|
+
* list_invoices…" while Accounting calls Odoo. Then the report appears. The
|
|
20
|
+
* page's own state is `useA2ATeam`'s, which the landing's home page runs too.
|
|
19
21
|
*
|
|
20
22
|
* Sales accepts a Jupyter notebook from Accounting besides its words
|
|
21
23
|
* (`acceptedOutputModes`, `NOTEBOOK_AND_WORDS`). When Accounting gives one,
|
|
22
|
-
*
|
|
24
|
+
* Sales' balloon shows it read-only (`NotebookPreview`: no kernel), and it
|
|
25
|
+
* opens right under the team, in place of the report, on the browser
|
|
23
26
|
* sandbox — a Pyodide kernel in this page — to run, change and download
|
|
24
|
-
* (`TeamNotebook`, loaded only then)
|
|
27
|
+
* (`TeamNotebook`, loaded only then); a click on the one in the balloon
|
|
28
|
+
* comes to it.
|
|
25
29
|
*
|
|
26
30
|
* Where Accounting is: `VITE_A2A_ACCOUNTING_URL`, else the examples' local
|
|
27
31
|
* server (`…:8765/api/v1/a2a/agents/accounting`), which answers this machine
|
|
@@ -112,7 +112,7 @@ function AgentA2ATeam() {
|
|
|
112
112
|
persona: team.peerPersona,
|
|
113
113
|
onAway: team.setPeerAway,
|
|
114
114
|
connections: ACCOUNTING_CONNECTIONS,
|
|
115
|
-
}, flow: team.flow, calls: team.calls, connected: peer !== null, label: peer ? `A2A · ${peer.skill.name}` : 'A2A · not connected' }) }), _jsxs(Box, { sx: { display: 'flex', gap: 3, flexWrap: 'wrap' }, children: [_jsxs(Box, { sx: { flex: '1 1 420px', minWidth: 0 }, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: ["Ask ", SALES.name] }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap', mb: 2 }, children: (SALES.interface.starters ?? []).map(starter => (_jsx(Button, { size: "small", disabled: !team.ready || team.busy, onClick: () => send(starter.message), children: starter.label }, starter.label))) }), _jsx(Textarea, { ref: composer, block: true, rows: 3, value: draft, placeholder: "Ask for a financial report\u2026", onChange: event => setDraft(event.target.value), onKeyDown: event => {
|
|
115
|
+
}, flow: team.flow, calls: team.calls, connected: peer !== null, label: peer ? `A2A · ${peer.skill.name}` : 'A2A · not connected' }) }), team.notebook && (_jsx(Box, { sx: { mb: 3 }, "data-team-notebook-placement": "under-graph", children: _jsx(TeamNotebook, { notebook: team.notebook, title: `${ACCOUNTING.name}\u2019s notebook` }) })), _jsxs(Box, { sx: { display: 'flex', gap: 3, flexWrap: 'wrap' }, children: [_jsxs(Box, { sx: { flex: '1 1 420px', minWidth: 0 }, children: [_jsxs(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: ["Ask ", SALES.name] }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap', mb: 2 }, children: (SALES.interface.starters ?? []).map(starter => (_jsx(Button, { size: "small", disabled: !team.ready || team.busy, onClick: () => send(starter.message), children: starter.label }, starter.label))) }), _jsx(Textarea, { ref: composer, block: true, rows: 3, value: draft, placeholder: "Ask for a financial report\u2026", onChange: event => setDraft(event.target.value), onKeyDown: event => {
|
|
116
116
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
117
117
|
event.preventDefault();
|
|
118
118
|
send(draft);
|
|
@@ -128,7 +128,7 @@ function AgentA2ATeam() {
|
|
|
128
128
|
borderColor: 'border.default',
|
|
129
129
|
borderRadius: 2,
|
|
130
130
|
minHeight: 120,
|
|
131
|
-
}, "data-team-report": "", children: team.report ? (_jsx(Streamdown, { children: team.report })) : (_jsx(Text, { sx: { color: 'fg.muted' }, children: "The report Accounting returns appears here, as it returned it, or as a notebook to run when it gives one." })) })] }))] })] })
|
|
131
|
+
}, "data-team-report": "", children: team.report ? (_jsx(Streamdown, { children: team.report })) : (_jsx(Text, { sx: { color: 'fg.muted' }, children: "The report Accounting returns appears here, as it returned it, or as a notebook to run when it gives one." })) })] }))] })] })] }));
|
|
132
132
|
}
|
|
133
133
|
export function AgentA2ATeamExample() {
|
|
134
134
|
return (_jsx(ThemedProvider, { children: _jsx(AgentA2ATeam, {}) }));
|
|
@@ -73,6 +73,8 @@ const ChatAssistantExample = () => {
|
|
|
73
73
|
? assistantCharacterNamed(reactor, character)
|
|
74
74
|
: character;
|
|
75
75
|
const [loadError, setLoadError] = useState();
|
|
76
|
+
// How the balloon shows the conversation (T-23): history, or current.
|
|
77
|
+
const [balloon, setBalloon] = useState('history');
|
|
76
78
|
// Pixel, the test sprite, read once through the clippy.js reader.
|
|
77
79
|
const [pixel, setPixel] = useState();
|
|
78
80
|
useEffect(() => {
|
|
@@ -135,7 +137,7 @@ const ChatAssistantExample = () => {
|
|
|
135
137
|
}, "aria-pressed": option.id === character, "data-assistant-character": option.id, children: option.name }, option.id))), pixel && (_jsxs(Button, { variant: character === pixel ? 'primary' : 'default', onClick: () => {
|
|
136
138
|
setCharacter(pixel);
|
|
137
139
|
setLoadError(undefined);
|
|
138
|
-
}, "aria-pressed": character === pixel, "data-assistant-character": "sprite", children: [pixel.name, " (sprite)"] }))] }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "The clippy.js characters" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["Fetched when picked from the", ' ', _jsxs("code", { children: ["clippyjs@", CLIPPY_JS_VERSION] }), " package on jsDelivr and read as data by the same reader. They are not in this repository: clippy.js\u2019s licence covers its code only, and the characters are Microsoft\u2019s."] }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: CLIPPY_JS_CHARACTERS.map(name => {
|
|
140
|
+
}, "aria-pressed": character === pixel, "data-assistant-character": "sprite", children: [pixel.name, " (sprite)"] }))] }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "The balloon" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: [_jsx("strong", { children: "History" }), ": open, the whole conversation, counted in its header, the composer last. ", _jsx("strong", { children: "Current" }), ": only what it says or does now \u2014 the answer as it is written, or the tool it calls (\u201CUsing current_time\u2026\u201D) \u2014 in one compact balloon. Either way, a tool call is said in the balloon."] }), _jsx(Box, { sx: { display: 'flex', gap: 2 }, children: ['history', 'current'].map(option => (_jsx(Button, { variant: option === balloon ? 'primary' : 'default', "aria-pressed": option === balloon, "data-assistant-balloon-choice": option, onClick: () => setBalloon(option), children: option === 'history' ? 'History' : 'Current' }, option))) })] }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "The clippy.js characters" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["Fetched when picked from the", ' ', _jsxs("code", { children: ["clippyjs@", CLIPPY_JS_VERSION] }), " package on jsDelivr and read as data by the same reader. They are not in this repository: clippy.js\u2019s licence covers its code only, and the characters are Microsoft\u2019s."] }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: CLIPPY_JS_CHARACTERS.map(name => {
|
|
139
141
|
const picked = clippyJs[name] !== undefined && character === clippyJs[name];
|
|
140
142
|
return (_jsx(Button, { variant: picked ? 'primary' : 'default', onClick: () => void pickClippyJs(name), disabled: fetching !== undefined, "aria-pressed": picked, "data-assistant-character": `clippy-js-${name.toLowerCase()}`, children: fetching === name ? `${name}…` : name }, name));
|
|
141
143
|
}) })] }), _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) => {
|
|
@@ -152,7 +154,7 @@ const ChatAssistantExample = () => {
|
|
|
152
154
|
}
|
|
153
155
|
} }), loadError && (_jsx(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mt: 2 }, children: loadError })), typeof character !== 'string' &&
|
|
154
156
|
character !== pixel &&
|
|
155
|
-
!isClippyJs && (_jsxs(Text, { as: "p", sx: { mt: 2 }, children: ["Playing ", character.name, "."] }))] })] }), _jsx(ChatFloating, { defaultViewMode: "assistant", assistantCharacter: drawn, protocol: "vercel-ai", endpoint: `${SERVER}/api/v1/vercel-ai/assistant`, title: "Assistant", description: "Hello! Ask me anything about this page.", position: "bottom-right", useStore: false,
|
|
157
|
+
!isClippyJs && (_jsxs(Text, { as: "p", sx: { mt: 2 }, children: ["Playing ", character.name, "."] }))] })] }), _jsx(ChatFloating, { defaultViewMode: "assistant", assistantCharacter: drawn, balloonDisplay: balloon, protocol: "vercel-ai", endpoint: `${SERVER}/api/v1/vercel-ai/assistant`, title: "Assistant", description: "Hello! Ask me anything about this page.", position: "bottom-right", useStore: false,
|
|
156
158
|
// Ask a decision beside the composer: Jev, through this server.
|
|
157
159
|
decisions: { serverUrl: SERVER } }, typeof character === 'string' ? character : character.sprite)] }) }));
|
|
158
160
|
};
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
* contributes to `loop.assistant.character`, a test sprite read through
|
|
5
5
|
* the clippy.js reader, and, when asked, the characters clippy.js publishes,
|
|
6
6
|
* fetched from jsDelivr — in each state it acts, stepped aside, with its
|
|
7
|
-
* balloon, in light and dark, moving or still.
|
|
7
|
+
* balloon, in light and dark, moving or still. The balloon shows the
|
|
8
|
+
* conversation either way (T-23): a peek of it (`history`), or only what is
|
|
9
|
+
* said or done now (`current`); a tool call is said in plain words ("Using
|
|
10
|
+
* list_invoices…", "Done: list_invoices", "list_invoices failed"), and a
|
|
11
|
+
* recorded notebook shows read-only in the current balloon.
|
|
8
12
|
*
|
|
9
13
|
* Static data, no agent: it opens without a server or an account.
|
|
10
14
|
*/
|
|
@@ -9,7 +9,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
9
9
|
* contributes to `loop.assistant.character`, a test sprite read through
|
|
10
10
|
* the clippy.js reader, and, when asked, the characters clippy.js publishes,
|
|
11
11
|
* fetched from jsDelivr — in each state it acts, stepped aside, with its
|
|
12
|
-
* balloon, in light and dark, moving or still.
|
|
12
|
+
* balloon, in light and dark, moving or still. The balloon shows the
|
|
13
|
+
* conversation either way (T-23): a peek of it (`history`), or only what is
|
|
14
|
+
* said or done now (`current`); a tool call is said in plain words ("Using
|
|
15
|
+
* list_invoices…", "Done: list_invoices", "list_invoices failed"), and a
|
|
16
|
+
* recorded notebook shows read-only in the current balloon.
|
|
13
17
|
*
|
|
14
18
|
* Static data, no agent: it opens without a server or an account.
|
|
15
19
|
*/
|
|
@@ -20,11 +24,11 @@ import { ThemedProvider } from './utils/themedProvider';
|
|
|
20
24
|
import { useExampleThemeStore } from './utils/themeStore';
|
|
21
25
|
import { AssistantStage } from '../chat/assistant/AssistantStage';
|
|
22
26
|
import { useViewportDrag } from '../chat/useViewportDrag';
|
|
23
|
-
import { AssistantGalleryGrid, GalleryObstacle, Still, useGalleryCharacters, } from './utils/AssistantGalleryGrid';
|
|
24
|
-
import { GALLERY_POSES, GALLERY_POSE_LABELS, balloonForPose, sampleApproval, sampleSaying, stateOfPose, } from './utils/assistantGallery';
|
|
27
|
+
import { AssistantGalleryGrid, GalleryBalloons, GalleryNotebook, GalleryObstacle, Still, useGalleryCharacters, } from './utils/AssistantGalleryGrid';
|
|
28
|
+
import { GALLERY_POSES, GALLERY_POSE_LABELS, SAMPLE_NOTEBOOK_SAYING, balloonForPose, sampleApproval, sampleSaying, sampleToolLine, stateOfPose, } from './utils/assistantGallery';
|
|
25
29
|
import { CLIPPY_JS_VERSION, useClippyJsCharacters, } from './utils/clippyJsCharacters';
|
|
26
30
|
const STAGE_SIZE = 96;
|
|
27
|
-
const PANEL_HEIGHT =
|
|
31
|
+
const PANEL_HEIGHT = 520;
|
|
28
32
|
const LEAVE_MS = 700;
|
|
29
33
|
const ARRIVE_MS = 1900;
|
|
30
34
|
const AWAY_WORDS = {
|
|
@@ -34,7 +38,7 @@ const AWAY_WORDS = {
|
|
|
34
38
|
always: 'for good',
|
|
35
39
|
};
|
|
36
40
|
/** One colour mode's panel, with the character on its own stage. */
|
|
37
|
-
function ModePanel({ mode, character, pose, presence, balloon, insist, onToggle, onDismiss, onCallBack, away, replay, }) {
|
|
41
|
+
function ModePanel({ mode, character, pose, presence, balloon, insist, onToggle, onDismiss, onCallBack, away, replay, display, }) {
|
|
38
42
|
const panelRef = useRef(null);
|
|
39
43
|
const stageRef = useRef(null);
|
|
40
44
|
const drag = useViewportDrag(stageRef, { whole: true });
|
|
@@ -80,7 +84,7 @@ function ModePanel({ mode, character, pose, presence, balloon, insist, onToggle,
|
|
|
80
84
|
position: 'absolute',
|
|
81
85
|
left: spot.left,
|
|
82
86
|
top: spot.top + STAGE_SIZE / 2 - 16,
|
|
83
|
-
}, children: [_jsxs(Button, { size: "small", onClick: onCallBack, "data-gallery-call-back": "", children: ["Call ", character.name, " back"] }), _jsxs(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', mt: 1 }, children: ["Sent away ", AWAY_WORDS[away], "."] })] })) : (_jsx(AssistantStage, { character: character.character, state: state, size: STAGE_SIZE, place: { position: 'absolute', left: spot.left, top: spot.top }, stageRef: stageRef, onDragStart: drag.onHandlePointerDown, open: false, onToggle: onToggle, balloon: presence === 'here' ? balloon : undefined, insist: insist, onDismiss: onDismiss }, `${character.id}-${replay}`)), pose === 'aside' && presence === 'here' && (_jsx(GalleryObstacle, { sx: {
|
|
87
|
+
}, children: [_jsxs(Button, { size: "small", onClick: onCallBack, "data-gallery-call-back": "", children: ["Call ", character.name, " back"] }), _jsxs(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', mt: 1 }, children: ["Sent away ", AWAY_WORDS[away], "."] })] })) : (_jsx(AssistantStage, { character: character.character, state: state, size: STAGE_SIZE, place: { position: 'absolute', left: spot.left, top: spot.top }, stageRef: stageRef, onDragStart: drag.onHandlePointerDown, open: false, onToggle: onToggle, balloon: presence === 'here' ? balloon : undefined, balloonDisplay: display, insist: insist, onDismiss: onDismiss }, `${character.id}-${replay}`)), pose === 'aside' && presence === 'here' && (_jsx(GalleryObstacle, { sx: {
|
|
84
88
|
left: spot.left - 12,
|
|
85
89
|
top: spot.top - 12,
|
|
86
90
|
width: STAGE_SIZE + 24,
|
|
@@ -115,6 +119,10 @@ const ChatAssistantGalleryExample = () => {
|
|
|
115
119
|
const [presence, setPresence] = useState('here');
|
|
116
120
|
const [away, setAway] = useState('none');
|
|
117
121
|
const [replay, setReplay] = useState(0);
|
|
122
|
+
// How the balloon shows the conversation, a tool being called, a notebook (T-23).
|
|
123
|
+
const [display, setDisplay] = useState('current');
|
|
124
|
+
const [toolPhase, setToolPhase] = useState();
|
|
125
|
+
const [withNotebook, setWithNotebook] = useState(false);
|
|
118
126
|
const character = characters.find(entry => entry.id === characterId) ?? characters[0];
|
|
119
127
|
// Leaving takes the goodbye's length; arriving, the greeting's.
|
|
120
128
|
useEffect(() => {
|
|
@@ -138,10 +146,20 @@ const ChatAssistantGalleryExample = () => {
|
|
|
138
146
|
};
|
|
139
147
|
const approval = sampleApproval(() => setDecision('Approved: send_reply.'), () => setDecision('Denied: send_reply.'));
|
|
140
148
|
const balloon = balloonForPose(pose, {
|
|
141
|
-
saying: saying === undefined
|
|
149
|
+
saying: saying === undefined
|
|
150
|
+
? withNotebook
|
|
151
|
+
? { text: SAMPLE_NOTEBOOK_SAYING, more: false }
|
|
152
|
+
: undefined
|
|
153
|
+
: sampleSaying(saying),
|
|
142
154
|
approval,
|
|
155
|
+
tool: toolPhase ? sampleToolLine(toolPhase) : undefined,
|
|
156
|
+
attachment: withNotebook && display === 'current' ? (_jsx(GalleryNotebook, { onOpen: () => setDecision('The notebook would open below to run.') })) : undefined,
|
|
143
157
|
});
|
|
144
|
-
const insist = saying !== undefined ||
|
|
158
|
+
const insist = saying !== undefined ||
|
|
159
|
+
toolPhase !== undefined ||
|
|
160
|
+
withNotebook ||
|
|
161
|
+
pose === 'waiting' ||
|
|
162
|
+
pose === 'paused';
|
|
145
163
|
return (_jsx(ThemedProvider, { children: _jsx(Box, { sx: {
|
|
146
164
|
minHeight: '100vh',
|
|
147
165
|
bg: 'canvas.default',
|
|
@@ -174,13 +192,25 @@ const ChatAssistantGalleryExample = () => {
|
|
|
174
192
|
gap: 2,
|
|
175
193
|
flexWrap: 'wrap',
|
|
176
194
|
alignItems: 'center',
|
|
177
|
-
}, children: [_jsx(
|
|
195
|
+
}, children: [_jsx(SegmentedControl, { "aria-label": "Balloon", children: ['history', 'current'].map(option => (_jsx(SegmentedControl.Button, { selected: display === option, onClick: () => setDisplay(option), "data-gallery-balloon": option, children: option === 'history' ? 'History' : 'Current' }, option))) }), _jsx(Button, { onClick: say, "data-gallery-say": "", children: "Say something" }), _jsx(Button, { "data-gallery-tool": toolPhase ?? 'none', onClick: () => setToolPhase(toolPhase === undefined
|
|
196
|
+
? 'running'
|
|
197
|
+
: toolPhase === 'running'
|
|
198
|
+
? 'done'
|
|
199
|
+
: toolPhase === 'done'
|
|
200
|
+
? 'failed'
|
|
201
|
+
: undefined), children: toolPhase === undefined
|
|
202
|
+
? 'Use a tool'
|
|
203
|
+
: toolPhase === 'running'
|
|
204
|
+
? 'Tool done'
|
|
205
|
+
: toolPhase === 'done'
|
|
206
|
+
? 'Tool failed'
|
|
207
|
+
: 'No tool' }), _jsx(Button, { "data-gallery-notebook": withNotebook ? 'on' : 'off', "aria-pressed": withNotebook, onClick: () => setWithNotebook(!withNotebook), children: withNotebook ? 'Take the notebook' : 'Give a notebook' }), saying !== undefined && (_jsx(Button, { variant: "invisible", onClick: () => setSaying(undefined), children: "Hush" })), presence === 'away' ? (_jsx(Button, { onClick: callBack, children: "Call it back" })) : (_jsx(Button, { onClick: () => sendAway('page'), disabled: presence !== 'here', "data-gallery-send-away": "", children: "Send it away" })), _jsx(Text, { sx: { color: 'fg.muted', fontSize: 1 }, children: "Drag the character to move it; hover it for its own send-away menu." }), decision && (_jsx(Text, { role: "status", sx: { fontSize: 1 }, children: decision }))] })] }))] }), problem && (_jsxs(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mb: 3 }, children: ["The test sprite could not be read: ", problem] })), clippyJs.problem && (_jsxs(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mb: 3 }, children: ["A clippy.js character could not be read: ", clippyJs.problem] })), _jsx(Still, { still: still, children: view === 'stage' ? (_jsx(InModes, { modes: modes, children: mode => (_jsx(ModePanel, { mode: mode, character: character, pose: pose, presence: presence, balloon: balloon, insist: insist, onToggle: () => saying === undefined ? say() : setSaying(undefined), onDismiss: sendAway, onCallBack: callBack, away: away, replay: replay, display: display })) })) : (_jsx(InModes, { modes: modes, stacked: true, children: mode => (_jsxs(Box, { "data-gallery-mode": mode, sx: {
|
|
178
208
|
bg: 'canvas.default',
|
|
179
209
|
border: '1px solid',
|
|
180
210
|
borderColor: 'border.default',
|
|
181
211
|
borderRadius: 2,
|
|
182
212
|
p: 2,
|
|
183
213
|
overflowX: 'auto',
|
|
184
|
-
}, children: _jsx(AssistantGalleryGrid, { characters: characters }, replay) })) })) })] }) }) }));
|
|
214
|
+
}, children: [_jsx(AssistantGalleryGrid, { characters: characters }, replay), _jsx(GalleryBalloons, { character: character })] })) })) })] }) }) }));
|
|
185
215
|
};
|
|
186
216
|
export default ChatAssistantGalleryExample;
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The floating assistant's characters, every one in every pose, small, for
|
|
3
|
-
* a quick visual check (LOOP T-16, T-22, T-24 to T-27): Datalayer's four and
|
|
4
|
-
* the owl as the enabled plugins contribute them to
|
|
5
|
-
* `loop.assistant.character`, and a test sprite read through the clippy.js
|
|
6
|
-
* reader. Each cell is the real `AssistantStage`, set in the grid's flow
|
|
7
|
-
* rather than fixed to the window; stepped aside is the real `useKeepClear`
|
|
8
|
-
* stepping it aside from a composer laid over the cell.
|
|
9
|
-
*
|
|
10
|
-
* @module examples/utils/AssistantGalleryGrid
|
|
11
|
-
*/
|
|
12
1
|
import type { JSX, ReactNode } from 'react';
|
|
13
2
|
import type { AssistantCharacter } from '../../chat/assistant/characters';
|
|
14
3
|
import type { AssistantCharacterData } from '../../chat/assistant/formats/types';
|
|
@@ -50,4 +39,18 @@ export declare function AssistantGalleryGrid({ characters, size, }: {
|
|
|
50
39
|
characters: readonly GalleryCharacter[];
|
|
51
40
|
size?: number;
|
|
52
41
|
}): JSX.Element;
|
|
42
|
+
/** How tall the notebook in a balloon is, in the stage and in the grid. */
|
|
43
|
+
export declare const GALLERY_NOTEBOOK_HEIGHT = 160;
|
|
44
|
+
/** The notebook a balloon holds in the gallery: jupyter-react's, read-only. */
|
|
45
|
+
export declare function GalleryNotebook({ onOpen, }: {
|
|
46
|
+
onOpen?: () => void;
|
|
47
|
+
}): JSX.Element;
|
|
48
|
+
/**
|
|
49
|
+
* The balloon's displays (T-23), one character: a peek of the history, the
|
|
50
|
+
* current balloon with a tool line, and with a notebook given — the same
|
|
51
|
+
* read-only jupyter-react notebook as in the stage, loaded when drawn.
|
|
52
|
+
*/
|
|
53
|
+
export declare function GalleryBalloons({ character, }: {
|
|
54
|
+
character: GalleryCharacter;
|
|
55
|
+
}): JSX.Element;
|
|
53
56
|
export default AssistantGalleryGrid;
|
|
@@ -6,8 +6,9 @@ import { buildReactorFromPlugins } from '@datalayer/reactor';
|
|
|
6
6
|
import { AssistantStage } from '../../chat/assistant/AssistantStage';
|
|
7
7
|
import { AssistantCharactersPlugin, assistantCharactersOf, } from '../../loop/plugins/assistant-characters';
|
|
8
8
|
import { OWL_CHARACTER, OwlCharacterPlugin } from './owlCharacterPlugin';
|
|
9
|
+
import { NotebookPreview } from '../../components/teams/NotebookPreview';
|
|
9
10
|
import { readTestSpriteCharacter } from './testSpriteCharacter';
|
|
10
|
-
import { GALLERY_POSES, GALLERY_POSE_LABELS, stateOfPose, } from './assistantGallery';
|
|
11
|
+
import { GALLERY_POSES, GALLERY_POSE_LABELS, SAMPLE_NOTEBOOK, SAMPLE_NOTEBOOK_SAYING, sampleSaying, sampleToolLine, stateOfPose, } from './assistantGallery';
|
|
11
12
|
/** The plugins the gallery enables: Datalayer's characters and the owl's. */
|
|
12
13
|
const reactor = buildReactorFromPlugins([
|
|
13
14
|
AssistantCharactersPlugin,
|
|
@@ -120,4 +121,44 @@ export function AssistantGalleryGrid({ characters, size = 48, }) {
|
|
|
120
121
|
'& td': { p: 0, textAlign: 'center' },
|
|
121
122
|
}, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", {}), GALLERY_POSES.map(pose => (_jsx("th", { scope: "col", children: GALLERY_POSE_LABELS[pose] }, pose)))] }) }), _jsx("tbody", { children: characters.map(character => (_jsxs("tr", { children: [_jsxs(Box, { as: "th", scope: "row", sx: { textAlign: 'left !important', pr: '12px !important' }, children: [_jsx(Text, { sx: { display: 'block', color: 'fg.default' }, children: character.name }), _jsx(Text, { sx: { fontWeight: 400 }, children: character.from })] }), GALLERY_POSES.map(pose => (_jsx("td", { children: _jsx(GalleryCell, { character: character, pose: pose, size: size }) }, pose)))] }, character.id))) })] }));
|
|
122
123
|
}
|
|
124
|
+
/** How tall the notebook in a balloon is, in the stage and in the grid. */
|
|
125
|
+
export const GALLERY_NOTEBOOK_HEIGHT = 160;
|
|
126
|
+
/** The notebook a balloon holds in the gallery: jupyter-react's, read-only. */
|
|
127
|
+
export function GalleryNotebook({ onOpen, }) {
|
|
128
|
+
return (_jsx(NotebookPreview, { notebook: SAMPLE_NOTEBOOK, title: "Accounting\u2019s notebook", maxHeight: GALLERY_NOTEBOOK_HEIGHT, onOpen: onOpen }));
|
|
129
|
+
}
|
|
130
|
+
/** One balloon display, pictured: a character with what its balloon says. */
|
|
131
|
+
function BalloonCell({ character, label, display, balloon, height, }) {
|
|
132
|
+
const stageRef = useRef(null);
|
|
133
|
+
return (_jsxs(Box, { "data-gallery-cell": `balloon-${label.toLowerCase().replace(/\W+/g, '-')}`, sx: {
|
|
134
|
+
position: 'relative',
|
|
135
|
+
width: 330,
|
|
136
|
+
height,
|
|
137
|
+
border: '1px solid',
|
|
138
|
+
borderColor: 'border.muted',
|
|
139
|
+
borderRadius: 2,
|
|
140
|
+
}, children: [_jsx(Text, { sx: {
|
|
141
|
+
position: 'absolute',
|
|
142
|
+
top: 1,
|
|
143
|
+
right: 2,
|
|
144
|
+
fontSize: 0,
|
|
145
|
+
fontWeight: 600,
|
|
146
|
+
color: 'fg.muted',
|
|
147
|
+
}, children: label }), _jsx(AssistantStage, { character: character.character, state: balloon.tool ? 'working' : 'speaking', size: 48, place: { position: 'absolute', left: '12px', bottom: '8px' }, stageRef: stageRef, onDragStart: () => undefined, open: false, onToggle: () => undefined, onDismiss: () => undefined, balloon: balloon, balloonDisplay: display, insist: true })] }));
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The balloon's displays (T-23), one character: a peek of the history, the
|
|
151
|
+
* current balloon with a tool line, and with a notebook given — the same
|
|
152
|
+
* read-only jupyter-react notebook as in the stage, loaded when drawn.
|
|
153
|
+
*/
|
|
154
|
+
export function GalleryBalloons({ character, }) {
|
|
155
|
+
return (_jsxs(Box, { "data-gallery-balloons": "", sx: { display: 'flex', gap: 3, flexWrap: 'wrap', mt: 3 }, children: [_jsx(BalloonCell, { character: character, label: "History", display: "history", height: 180, balloon: { ...sampleSaying(0), onDismiss: () => undefined } }), _jsx(BalloonCell, { character: character, label: "Current \u00B7 tool", display: "current", height: 180, balloon: {
|
|
156
|
+
text: 'Using list_invoices…',
|
|
157
|
+
tool: sampleToolLine('running'),
|
|
158
|
+
busy: true,
|
|
159
|
+
} }), _jsx(BalloonCell, { character: character, label: "Current \u00B7 notebook", display: "current", height: GALLERY_NOTEBOOK_HEIGHT + 210, balloon: {
|
|
160
|
+
text: SAMPLE_NOTEBOOK_SAYING,
|
|
161
|
+
attachment: _jsx(GalleryNotebook, {}),
|
|
162
|
+
} })] }));
|
|
163
|
+
}
|
|
123
164
|
export default AssistantGalleryGrid;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @module examples/utils/assistantGallery
|
|
8
8
|
*/
|
|
9
9
|
import type { AssistantStageProps } from '../../chat/assistant/AssistantStage';
|
|
10
|
+
import { type BalloonToolLine, type ToolLinePhase } from '../../chat/assistant/toolLine';
|
|
10
11
|
import { type AssistantState, type BalloonApproval } from '../../chat/assistant/state';
|
|
11
12
|
/** What the gallery poses a character in: a state, or stepped aside. */
|
|
12
13
|
export type GalleryPose = AssistantState | 'aside';
|
|
@@ -27,16 +28,63 @@ export declare function sampleSaying(index: number): {
|
|
|
27
28
|
text: string;
|
|
28
29
|
more: boolean;
|
|
29
30
|
};
|
|
31
|
+
/** The tool the gallery calls (T-23): Odoo's, as Accounting calls it. */
|
|
32
|
+
export declare const SAMPLE_TOOL = "list_invoices";
|
|
33
|
+
/** The sample tool's line, at a phase: "Using list_invoices…", "Done: …", "… failed". */
|
|
34
|
+
export declare function sampleToolLine(phase: ToolLinePhase): BalloonToolLine;
|
|
35
|
+
/**
|
|
36
|
+
* A small recorded notebook (nbformat 4), as Accounting gives one: a
|
|
37
|
+
* heading, the code that totals the open invoices, and its one output.
|
|
38
|
+
*/
|
|
39
|
+
export declare const SAMPLE_NOTEBOOK: {
|
|
40
|
+
nbformat: number;
|
|
41
|
+
nbformat_minor: number;
|
|
42
|
+
metadata: {
|
|
43
|
+
kernelspec: {
|
|
44
|
+
name: string;
|
|
45
|
+
display_name: string;
|
|
46
|
+
language: string;
|
|
47
|
+
};
|
|
48
|
+
language_info: {
|
|
49
|
+
name: string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
cells: ({
|
|
53
|
+
id: string;
|
|
54
|
+
cell_type: string;
|
|
55
|
+
metadata: {};
|
|
56
|
+
source: string;
|
|
57
|
+
execution_count?: undefined;
|
|
58
|
+
outputs?: undefined;
|
|
59
|
+
} | {
|
|
60
|
+
id: string;
|
|
61
|
+
cell_type: string;
|
|
62
|
+
metadata: {};
|
|
63
|
+
execution_count: number;
|
|
64
|
+
source: string;
|
|
65
|
+
outputs: {
|
|
66
|
+
output_type: string;
|
|
67
|
+
name: string;
|
|
68
|
+
text: string;
|
|
69
|
+
}[];
|
|
70
|
+
})[];
|
|
71
|
+
};
|
|
72
|
+
/** What the gallery says with the notebook it was given. */
|
|
73
|
+
export declare const SAMPLE_NOTEBOOK_SAYING = "Here is the notebook Accounting gave: the open invoices.";
|
|
30
74
|
/** The approval the gallery waits on (T-23): answered in the balloon. */
|
|
31
75
|
export declare function sampleApproval(onApprove: () => void, onDeny: () => void): BalloonApproval;
|
|
32
76
|
/**
|
|
33
77
|
* The balloon for a pose: waiting holds the approval, paused says so, and
|
|
34
78
|
* any other pose says the sample's words when asked to (`saying`).
|
|
35
79
|
*/
|
|
36
|
-
export declare function balloonForPose(pose: GalleryPose, { saying, approval, }: {
|
|
80
|
+
export declare function balloonForPose(pose: GalleryPose, { saying, approval, tool, attachment, }: {
|
|
37
81
|
saying?: {
|
|
38
82
|
text: string;
|
|
39
83
|
more: boolean;
|
|
40
84
|
};
|
|
41
85
|
approval: BalloonApproval;
|
|
86
|
+
/** A tool being called: its line stands in for the words. */
|
|
87
|
+
tool?: BalloonToolLine;
|
|
88
|
+
/** What goes with the words in a `current` balloon: a notebook. */
|
|
89
|
+
attachment?: NonNullable<AssistantStageProps['balloon']>['attachment'];
|
|
42
90
|
}): AssistantStageProps['balloon'];
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
+
import { toolLineText, } from '../../chat/assistant/toolLine';
|
|
5
6
|
import { ASSISTANT_WORDS, latestSaying, } from '../../chat/assistant/state';
|
|
6
7
|
/** Every state, in the order an assistant lives them, then stepped aside. */
|
|
7
8
|
export const GALLERY_POSES = [
|
|
@@ -73,6 +74,46 @@ export function sampleSaying(index) {
|
|
|
73
74
|
}
|
|
74
75
|
return { text: saying.text, more: saying.more };
|
|
75
76
|
}
|
|
77
|
+
/** The tool the gallery calls (T-23): Odoo's, as Accounting calls it. */
|
|
78
|
+
export const SAMPLE_TOOL = 'list_invoices';
|
|
79
|
+
/** The sample tool's line, at a phase: "Using list_invoices…", "Done: …", "… failed". */
|
|
80
|
+
export function sampleToolLine(phase) {
|
|
81
|
+
return { id: 'gallery-tool', tool: SAMPLE_TOOL, name: SAMPLE_TOOL, phase };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A small recorded notebook (nbformat 4), as Accounting gives one: a
|
|
85
|
+
* heading, the code that totals the open invoices, and its one output.
|
|
86
|
+
*/
|
|
87
|
+
export const SAMPLE_NOTEBOOK = {
|
|
88
|
+
nbformat: 4,
|
|
89
|
+
nbformat_minor: 5,
|
|
90
|
+
metadata: {
|
|
91
|
+
kernelspec: {
|
|
92
|
+
name: 'python3',
|
|
93
|
+
display_name: 'Python 3',
|
|
94
|
+
language: 'python',
|
|
95
|
+
},
|
|
96
|
+
language_info: { name: 'python' },
|
|
97
|
+
},
|
|
98
|
+
cells: [
|
|
99
|
+
{
|
|
100
|
+
id: 'gallery-markdown',
|
|
101
|
+
cell_type: 'markdown',
|
|
102
|
+
metadata: {},
|
|
103
|
+
source: '## Open invoices\n\nFrom the books, **in EUR**.',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'gallery-code',
|
|
107
|
+
cell_type: 'code',
|
|
108
|
+
metadata: {},
|
|
109
|
+
execution_count: 1,
|
|
110
|
+
source: 'due = {"Ada": 1200, "Grace": 860}\nprint(sum(due.values()))',
|
|
111
|
+
outputs: [{ output_type: 'stream', name: 'stdout', text: '2060\n' }],
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
/** What the gallery says with the notebook it was given. */
|
|
116
|
+
export const SAMPLE_NOTEBOOK_SAYING = 'Here is the notebook Accounting gave: the open invoices.';
|
|
76
117
|
/** The approval the gallery waits on (T-23): answered in the balloon. */
|
|
77
118
|
export function sampleApproval(onApprove, onDeny) {
|
|
78
119
|
return {
|
|
@@ -88,7 +129,7 @@ export function sampleApproval(onApprove, onDeny) {
|
|
|
88
129
|
* The balloon for a pose: waiting holds the approval, paused says so, and
|
|
89
130
|
* any other pose says the sample's words when asked to (`saying`).
|
|
90
131
|
*/
|
|
91
|
-
export function balloonForPose(pose, { saying, approval, }) {
|
|
132
|
+
export function balloonForPose(pose, { saying, approval, tool, attachment, }) {
|
|
92
133
|
if (pose === 'waiting') {
|
|
93
134
|
return { text: ASSISTANT_WORDS.approval, approval };
|
|
94
135
|
}
|
|
@@ -98,5 +139,15 @@ export function balloonForPose(pose, { saying, approval, }) {
|
|
|
98
139
|
if (pose === 'goodbye' || pose === 'aside') {
|
|
99
140
|
return undefined;
|
|
100
141
|
}
|
|
142
|
+
if (tool) {
|
|
143
|
+
return {
|
|
144
|
+
text: toolLineText(tool),
|
|
145
|
+
tool,
|
|
146
|
+
busy: tool.phase === 'running',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (saying && attachment) {
|
|
150
|
+
return { ...saying, attachment };
|
|
151
|
+
}
|
|
101
152
|
return saying;
|
|
102
153
|
}
|
package/lib/loop/apps/appspec.js
CHANGED
|
@@ -255,6 +255,9 @@ function parseInterface(data, kind) {
|
|
|
255
255
|
if (isAssistantCharacterId(data.assistant)) {
|
|
256
256
|
parsed.assistant = data.assistant;
|
|
257
257
|
}
|
|
258
|
+
if (data.balloon === 'history' || data.balloon === 'current') {
|
|
259
|
+
parsed.balloon = data.balloon;
|
|
260
|
+
}
|
|
258
261
|
return parsed;
|
|
259
262
|
}
|
|
260
263
|
function parseCriterion(data) {
|
|
@@ -516,6 +519,9 @@ function dumpInterface(spec, kind) {
|
|
|
516
519
|
if (spec.assistant) {
|
|
517
520
|
writer.data.assistant = spec.assistant;
|
|
518
521
|
}
|
|
522
|
+
if (spec.balloon) {
|
|
523
|
+
writer.data.balloon = spec.balloon;
|
|
524
|
+
}
|
|
519
525
|
// Its voice, when it says one (VO-41): a spec made before voice has none.
|
|
520
526
|
if (spec.voice) {
|
|
521
527
|
writer.part('voice', new Writer()
|
package/lib/loop/apps/checks.js
CHANGED
|
@@ -398,6 +398,7 @@ const ENUMS = {
|
|
|
398
398
|
],
|
|
399
399
|
visibility: ['private', 'invited', 'organization', 'link', 'public'],
|
|
400
400
|
mode: ['inline', 'bubble', 'panel', 'assistant'],
|
|
401
|
+
balloon: ['history', 'current'],
|
|
401
402
|
trigger: ['schedule', 'event', 'once'],
|
|
402
403
|
criterion: ['metric', 'noul', 'choice', 'score'],
|
|
403
404
|
direction: ['higher', 'lower'],
|
|
@@ -525,6 +526,7 @@ export function documentShapeProblems(document) {
|
|
|
525
526
|
mapping(d.interface, 'interface', ui => {
|
|
526
527
|
oneOf(ui.layout, ENUMS.layout, 'interface.layout');
|
|
527
528
|
oneOf(ui.accent, ENUMS.accent, 'interface.accent');
|
|
529
|
+
oneOf(ui.balloon, ENUMS.balloon, 'interface.balloon');
|
|
528
530
|
if (ui.assistant !== undefined && !isAssistantCharacterId(ui.assistant)) {
|
|
529
531
|
at('interface.assistant', 'names a character by the id a plugin contributes it under, lowercase words joined by a hyphen: `paperclip`, `acme-owl`');
|
|
530
532
|
}
|
|
@@ -80,7 +80,9 @@ function FloatingApp({ app, view, colorMode, themeOverrides, character, serverUr
|
|
|
80
80
|
// Datalayer knows — said, and nothing launched, for one it does not.
|
|
81
81
|
const token = useIAMStore(state => state.token);
|
|
82
82
|
const signedOut = !serverUrl && !token;
|
|
83
|
-
return (_jsx(ChatFloating, { defaultViewMode: view, ...(character ? { assistantCharacter: character } : {}),
|
|
83
|
+
return (_jsx(ChatFloating, { defaultViewMode: view, ...(character ? { assistantCharacter: character } : {}), ...(app.interface.balloon
|
|
84
|
+
? { balloonDisplay: app.interface.balloon }
|
|
85
|
+
: {}), title: app.name, description: welcome, brandIcon: _jsx(Face, { emoji: app.emoji }), buttonIcon: _jsx(Face, { emoji: app.emoji }), buttonTooltip: `Talk to ${app.name}`, colorMode: colorMode, useStore: false, conversation: {
|
|
84
86
|
body: signedOut ? (_jsx(Text, { as: "p", role: "status", sx: { p: 3, m: 0, fontSize: 1 }, children: signedOutSentence(app) })) : (_jsx(AppRenderer, { app: app, target: serverUrl ? 'local' : 'datalayer', ...(serverUrl ? { serverUrl } : {}), instance: instance,
|
|
85
87
|
// The host's accent and face over the application's own, inside
|
|
86
88
|
// its conversation too, in the embed's mode.
|