@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.
- 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.d.ts +23 -0
- package/lib/examples/AgentA2ATeamExample.js +296 -0
- 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/LoopStrategyExample.d.ts +12 -0
- package/lib/examples/{AgentStrategyExample.js → LoopStrategyExample.js} +9 -9
- package/lib/examples/example-selector.js +2 -1
- package/lib/examples/index.d.ts +2 -1
- package/lib/examples/index.js +2 -1
- package/lib/examples/main.js +7 -8
- package/lib/examples/utils/clippyJsCharacters.d.ts +18 -0
- package/lib/examples/utils/clippyJsCharacters.js +129 -0
- package/lib/hooks/useA2A.d.ts +37 -1
- package/lib/loop/apps/checks.js +1 -1
- package/lib/runtimes/__tests__/fixtures/accounting-agent-card.json +60 -0
- package/lib/runtimes/browser/a2aPeer.d.ts +93 -0
- package/lib/runtimes/browser/a2aPeer.js +241 -0
- package/lib/specs/actions.js +54 -0
- package/lib/specs/apps.d.ts +2 -0
- package/lib/specs/apps.js +387 -0
- package/lib/specs/teams/teams.d.ts +1 -0
- package/lib/specs/teams/teams.js +80 -0
- package/lib/types/agents.d.ts +1 -1
- package/lib/types/teams.d.ts +20 -0
- package/package.json +2 -2
- package/scripts/codegen/generate_teams.py +42 -3
- 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: "
|
|
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
|
-
|
|
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: "
|
|
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',
|