@datalayer/agent-runtimes 1.3.28 → 1.3.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/AgentNode.js +14 -33
- package/lib/chat/ChatFloating.d.ts +12 -2
- package/lib/chat/ChatFloating.js +196 -25
- package/lib/chat/assistant/AssistantStage.d.ts +70 -0
- package/lib/chat/assistant/AssistantStage.js +192 -0
- package/lib/chat/assistant/SpeechBalloon.d.ts +26 -0
- package/lib/chat/assistant/SpeechBalloon.js +46 -0
- package/lib/chat/assistant/SpriteCharacter.d.ts +26 -0
- package/lib/chat/assistant/SpriteCharacter.js +73 -0
- package/lib/chat/assistant/characters.d.ts +30 -0
- package/lib/chat/assistant/characters.js +41 -0
- package/lib/chat/assistant/formats/acs.d.ts +88 -0
- package/lib/chat/assistant/formats/acs.js +493 -0
- package/lib/chat/assistant/formats/agentCompression.d.ts +5 -0
- package/lib/chat/assistant/formats/agentCompression.js +92 -0
- package/lib/chat/assistant/formats/blobs.d.ts +6 -0
- package/lib/chat/assistant/formats/blobs.js +52 -0
- package/lib/chat/assistant/formats/clippy.d.ts +21 -0
- package/lib/chat/assistant/formats/clippy.js +167 -0
- package/lib/chat/assistant/formats/index.d.ts +13 -0
- package/lib/chat/assistant/formats/index.js +15 -0
- package/lib/chat/assistant/formats/objectLiteral.d.ts +23 -0
- package/lib/chat/assistant/formats/objectLiteral.js +233 -0
- package/lib/chat/assistant/formats/stateAnimations.d.ts +16 -0
- package/lib/chat/assistant/formats/stateAnimations.js +115 -0
- package/lib/chat/assistant/formats/types.d.ts +56 -0
- package/lib/chat/assistant/formats/types.js +11 -0
- package/lib/chat/assistant/state.d.ts +62 -0
- package/lib/chat/assistant/state.js +116 -0
- package/lib/chat/base/ChatBase.js +41 -32
- package/lib/chat/base/modelChoice.d.ts +13 -0
- package/lib/chat/base/modelChoice.js +13 -0
- package/lib/chat/header/ChatViewModeToggle.js +6 -3
- package/lib/{loop/plugins/chat → chat/presence}/Presence.js +1 -1
- package/lib/{loop/plugins/chat → chat/presence}/presenceStatus.d.ts +7 -1
- package/lib/{loop/plugins/chat → chat/presence}/presenceStatus.js +18 -0
- package/lib/chat/viewModes.d.ts +6 -0
- package/lib/chat/viewModes.js +10 -1
- package/lib/examples/ChatAssistantExample.d.ts +8 -0
- package/lib/examples/ChatAssistantExample.js +59 -0
- package/lib/examples/example-selector.js +1 -0
- package/lib/examples/main.js +3 -0
- package/lib/loop/apps/AppRenderer.d.ts +34 -2
- package/lib/loop/apps/AppRenderer.js +96 -29
- package/lib/loop/apps/agent.d.ts +15 -0
- package/lib/loop/apps/agent.js +42 -0
- package/lib/loop/apps/appspec.d.ts +5 -1
- package/lib/loop/apps/appspec.js +21 -1
- package/lib/loop/apps/checks.js +3 -1
- package/lib/loop/apps/index.d.ts +1 -0
- package/lib/loop/apps/index.js +1 -0
- package/lib/loop/core/index.d.ts +23 -0
- package/lib/loop/core/index.js +1 -0
- package/lib/loop/embed/AppEmbed.d.ts +42 -0
- package/lib/loop/embed/AppEmbed.js +205 -0
- package/lib/loop/embed/datalayer-app.d.ts +1 -0
- package/lib/loop/embed/datalayer-app.js +16 -0
- package/lib/loop/embed/element.d.ts +57 -0
- package/lib/loop/embed/element.js +405 -0
- package/lib/loop/embed/embedConfig.d.ts +120 -0
- package/lib/loop/embed/embedConfig.js +148 -0
- package/lib/loop/embed/embedTheme.d.ts +24 -0
- package/lib/loop/embed/embedTheme.js +83 -0
- package/lib/loop/index.d.ts +6 -2
- package/lib/loop/index.js +8 -2
- package/lib/loop/plugins/a2ui-surface/InlineSurface.d.ts +21 -3
- package/lib/loop/plugins/a2ui-surface/InlineSurface.js +34 -6
- package/lib/loop/plugins/agent-capacity/index.d.ts +4 -0
- package/lib/loop/plugins/agent-capacity/index.js +3 -1
- package/lib/loop/plugins/agents/switchable.d.ts +7 -0
- package/lib/loop/plugins/agents/switchable.js +91 -88
- package/lib/loop/plugins/app-page/AppPage.d.ts +16 -0
- package/lib/loop/plugins/app-page/AppPage.js +53 -0
- package/lib/loop/plugins/app-page/appPage.d.ts +180 -0
- package/lib/loop/plugins/app-page/appPage.js +443 -0
- package/lib/loop/plugins/app-page/index.d.ts +8 -0
- package/lib/loop/plugins/app-page/index.js +34 -0
- package/lib/loop/plugins/assistant-characters/index.d.ts +30 -0
- package/lib/loop/plugins/assistant-characters/index.js +41 -0
- package/lib/loop/plugins/chat/ChatView.js +50 -39
- package/lib/loop/plugins/chat/index.d.ts +1 -1
- package/lib/loop/plugins/page-layout/SplitLayout.d.ts +31 -0
- package/lib/loop/plugins/page-layout/SplitLayout.js +129 -0
- package/lib/loop/plugins/page-layout/index.d.ts +13 -0
- package/lib/loop/plugins/page-layout/index.js +15 -2
- package/lib/loop/presets.d.ts +12 -0
- package/lib/loop/presets.js +5 -3
- package/lib/specs/actions.js +14 -0
- package/lib/specs/agents/agents.js +4 -0
- package/lib/specs/apps.d.ts +13 -1
- package/lib/specs/apps.js +2715 -373
- package/lib/specs/appspecSchema.js +21 -3
- package/lib/types/agentspecs.d.ts +21 -1
- package/lib/types/chat.d.ts +1 -1
- package/lib/types/config.d.ts +12 -0
- package/package.json +2 -1
- package/scripts/codegen/generate_agents.py +50 -2
- package/scripts/codegen/generate_apps.py +36 -6
- /package/lib/{loop/plugins/chat → chat/presence}/Presence.d.ts +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState } from 'react';
|
|
3
|
+
import { ActionList, ActionMenu, IconButton } from '@primer/react';
|
|
4
|
+
import { Box } from '@datalayer/primer-addons';
|
|
5
|
+
import { XIcon } from '@primer/octicons-react';
|
|
6
|
+
import { assistantCharacter } from './characters';
|
|
7
|
+
import { SpeechBalloon } from './SpeechBalloon';
|
|
8
|
+
import { SpriteCharacter } from './SpriteCharacter';
|
|
9
|
+
/** The motions, by the state the stage is in. */
|
|
10
|
+
const MOTIONS = {
|
|
11
|
+
'@keyframes assistantFloat': {
|
|
12
|
+
'0%, 100%': { transform: 'translateY(0)' },
|
|
13
|
+
'50%': { transform: 'translateY(-3px)' },
|
|
14
|
+
},
|
|
15
|
+
'@keyframes assistantBlink': {
|
|
16
|
+
'0%, 94%, 100%': { transform: 'scaleY(0)' },
|
|
17
|
+
'97%': { transform: 'scaleY(1)' },
|
|
18
|
+
},
|
|
19
|
+
'@keyframes assistantTilt': {
|
|
20
|
+
'0%, 100%': { transform: 'rotate(-5deg)' },
|
|
21
|
+
'50%': { transform: 'rotate(-8deg) translateY(-2px)' },
|
|
22
|
+
},
|
|
23
|
+
'@keyframes assistantWork': {
|
|
24
|
+
'0%, 100%': { transform: 'rotate(-3deg) translateY(0)' },
|
|
25
|
+
'50%': { transform: 'rotate(3deg) translateY(-4px)' },
|
|
26
|
+
},
|
|
27
|
+
'@keyframes assistantAttention': {
|
|
28
|
+
'0%, 100%': { transform: 'scale(1) rotate(0)' },
|
|
29
|
+
'20%': { transform: 'scale(1.08) rotate(-6deg)' },
|
|
30
|
+
'40%': { transform: 'scale(1.08) rotate(6deg)' },
|
|
31
|
+
'60%': { transform: 'scale(1)' },
|
|
32
|
+
},
|
|
33
|
+
'@keyframes assistantHop': {
|
|
34
|
+
'0%, 100%': { transform: 'translateY(0)' },
|
|
35
|
+
'30%': { transform: 'translateY(-14px)' },
|
|
36
|
+
'55%': { transform: 'translateY(0)' },
|
|
37
|
+
'75%': { transform: 'translateY(-6px)' },
|
|
38
|
+
},
|
|
39
|
+
'@keyframes assistantTalk': {
|
|
40
|
+
'0%, 100%': { transform: 'scaleY(1)' },
|
|
41
|
+
'50%': { transform: 'scaleY(2.2)' },
|
|
42
|
+
},
|
|
43
|
+
'@keyframes assistantLeave': {
|
|
44
|
+
from: { transform: 'scale(1)', opacity: 1 },
|
|
45
|
+
to: { transform: 'scale(0.2) translateY(20px)', opacity: 0 },
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
const BODY = '& .assistant-body';
|
|
49
|
+
/** How long it may be sent away for, in the menu's words (T-27). */
|
|
50
|
+
const AWAY_CHOICES = [
|
|
51
|
+
{ away: 'page', label: 'Hide for now' },
|
|
52
|
+
{ away: 'session', label: 'Hide for this session' },
|
|
53
|
+
{ away: 'always', label: 'Don\u2019t show again' },
|
|
54
|
+
];
|
|
55
|
+
/** Room the balloon needs above the character before it goes below. */
|
|
56
|
+
const BALLOON_ROOM = 200;
|
|
57
|
+
/**
|
|
58
|
+
* Where the balloon goes so that it stays inside the window (T-23): toward
|
|
59
|
+
* the middle of the page from whichever half the character stands in, and
|
|
60
|
+
* below it when it stands too near the top.
|
|
61
|
+
*/
|
|
62
|
+
export function balloonSide(place, viewport = {
|
|
63
|
+
width: typeof window === 'undefined' ? 1280 : window.innerWidth,
|
|
64
|
+
height: typeof window === 'undefined' ? 800 : window.innerHeight,
|
|
65
|
+
}) {
|
|
66
|
+
const left = typeof place.left === 'number' ? place.left : undefined;
|
|
67
|
+
const top = typeof place.top === 'number' ? place.top : undefined;
|
|
68
|
+
const align = left !== undefined
|
|
69
|
+
? left < viewport.width / 2
|
|
70
|
+
? 'left'
|
|
71
|
+
: 'right'
|
|
72
|
+
: place.left !== undefined && place.right === undefined
|
|
73
|
+
? 'left'
|
|
74
|
+
: 'right';
|
|
75
|
+
const side = top !== undefined
|
|
76
|
+
? top < BALLOON_ROOM
|
|
77
|
+
? 'below'
|
|
78
|
+
: 'above'
|
|
79
|
+
: place.top !== undefined && place.bottom === undefined
|
|
80
|
+
? 'below'
|
|
81
|
+
: 'above';
|
|
82
|
+
return { side, align };
|
|
83
|
+
}
|
|
84
|
+
export function AssistantStage({ character, state, size = 88, place, stageRef, onDragStart, open, onToggle, balloon, insist = false, onDismiss, }) {
|
|
85
|
+
// A shipped one by id, a drawing contributed by a plugin (T-24), or a
|
|
86
|
+
// character read from a file (T-26).
|
|
87
|
+
const shipped = typeof character === 'string'
|
|
88
|
+
? assistantCharacter(character)
|
|
89
|
+
: 'Drawing' in character
|
|
90
|
+
? character
|
|
91
|
+
: undefined;
|
|
92
|
+
const name = shipped
|
|
93
|
+
? shipped.name
|
|
94
|
+
: character.name;
|
|
95
|
+
const [hovered, setHovered] = useState(false);
|
|
96
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
97
|
+
// Where the press began: a press that moves is a drag, not a click.
|
|
98
|
+
const pressedAt = useRef(null);
|
|
99
|
+
const showBalloon = !open && !!balloon && (hovered || insist);
|
|
100
|
+
return (_jsxs(Box, { ref: stageRef, "data-assistant-state": state, sx: {
|
|
101
|
+
position: 'fixed',
|
|
102
|
+
zIndex: 1002,
|
|
103
|
+
...place,
|
|
104
|
+
width: size,
|
|
105
|
+
height: size,
|
|
106
|
+
...MOTIONS,
|
|
107
|
+
'& .assistant-lids ellipse': {
|
|
108
|
+
transformBox: 'fill-box',
|
|
109
|
+
transformOrigin: 'top',
|
|
110
|
+
transform: 'scaleY(0)',
|
|
111
|
+
animation: 'assistantBlink 5.2s ease-in-out infinite',
|
|
112
|
+
},
|
|
113
|
+
[BODY]: {
|
|
114
|
+
transformBox: 'fill-box',
|
|
115
|
+
transformOrigin: '50% 100%',
|
|
116
|
+
},
|
|
117
|
+
'& .assistant-pupils': {
|
|
118
|
+
transition: 'transform var(--theme-motion-status, 120ms) var(--theme-motion-easing, ease)',
|
|
119
|
+
},
|
|
120
|
+
'& .assistant-mouth': {
|
|
121
|
+
transformBox: 'fill-box',
|
|
122
|
+
transformOrigin: 'center',
|
|
123
|
+
},
|
|
124
|
+
'&[data-assistant-state="idle"] .assistant-body': {
|
|
125
|
+
animation: 'assistantFloat 3.2s ease-in-out infinite',
|
|
126
|
+
},
|
|
127
|
+
'&[data-assistant-state="thinking"] .assistant-body': {
|
|
128
|
+
animation: 'assistantTilt 2.4s ease-in-out infinite',
|
|
129
|
+
},
|
|
130
|
+
'&[data-assistant-state="thinking"] .assistant-pupils': {
|
|
131
|
+
transform: 'translate(-1px, -3px)',
|
|
132
|
+
},
|
|
133
|
+
'&[data-assistant-state="working"] .assistant-body': {
|
|
134
|
+
animation: 'assistantWork 0.7s ease-in-out infinite',
|
|
135
|
+
},
|
|
136
|
+
'&[data-assistant-state="working"] .assistant-pupils': {
|
|
137
|
+
transform: 'translate(2px, 2px)',
|
|
138
|
+
},
|
|
139
|
+
'&[data-assistant-state="waiting"] .assistant-body': {
|
|
140
|
+
animation: 'assistantAttention 1.4s ease-in-out infinite',
|
|
141
|
+
},
|
|
142
|
+
'&[data-assistant-state="greeting"] .assistant-body': {
|
|
143
|
+
animation: 'assistantHop 0.9s ease-out 2',
|
|
144
|
+
},
|
|
145
|
+
'&[data-assistant-state="speaking"] .assistant-mouth': {
|
|
146
|
+
animation: 'assistantTalk 0.24s ease-in-out infinite',
|
|
147
|
+
},
|
|
148
|
+
'&[data-assistant-state="speaking"] .assistant-body': {
|
|
149
|
+
animation: 'assistantFloat 1.6s ease-in-out infinite',
|
|
150
|
+
},
|
|
151
|
+
'&[data-assistant-state="goodbye"] .assistant-body': {
|
|
152
|
+
animation: 'assistantLeave 0.6s ease-in forwards',
|
|
153
|
+
},
|
|
154
|
+
// One still frame per state for a reader who asks for no motion.
|
|
155
|
+
'@media (prefers-reduced-motion: reduce)': {
|
|
156
|
+
'& *': { animation: 'none !important' },
|
|
157
|
+
},
|
|
158
|
+
}, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), children: [showBalloon && balloon && (_jsx(SpeechBalloon, { text: balloon.text, more: balloon.more, onOpen: onToggle, above: size + 8, side: balloonSide(place).side, align: balloonSide(place).align, tailAt: size / 2 })), _jsx(Box, { as: "button", type: "button", "aria-label": open ? `Close the conversation with ${name}` : `Talk to ${name}`, "aria-expanded": open, onPointerDown: (event) => {
|
|
159
|
+
pressedAt.current = { x: event.clientX, y: event.clientY };
|
|
160
|
+
onDragStart(event);
|
|
161
|
+
}, onClick: (event) => {
|
|
162
|
+
const from = pressedAt.current;
|
|
163
|
+
pressedAt.current = null;
|
|
164
|
+
if (from &&
|
|
165
|
+
Math.hypot(event.clientX - from.x, event.clientY - from.y) > 4) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
onToggle();
|
|
169
|
+
}, sx: {
|
|
170
|
+
display: 'block',
|
|
171
|
+
width: size,
|
|
172
|
+
height: size,
|
|
173
|
+
p: 0,
|
|
174
|
+
border: 0,
|
|
175
|
+
bg: 'transparent',
|
|
176
|
+
cursor: 'grab',
|
|
177
|
+
touchAction: 'none',
|
|
178
|
+
filter: 'drop-shadow(0 6px 10px rgba(0, 0, 0, 0.18))',
|
|
179
|
+
'&:active': { cursor: 'grabbing' },
|
|
180
|
+
'&:focus-visible': {
|
|
181
|
+
outline: '2px solid',
|
|
182
|
+
outlineColor: 'var(--focus-outlineColor, var(--fgColor-accent))',
|
|
183
|
+
outlineOffset: 2,
|
|
184
|
+
borderRadius: '50%',
|
|
185
|
+
},
|
|
186
|
+
}, children: shipped ? (_jsx(shipped.Drawing, { size: size })) : (_jsx(SpriteCharacter, { character: character, state: state, size: size })) }), (hovered || menuOpen) && state !== 'goodbye' && (_jsx(Box, { sx: { position: 'absolute', top: -6, right: -6 }, children: _jsxs(ActionMenu, { open: menuOpen, onOpenChange: setMenuOpen, children: [_jsx(ActionMenu.Anchor, { children: _jsx(IconButton, { icon: XIcon, "aria-label": `Send ${name} away`, size: "small", variant: "invisible", "data-assistant-dismiss": "", sx: {
|
|
187
|
+
bg: 'canvas.default',
|
|
188
|
+
borderRadius: '50%',
|
|
189
|
+
boxShadow: 'shadow.small',
|
|
190
|
+
} }) }), _jsx(ActionMenu.Overlay, { width: "auto", children: _jsx(ActionList, { children: AWAY_CHOICES.map(choice => (_jsx(ActionList.Item, { "data-assistant-away": choice.away, onSelect: () => onDismiss(choice.away), children: choice.label }, choice.away))) }) })] }) }))] }));
|
|
191
|
+
}
|
|
192
|
+
export default AssistantStage;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A speech balloon over something that stands for the agent — the floating
|
|
3
|
+
* assistant's character, or the floating popup's button while the chat is
|
|
4
|
+
* closed: the agent's newest words, as the Office Assistant said them
|
|
5
|
+
* (LOOP T-23), with *Open the conversation* when there is more.
|
|
6
|
+
*
|
|
7
|
+
* @module chat/assistant/SpeechBalloon
|
|
8
|
+
*/
|
|
9
|
+
import type { JSX } from 'react';
|
|
10
|
+
export interface SpeechBalloonProps {
|
|
11
|
+
text: string;
|
|
12
|
+
/** There is more than the balloon holds. */
|
|
13
|
+
more?: boolean;
|
|
14
|
+
/** Opens the conversation. */
|
|
15
|
+
onOpen: () => void;
|
|
16
|
+
/** How far from what it stands over, in pixels: above it, or below. */
|
|
17
|
+
above: number;
|
|
18
|
+
/** Above what speaks (the default), or below it when it sits at the top. */
|
|
19
|
+
side?: 'above' | 'below';
|
|
20
|
+
/** Which edge it is aligned to, toward the page's inside. */
|
|
21
|
+
align: 'left' | 'right';
|
|
22
|
+
/** Where the tail points, from that edge, in pixels. */
|
|
23
|
+
tailAt: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function SpeechBalloon({ text, more, onOpen, above, side, align, tailAt, }: SpeechBalloonProps): JSX.Element;
|
|
26
|
+
export default SpeechBalloon;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Text } from '@primer/react';
|
|
3
|
+
import { Box } from '@datalayer/primer-addons';
|
|
4
|
+
export function SpeechBalloon({ text, more = false, onOpen, above, side = 'above', align, tailAt, }) {
|
|
5
|
+
return (_jsxs(Box, { role: "status", "aria-live": "polite", "data-speech-balloon": "", sx: {
|
|
6
|
+
position: 'absolute',
|
|
7
|
+
...(side === 'above' ? { bottom: above } : { top: above }),
|
|
8
|
+
[align]: 0,
|
|
9
|
+
maxWidth: 280,
|
|
10
|
+
width: 'max-content',
|
|
11
|
+
px: 3,
|
|
12
|
+
py: 2,
|
|
13
|
+
bg: 'canvas.default',
|
|
14
|
+
color: 'fg.default',
|
|
15
|
+
border: '1px solid',
|
|
16
|
+
borderColor: 'border.default',
|
|
17
|
+
borderRadius: 'var(--theme-radius-bubble, 16px)',
|
|
18
|
+
boxShadow: 'shadow.medium',
|
|
19
|
+
fontSize: 1,
|
|
20
|
+
textAlign: 'left',
|
|
21
|
+
// The tail, toward what speaks.
|
|
22
|
+
'&::after': {
|
|
23
|
+
content: '""',
|
|
24
|
+
position: 'absolute',
|
|
25
|
+
...(side === 'above' ? { bottom: -7 } : { top: -7 }),
|
|
26
|
+
[align]: tailAt - 6,
|
|
27
|
+
width: 12,
|
|
28
|
+
height: 12,
|
|
29
|
+
bg: 'canvas.default',
|
|
30
|
+
borderRight: '1px solid',
|
|
31
|
+
borderBottom: '1px solid',
|
|
32
|
+
borderColor: 'border.default',
|
|
33
|
+
transform: side === 'above' ? 'rotate(45deg)' : 'rotate(225deg)',
|
|
34
|
+
},
|
|
35
|
+
}, children: [_jsx(Text, { as: "p", sx: { m: 0 }, children: text }), more && (_jsx(Box, { as: "button", type: "button", onClick: onOpen, sx: {
|
|
36
|
+
mt: 1,
|
|
37
|
+
p: 0,
|
|
38
|
+
border: 0,
|
|
39
|
+
bg: 'transparent',
|
|
40
|
+
color: 'accent.fg',
|
|
41
|
+
textDecoration: 'underline',
|
|
42
|
+
cursor: 'pointer',
|
|
43
|
+
fontSize: 1,
|
|
44
|
+
}, children: "Open the conversation" }))] }));
|
|
45
|
+
}
|
|
46
|
+
export default SpeechBalloon;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A character one brings, played (LOOP T-26): the frames of a character file
|
|
3
|
+
* read by `formats` — clippy.js or Microsoft Agent — drawn from its sprite
|
|
4
|
+
* sheet and stepped as the character authored them, its branches included,
|
|
5
|
+
* the animation chosen by the state the stage acts (T-22). Nothing plays for
|
|
6
|
+
* a reader who asks for reduced motion: the first frame of the state stands.
|
|
7
|
+
*
|
|
8
|
+
* @module chat/assistant/SpriteCharacter
|
|
9
|
+
*/
|
|
10
|
+
import type { JSX } from 'react';
|
|
11
|
+
import type { AssistantCharacterAnimation, AssistantCharacterData } from './formats/types';
|
|
12
|
+
import type { AssistantState } from './state';
|
|
13
|
+
/**
|
|
14
|
+
* The frame after `index`: a branch, when the frame has some and the draw
|
|
15
|
+
* falls in one (weights are percents; what is left over goes on), else the
|
|
16
|
+
* next frame; `undefined` at the end of the animation.
|
|
17
|
+
*/
|
|
18
|
+
export declare function nextFrameIndex(animation: AssistantCharacterAnimation, index: number, draw?: number): number | undefined;
|
|
19
|
+
/** One of the animations offered for a state, drawn at random. */
|
|
20
|
+
export declare function animationFor(character: AssistantCharacterData, state: AssistantState, draw?: number): string;
|
|
21
|
+
export declare function SpriteCharacter({ character, state, size, }: {
|
|
22
|
+
character: AssistantCharacterData;
|
|
23
|
+
state: AssistantState;
|
|
24
|
+
size: number;
|
|
25
|
+
}): JSX.Element;
|
|
26
|
+
export default SpriteCharacter;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { stateAnimations } from './formats/stateAnimations';
|
|
4
|
+
/**
|
|
5
|
+
* The frame after `index`: a branch, when the frame has some and the draw
|
|
6
|
+
* falls in one (weights are percents; what is left over goes on), else the
|
|
7
|
+
* next frame; `undefined` at the end of the animation.
|
|
8
|
+
*/
|
|
9
|
+
export function nextFrameIndex(animation, index, draw = Math.random()) {
|
|
10
|
+
const frame = animation.frames[index];
|
|
11
|
+
let at = draw * 100;
|
|
12
|
+
for (const branch of frame?.branching ?? []) {
|
|
13
|
+
if (at < branch.weight) {
|
|
14
|
+
return branch.frameIndex;
|
|
15
|
+
}
|
|
16
|
+
at -= branch.weight;
|
|
17
|
+
}
|
|
18
|
+
return index + 1 < animation.frames.length ? index + 1 : undefined;
|
|
19
|
+
}
|
|
20
|
+
/** One of the animations offered for a state, drawn at random. */
|
|
21
|
+
export function animationFor(character, state, draw = Math.random()) {
|
|
22
|
+
const names = stateAnimations(character)[state];
|
|
23
|
+
return names[Math.min(names.length - 1, Math.floor(draw * names.length))];
|
|
24
|
+
}
|
|
25
|
+
const reducedMotion = () => typeof window !== 'undefined' &&
|
|
26
|
+
!!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
27
|
+
export function SpriteCharacter({ character, state, size, }) {
|
|
28
|
+
const [play, setPlay] = useState(() => ({
|
|
29
|
+
name: animationFor(character, state),
|
|
30
|
+
index: 0,
|
|
31
|
+
}));
|
|
32
|
+
// A new state starts one of its own animations from the first frame.
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
setPlay({ name: animationFor(character, state), index: 0 });
|
|
35
|
+
}, [character, state]);
|
|
36
|
+
const animation = character.animations[play.name];
|
|
37
|
+
const frame = animation?.frames[play.index];
|
|
38
|
+
const still = useMemo(reducedMotion, []);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!animation || !frame || still) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
const next = nextFrameIndex(animation, play.index);
|
|
45
|
+
// At its end an animation gives way to another of the same state.
|
|
46
|
+
setPlay(next === undefined
|
|
47
|
+
? { name: animationFor(character, state), index: 0 }
|
|
48
|
+
: { name: play.name, index: next });
|
|
49
|
+
}, Math.max(frame.duration, 16));
|
|
50
|
+
return () => clearTimeout(timer);
|
|
51
|
+
}, [animation, frame, play, character, state, still]);
|
|
52
|
+
const { width, height } = character.frameSize;
|
|
53
|
+
const scale = size / Math.max(width, height);
|
|
54
|
+
return (_jsx("span", { role: "img", "aria-label": character.name, "data-sprite-animation": play.name, style: {
|
|
55
|
+
position: 'relative',
|
|
56
|
+
display: 'block',
|
|
57
|
+
width: size,
|
|
58
|
+
height: size,
|
|
59
|
+
}, children: (frame?.images ?? []).map((image, layer) => (_jsx("span", { "aria-hidden": true, style: {
|
|
60
|
+
position: 'absolute',
|
|
61
|
+
left: (size - width * scale) / 2,
|
|
62
|
+
top: (size - height * scale) / 2,
|
|
63
|
+
width,
|
|
64
|
+
height,
|
|
65
|
+
transform: `scale(${scale})`,
|
|
66
|
+
transformOrigin: 'top left',
|
|
67
|
+
backgroundImage: `url(${character.sprite})`,
|
|
68
|
+
backgroundPosition: `-${image.x}px -${image.y}px`,
|
|
69
|
+
backgroundRepeat: 'no-repeat',
|
|
70
|
+
imageRendering: 'pixelated',
|
|
71
|
+
} }, layer))) }));
|
|
72
|
+
}
|
|
73
|
+
export default SpriteCharacter;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Datalayer's own characters for the floating assistant (LOOP T-25): drawn
|
|
3
|
+
* for LOOP and owned by Datalayer, in the spirit of the Office Assistant —
|
|
4
|
+
* a paper clip of our own, a wizard, a cat and the L👀P eyes. The characters
|
|
5
|
+
* of Office are Microsoft's and are never bundled (§6.9); a person loads
|
|
6
|
+
* one they hold the rights to (T-26).
|
|
7
|
+
*
|
|
8
|
+
* Each drawing names its moving parts with classes the stage animates by
|
|
9
|
+
* state — `assistant-body`, `assistant-pupils`, `assistant-lids`,
|
|
10
|
+
* `assistant-mouth` — so one set of motions serves every character.
|
|
11
|
+
*
|
|
12
|
+
* @module chat/assistant/characters
|
|
13
|
+
*/
|
|
14
|
+
import type { JSX } from 'react';
|
|
15
|
+
/** A character Datalayer ships. */
|
|
16
|
+
export interface AssistantCharacter {
|
|
17
|
+
id: string;
|
|
18
|
+
/** Its name, as the picker shows it. */
|
|
19
|
+
name: string;
|
|
20
|
+
/** The drawing, at the size asked; its parts animate by class. */
|
|
21
|
+
Drawing: (props: {
|
|
22
|
+
size: number;
|
|
23
|
+
}) => JSX.Element;
|
|
24
|
+
}
|
|
25
|
+
/** The characters Datalayer ships, in the order a picker shows them. */
|
|
26
|
+
export declare const ASSISTANT_CHARACTERS: readonly AssistantCharacter[];
|
|
27
|
+
/** The character an assistant shows when none is chosen. */
|
|
28
|
+
export declare const DEFAULT_ASSISTANT_CHARACTER = "paperclip";
|
|
29
|
+
/** A shipped character by id; an unknown id is an error, said plainly. */
|
|
30
|
+
export declare function assistantCharacter(id: string): AssistantCharacter;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
const INK = '#1D1D1F';
|
|
3
|
+
/** Two eyes with lids that blink and pupils that look about. */
|
|
4
|
+
function Eyes({ cx, cy, gap, r, }) {
|
|
5
|
+
const left = cx - gap / 2;
|
|
6
|
+
const right = cx + gap / 2;
|
|
7
|
+
return (_jsxs("g", { children: [_jsx("circle", { cx: left, cy: cy, r: r, fill: "#FFFFFF", stroke: INK, strokeWidth: 1.5 }), _jsx("circle", { cx: right, cy: cy, r: r, fill: "#FFFFFF", stroke: INK, strokeWidth: 1.5 }), _jsxs("g", { className: "assistant-pupils", children: [_jsx("circle", { cx: left + r * 0.15, cy: cy + r * 0.1, r: r * 0.45, fill: INK }), _jsx("circle", { cx: right + r * 0.15, cy: cy + r * 0.1, r: r * 0.45, fill: INK }), _jsx("circle", { cx: left + r * 0.3, cy: cy - r * 0.1, r: r * 0.14, fill: "#FFFFFF" }), _jsx("circle", { cx: right + r * 0.3, cy: cy - r * 0.1, r: r * 0.14, fill: "#FFFFFF" })] }), _jsxs("g", { className: "assistant-lids", children: [_jsx("ellipse", { cx: left, cy: cy, rx: r + 0.5, ry: r + 0.5, fill: "currentColor" }), _jsx("ellipse", { cx: right, cy: cy, rx: r + 0.5, ry: r + 0.5, fill: "currentColor" })] })] }));
|
|
8
|
+
}
|
|
9
|
+
/** A paper clip of our own: a bent wire with eyes on its upper loop. */
|
|
10
|
+
function PaperClip({ size }) {
|
|
11
|
+
return (_jsx("svg", { width: size, height: size, viewBox: "0 0 96 96", role: "img", "aria-label": "Paper clip", style: { color: '#B8BDC6' }, children: _jsxs("g", { className: "assistant-body", children: [_jsx("path", { d: "M40 86 L40 30 Q40 14 52 14 Q64 14 64 30 L64 74 Q64 82 56 82 Q48 82 48 74 L48 36", fill: "none", stroke: "#9AA0AA", strokeWidth: 6, strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M40 86 L40 30 Q40 14 52 14 Q64 14 64 30 L64 74 Q64 82 56 82 Q48 82 48 74 L48 36", fill: "none", stroke: "#E3E6EB", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" }), _jsx("path", { d: "M36 22 Q42 18 47 21", stroke: INK, strokeWidth: 2, fill: "none", strokeLinecap: "round" }), _jsx("path", { d: "M57 21 Q62 18 68 22", stroke: INK, strokeWidth: 2, fill: "none", strokeLinecap: "round" }), _jsx(Eyes, { cx: 52, cy: 30, gap: 16, r: 6.5 }), _jsx("path", { className: "assistant-mouth", d: "M48 44 Q52 47 56 44", stroke: INK, strokeWidth: 2, fill: "none", strokeLinecap: "round" })] }) }));
|
|
12
|
+
}
|
|
13
|
+
/** A wizard: a starred hat, a white beard, kind eyes. */
|
|
14
|
+
function Wizard({ size }) {
|
|
15
|
+
return (_jsx("svg", { width: size, height: size, viewBox: "0 0 96 96", role: "img", "aria-label": "Wizard", style: { color: '#F2D3B3' }, children: _jsxs("g", { className: "assistant-body", children: [_jsx("path", { d: "M24 92 Q48 58 72 92 Z", fill: "#5B4BB7" }), _jsx("circle", { cx: 48, cy: 48, r: 17, fill: "#F2D3B3" }), _jsx("path", { d: "M31 50 Q48 92 65 50 Q57 60 48 60 Q39 60 31 50 Z", fill: "#F4F4F6", stroke: "#D9DAE0", strokeWidth: 1 }), _jsx("path", { d: "M22 36 L48 2 L74 36 Q48 30 22 36 Z", fill: "#6E5CD6" }), _jsx("path", { d: "M20 37 Q48 29 76 37 Q48 43 20 37 Z", fill: "#4A3C9E" }), _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: "#F8D469" }), _jsx("circle", { cx: 40, cy: 26, r: 1.8, fill: "#F8D469" }), _jsx(Eyes, { cx: 48, cy: 46, gap: 13, r: 5 }), _jsx("path", { className: "assistant-mouth", d: "M44 58 Q48 61 52 58", stroke: INK, strokeWidth: 2, fill: "none", strokeLinecap: "round" })] }) }));
|
|
16
|
+
}
|
|
17
|
+
/** A cat: round, two ears, whiskers, a tail. */
|
|
18
|
+
function Cat({ size }) {
|
|
19
|
+
return (_jsx("svg", { width: size, height: size, viewBox: "0 0 96 96", role: "img", "aria-label": "Cat", style: { color: '#F4A261' }, children: _jsxs("g", { className: "assistant-body", children: [_jsx("path", { d: "M70 84 Q90 78 84 58", stroke: "#E08A45", strokeWidth: 6, fill: "none", strokeLinecap: "round" }), _jsx("ellipse", { cx: 48, cy: 78, rx: 24, ry: 14, fill: "#F4A261" }), _jsx("path", { d: "M24 30 L28 8 L42 22 Z", fill: "#F4A261" }), _jsx("path", { d: "M72 30 L68 8 L54 22 Z", fill: "#F4A261" }), _jsx("path", { d: "M28 26 L30 14 L38 22 Z", fill: "#F6A5C1" }), _jsx("path", { d: "M68 26 L66 14 L58 22 Z", fill: "#F6A5C1" }), _jsx("circle", { cx: 48, cy: 42, r: 24, fill: "#F4A261" }), _jsx(Eyes, { cx: 48, cy: 38, gap: 18, r: 6.5 }), _jsx("path", { d: "M45 48 L51 48 L48 52 Z", fill: "#B83A6B" }), _jsx("path", { className: "assistant-mouth", d: "M43 55 Q48 58 53 55", stroke: INK, strokeWidth: 2, fill: "none", strokeLinecap: "round" }), _jsx("path", { d: "M30 48 L18 46 M30 52 L18 54 M66 48 L78 46 M66 52 L78 54", stroke: INK, strokeWidth: 1.2, strokeLinecap: "round" })] }) }));
|
|
20
|
+
}
|
|
21
|
+
/** The L👀P eyes: the product's own face, two eyes and nothing else. */
|
|
22
|
+
function LoopEyes({ size }) {
|
|
23
|
+
return (_jsx("svg", { width: size, height: size, viewBox: "0 0 96 96", role: "img", "aria-label": "L\uD83D\uDC40P eyes", style: { color: '#FFFFFF' }, children: _jsxs("g", { className: "assistant-body", children: [_jsx(Eyes, { cx: 48, cy: 50, gap: 40, r: 18 }), _jsx("path", { className: "assistant-mouth", d: "M42 80 Q48 84 54 80", stroke: INK, strokeWidth: 2.5, fill: "none", strokeLinecap: "round", opacity: 0 })] }) }));
|
|
24
|
+
}
|
|
25
|
+
/** The characters Datalayer ships, in the order a picker shows them. */
|
|
26
|
+
export const ASSISTANT_CHARACTERS = [
|
|
27
|
+
{ id: 'paperclip', name: 'Paper clip', Drawing: PaperClip },
|
|
28
|
+
{ id: 'wizard', name: 'Wizard', Drawing: Wizard },
|
|
29
|
+
{ id: 'cat', name: 'Cat', Drawing: Cat },
|
|
30
|
+
{ id: 'eyes', name: 'L👀P eyes', Drawing: LoopEyes },
|
|
31
|
+
];
|
|
32
|
+
/** The character an assistant shows when none is chosen. */
|
|
33
|
+
export const DEFAULT_ASSISTANT_CHARACTER = 'paperclip';
|
|
34
|
+
/** A shipped character by id; an unknown id is an error, said plainly. */
|
|
35
|
+
export function assistantCharacter(id) {
|
|
36
|
+
const found = ASSISTANT_CHARACTERS.find(character => character.id === id);
|
|
37
|
+
if (!found) {
|
|
38
|
+
throw new Error(`There is no assistant character "${id}"; the characters are ${ASSISTANT_CHARACTERS.map(c => c.id).join(', ')}.`);
|
|
39
|
+
}
|
|
40
|
+
return found;
|
|
41
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type AssistantCharacterData } from './types';
|
|
2
|
+
export declare const ACS_SIGNATURE = 2882382787;
|
|
3
|
+
export declare const ACF_SIGNATURE = 2882382788;
|
|
4
|
+
export interface AcsLocator {
|
|
5
|
+
offset: number;
|
|
6
|
+
size: number;
|
|
7
|
+
}
|
|
8
|
+
export interface AcsFrameImage {
|
|
9
|
+
image: number;
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
}
|
|
13
|
+
export interface AcsFrame {
|
|
14
|
+
/** Images, as stored: the first is drawn on top. */
|
|
15
|
+
images: AcsFrameImage[];
|
|
16
|
+
/** Index into the audio list, or -1. */
|
|
17
|
+
sound: number;
|
|
18
|
+
/** In 1/100 s. */
|
|
19
|
+
duration: number;
|
|
20
|
+
/** Frame index, or a negative value for none. */
|
|
21
|
+
exitBranch: number;
|
|
22
|
+
branches: {
|
|
23
|
+
frameIndex: number;
|
|
24
|
+
probability: number;
|
|
25
|
+
}[];
|
|
26
|
+
}
|
|
27
|
+
export interface AcsAnimation {
|
|
28
|
+
name: string;
|
|
29
|
+
transition: number;
|
|
30
|
+
returnAnimation: string;
|
|
31
|
+
frames: AcsFrame[];
|
|
32
|
+
}
|
|
33
|
+
export interface AcsImageEntry {
|
|
34
|
+
locator: AcsLocator;
|
|
35
|
+
}
|
|
36
|
+
/** An `.acs` read into its tables, the images not yet decoded. */
|
|
37
|
+
export interface AcsFile {
|
|
38
|
+
version: {
|
|
39
|
+
major: number;
|
|
40
|
+
minor: number;
|
|
41
|
+
};
|
|
42
|
+
name: string;
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
transparentIndex: number;
|
|
46
|
+
/** RGBA, one entry per palette index. */
|
|
47
|
+
palette: Uint32Array;
|
|
48
|
+
states: Record<string, string[]>;
|
|
49
|
+
animations: AcsAnimation[];
|
|
50
|
+
images: AcsImageEntry[];
|
|
51
|
+
sounds: AcsLocator[];
|
|
52
|
+
bytes: Uint8Array;
|
|
53
|
+
}
|
|
54
|
+
/** A decoded image: palette indices, top row first, no padding. */
|
|
55
|
+
export interface AcsImage {
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
pixels: Uint8Array;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Read the tables of an `.acs`: the character, its animations and frames,
|
|
62
|
+
* where its images and sounds are. Images are decoded by `decodeAcsImage`.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseAcs(buffer: ArrayBuffer): AcsFile;
|
|
65
|
+
/** Decode one image of an `.acs` into palette indices, top row first. */
|
|
66
|
+
export declare function decodeAcsImage(file: AcsFile, index: number): AcsImage;
|
|
67
|
+
/** One cell of the sprite sheet: the frames that look the same share it. */
|
|
68
|
+
export interface AcsSpriteLayout {
|
|
69
|
+
columns: number;
|
|
70
|
+
rows: number;
|
|
71
|
+
/** The images of each cell, as stored in the frame (first on top). */
|
|
72
|
+
cells: AcsFrameImage[][];
|
|
73
|
+
/** For each animation, for each frame, its cell, or -1 for an empty frame. */
|
|
74
|
+
frameCells: number[][];
|
|
75
|
+
}
|
|
76
|
+
/** Assign a cell to each distinct, non-empty frame. */
|
|
77
|
+
export declare function layoutAcsSprite(file: AcsFile): AcsSpriteLayout;
|
|
78
|
+
/**
|
|
79
|
+
* Composite a cell as Agent draws a frame: its images from last to first,
|
|
80
|
+
* each at its offset, the transparent colour left clear. RGBA, frame-sized.
|
|
81
|
+
*/
|
|
82
|
+
export declare function composeAcsCell(file: AcsFile, images: AcsFrameImage[], decode?: (index: number) => AcsImage): Uint8ClampedArray;
|
|
83
|
+
/**
|
|
84
|
+
* Read a Microsoft Agent character (`.acs`) a person picked: its animations,
|
|
85
|
+
* a sprite sheet drawn in the page (an object URL), its sounds as object
|
|
86
|
+
* URLs. Nothing leaves the page.
|
|
87
|
+
*/
|
|
88
|
+
export declare function readAcsCharacter(buffer: ArrayBuffer): Promise<AssistantCharacterData>;
|