@datalayer/agent-runtimes 1.3.62 → 1.3.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/chat/assistant/AssistantStage.js +5 -0
- package/lib/chat/assistant/SpriteCharacter.js +1 -1
- package/lib/chat/assistant/characters.js +1 -5
- package/lib/config/AgentConfiguration.js +0 -6
- package/lib/examples/AgentA2ATeamExample.js +19 -2
- package/lib/examples/ChatAssistantExample.d.ts +3 -1
- package/lib/examples/ChatAssistantExample.js +75 -7
- package/lib/examples/ChatAssistantGalleryExample.d.ts +3 -2
- package/lib/examples/ChatAssistantGalleryExample.js +11 -6
- package/lib/examples/DecksAgent.js +4 -2
- package/lib/examples/LoopShellExample.js +4 -2
- package/lib/examples/main.js +5 -6
- package/lib/examples/utils/clippyJsCharacters.d.ts +18 -0
- package/lib/examples/utils/clippyJsCharacters.js +129 -0
- package/lib/loop/apps/checks.js +1 -1
- package/lib/types/agents.d.ts +1 -1
- package/package.json +1 -1
|
@@ -275,6 +275,11 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
|
|
|
275
275
|
'&[data-assistant-state="goodbye"] .assistant-body': {
|
|
276
276
|
animation: 'assistantLeave 0.6s ease-in forwards',
|
|
277
277
|
},
|
|
278
|
+
// A sprite plays its own goodbye, and leaves as the drawn ones do:
|
|
279
|
+
// a character whose file has no goodbye would otherwise stay.
|
|
280
|
+
'&[data-assistant-state="goodbye"] .assistant-sprite': {
|
|
281
|
+
animation: 'assistantLeave 0.6s ease-in forwards',
|
|
282
|
+
},
|
|
278
283
|
// One still frame per state for a reader who asks for no motion.
|
|
279
284
|
'@media (prefers-reduced-motion: reduce)': {
|
|
280
285
|
'& *': { animation: 'none !important' },
|
|
@@ -51,7 +51,7 @@ export function SpriteCharacter({ character, state, size, }) {
|
|
|
51
51
|
}, [animation, frame, play, character, state, still]);
|
|
52
52
|
const { width, height } = character.frameSize;
|
|
53
53
|
const scale = size / Math.max(width, height);
|
|
54
|
-
return (_jsx("span", { role: "img", "aria-label": character.name, "data-sprite-animation": play.name, style: {
|
|
54
|
+
return (_jsx("span", { className: "assistant-sprite", role: "img", "aria-label": character.name, "data-sprite-animation": play.name, style: {
|
|
55
55
|
position: 'relative',
|
|
56
56
|
display: 'block',
|
|
57
57
|
width: size,
|
|
@@ -40,7 +40,6 @@ const WIZARD = {
|
|
|
40
40
|
brim: '#4A3C9E',
|
|
41
41
|
robe: '#5B4BB7',
|
|
42
42
|
face: '#F2D3B3',
|
|
43
|
-
faceEdge: '#A0694A',
|
|
44
43
|
beard: '#F4F4F6',
|
|
45
44
|
beardEdge: '#7A808A',
|
|
46
45
|
star: '#F8D469',
|
|
@@ -51,7 +50,6 @@ const WIZARD = {
|
|
|
51
50
|
brim: '#7B67DB',
|
|
52
51
|
robe: '#8F7CE8',
|
|
53
52
|
face: '#E8C4A0',
|
|
54
|
-
faceEdge: '#E8C4A0',
|
|
55
53
|
beard: '#E4E4EA',
|
|
56
54
|
beardEdge: loopColors.grayDark,
|
|
57
55
|
star: '#F8D469',
|
|
@@ -100,7 +98,7 @@ function PaperClip({ size, mode = 'light', }) {
|
|
|
100
98
|
/** A wizard: a starred hat, a white beard, kind eyes. */
|
|
101
99
|
function Wizard({ size, mode = 'light', }) {
|
|
102
100
|
const c = WIZARD[mode];
|
|
103
|
-
return (_jsx("svg", { width: size, height: size, viewBox: "0 0 96 96", role: "img", "aria-label": "Wizard", "data-assistant-mode": mode, children: _jsxs("g", { className: "assistant-body", children: [_jsx("path", { d: "
|
|
101
|
+
return (_jsx("svg", { width: size, height: size, viewBox: "0 0 96 96", role: "img", "aria-label": "Wizard", "data-assistant-mode": mode, children: _jsxs("g", { className: "assistant-body", children: [_jsx("path", { d: "M34 58 Q48 52 62 58 Q70 74 76 92 Q48 98 20 92 Q26 74 34 58 Z", fill: c.robe }), _jsx("path", { d: "M21 90 Q48 96 75 90 L76 92 Q48 98 20 92 Z", fill: c.brim }), _jsx("circle", { cx: 30, cy: 82, r: 1.6, fill: c.star }), _jsx("circle", { cx: 67, cy: 78, r: 1.3, fill: c.star }), _jsx("ellipse", { cx: 48, cy: 46, rx: 13, ry: 12, fill: c.face }), _jsx("path", { d: "M33 38 Q31 66 40 77 Q48 89 56 77 Q65 66 63 38 Q61 50 56 53 Q48 58 40 53 Q35 50 33 38 Z", fill: c.beard, stroke: c.beardEdge, strokeWidth: 1, strokeLinejoin: "round" }), _jsx("path", { className: "assistant-mouth", d: "M44 59 Q48 62 52 59", stroke: INK, strokeWidth: 1.8, fill: "none", strokeLinecap: "round" }), _jsx("path", { d: "M39 55 Q44 51 48 54 Q52 51 57 55 Q52 57 48 56 Q44 57 39 55 Z", fill: c.beard, stroke: c.beardEdge, strokeWidth: 0.8 }), _jsx("path", { d: "M25 36 Q35 26 39 14 Q43 2 58 3 Q50 8 53 18 Q58 30 71 36 Q48 30 25 36 Z", fill: c.hat }), _jsx("path", { d: "M18 37 Q48 27 78 37 Q48 45 18 37 Z", fill: c.brim }), _jsx("path", { d: "M47 17 l1.8 3.6 4 .6 -2.9 2.8 .7 4 -3.6 -1.9 -3.6 1.9 .7 -4 -2.9 -2.8 4 -.6 Z", fill: c.star }), _jsx("circle", { cx: 56, cy: 28, r: 1.4, fill: c.star }), _jsx(Eyes, { cx: 48, cy: 46, gap: 11, r: 4, colours: c.eye })] }) }));
|
|
104
102
|
}
|
|
105
103
|
/** A cat: round, two ears, whiskers, a tail. */
|
|
106
104
|
function Cat({ size, mode = 'light', }) {
|
|
@@ -146,7 +144,6 @@ export const ASSISTANT_CHARACTERS = [
|
|
|
146
144
|
WIZARD.light.hat,
|
|
147
145
|
WIZARD.light.brim,
|
|
148
146
|
WIZARD.light.robe,
|
|
149
|
-
WIZARD.light.faceEdge,
|
|
150
147
|
WIZARD.light.beardEdge,
|
|
151
148
|
],
|
|
152
149
|
eye: eyeOf(WIZARD.light.eye, WIZARD.light.face),
|
|
@@ -156,7 +153,6 @@ export const ASSISTANT_CHARACTERS = [
|
|
|
156
153
|
WIZARD.dark.hat,
|
|
157
154
|
WIZARD.dark.brim,
|
|
158
155
|
WIZARD.dark.robe,
|
|
159
|
-
WIZARD.dark.faceEdge,
|
|
160
156
|
WIZARD.dark.beardEdge,
|
|
161
157
|
],
|
|
162
158
|
eye: eyeOf(WIZARD.dark.eye, WIZARD.dark.face),
|
|
@@ -193,12 +193,6 @@ const AGENT_LIBRARIES = [
|
|
|
193
193
|
description: 'LangChain agent implementations and tools',
|
|
194
194
|
disabled: true,
|
|
195
195
|
},
|
|
196
|
-
{
|
|
197
|
-
value: 'google-adk',
|
|
198
|
-
label: 'Google ADK',
|
|
199
|
-
description: 'Google’s Agent Development Kit',
|
|
200
|
-
disabled: true,
|
|
201
|
-
},
|
|
202
196
|
];
|
|
203
197
|
const TRANSPORTS = [
|
|
204
198
|
{
|
|
@@ -2,6 +2,8 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { Button, Flash, Heading, Label, Text, Textarea } from '@primer/react';
|
|
4
4
|
import { Box } from '@datalayer/primer-addons';
|
|
5
|
+
import { AnonymousKeyExpired } from '@datalayer/core/lib/components/anonymous/AnonymousKeyExpired';
|
|
6
|
+
import { AnonymousKeyTimer } from '@datalayer/core/lib/components/anonymous/AnonymousKeyTimer';
|
|
5
7
|
import { stepCountIs, ToolLoopAgent } from 'ai';
|
|
6
8
|
import { Streamdown } from 'streamdown';
|
|
7
9
|
import { ThemedProvider } from './utils/themedProvider';
|
|
@@ -9,6 +11,7 @@ import { useExampleAgentRuntimesUrl } from './utils/useExampleAgentRuntimesUrl';
|
|
|
9
11
|
import { AssistantStage } from '../chat/assistant/AssistantStage';
|
|
10
12
|
import { useBrowserInference } from '../hooks/useBrowserInference';
|
|
11
13
|
import { createBrowserModel } from '../runtimes/browser/model';
|
|
14
|
+
import { readJwtClaims, tokenLifetimeMs, useAnonymousSessionStore, } from '../runtimes/browser/anonymousToken';
|
|
12
15
|
import { a2aPeerTool, connectA2APeer, } from '../runtimes/browser/a2aPeer';
|
|
13
16
|
import { ACCOUNTING_APP_0_0_1, SALES_APP_0_0_1 } from '../specs/apps';
|
|
14
17
|
import { SALES_AND_ACCOUNTING_TEAM_SPEC_0_0_1 } from '../specs/teams/teams';
|
|
@@ -60,7 +63,17 @@ function AgentA2ATeam() {
|
|
|
60
63
|
const accountingUrl = import.meta.env.VITE_A2A_ACCOUNTING_URL ||
|
|
61
64
|
`${runtimeUrl}/api/v1/a2a/agents/${ACCOUNTING.id}`;
|
|
62
65
|
const accountingKey = import.meta.env.VITE_A2A_ACCOUNTING_KEY || undefined;
|
|
63
|
-
const { inference, needsSignIn } = useBrowserInference(true);
|
|
66
|
+
const { inference, needsSignIn, anonymous } = useBrowserInference(true);
|
|
67
|
+
// The temporary key's own clock, read out of it: when it ends and how long
|
|
68
|
+
// it was signed for, as core's countdown draws them.
|
|
69
|
+
const accountingKeyClock = useMemo(() => {
|
|
70
|
+
const exp = accountingKey ? readJwtClaims(accountingKey)?.exp : undefined;
|
|
71
|
+
return accountingKey && exp
|
|
72
|
+
? { expiresAt: exp * 1000, grantedMs: tokenLifetimeMs(accountingKey) }
|
|
73
|
+
: undefined;
|
|
74
|
+
}, [accountingKey]);
|
|
75
|
+
const [accountingKeyEnded, setAccountingKeyEnded] = useState(() => accountingKeyClock !== undefined &&
|
|
76
|
+
accountingKeyClock.expiresAt <= Date.now());
|
|
64
77
|
const [peer, setPeer] = useState(null);
|
|
65
78
|
const [peerError, setPeerError] = useState(null);
|
|
66
79
|
const [sales, setSales] = useState({
|
|
@@ -235,7 +248,11 @@ function AgentA2ATeam() {
|
|
|
235
248
|
const accountingWhere = accountingKey
|
|
236
249
|
? `on a runtime, ${new URL(accountingUrl).host}, with a temporary key`
|
|
237
250
|
: `on a runtime, ${new URL(accountingUrl).host}`;
|
|
238
|
-
return (_jsxs(Box, { sx: { p: 4, maxWidth: 1080, mx: 'auto' }, children: [_jsxs(Heading, { as: "h2", sx: { fontSize: 4, mb: 1 }, children: [TEAM.emoji, " ", TEAM.name] }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', mt: 0 }, children: TEAM.description }),
|
|
251
|
+
return (_jsxs(Box, { sx: { p: 4, maxWidth: 1080, mx: 'auto' }, children: [_jsxs(Heading, { as: "h2", sx: { fontSize: 4, mb: 1 }, children: [TEAM.emoji, " ", TEAM.name] }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', mt: 0 }, children: TEAM.description }), ((anonymous.status === 'active' && anonymous.expiresAt) ||
|
|
252
|
+
(accountingKeyClock && !accountingKeyEnded)) && (_jsxs(Box, { sx: { display: 'flex', gap: 4, flexWrap: 'wrap', mb: 3 }, children: [anonymous.status === 'active' && anonymous.expiresAt && (_jsx(AnonymousKeyTimer, { expiresAt: anonymous.expiresAt, grantedMs: anonymous.grantedMs, label: `${SALES.name}'s trial key`, onExpire: () => useAnonymousSessionStore.getState().expire() })), accountingKeyClock && !accountingKeyEnded && (_jsx(AnonymousKeyTimer, { expiresAt: accountingKeyClock.expiresAt, grantedMs: accountingKeyClock.grantedMs, label: `${ACCOUNTING.name}'s temporary key`, onExpire: () => setAccountingKeyEnded(true) }))] })), anonymous.status === 'expired' && (_jsx(Box, { sx: { mb: 3 }, children: _jsx(AnonymousKeyExpired, { agentName: SALES.name,
|
|
253
|
+
// Sales runs in this page: what ended is its model's key, and
|
|
254
|
+
// signing in gives it the person's own.
|
|
255
|
+
onSignedIn: () => useAnonymousSessionStore.getState().clear() }) })), accountingKeyEnded && (_jsxs(Flash, { variant: "danger", sx: { mb: 3 }, children: [ACCOUNTING.name, "\u2019s temporary key has ended: it no longer answers. Make a new one with", ' ', _jsx("code", { children: "examples/sales-accounting-a2a/make_temp_key.py" }), " and reload."] })), needsSignIn && (_jsx(Flash, { variant: "warning", sx: { mb: 3 }, children: "Sales asks its model through the Datalayer inference service: sign in, or wait for a visitor's trial key." })), peerError && (_jsxs(Flash, { variant: "danger", sx: { mb: 3 }, children: ["Accounting is not reachable at ", accountingUrl, ": ", peerError, " Serve it with", ' ', _jsx("code", { children: "python examples/sales-accounting-a2a/serve_accounting.py" }), ' ', "on the local server, or", ' ', _jsx("code", { children: "loop apps run accounting.yaml --cloud --a2a" }), " and set", ' ', _jsx("code", { children: "VITE_A2A_ACCOUNTING_URL" }), "."] })), _jsxs(Box, { sx: {
|
|
239
256
|
position: 'relative',
|
|
240
257
|
height: STAGE_HEIGHT,
|
|
241
258
|
border: '1px solid',
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* The characters offered are what the enabled plugins contribute to
|
|
7
7
|
* `loop.assistant.character` (T-24): Datalayer's four, and an owl from a
|
|
8
|
-
* small example plugin (`utils/owlCharacterPlugin`).
|
|
8
|
+
* small example plugin (`utils/owlCharacterPlugin`). Pixel, the test sprite,
|
|
9
|
+
* is read through the clippy.js reader, as a character a person brings is,
|
|
10
|
+
* and so are the characters clippy.js publishes, fetched when picked.
|
|
9
11
|
*/
|
|
10
12
|
import React from 'react';
|
|
11
13
|
declare const ChatAssistantExample: React.FC;
|
|
@@ -10,9 +10,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
*
|
|
11
11
|
* The characters offered are what the enabled plugins contribute to
|
|
12
12
|
* `loop.assistant.character` (T-24): Datalayer's four, and an owl from a
|
|
13
|
-
* small example plugin (`utils/owlCharacterPlugin`).
|
|
13
|
+
* small example plugin (`utils/owlCharacterPlugin`). Pixel, the test sprite,
|
|
14
|
+
* is read through the clippy.js reader, as a character a person brings is,
|
|
15
|
+
* and so are the characters clippy.js publishes, fetched when picked.
|
|
14
16
|
*/
|
|
15
|
-
import { useState } from 'react';
|
|
17
|
+
import { useEffect, useRef, useState } from 'react';
|
|
16
18
|
import { buildReactorFromPlugins } from '@datalayer/reactor';
|
|
17
19
|
import { Button, Heading, Text } from '@primer/react';
|
|
18
20
|
import { Box } from '@datalayer/primer-addons';
|
|
@@ -20,6 +22,8 @@ import { ThemedProvider } from './utils/themedProvider';
|
|
|
20
22
|
import { ChatFloating } from '../chat';
|
|
21
23
|
import { AssistantCharactersPlugin, assistantCharacterNamed, assistantCharactersOf, } from '../loop/plugins/assistant-characters';
|
|
22
24
|
import { OwlCharacterPlugin } from './utils/owlCharacterPlugin';
|
|
25
|
+
import { readTestSpriteCharacter } from './utils/testSpriteCharacter';
|
|
26
|
+
import { CLIPPY_JS_CHARACTERS, CLIPPY_JS_VERSION, readClippyJsCharacter, } from './utils/clippyJsCharacters';
|
|
23
27
|
import { readAcsCharacter, readClippyCharacter, } from '../chat/assistant/formats';
|
|
24
28
|
/**
|
|
25
29
|
* The agent-runtimes server the assistant talks to: `?agentRuntimesUrl=`
|
|
@@ -69,10 +73,72 @@ const ChatAssistantExample = () => {
|
|
|
69
73
|
? assistantCharacterNamed(reactor, character)
|
|
70
74
|
: character;
|
|
71
75
|
const [loadError, setLoadError] = useState();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
// Pixel, the test sprite, read once through the clippy.js reader.
|
|
77
|
+
const [pixel, setPixel] = useState();
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
let cancelled = false;
|
|
80
|
+
readTestSpriteCharacter().then(read => {
|
|
81
|
+
if (!cancelled) {
|
|
82
|
+
setPixel(read);
|
|
83
|
+
}
|
|
84
|
+
}, error => {
|
|
85
|
+
if (!cancelled) {
|
|
86
|
+
setLoadError(error instanceof Error ? error.message : String(error));
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
return () => {
|
|
90
|
+
cancelled = true;
|
|
91
|
+
};
|
|
92
|
+
}, []);
|
|
93
|
+
// The object URL lives as long as the page does.
|
|
94
|
+
useEffect(() => () => {
|
|
95
|
+
if (pixel) {
|
|
96
|
+
URL.revokeObjectURL(pixel.sprite);
|
|
97
|
+
}
|
|
98
|
+
}, [pixel]);
|
|
99
|
+
// The clippy.js characters, each fetched the first time it is picked.
|
|
100
|
+
const [clippyJs, setClippyJs] = useState({});
|
|
101
|
+
const [fetching, setFetching] = useState();
|
|
102
|
+
const fetched = useRef([]);
|
|
103
|
+
useEffect(() => () => {
|
|
104
|
+
for (const read of fetched.current) {
|
|
105
|
+
URL.revokeObjectURL(read.sprite);
|
|
106
|
+
}
|
|
107
|
+
}, []);
|
|
108
|
+
const pickClippyJs = async (name) => {
|
|
109
|
+
const known = clippyJs[name];
|
|
110
|
+
if (known) {
|
|
111
|
+
setCharacter(known);
|
|
112
|
+
setLoadError(undefined);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
setFetching(name);
|
|
116
|
+
try {
|
|
117
|
+
const read = await readClippyJsCharacter(name);
|
|
118
|
+
fetched.current.push(read);
|
|
119
|
+
setClippyJs(previous => ({ ...previous, [name]: read }));
|
|
120
|
+
setCharacter(read);
|
|
121
|
+
setLoadError(undefined);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
setLoadError(error instanceof Error ? error.message : String(error));
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
setFetching(undefined);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const isClippyJs = typeof character !== 'string' &&
|
|
131
|
+
Object.values(clippyJs).includes(character);
|
|
132
|
+
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: "Chat 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." }), _jsxs(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: [CATALOGUE.map(option => (_jsx(Button, { variant: option.id === character ? 'primary' : 'default', onClick: () => {
|
|
133
|
+
setCharacter(option.id);
|
|
134
|
+
setLoadError(undefined);
|
|
135
|
+
}, "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
|
+
setCharacter(pixel);
|
|
137
|
+
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 => {
|
|
139
|
+
const picked = clippyJs[name] !== undefined && character === clippyJs[name];
|
|
140
|
+
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
|
+
}) })] }), _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) => {
|
|
76
142
|
const files = Array.from(event.target.files ?? []);
|
|
77
143
|
if (!files.length) {
|
|
78
144
|
return;
|
|
@@ -84,7 +150,9 @@ const ChatAssistantExample = () => {
|
|
|
84
150
|
catch (error) {
|
|
85
151
|
setLoadError(error instanceof Error ? error.message : String(error));
|
|
86
152
|
}
|
|
87
|
-
} }), loadError && (_jsx(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mt: 2 }, children: loadError })), typeof character !== 'string' &&
|
|
153
|
+
} }), loadError && (_jsx(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mt: 2 }, children: loadError })), typeof character !== 'string' &&
|
|
154
|
+
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,
|
|
88
156
|
// Ask a decision beside the composer: Jev, through this server.
|
|
89
157
|
decisions: { serverUrl: SERVER } }, typeof character === 'string' ? character : character.sprite)] }) }));
|
|
90
158
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Every representation of the floating assistant, as a gallery (LOOP T-21
|
|
3
3
|
* to T-27): each character — Datalayer's four, the owl an example plugin
|
|
4
|
-
* contributes to `loop.assistant.character`,
|
|
5
|
-
* the clippy.js reader
|
|
4
|
+
* contributes to `loop.assistant.character`, a test sprite read through
|
|
5
|
+
* the clippy.js reader, and, when asked, the characters clippy.js publishes,
|
|
6
|
+
* fetched from jsDelivr — in each state it acts, stepped aside, with its
|
|
6
7
|
* balloon, in light and dark, moving or still.
|
|
7
8
|
*
|
|
8
9
|
* Static data, no agent: it opens without a server or an account.
|
|
@@ -6,8 +6,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
6
6
|
/**
|
|
7
7
|
* Every representation of the floating assistant, as a gallery (LOOP T-21
|
|
8
8
|
* to T-27): each character — Datalayer's four, the owl an example plugin
|
|
9
|
-
* contributes to `loop.assistant.character`,
|
|
10
|
-
* the clippy.js reader
|
|
9
|
+
* contributes to `loop.assistant.character`, a test sprite read through
|
|
10
|
+
* the clippy.js reader, and, when asked, the characters clippy.js publishes,
|
|
11
|
+
* fetched from jsDelivr — in each state it acts, stepped aside, with its
|
|
11
12
|
* balloon, in light and dark, moving or still.
|
|
12
13
|
*
|
|
13
14
|
* Static data, no agent: it opens without a server or an account.
|
|
@@ -21,6 +22,7 @@ import { AssistantStage } from '../chat/assistant/AssistantStage';
|
|
|
21
22
|
import { useViewportDrag } from '../chat/useViewportDrag';
|
|
22
23
|
import { AssistantGalleryGrid, GalleryObstacle, Still, useGalleryCharacters, } from './utils/AssistantGalleryGrid';
|
|
23
24
|
import { GALLERY_POSES, GALLERY_POSE_LABELS, balloonForPose, sampleApproval, sampleSaying, stateOfPose, } from './utils/assistantGallery';
|
|
25
|
+
import { CLIPPY_JS_VERSION, useClippyJsCharacters, } from './utils/clippyJsCharacters';
|
|
24
26
|
const STAGE_SIZE = 96;
|
|
25
27
|
const PANEL_HEIGHT = 420;
|
|
26
28
|
const LEAVE_MS = 700;
|
|
@@ -98,7 +100,10 @@ function InModes({ modes, stacked = false, children, }) {
|
|
|
98
100
|
}, children: shown.map(mode => (_jsx(Box, { sx: { flex: '1 1 320px', minWidth: 0 }, children: _jsx(DatalayerThemeProvider, { colorMode: mode, theme: config.primerTheme, themeStyles: config.themeStyles, children: children(mode) }) }, mode))) }));
|
|
99
101
|
}
|
|
100
102
|
const ChatAssistantGalleryExample = () => {
|
|
101
|
-
const { characters, problem } = useGalleryCharacters();
|
|
103
|
+
const { characters: ours, problem } = useGalleryCharacters();
|
|
104
|
+
const [withClippyJs, setWithClippyJs] = useState(false);
|
|
105
|
+
const clippyJs = useClippyJsCharacters(withClippyJs);
|
|
106
|
+
const characters = [...ours, ...clippyJs.characters];
|
|
102
107
|
const [characterId, setCharacterId] = useState('paperclip');
|
|
103
108
|
const [pose, setPose] = useState('idle');
|
|
104
109
|
const [view, setView] = useState('stage');
|
|
@@ -142,7 +147,7 @@ const ChatAssistantGalleryExample = () => {
|
|
|
142
147
|
bg: 'canvas.default',
|
|
143
148
|
color: 'fg.default',
|
|
144
149
|
p: 4,
|
|
145
|
-
}, children: _jsxs(Box, { sx: { maxWidth: 1200, mx: 'auto' }, children: [_jsx(Heading, { as: "h1", sx: { mb: 2 }, children: "Chat Assistant gallery" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 3, maxWidth: 760 }, children: ["Every character in every state the floating assistant acts, with its balloon, in light and dark. Datalayer\u2019s four and the owl are what the enabled plugins contribute to", ' ', _jsx("code", { children: "loop.assistant.character" }), "; Pixel is a test sprite of our own, read through the clippy.js reader. No agent is behind it: the words are a sample conversation."] }), _jsxs(Box, { as: "section", "aria-label": "Gallery controls", sx: {
|
|
150
|
+
}, children: _jsxs(Box, { sx: { maxWidth: 1200, mx: 'auto' }, children: [_jsx(Heading, { as: "h1", sx: { mb: 2 }, children: "Chat Assistant gallery" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 3, maxWidth: 760 }, children: ["Every character in every state the floating assistant acts, with its balloon, in light and dark. Datalayer\u2019s four and the owl are what the enabled plugins contribute to", ' ', _jsx("code", { children: "loop.assistant.character" }), "; Pixel is a test sprite of our own, read through the clippy.js reader. No agent is behind it: the words are a sample conversation."] }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 3, maxWidth: 760 }, children: ["The characters clippy.js publishes \u2014 Clippy, Merlin, Links, Rover and the others \u2014 are loaded on request from the", ' ', _jsxs("code", { children: ["clippyjs@", CLIPPY_JS_VERSION] }), " package on jsDelivr (about 15 MB) and read by the same reader. They are not in this repository: clippy.js\u2019s licence covers its code only, and the characters are Microsoft\u2019s."] }), _jsxs(Box, { as: "section", "aria-label": "Gallery controls", sx: {
|
|
146
151
|
display: 'grid',
|
|
147
152
|
gap: 3,
|
|
148
153
|
mb: 4,
|
|
@@ -160,7 +165,7 @@ const ChatAssistantGalleryExample = () => {
|
|
|
160
165
|
? 'Side by side'
|
|
161
166
|
: option === 'light'
|
|
162
167
|
? 'Light'
|
|
163
|
-
: 'Dark' }, option))) }), _jsxs(FormControl, { children: [_jsx(Checkbox, { checked: still, onChange: event => setStill(event.target.checked) }), _jsx(FormControl.Label, { children: "Reduced motion" })] }), _jsx(Button, { onClick: () => setReplay(replay + 1), children: "Replay the motions" })] }), view === 'stage' && (_jsxs(_Fragment, { children: [_jsx(Box, { role: "group", "aria-label": "Character", sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: characters.map(option => (_jsx(Button, { size: "small", variant: option.id === character.id ? 'primary' : 'default', "aria-pressed": option.id === character.id, "data-gallery-character": option.id, onClick: () => setCharacterId(option.id), children: option.name }, option.id))) }), _jsx(Box, { role: "group", "aria-label": "State", sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: GALLERY_POSES.map(option => (_jsx(Button, { size: "small", variant: option === pose ? 'primary' : 'default', "aria-pressed": option === pose, "data-gallery-pose": option, onClick: () => {
|
|
168
|
+
: 'Dark' }, option))) }), _jsxs(FormControl, { children: [_jsx(Checkbox, { checked: still, onChange: event => setStill(event.target.checked) }), _jsx(FormControl.Label, { children: "Reduced motion" })] }), _jsxs(FormControl, { children: [_jsx(Checkbox, { checked: withClippyJs, onChange: event => setWithClippyJs(event.target.checked), "data-gallery-clippy-js": "" }), _jsxs(FormControl.Label, { children: ["clippy.js characters", clippyJs.loading ? ' (loading…)' : ''] })] }), _jsx(Button, { onClick: () => setReplay(replay + 1), children: "Replay the motions" })] }), view === 'stage' && (_jsxs(_Fragment, { children: [_jsx(Box, { role: "group", "aria-label": "Character", sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: characters.map(option => (_jsx(Button, { size: "small", variant: option.id === character.id ? 'primary' : 'default', "aria-pressed": option.id === character.id, "data-gallery-character": option.id, onClick: () => setCharacterId(option.id), children: option.name }, option.id))) }), _jsx(Box, { role: "group", "aria-label": "State", sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: GALLERY_POSES.map(option => (_jsx(Button, { size: "small", variant: option === pose ? 'primary' : 'default', "aria-pressed": option === pose, "data-gallery-pose": option, onClick: () => {
|
|
164
169
|
setPose(option);
|
|
165
170
|
setDecision(undefined);
|
|
166
171
|
setReplay(replay + 1);
|
|
@@ -169,7 +174,7 @@ const ChatAssistantGalleryExample = () => {
|
|
|
169
174
|
gap: 2,
|
|
170
175
|
flexWrap: 'wrap',
|
|
171
176
|
alignItems: 'center',
|
|
172
|
-
}, children: [_jsx(Button, { onClick: say, "data-gallery-say": "", children: "Say something" }), 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] })), _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 })) })) : (_jsx(InModes, { modes: modes, stacked: true, children: mode => (_jsx(Box, { "data-gallery-mode": mode, sx: {
|
|
177
|
+
}, children: [_jsx(Button, { onClick: say, "data-gallery-say": "", children: "Say something" }), 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 })) })) : (_jsx(InModes, { modes: modes, stacked: true, children: mode => (_jsx(Box, { "data-gallery-mode": mode, sx: {
|
|
173
178
|
bg: 'canvas.default',
|
|
174
179
|
border: '1px solid',
|
|
175
180
|
borderColor: 'border.default',
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box } from '@datalayer/primer-addons';
|
|
3
3
|
import { ThemedProvider } from './utils/themedProvider';
|
|
4
|
-
import { registerDecks } from '@datalayer/decks';
|
|
5
|
-
import { exampleDecks } from '@datalayer/decks/examples';
|
|
4
|
+
import { registerDeckComponents, registerDecks } from '@datalayer/decks';
|
|
5
|
+
import { exampleDeckComponents, exampleDecks } from '@datalayer/decks/examples';
|
|
6
6
|
import { LoopEmbed } from '../loop';
|
|
7
7
|
import { DeckViewPlugin, LoopDecksPlugin } from '../loop/plugins/decks';
|
|
8
8
|
// The package's example decks, so the catalog has something to open and the
|
|
9
9
|
// agent's suggestions point at decks that exist. Once, at module load.
|
|
10
10
|
registerDecks(exampleDecks);
|
|
11
|
+
// And the components their slides name (AgentsHero, LiveAppearance…).
|
|
12
|
+
registerDeckComponents(exampleDeckComponents);
|
|
11
13
|
// Module-level, so the embed builds its reactor once rather than on every
|
|
12
14
|
// render of this component.
|
|
13
15
|
const DECK_PLUGINS = [LoopDecksPlugin, DeckViewPlugin];
|
|
@@ -3,8 +3,8 @@ import { useMemo } from 'react';
|
|
|
3
3
|
import { Box } from '@primer/react';
|
|
4
4
|
import { QueryClientProvider } from '@tanstack/react-query';
|
|
5
5
|
import { useReactor } from '@datalayer/reactor/react';
|
|
6
|
-
import { registerDecks } from '@datalayer/decks';
|
|
7
|
-
import { exampleDecks } from '@datalayer/decks/examples';
|
|
6
|
+
import { registerDeckComponents, registerDecks } from '@datalayer/decks';
|
|
7
|
+
import { exampleDeckComponents, exampleDecks } from '@datalayer/decks/examples';
|
|
8
8
|
import { buildLoopReactor, LoopWorkspace } from '../loop/shell';
|
|
9
9
|
import { DeckViewPlugin, LoopDecksPlugin } from '../loop/plugins/decks';
|
|
10
10
|
import { loopPlugins } from '../loop/presets';
|
|
@@ -18,6 +18,8 @@ const FRONT_DOOR = 'jupyter-data-analyst';
|
|
|
18
18
|
// The package's example decks, so the Decks member has something to open and
|
|
19
19
|
// change before it has made one. Once, at module load.
|
|
20
20
|
registerDecks(exampleDecks);
|
|
21
|
+
// And the components their slides name (AgentsHero, LiveAppearance…).
|
|
22
|
+
registerDeckComponents(exampleDeckComponents);
|
|
21
23
|
export function LoopShellExample({ serverUrl = resolveExampleAgentRuntimesUrl('local'), agentId = FRONT_DOOR, defaultEditor = 'none', inheritTheme = false, }) {
|
|
22
24
|
// Built once: rebuilding would restart every plugin on each render.
|
|
23
25
|
const reactor = useMemo(() => buildLoopReactor([
|
package/lib/examples/main.js
CHANGED
|
@@ -47,7 +47,7 @@ const DEFAULT_LOCAL_JUPYTER_SERVER_URL = 'http://localhost:8888/api/jupyter-serv
|
|
|
47
47
|
const DEFAULT_LOCAL_JUPYTER_SERVER_TOKEN = '60c1661cc408f978c309d04157af55c9588ff9557c9380e4fb50785750703da6';
|
|
48
48
|
const DEFAULT_CLOUD_RUNTIME_ENVIRONMENT = 'ai-agents-env';
|
|
49
49
|
const EXAMPLE_GROUP_ORDER = [
|
|
50
|
-
'
|
|
50
|
+
'Apps',
|
|
51
51
|
'A2UI',
|
|
52
52
|
'A2A',
|
|
53
53
|
'AG-UI',
|
|
@@ -91,11 +91,9 @@ const ANONYMOUS_EXAMPLES = new Set([
|
|
|
91
91
|
]);
|
|
92
92
|
const getExampleGroup = (id) => {
|
|
93
93
|
if (id === 'AgentspecsExample' ||
|
|
94
|
-
id
|
|
95
|
-
id === 'LoopWorkspaceExample' ||
|
|
96
|
-
id === 'LoopShellExample' ||
|
|
94
|
+
id.startsWith('Loop') ||
|
|
97
95
|
id === 'DecksAgent') {
|
|
98
|
-
return '
|
|
96
|
+
return 'Apps';
|
|
99
97
|
}
|
|
100
98
|
if (id.startsWith('A2Ui'))
|
|
101
99
|
return 'A2UI';
|
|
@@ -1072,13 +1070,14 @@ const ExampleAppThemed = ({ selectedExample, isChangingExample, error, ExampleCo
|
|
|
1072
1070
|
}
|
|
1073
1071
|
for (const [groupName, examples] of groups) {
|
|
1074
1072
|
examples.sort((left, right) => {
|
|
1075
|
-
if (groupName === '
|
|
1073
|
+
if (groupName === 'Apps') {
|
|
1076
1074
|
// The shells first, most naked first; then the strategy that drives a
|
|
1077
1075
|
// notebook; then the library of specs behind them all.
|
|
1078
1076
|
const LOOP_ORDER = [
|
|
1079
1077
|
'LoopShellExample',
|
|
1080
1078
|
'LoopWorkspaceExample',
|
|
1081
1079
|
'LoopStrategyExample',
|
|
1080
|
+
'LoopAppComputerExample',
|
|
1082
1081
|
'AgentspecsExample',
|
|
1083
1082
|
];
|
|
1084
1083
|
const loopOrder = (id) => {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AssistantCharacterData } from '../../chat/assistant/formats/types';
|
|
2
|
+
import type { GalleryCharacter } from './AssistantGalleryGrid';
|
|
3
|
+
/** The version of `clippyjs` the characters are read from. */
|
|
4
|
+
export declare const CLIPPY_JS_VERSION = "0.1.0";
|
|
5
|
+
/** The characters the package publishes: their names, and their folders. */
|
|
6
|
+
export declare const CLIPPY_JS_CHARACTERS: readonly ["Clippy", "Merlin", "Links", "Rover", "Genie", "Genius", "F1", "Peedy", "Rocky", "Bonzi"];
|
|
7
|
+
export type ClippyJsCharacterName = (typeof CLIPPY_JS_CHARACTERS)[number];
|
|
8
|
+
/** One clippy.js character, fetched and read. */
|
|
9
|
+
export declare function readClippyJsCharacter(name: ClippyJsCharacterName): Promise<AssistantCharacterData>;
|
|
10
|
+
/**
|
|
11
|
+
* The clippy.js characters as the gallery shows them, fetched once `enabled`
|
|
12
|
+
* and kept while it stays so; each as soon as it is read.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useClippyJsCharacters(enabled: boolean): {
|
|
15
|
+
characters: readonly GalleryCharacter[];
|
|
16
|
+
loading: boolean;
|
|
17
|
+
problem?: string;
|
|
18
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The characters clippy.js publishes — Clippy, Merlin, Links, Rover and the
|
|
7
|
+
* others — loaded into the floating assistant's gallery from the `clippyjs`
|
|
8
|
+
* package on jsDelivr, at the reader's request, and read through the same
|
|
9
|
+
* clippy.js reader a person's files go through.
|
|
10
|
+
*
|
|
11
|
+
* Nothing of them is in this repository: clippy.js's licence covers its code
|
|
12
|
+
* only, and the characters, their names and their drawings are Microsoft's
|
|
13
|
+
* (§6.9). The page fetches them when asked and keeps them in memory.
|
|
14
|
+
*
|
|
15
|
+
* The package ships each character as ES modules: `agent.mjs` exports the
|
|
16
|
+
* character's data, `map.mjs` its sprite sheet as a data URL. Both are read as
|
|
17
|
+
* text, never run: the data is rewritten as the `clippy.ready(...)` call the
|
|
18
|
+
* reader expects. Sounds are left out.
|
|
19
|
+
*
|
|
20
|
+
* @module examples/utils/clippyJsCharacters
|
|
21
|
+
*/
|
|
22
|
+
import { useEffect, useState } from 'react';
|
|
23
|
+
import { readClippyCharacter } from '../../chat/assistant/formats/clippy';
|
|
24
|
+
/** The version of `clippyjs` the characters are read from. */
|
|
25
|
+
export const CLIPPY_JS_VERSION = '0.1.0';
|
|
26
|
+
const BASE = `https://cdn.jsdelivr.net/npm/clippyjs@${CLIPPY_JS_VERSION}/dist/agents`;
|
|
27
|
+
/** The characters the package publishes: their names, and their folders. */
|
|
28
|
+
export const CLIPPY_JS_CHARACTERS = [
|
|
29
|
+
'Clippy',
|
|
30
|
+
'Merlin',
|
|
31
|
+
'Links',
|
|
32
|
+
'Rover',
|
|
33
|
+
'Genie',
|
|
34
|
+
'Genius',
|
|
35
|
+
'F1',
|
|
36
|
+
'Peedy',
|
|
37
|
+
'Rocky',
|
|
38
|
+
'Bonzi',
|
|
39
|
+
];
|
|
40
|
+
async function fetchText(url) {
|
|
41
|
+
const response = await fetch(url);
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw new Error(`${url} answered ${response.status}.`);
|
|
44
|
+
}
|
|
45
|
+
return response.text();
|
|
46
|
+
}
|
|
47
|
+
/** The value a module exports by default: `var x = <value>;\nexport …`. */
|
|
48
|
+
function defaultExportText(module, what) {
|
|
49
|
+
const match = /^var [A-Za-z_$][\w$]* = ([\s\S]*?);\s*export\s*\{/m.exec(module);
|
|
50
|
+
if (!match) {
|
|
51
|
+
throw new Error(`${what} does not export its value as clippy.js 0.1 does.`);
|
|
52
|
+
}
|
|
53
|
+
return match[1];
|
|
54
|
+
}
|
|
55
|
+
/** One clippy.js character, fetched and read. */
|
|
56
|
+
export async function readClippyJsCharacter(name) {
|
|
57
|
+
const folder = `${BASE}/${name.toLowerCase()}`;
|
|
58
|
+
const [agentModule, mapModule] = await Promise.all([
|
|
59
|
+
fetchText(`${folder}/agent.mjs`),
|
|
60
|
+
fetchText(`${folder}/map.mjs`),
|
|
61
|
+
]);
|
|
62
|
+
const data = defaultExportText(agentModule, `${name}'s agent.mjs`);
|
|
63
|
+
const mapUrl = defaultExportText(mapModule, `${name}'s map.mjs`);
|
|
64
|
+
if (!/^"data:image\/(png|gif|webp|jpeg);base64,[A-Za-z0-9+/=]+"$/.test(mapUrl)) {
|
|
65
|
+
throw new Error(`${name}'s map.mjs does not hold an image.`);
|
|
66
|
+
}
|
|
67
|
+
const mapPng = await (await fetch(mapUrl.slice(1, -1))).blob();
|
|
68
|
+
return readClippyCharacter({
|
|
69
|
+
agentJs: `clippy.ready(${JSON.stringify(name)}, ${data});`,
|
|
70
|
+
mapPng,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The clippy.js characters as the gallery shows them, fetched once `enabled`
|
|
75
|
+
* and kept while it stays so; each as soon as it is read.
|
|
76
|
+
*/
|
|
77
|
+
export function useClippyJsCharacters(enabled) {
|
|
78
|
+
const [read, setRead] = useState([]);
|
|
79
|
+
const [pending, setPending] = useState(0);
|
|
80
|
+
const [problem, setProblem] = useState();
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!enabled) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
let cancelled = false;
|
|
86
|
+
const loaded = [];
|
|
87
|
+
setRead([]);
|
|
88
|
+
setProblem(undefined);
|
|
89
|
+
setPending(CLIPPY_JS_CHARACTERS.length);
|
|
90
|
+
for (const name of CLIPPY_JS_CHARACTERS) {
|
|
91
|
+
readClippyJsCharacter(name).then(character => {
|
|
92
|
+
if (cancelled) {
|
|
93
|
+
URL.revokeObjectURL(character.sprite);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
loaded.push(character);
|
|
97
|
+
setRead(previous => [
|
|
98
|
+
...previous,
|
|
99
|
+
{
|
|
100
|
+
id: `clippy-js-${name.toLowerCase()}`,
|
|
101
|
+
name: character.name,
|
|
102
|
+
character,
|
|
103
|
+
from: `clippy.js ${CLIPPY_JS_VERSION}`,
|
|
104
|
+
},
|
|
105
|
+
].sort((a, b) => CLIPPY_JS_CHARACTERS.indexOf(a.name) -
|
|
106
|
+
CLIPPY_JS_CHARACTERS.indexOf(b.name)));
|
|
107
|
+
setPending(count => count - 1);
|
|
108
|
+
}, error => {
|
|
109
|
+
if (cancelled) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
setProblem(`${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
113
|
+
setPending(count => count - 1);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// The object URLs live as long as the characters are shown.
|
|
117
|
+
return () => {
|
|
118
|
+
cancelled = true;
|
|
119
|
+
for (const character of loaded) {
|
|
120
|
+
URL.revokeObjectURL(character.sprite);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}, [enabled]);
|
|
124
|
+
return {
|
|
125
|
+
characters: enabled ? read : [],
|
|
126
|
+
loading: enabled && pending > 0,
|
|
127
|
+
problem: enabled ? problem : undefined,
|
|
128
|
+
};
|
|
129
|
+
}
|
package/lib/loop/apps/checks.js
CHANGED
package/lib/types/agents.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { Agentspec } from './agentspecs';
|
|
5
5
|
import type { AgentConnection } from './connection';
|
|
6
|
-
export type AgentLibrary = 'pydantic-ai' | 'langchain'
|
|
6
|
+
export type AgentLibrary = 'pydantic-ai' | 'langchain';
|
|
7
7
|
/**
|
|
8
8
|
* Unified agent status covering runtime lifecycle and UI lifecycle.
|
|
9
9
|
*/
|