@datalayer/agent-runtimes 1.3.61 → 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.
Files changed (35) hide show
  1. package/lib/chat/assistant/AssistantStage.js +5 -0
  2. package/lib/chat/assistant/SpriteCharacter.js +1 -1
  3. package/lib/chat/assistant/characters.js +1 -5
  4. package/lib/config/AgentConfiguration.js +0 -6
  5. package/lib/examples/AgentA2ATeamExample.d.ts +23 -0
  6. package/lib/examples/AgentA2ATeamExample.js +296 -0
  7. package/lib/examples/ChatAssistantExample.d.ts +3 -1
  8. package/lib/examples/ChatAssistantExample.js +75 -7
  9. package/lib/examples/ChatAssistantGalleryExample.d.ts +3 -2
  10. package/lib/examples/ChatAssistantGalleryExample.js +11 -6
  11. package/lib/examples/DecksAgent.js +4 -2
  12. package/lib/examples/LoopShellExample.js +4 -2
  13. package/lib/examples/LoopStrategyExample.d.ts +12 -0
  14. package/lib/examples/{AgentStrategyExample.js → LoopStrategyExample.js} +9 -9
  15. package/lib/examples/example-selector.js +2 -1
  16. package/lib/examples/index.d.ts +2 -1
  17. package/lib/examples/index.js +2 -1
  18. package/lib/examples/main.js +7 -8
  19. package/lib/examples/utils/clippyJsCharacters.d.ts +18 -0
  20. package/lib/examples/utils/clippyJsCharacters.js +129 -0
  21. package/lib/hooks/useA2A.d.ts +37 -1
  22. package/lib/loop/apps/checks.js +1 -1
  23. package/lib/runtimes/__tests__/fixtures/accounting-agent-card.json +60 -0
  24. package/lib/runtimes/browser/a2aPeer.d.ts +93 -0
  25. package/lib/runtimes/browser/a2aPeer.js +241 -0
  26. package/lib/specs/actions.js +54 -0
  27. package/lib/specs/apps.d.ts +2 -0
  28. package/lib/specs/apps.js +387 -0
  29. package/lib/specs/teams/teams.d.ts +1 -0
  30. package/lib/specs/teams/teams.js +80 -0
  31. package/lib/types/agents.d.ts +1 -1
  32. package/lib/types/teams.d.ts +20 -0
  33. package/package.json +2 -2
  34. package/scripts/codegen/generate_teams.py +42 -3
  35. package/lib/examples/AgentStrategyExample.d.ts +0 -12
@@ -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: "M24 92 Q48 58 72 92 Z", fill: c.robe }), _jsx("circle", { cx: 48, cy: 48, r: 17, fill: c.face, stroke: c.faceEdge, strokeWidth: 1.5 }), _jsx("path", { d: "M31 50 Q48 92 65 50 Q57 60 48 60 Q39 60 31 50 Z", fill: c.beard, stroke: c.beardEdge, strokeWidth: 1.2 }), _jsx("path", { d: "M22 36 L48 2 L74 36 Q48 30 22 36 Z", fill: c.hat }), _jsx("path", { d: "M20 37 Q48 29 76 37 Q48 43 20 37 Z", fill: c.brim }), _jsx("path", { d: "M52 14 l2 4 4 1 -3 3 1 4 -4 -2 -4 2 1 -4 -3 -3 4 -1 Z", fill: c.star }), _jsx("circle", { cx: 40, cy: 26, r: 1.8, fill: c.star }), _jsx(Eyes, { cx: 48, cy: 46, gap: 13, r: 5, colours: c.eye }), _jsx("path", { className: "assistant-mouth", d: "M44 58 Q48 61 52 58", stroke: INK, strokeWidth: 2, fill: "none", strokeLinecap: "round" })] }) }));
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
  {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A team of two applications over A2A (agentspecs `sales-and-accounting`).
3
+ *
4
+ * Sales runs here, in the browser: its instructions and model turn in the
5
+ * page (the Vercel AI SDK's loop, the in-browser harness), and its one tool
6
+ * asks Accounting over A2A with `@a2a-js/sdk` 1.x. Accounting runs on a
7
+ * runtime, served with fasta2a: its agent reads the Odoo books through the
8
+ * `odoo-accounting` toolset, and only reads them.
9
+ *
10
+ * Both are on screen as Office Assistant characters, Sales the paper clip and
11
+ * Accounting the wizard, as their Appspecs say. The exchange is shown as it
12
+ * happens: Sales asks, Accounting works and then answers, each in its own
13
+ * state and balloon. Then the report appears.
14
+ *
15
+ * Where Accounting is: `VITE_A2A_ACCOUNTING_URL`, else the examples' local
16
+ * server (`…:8765/api/v1/a2a/agents/accounting`), which answers this machine
17
+ * without a key. A cloud runtime needs a key granted to its route:
18
+ * `VITE_A2A_ACCOUNTING_KEY`, which `examples/sales-accounting-a2a/make_temp_key.py`
19
+ * writes to `.env.local`, never committed.
20
+ */
21
+ import type { JSX } from 'react';
22
+ export declare function AgentA2ATeamExample(): JSX.Element;
23
+ export default AgentA2ATeamExample;
@@ -0,0 +1,296 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { Button, Flash, Heading, Label, Text, Textarea } from '@primer/react';
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';
7
+ import { stepCountIs, ToolLoopAgent } from 'ai';
8
+ import { Streamdown } from 'streamdown';
9
+ import { ThemedProvider } from './utils/themedProvider';
10
+ import { useExampleAgentRuntimesUrl } from './utils/useExampleAgentRuntimesUrl';
11
+ import { AssistantStage } from '../chat/assistant/AssistantStage';
12
+ import { useBrowserInference } from '../hooks/useBrowserInference';
13
+ import { createBrowserModel } from '../runtimes/browser/model';
14
+ import { readJwtClaims, tokenLifetimeMs, useAnonymousSessionStore, } from '../runtimes/browser/anonymousToken';
15
+ import { a2aPeerTool, connectA2APeer, } from '../runtimes/browser/a2aPeer';
16
+ import { ACCOUNTING_APP_0_0_1, SALES_APP_0_0_1 } from '../specs/apps';
17
+ import { SALES_AND_ACCOUNTING_TEAM_SPEC_0_0_1 } from '../specs/teams/teams';
18
+ const TEAM = SALES_AND_ACCOUNTING_TEAM_SPEC_0_0_1;
19
+ const SALES = SALES_APP_0_0_1;
20
+ const ACCOUNTING = ACCOUNTING_APP_0_0_1;
21
+ /** The name Sales's tool goes by, as its instructions call it. */
22
+ const ASK_TOOL = 'ask_accounting';
23
+ const STAGE_SIZE = 104;
24
+ const STAGE_HEIGHT = 300;
25
+ const AT_REST = { state: 'idle', insist: false, away: false };
26
+ /** A line short enough for a balloon. */
27
+ function line(text, length = 120) {
28
+ const flat = text.replace(/\s+/g, ' ').trim();
29
+ return flat.length > length ? `${flat.slice(0, length - 1)}…` : flat;
30
+ }
31
+ /** The members of the team, as the team spec says them. */
32
+ function members() {
33
+ const sales = TEAM.agents.find(member => member.id === TEAM.entry);
34
+ const asked = sales?.talksTo?.find(link => link.over === 'a2a');
35
+ const accounting = TEAM.agents.find(member => member.id === asked?.member);
36
+ if (!sales || !accounting) {
37
+ throw new Error(`The team ${TEAM.id} does not say who a person talks to and whom it asks over A2A.`);
38
+ }
39
+ return { sales, accounting };
40
+ }
41
+ /** One persona at its desk, with its name and where it runs under it. */
42
+ function Desk({ app, persona, where, left, onToggle, onAway, }) {
43
+ const stageRef = useRef(null);
44
+ const character = app.interface.assistant ?? 'paperclip';
45
+ return (_jsxs(Box, { sx: {
46
+ position: 'absolute',
47
+ left,
48
+ top: 0,
49
+ width: 220,
50
+ height: STAGE_HEIGHT,
51
+ }, "data-team-member": app.id, "data-member-state": persona.state, children: [persona.away ? (_jsxs(Button, { size: "small", sx: { position: 'absolute', top: STAGE_HEIGHT - 150 }, onClick: () => onAway(false), children: ["Call ", app.name, " back"] })) : (_jsx(AssistantStage, { character: character, state: persona.state, size: STAGE_SIZE, place: { position: 'absolute', left: 56, top: STAGE_HEIGHT - 170 }, stageRef: stageRef, onDragStart: () => undefined, open: false, onToggle: onToggle, balloon: persona.saying
52
+ ? { text: persona.saying, more: persona.saying.endsWith('…') }
53
+ : undefined, insist: persona.insist, onDismiss: away => onAway(away !== 'none') })), _jsxs(Box, { sx: {
54
+ position: 'absolute',
55
+ bottom: 0,
56
+ width: '100%',
57
+ textAlign: 'center',
58
+ }, children: [_jsxs(Text, { sx: { fontWeight: 600, display: 'block' }, children: [app.emoji, " ", app.name] }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: where })] })] }));
59
+ }
60
+ function AgentA2ATeam() {
61
+ const { sales: salesMember } = useMemo(members, []);
62
+ const runtimeUrl = useExampleAgentRuntimesUrl();
63
+ const accountingUrl = import.meta.env.VITE_A2A_ACCOUNTING_URL ||
64
+ `${runtimeUrl}/api/v1/a2a/agents/${ACCOUNTING.id}`;
65
+ const accountingKey = import.meta.env.VITE_A2A_ACCOUNTING_KEY || undefined;
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());
77
+ const [peer, setPeer] = useState(null);
78
+ const [peerError, setPeerError] = useState(null);
79
+ const [sales, setSales] = useState({
80
+ ...AT_REST,
81
+ state: 'greeting',
82
+ saying: line(SALES.interface.welcome ?? ''),
83
+ insist: true,
84
+ });
85
+ const [accounting, setAccounting] = useState(AT_REST);
86
+ const [turns, setTurns] = useState([]);
87
+ const [report, setReport] = useState(null);
88
+ const [exchange, setExchange] = useState([]);
89
+ const [draft, setDraft] = useState('');
90
+ const [busy, setBusy] = useState(false);
91
+ const composer = useRef(null);
92
+ const history = useRef([]);
93
+ const abort = useRef(null);
94
+ useEffect(() => {
95
+ let current = true;
96
+ setPeer(null);
97
+ setPeerError(null);
98
+ connectA2APeer({ url: accountingUrl, key: accountingKey })
99
+ .then(found => current && setPeer(found))
100
+ .catch(error => current &&
101
+ setPeerError(error instanceof Error ? error.message : String(error)));
102
+ return () => {
103
+ current = false;
104
+ };
105
+ }, [accountingUrl, accountingKey]);
106
+ // What Accounting does, as it does it: its own state and balloon.
107
+ const onPeerEvent = useCallback((event) => {
108
+ if (event.phase === 'asked') {
109
+ setExchange(prev => [...prev, `Sales → Accounting: ${event.request}`]);
110
+ setAccounting(prev => ({
111
+ ...prev,
112
+ state: 'greeting',
113
+ saying: 'On it. Let me read the books.',
114
+ insist: true,
115
+ }));
116
+ }
117
+ else if (event.phase === 'working') {
118
+ if (event.note) {
119
+ setExchange(prev => [...prev, `Accounting: ${event.note}`]);
120
+ }
121
+ setAccounting(prev => ({
122
+ ...prev,
123
+ state: 'working',
124
+ saying: event.note ? line(event.note) : prev.saying,
125
+ insist: Boolean(event.note) || prev.insist,
126
+ }));
127
+ }
128
+ else if (event.phase === 'answered') {
129
+ setExchange(prev => [...prev, 'Accounting → Sales: the report']);
130
+ setReport(event.answer);
131
+ setAccounting(prev => ({
132
+ ...prev,
133
+ state: 'speaking',
134
+ saying: line(event.answer),
135
+ insist: true,
136
+ }));
137
+ }
138
+ else {
139
+ setExchange(prev => [
140
+ ...prev,
141
+ `Accounting could not answer: ${event.error}`,
142
+ ]);
143
+ setAccounting(prev => ({
144
+ ...prev,
145
+ state: 'idle',
146
+ saying: line(event.error),
147
+ insist: true,
148
+ }));
149
+ }
150
+ }, []);
151
+ const agent = useMemo(() => {
152
+ if (!peer) {
153
+ return null;
154
+ }
155
+ return new ToolLoopAgent({
156
+ id: SALES.id,
157
+ model: createBrowserModel({
158
+ ...inference,
159
+ model: SALES.model || undefined,
160
+ }),
161
+ instructions: SALES.instructions,
162
+ tools: { [ASK_TOOL]: a2aPeerTool({ peer, onEvent: onPeerEvent }) },
163
+ stopWhen: stepCountIs(6),
164
+ });
165
+ }, [peer, inference, onPeerEvent]);
166
+ const send = useCallback(async (text) => {
167
+ const asked = text.trim();
168
+ if (!asked || !agent || busy) {
169
+ return;
170
+ }
171
+ setBusy(true);
172
+ setDraft('');
173
+ setReport(null);
174
+ setExchange([]);
175
+ setTurns(prev => [...prev, { role: 'user', text: asked }]);
176
+ setSales({
177
+ ...AT_REST,
178
+ state: 'thinking',
179
+ saying: 'Let me see…',
180
+ insist: true,
181
+ });
182
+ setAccounting(AT_REST);
183
+ history.current = [...history.current, { role: 'user', content: asked }];
184
+ abort.current = new AbortController();
185
+ let said = '';
186
+ try {
187
+ const result = await agent.stream({
188
+ messages: history.current,
189
+ abortSignal: abort.current.signal,
190
+ });
191
+ for await (const part of result.fullStream) {
192
+ if (part.type === 'tool-call' && part.toolName === ASK_TOOL) {
193
+ const request = String(part.input?.request ?? '');
194
+ setSales(prev => ({
195
+ ...prev,
196
+ state: 'waiting',
197
+ saying: line(`Accounting, could you: ${request}`),
198
+ insist: true,
199
+ }));
200
+ }
201
+ else if (part.type === 'tool-result' &&
202
+ part.toolName === ASK_TOOL) {
203
+ setSales(prev => ({
204
+ ...prev,
205
+ state: 'thinking',
206
+ saying: 'Thanks!',
207
+ }));
208
+ }
209
+ else if (part.type === 'text-delta') {
210
+ said += part.text;
211
+ setSales(prev => ({
212
+ ...prev,
213
+ state: 'speaking',
214
+ saying: line(said),
215
+ insist: true,
216
+ }));
217
+ }
218
+ else if (part.type === 'error') {
219
+ throw part.error;
220
+ }
221
+ }
222
+ history.current = [
223
+ ...history.current,
224
+ { role: 'assistant', content: said },
225
+ ];
226
+ setTurns(prev => [...prev, { role: 'assistant', text: said }]);
227
+ setSales(prev => ({ ...prev, state: 'idle', saying: line(said) }));
228
+ }
229
+ catch (error) {
230
+ const message = error instanceof Error ? error.message : String(error);
231
+ setTurns(prev => [
232
+ ...prev,
233
+ { role: 'assistant', text: `Something went wrong: ${message}` },
234
+ ]);
235
+ setSales(prev => ({
236
+ ...prev,
237
+ state: 'idle',
238
+ saying: line(`Something went wrong: ${message}`),
239
+ insist: true,
240
+ }));
241
+ }
242
+ finally {
243
+ abort.current = null;
244
+ setBusy(false);
245
+ }
246
+ }, [agent, busy]);
247
+ useEffect(() => () => abort.current?.abort(), []);
248
+ const accountingWhere = accountingKey
249
+ ? `on a runtime, ${new URL(accountingUrl).host}, with a temporary key`
250
+ : `on a runtime, ${new URL(accountingUrl).host}`;
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: {
256
+ position: 'relative',
257
+ height: STAGE_HEIGHT,
258
+ border: '1px solid',
259
+ borderColor: 'border.default',
260
+ borderRadius: 2,
261
+ bg: 'canvas.subtle',
262
+ overflow: 'visible',
263
+ mb: 3,
264
+ }, children: [_jsx(Desk, { app: SALES, persona: sales, where: salesMember.runsIn === 'browser'
265
+ ? 'in your browser'
266
+ : 'on a runtime', left: "8%", onToggle: () => composer.current?.focus(), onAway: away => setSales(prev => ({ ...prev, away })) }), _jsxs(Box, { sx: {
267
+ position: 'absolute',
268
+ left: '38%',
269
+ right: '38%',
270
+ top: STAGE_HEIGHT / 2,
271
+ textAlign: 'center',
272
+ }, children: [_jsx(Box, { sx: {
273
+ borderTop: '2px dashed',
274
+ borderColor: peer ? 'accent.emphasis' : 'border.muted',
275
+ } }), _jsxs(Label, { variant: peer ? 'accent' : 'secondary', sx: { mt: 2 }, children: ["A2A ", peer ? `· ${peer.skill.name}` : '· not connected'] })] }), _jsx(Desk, { app: ACCOUNTING, persona: accounting, where: accountingWhere, left: "66%", onToggle: () => undefined, onAway: away => setAccounting(prev => ({ ...prev, away })) })] }), _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: !agent || busy, onClick: () => void 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 => {
276
+ if (event.key === 'Enter' && !event.shiftKey) {
277
+ event.preventDefault();
278
+ void send(draft);
279
+ }
280
+ } }), _jsxs(Box, { sx: { display: 'flex', gap: 2, mt: 2 }, children: [_jsx(Button, { variant: "primary", disabled: !agent || busy || !draft.trim(), onClick: () => void send(draft), children: "Ask" }), busy && (_jsx(Button, { onClick: () => abort.current?.abort(), children: "Stop" }))] }), _jsx(Box, { sx: { mt: 3 }, "data-team-conversation": "", children: turns.map((turn, index) => (_jsxs(Box, { sx: {
281
+ p: 2,
282
+ mb: 2,
283
+ borderRadius: 2,
284
+ bg: turn.role === 'user' ? 'accent.subtle' : 'canvas.subtle',
285
+ }, children: [_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', display: 'block' }, children: turn.role === 'user' ? 'You' : SALES.name }), _jsx(Streamdown, { children: turn.text })] }, index))) })] }), _jsxs(Box, { sx: { flex: '1 1 360px', minWidth: 0 }, children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 2 }, children: "The exchange" }), _jsx(Box, { as: "ol", sx: { pl: 3, color: 'fg.muted', fontSize: 1 }, "data-team-exchange": "", children: exchange.length === 0 ? (_jsx(Text, { as: "li", children: "Nothing asked yet." })) : (exchange.map((step, index) => (_jsx(Text, { as: "li", children: step }, index)))) }), _jsx(Heading, { as: "h3", sx: { fontSize: 2, mt: 3, mb: 2 }, children: "The report" }), _jsx(Box, { sx: {
286
+ p: 3,
287
+ border: '1px solid',
288
+ borderColor: 'border.default',
289
+ borderRadius: 2,
290
+ minHeight: 120,
291
+ }, "data-team-report": "", children: report ? (_jsx(Streamdown, { children: report })) : (_jsx(Text, { sx: { color: 'fg.muted' }, children: "The report Accounting returns appears here, as it returned it." })) })] })] })] }));
292
+ }
293
+ export function AgentA2ATeamExample() {
294
+ return (_jsx(ThemedProvider, { children: _jsx(AgentA2ATeam, {}) }));
295
+ }
296
+ export default AgentA2ATeamExample;
@@ -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
- return (_jsx(ThemedProvider, { children: _jsxs(Box, { sx: { minHeight: '100vh', bg: 'canvas.default', p: 4 }, children: [_jsxs(Box, { sx: { maxWidth: 720, mx: 'auto' }, children: [_jsx(Heading, { as: "h1", sx: { mb: 2 }, children: "Floating assistant" }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', mb: 3 }, children: "The chat as a character on the page. It greets you, acts out what the agent is doing, and says the agent\u2019s words in a balloon while the conversation is closed. Click it to talk, drag it to move it, hover it to send it away." }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: CATALOGUE.map(option => (_jsx(Button, { variant: option.id === character ? 'primary' : 'default', onClick: () => {
73
- setCharacter(option.id);
74
- setLoadError(undefined);
75
- }, "aria-pressed": option.id === character, "data-assistant-character": option.id, children: option.name }, option.id))) }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "A character you bring" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["The characters of Microsoft Office \u2014 Clippy, Merlin, Links and the others \u2014 are Microsoft\u2019s. Load only a character file you have the right to use: a Microsoft Agent ", _jsx("code", { children: ".acs" }), " file, or a clippy.js character\u2019s ", _jsx("code", { children: "agent.js" }), " and map image. It is read in this page and sent nowhere."] }), _jsx("input", { type: "file", multiple: true, accept: ".acs,.js,.png,.gif,.webp,.jpg,.jpeg", "aria-label": "Character files", onChange: async (event) => {
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' && (_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,
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`, and a test sprite read through
5
- * the clippy.js reader — in each state it acts, stepped aside, with its
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`, and a test sprite read through
10
- * the clippy.js reader — in each state it acts, stepped aside, with its
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: "Floating 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',