@datalayer/agent-runtimes 1.3.57 → 1.3.59
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/marks/SpecMark.d.ts +2 -0
- package/lib/chat/marks/SpecMark.js +3 -1
- package/lib/examples/ChatAssistantGalleryExample.d.ts +12 -0
- package/lib/examples/ChatAssistantGalleryExample.js +181 -0
- package/lib/examples/example-selector.js +1 -0
- package/lib/examples/index.d.ts +1 -0
- package/lib/examples/index.js +1 -0
- package/lib/examples/main.js +2 -0
- package/lib/examples/utils/AssistantGalleryGrid.d.ts +53 -0
- package/lib/examples/utils/AssistantGalleryGrid.js +123 -0
- package/lib/examples/utils/assistantGallery.d.ts +42 -0
- package/lib/examples/utils/assistantGallery.js +102 -0
- package/lib/examples/utils/testSpriteCharacter.d.ts +9 -0
- package/lib/examples/utils/testSpriteCharacter.js +60 -0
- package/lib/specs/actions.js +2 -0
- package/lib/specs/apps.d.ts +1 -0
- package/lib/specs/apps.js +178 -0
- package/lib/stories/loop/LoopReference.stories.d.ts +2 -0
- package/lib/stories/loop/LoopReference.stories.js +3 -1
- package/lib/stories/loop/ReferenceScreens.d.ts +4 -2
- package/lib/stories/loop/ReferenceScreens.js +27 -3
- package/lib/stories/loop/pictures-main.js +16 -1
- package/package.json +1 -1
|
@@ -15,6 +15,8 @@ import { type IconPackage, type IconRef } from './iconRef';
|
|
|
15
15
|
export type MarkIcon = React.ComponentType<{
|
|
16
16
|
size?: number;
|
|
17
17
|
'aria-hidden'?: boolean | 'true' | 'false';
|
|
18
|
+
/** The Datalayer icons' own colours: a brand drawn as itself. */
|
|
19
|
+
colored?: boolean;
|
|
18
20
|
}>;
|
|
19
21
|
type IconModule = Record<string, unknown>;
|
|
20
22
|
/** Import a package of icons, once. */
|
|
@@ -71,7 +71,9 @@ export function useMarkIcon(icon) {
|
|
|
71
71
|
export function SpecMark({ icon, emoji, size = 16 }) {
|
|
72
72
|
const Icon = useMarkIcon(icon);
|
|
73
73
|
if (icon) {
|
|
74
|
-
return Icon ? (_jsx("span", { "data-mark": icon, style: { display: 'inline-flex', flexShrink: 0, lineHeight: 0 }, children: _jsx(Icon, { size: size, "aria-hidden": "true"
|
|
74
|
+
return Icon ? (_jsx("span", { "data-mark": icon, style: { display: 'inline-flex', flexShrink: 0, lineHeight: 0 }, children: _jsx(Icon, { size: size, "aria-hidden": "true", ...(icon.startsWith('@datalayer/icons-react:')
|
|
75
|
+
? { colored: true }
|
|
76
|
+
: {}) }) })) : (_jsx("span", { "data-mark": icon, "aria-hidden": "true", style: { display: 'inline-block', width: size, height: size } }));
|
|
75
77
|
}
|
|
76
78
|
if (emoji) {
|
|
77
79
|
return (_jsx("span", { "data-mark": "emoji", "aria-hidden": "true", style: {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every representation of the floating assistant, as a gallery (LOOP T-21
|
|
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
|
|
6
|
+
* balloon, in light and dark, moving or still.
|
|
7
|
+
*
|
|
8
|
+
* Static data, no agent: it opens without a server or an account.
|
|
9
|
+
*/
|
|
10
|
+
import React from 'react';
|
|
11
|
+
declare const ChatAssistantGalleryExample: React.FC;
|
|
12
|
+
export default ChatAssistantGalleryExample;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
|
+
* Distributed under the terms of the Modified BSD License.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Every representation of the floating assistant, as a gallery (LOOP T-21
|
|
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
|
|
11
|
+
* balloon, in light and dark, moving or still.
|
|
12
|
+
*
|
|
13
|
+
* Static data, no agent: it opens without a server or an account.
|
|
14
|
+
*/
|
|
15
|
+
import { useEffect, useRef, useState } from 'react';
|
|
16
|
+
import { Button, Checkbox, FormControl, Heading, SegmentedControl, Text, } from '@primer/react';
|
|
17
|
+
import { Box, DatalayerThemeProvider, themeConfigs, } from '@datalayer/primer-addons';
|
|
18
|
+
import { ThemedProvider } from './utils/themedProvider';
|
|
19
|
+
import { useExampleThemeStore } from './utils/themeStore';
|
|
20
|
+
import { AssistantStage } from '../chat/assistant/AssistantStage';
|
|
21
|
+
import { useViewportDrag } from '../chat/useViewportDrag';
|
|
22
|
+
import { AssistantGalleryGrid, GalleryObstacle, Still, useGalleryCharacters, } from './utils/AssistantGalleryGrid';
|
|
23
|
+
import { GALLERY_POSES, GALLERY_POSE_LABELS, balloonForPose, sampleApproval, sampleSaying, stateOfPose, } from './utils/assistantGallery';
|
|
24
|
+
const STAGE_SIZE = 96;
|
|
25
|
+
const PANEL_HEIGHT = 420;
|
|
26
|
+
const LEAVE_MS = 700;
|
|
27
|
+
const ARRIVE_MS = 1900;
|
|
28
|
+
const AWAY_WORDS = {
|
|
29
|
+
none: '',
|
|
30
|
+
page: 'for now',
|
|
31
|
+
session: 'for this session',
|
|
32
|
+
always: 'for good',
|
|
33
|
+
};
|
|
34
|
+
/** One colour mode's panel, with the character on its own stage. */
|
|
35
|
+
function ModePanel({ mode, character, pose, presence, balloon, insist, onToggle, onDismiss, onCallBack, away, replay, }) {
|
|
36
|
+
const panelRef = useRef(null);
|
|
37
|
+
const stageRef = useRef(null);
|
|
38
|
+
const drag = useViewportDrag(stageRef, { whole: true });
|
|
39
|
+
// Where it stands in the panel: the drag's viewport place, made the
|
|
40
|
+
// panel's own once, so a scroll does not move it.
|
|
41
|
+
const [spot, setSpot] = useState({ left: 48, top: PANEL_HEIGHT - 150 });
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const panel = panelRef.current;
|
|
44
|
+
if (drag.position && panel) {
|
|
45
|
+
const box = panel.getBoundingClientRect();
|
|
46
|
+
setSpot({
|
|
47
|
+
left: drag.position.left - box.left,
|
|
48
|
+
top: drag.position.top - box.top,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}, [drag.position]);
|
|
52
|
+
const state = presence === 'leaving'
|
|
53
|
+
? 'goodbye'
|
|
54
|
+
: presence === 'arriving'
|
|
55
|
+
? 'greeting'
|
|
56
|
+
: stateOfPose(pose);
|
|
57
|
+
return (_jsxs(Box, { ref: panelRef, "data-gallery-mode": mode, sx: {
|
|
58
|
+
position: 'relative',
|
|
59
|
+
flex: '1 1 320px',
|
|
60
|
+
minWidth: 0,
|
|
61
|
+
height: PANEL_HEIGHT,
|
|
62
|
+
bg: 'canvas.default',
|
|
63
|
+
color: 'fg.default',
|
|
64
|
+
border: '1px solid',
|
|
65
|
+
borderColor: 'border.default',
|
|
66
|
+
borderRadius: 2,
|
|
67
|
+
overflow: 'visible',
|
|
68
|
+
}, children: [_jsx(Text, { sx: {
|
|
69
|
+
position: 'absolute',
|
|
70
|
+
top: 2,
|
|
71
|
+
left: 3,
|
|
72
|
+
fontSize: 0,
|
|
73
|
+
fontWeight: 600,
|
|
74
|
+
color: 'fg.muted',
|
|
75
|
+
textTransform: 'uppercase',
|
|
76
|
+
letterSpacing: '0.04em',
|
|
77
|
+
}, children: mode }), presence === 'away' ? (_jsxs(Box, { sx: {
|
|
78
|
+
position: 'absolute',
|
|
79
|
+
left: spot.left,
|
|
80
|
+
top: spot.top + STAGE_SIZE / 2 - 16,
|
|
81
|
+
}, children: [_jsxs(Button, { size: "small", onClick: onCallBack, "data-gallery-call-back": "", children: ["Call ", character.name, " back"] }), _jsxs(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', mt: 1 }, children: ["Sent away ", AWAY_WORDS[away], "."] })] })) : (_jsx(AssistantStage, { character: character.character, state: state, size: STAGE_SIZE, place: { position: 'absolute', left: spot.left, top: spot.top }, stageRef: stageRef, onDragStart: drag.onHandlePointerDown, open: false, onToggle: onToggle, balloon: presence === 'here' ? balloon : undefined, insist: insist, onDismiss: onDismiss }, `${character.id}-${replay}`)), pose === 'aside' && presence === 'here' && (_jsx(GalleryObstacle, { sx: {
|
|
82
|
+
left: spot.left - 12,
|
|
83
|
+
top: spot.top - 12,
|
|
84
|
+
width: STAGE_SIZE + 24,
|
|
85
|
+
height: STAGE_SIZE + 24,
|
|
86
|
+
} }))] }));
|
|
87
|
+
}
|
|
88
|
+
/** The panels in the modes asked, each in the examples' theme. */
|
|
89
|
+
function InModes({ modes, stacked = false, children, }) {
|
|
90
|
+
const { theme } = useExampleThemeStore();
|
|
91
|
+
const config = themeConfigs[theme] ?? themeConfigs.loop;
|
|
92
|
+
const shown = modes === 'both' ? ['light', 'dark'] : [modes];
|
|
93
|
+
return (_jsx(Box, { sx: {
|
|
94
|
+
display: 'flex',
|
|
95
|
+
flexDirection: stacked ? 'column' : 'row',
|
|
96
|
+
gap: 3,
|
|
97
|
+
flexWrap: 'wrap',
|
|
98
|
+
}, 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
|
+
}
|
|
100
|
+
const ChatAssistantGalleryExample = () => {
|
|
101
|
+
const { characters, problem } = useGalleryCharacters();
|
|
102
|
+
const [characterId, setCharacterId] = useState('paperclip');
|
|
103
|
+
const [pose, setPose] = useState('idle');
|
|
104
|
+
const [view, setView] = useState('stage');
|
|
105
|
+
const [modes, setModes] = useState('both');
|
|
106
|
+
const [still, setStill] = useState(false);
|
|
107
|
+
const [saying, setSaying] = useState();
|
|
108
|
+
const [said, setSaid] = useState(0);
|
|
109
|
+
const [decision, setDecision] = useState();
|
|
110
|
+
const [presence, setPresence] = useState('here');
|
|
111
|
+
const [away, setAway] = useState('none');
|
|
112
|
+
const [replay, setReplay] = useState(0);
|
|
113
|
+
const character = characters.find(entry => entry.id === characterId) ?? characters[0];
|
|
114
|
+
// Leaving takes the goodbye's length; arriving, the greeting's.
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (presence !== 'leaving' && presence !== 'arriving') {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const timer = window.setTimeout(() => setPresence(presence === 'leaving' ? 'away' : 'here'), presence === 'leaving' ? LEAVE_MS : ARRIVE_MS);
|
|
120
|
+
return () => window.clearTimeout(timer);
|
|
121
|
+
}, [presence]);
|
|
122
|
+
const sendAway = (choice) => {
|
|
123
|
+
setAway(choice);
|
|
124
|
+
setPresence('leaving');
|
|
125
|
+
};
|
|
126
|
+
const callBack = () => {
|
|
127
|
+
setAway('none');
|
|
128
|
+
setPresence('arriving');
|
|
129
|
+
};
|
|
130
|
+
const say = () => {
|
|
131
|
+
setSaying(said);
|
|
132
|
+
setSaid(said + 1);
|
|
133
|
+
};
|
|
134
|
+
const approval = sampleApproval(() => setDecision('Approved: send_reply.'), () => setDecision('Denied: send_reply.'));
|
|
135
|
+
const balloon = balloonForPose(pose, {
|
|
136
|
+
saying: saying === undefined ? undefined : sampleSaying(saying),
|
|
137
|
+
approval,
|
|
138
|
+
});
|
|
139
|
+
const insist = saying !== undefined || pose === 'waiting' || pose === 'paused';
|
|
140
|
+
return (_jsx(ThemedProvider, { children: _jsx(Box, { sx: {
|
|
141
|
+
minHeight: '100vh',
|
|
142
|
+
bg: 'canvas.default',
|
|
143
|
+
color: 'fg.default',
|
|
144
|
+
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: {
|
|
146
|
+
display: 'grid',
|
|
147
|
+
gap: 3,
|
|
148
|
+
mb: 4,
|
|
149
|
+
p: 3,
|
|
150
|
+
border: '1px solid',
|
|
151
|
+
borderColor: 'border.default',
|
|
152
|
+
borderRadius: 2,
|
|
153
|
+
bg: 'canvas.subtle',
|
|
154
|
+
}, children: [_jsxs(Box, { sx: {
|
|
155
|
+
display: 'flex',
|
|
156
|
+
gap: 3,
|
|
157
|
+
flexWrap: 'wrap',
|
|
158
|
+
alignItems: 'center',
|
|
159
|
+
}, children: [_jsxs(SegmentedControl, { "aria-label": "View", children: [_jsx(SegmentedControl.Button, { selected: view === 'stage', onClick: () => setView('stage'), children: "One at a time" }), _jsx(SegmentedControl.Button, { selected: view === 'grid', onClick: () => setView('grid'), "data-gallery-view": "grid", children: "Grid" })] }), _jsx(SegmentedControl, { "aria-label": "Colour mode", children: ['light', 'dark', 'both'].map(option => (_jsx(SegmentedControl.Button, { selected: modes === option, onClick: () => setModes(option), children: option === 'both'
|
|
160
|
+
? 'Side by side'
|
|
161
|
+
: option === 'light'
|
|
162
|
+
? '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: () => {
|
|
164
|
+
setPose(option);
|
|
165
|
+
setDecision(undefined);
|
|
166
|
+
setReplay(replay + 1);
|
|
167
|
+
}, children: GALLERY_POSE_LABELS[option] }, option))) }), _jsxs(Box, { sx: {
|
|
168
|
+
display: 'flex',
|
|
169
|
+
gap: 2,
|
|
170
|
+
flexWrap: 'wrap',
|
|
171
|
+
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: {
|
|
173
|
+
bg: 'canvas.default',
|
|
174
|
+
border: '1px solid',
|
|
175
|
+
borderColor: 'border.default',
|
|
176
|
+
borderRadius: 2,
|
|
177
|
+
p: 2,
|
|
178
|
+
overflowX: 'auto',
|
|
179
|
+
}, children: _jsx(AssistantGalleryGrid, { characters: characters }, replay) })) })) })] }) }) }));
|
|
180
|
+
};
|
|
181
|
+
export default ChatAssistantGalleryExample;
|
|
@@ -88,6 +88,7 @@ export const EXAMPLE_ENTRIES = [
|
|
|
88
88
|
makeEntry('DecksAgent', () => import('./DecksAgent'), 'A deck beside the chat: the decks plugin in a Loop, and an agent that writes presentations as data.'),
|
|
89
89
|
makeEntry('CellExample', () => import('./CellExample'), 'Simple cell example.'),
|
|
90
90
|
makeEntry('ChatAssistantExample', () => import('./ChatAssistantExample'), 'The floating assistant: the chat as a character on the page.'),
|
|
91
|
+
makeEntry('ChatAssistantGalleryExample', () => import('./ChatAssistantGalleryExample'), 'Every representation of the floating assistant: each character in each state, its balloon, light and dark, still or moving, one at a time or as a grid.'),
|
|
91
92
|
makeEntry('ChatCustomExample', () => import('./ChatCustomExample'), 'Custom chat experience composition example.'),
|
|
92
93
|
makeEntry('ChatExample', () => import('./ChatExample'), 'Baseline chat integration example.'),
|
|
93
94
|
makeEntry('ChatStandaloneExample', () => import('./ChatStandaloneExample'), 'Standalone chat component usage example.'),
|
package/lib/examples/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { default as AgUiHumanInTheLoopExample } from './AgUiHumanInTheLoopExampl
|
|
|
15
15
|
export { default as AgUiSharedStateExample } from './AgUiSharedStateExample';
|
|
16
16
|
export { default as AgUiToolsBasedGenUiExample } from './AgUiToolsBasedGenUiExample';
|
|
17
17
|
export { default as CellExample } from './CellExample';
|
|
18
|
+
export { default as ChatAssistantGalleryExample } from './ChatAssistantGalleryExample';
|
|
18
19
|
export { default as ChatStandaloneExample } from './ChatStandaloneExample';
|
|
19
20
|
export { default as CopilotKitLexicalExample } from './CopilotKitLexicalExample';
|
|
20
21
|
export { default as CopilotKitNotebookExample } from './CopilotKitNotebookExample';
|
package/lib/examples/index.js
CHANGED
|
@@ -21,6 +21,7 @@ export { default as AgUiHumanInTheLoopExample } from './AgUiHumanInTheLoopExampl
|
|
|
21
21
|
export { default as AgUiSharedStateExample } from './AgUiSharedStateExample';
|
|
22
22
|
export { default as AgUiToolsBasedGenUiExample } from './AgUiToolsBasedGenUiExample';
|
|
23
23
|
export { default as CellExample } from './CellExample';
|
|
24
|
+
export { default as ChatAssistantGalleryExample } from './ChatAssistantGalleryExample';
|
|
24
25
|
export { default as ChatStandaloneExample } from './ChatStandaloneExample';
|
|
25
26
|
export { default as CopilotKitLexicalExample } from './CopilotKitLexicalExample';
|
|
26
27
|
export { default as CopilotKitNotebookExample } from './CopilotKitNotebookExample';
|
package/lib/examples/main.js
CHANGED
|
@@ -83,6 +83,8 @@ const ANONYMOUS_EXAMPLES = new Set([
|
|
|
83
83
|
// The floating assistant: the character, its balloon and its motions need
|
|
84
84
|
// no runtime; a conversation does, and says so when there is none.
|
|
85
85
|
'ChatAssistantExample',
|
|
86
|
+
// Every character in every state, from static data: no agent at all.
|
|
87
|
+
'ChatAssistantGalleryExample',
|
|
86
88
|
]);
|
|
87
89
|
const getExampleGroup = (id) => {
|
|
88
90
|
if (id === 'AgentspecsExample' ||
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The floating assistant's characters, every one in every pose, small, for
|
|
3
|
+
* a quick visual check (LOOP T-16, T-22, T-24 to T-27): Datalayer's four and
|
|
4
|
+
* the owl as the enabled plugins contribute them to
|
|
5
|
+
* `loop.assistant.character`, and a test sprite read through the clippy.js
|
|
6
|
+
* reader. Each cell is the real `AssistantStage`, set in the grid's flow
|
|
7
|
+
* rather than fixed to the window; stepped aside is the real `useKeepClear`
|
|
8
|
+
* stepping it aside from a composer laid over the cell.
|
|
9
|
+
*
|
|
10
|
+
* @module examples/utils/AssistantGalleryGrid
|
|
11
|
+
*/
|
|
12
|
+
import type { JSX, ReactNode } from 'react';
|
|
13
|
+
import type { AssistantCharacter } from '../../chat/assistant/characters';
|
|
14
|
+
import type { AssistantCharacterData } from '../../chat/assistant/formats/types';
|
|
15
|
+
/** A character of the gallery: by id, with its name, as contributed. */
|
|
16
|
+
export interface GalleryCharacter {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
character: AssistantCharacter | AssistantCharacterData;
|
|
20
|
+
/** Where it comes from, as the gallery says it. */
|
|
21
|
+
from: string;
|
|
22
|
+
}
|
|
23
|
+
/** What the enabled plugins contribute, in contribution order. */
|
|
24
|
+
export declare const CONTRIBUTED_CHARACTERS: readonly GalleryCharacter[];
|
|
25
|
+
/** The id the test sprite is shown under. */
|
|
26
|
+
export declare const SPRITE_CHARACTER_ID = "sprite";
|
|
27
|
+
/**
|
|
28
|
+
* The gallery's characters: the contributed ones at once, and the test
|
|
29
|
+
* sprite once its files are read; `ready` once they all are.
|
|
30
|
+
*/
|
|
31
|
+
export declare function useGalleryCharacters(): {
|
|
32
|
+
characters: readonly GalleryCharacter[];
|
|
33
|
+
ready: boolean;
|
|
34
|
+
problem?: string;
|
|
35
|
+
};
|
|
36
|
+
/** Its children without motion, as a reader asking for reduced motion sees them. */
|
|
37
|
+
export declare function Still({ still, children, }: {
|
|
38
|
+
still: boolean;
|
|
39
|
+
children: ReactNode;
|
|
40
|
+
}): JSX.Element;
|
|
41
|
+
/**
|
|
42
|
+
* What the assistant steps aside from (T-27): a composer laid over it. The
|
|
43
|
+
* stage's own `useKeepClear` finds it and steps aside.
|
|
44
|
+
*/
|
|
45
|
+
export declare function GalleryObstacle({ sx, }: {
|
|
46
|
+
sx?: Record<string, unknown>;
|
|
47
|
+
}): JSX.Element;
|
|
48
|
+
/** Every character in every pose: a row per character, a column per pose. */
|
|
49
|
+
export declare function AssistantGalleryGrid({ characters, size, }: {
|
|
50
|
+
characters: readonly GalleryCharacter[];
|
|
51
|
+
size?: number;
|
|
52
|
+
}): JSX.Element;
|
|
53
|
+
export default AssistantGalleryGrid;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Text } from '@primer/react';
|
|
4
|
+
import { Box } from '@datalayer/primer-addons';
|
|
5
|
+
import { buildReactorFromPlugins } from '@datalayer/reactor';
|
|
6
|
+
import { AssistantStage } from '../../chat/assistant/AssistantStage';
|
|
7
|
+
import { AssistantCharactersPlugin, assistantCharactersOf, } from '../../loop/plugins/assistant-characters';
|
|
8
|
+
import { OWL_CHARACTER, OwlCharacterPlugin } from './owlCharacterPlugin';
|
|
9
|
+
import { readTestSpriteCharacter } from './testSpriteCharacter';
|
|
10
|
+
import { GALLERY_POSES, GALLERY_POSE_LABELS, stateOfPose, } from './assistantGallery';
|
|
11
|
+
/** The plugins the gallery enables: Datalayer's characters and the owl's. */
|
|
12
|
+
const reactor = buildReactorFromPlugins([
|
|
13
|
+
AssistantCharactersPlugin,
|
|
14
|
+
OwlCharacterPlugin,
|
|
15
|
+
]);
|
|
16
|
+
reactor.start();
|
|
17
|
+
/** What the enabled plugins contribute, in contribution order. */
|
|
18
|
+
export const CONTRIBUTED_CHARACTERS = assistantCharactersOf(reactor).map(entry => ({
|
|
19
|
+
id: entry.id,
|
|
20
|
+
name: entry.character.name,
|
|
21
|
+
character: entry.character,
|
|
22
|
+
from: entry.id === OWL_CHARACTER.id ? 'Example plugin' : 'Datalayer',
|
|
23
|
+
}));
|
|
24
|
+
/** The id the test sprite is shown under. */
|
|
25
|
+
export const SPRITE_CHARACTER_ID = 'sprite';
|
|
26
|
+
/**
|
|
27
|
+
* The gallery's characters: the contributed ones at once, and the test
|
|
28
|
+
* sprite once its files are read; `ready` once they all are.
|
|
29
|
+
*/
|
|
30
|
+
export function useGalleryCharacters() {
|
|
31
|
+
const [sprite, setSprite] = useState();
|
|
32
|
+
const [problem, setProblem] = useState();
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
let cancelled = false;
|
|
35
|
+
readTestSpriteCharacter().then(read => {
|
|
36
|
+
if (!cancelled) {
|
|
37
|
+
setSprite(read);
|
|
38
|
+
}
|
|
39
|
+
}, error => {
|
|
40
|
+
if (!cancelled) {
|
|
41
|
+
setProblem(error instanceof Error ? error.message : String(error));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return () => {
|
|
45
|
+
cancelled = true;
|
|
46
|
+
};
|
|
47
|
+
}, []);
|
|
48
|
+
// The object URL lives as long as the gallery does.
|
|
49
|
+
useEffect(() => () => {
|
|
50
|
+
if (sprite) {
|
|
51
|
+
URL.revokeObjectURL(sprite.sprite);
|
|
52
|
+
}
|
|
53
|
+
}, [sprite]);
|
|
54
|
+
return {
|
|
55
|
+
characters: sprite
|
|
56
|
+
? [
|
|
57
|
+
...CONTRIBUTED_CHARACTERS,
|
|
58
|
+
{
|
|
59
|
+
id: SPRITE_CHARACTER_ID,
|
|
60
|
+
name: `${sprite.name} (sprite)`,
|
|
61
|
+
character: sprite,
|
|
62
|
+
from: 'clippy.js reader',
|
|
63
|
+
},
|
|
64
|
+
]
|
|
65
|
+
: CONTRIBUTED_CHARACTERS,
|
|
66
|
+
ready: !!sprite || !!problem,
|
|
67
|
+
problem,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Its children without motion, as a reader asking for reduced motion sees them. */
|
|
71
|
+
export function Still({ still, children, }) {
|
|
72
|
+
return (_jsx(Box, { "data-gallery-still": still ? '' : undefined, sx: still ? { '& *': { animation: 'none !important' } } : undefined, children: children }));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* What the assistant steps aside from (T-27): a composer laid over it. The
|
|
76
|
+
* stage's own `useKeepClear` finds it and steps aside.
|
|
77
|
+
*/
|
|
78
|
+
export function GalleryObstacle({ sx, }) {
|
|
79
|
+
return (_jsx(Box, { "data-chat-composer": "", "aria-label": "A composer over the assistant", sx: {
|
|
80
|
+
position: 'absolute',
|
|
81
|
+
display: 'flex',
|
|
82
|
+
alignItems: 'center',
|
|
83
|
+
justifyContent: 'center',
|
|
84
|
+
border: '1px dashed',
|
|
85
|
+
borderColor: 'border.default',
|
|
86
|
+
borderRadius: 2,
|
|
87
|
+
bg: 'canvas.subtle',
|
|
88
|
+
color: 'fg.muted',
|
|
89
|
+
fontSize: 0,
|
|
90
|
+
textAlign: 'center',
|
|
91
|
+
p: 1,
|
|
92
|
+
...sx,
|
|
93
|
+
}, children: "composer" }));
|
|
94
|
+
}
|
|
95
|
+
function GalleryCell({ character, pose, size, }) {
|
|
96
|
+
const stageRef = useRef(null);
|
|
97
|
+
return (_jsxs(Box, { "data-gallery-cell": `${character.id}-${pose}`, sx: {
|
|
98
|
+
position: 'relative',
|
|
99
|
+
width: size + 16,
|
|
100
|
+
height: size + 16,
|
|
101
|
+
display: 'flex',
|
|
102
|
+
alignItems: 'center',
|
|
103
|
+
justifyContent: 'center',
|
|
104
|
+
}, children: [_jsx(AssistantStage, { character: character.character, state: stateOfPose(pose), size: size, place: { position: 'relative', zIndex: 0 }, stageRef: stageRef, onDragStart: () => undefined, open: false, onToggle: () => undefined, onDismiss: () => undefined }), pose === 'aside' && _jsx(GalleryObstacle, { sx: { inset: 0 } })] }));
|
|
105
|
+
}
|
|
106
|
+
/** Every character in every pose: a row per character, a column per pose. */
|
|
107
|
+
export function AssistantGalleryGrid({ characters, size = 48, }) {
|
|
108
|
+
return (_jsxs(Box, { as: "table", "data-assistant-gallery-grid": "", sx: {
|
|
109
|
+
borderCollapse: 'collapse',
|
|
110
|
+
color: 'fg.default',
|
|
111
|
+
'& th': {
|
|
112
|
+
fontSize: 0,
|
|
113
|
+
fontWeight: 600,
|
|
114
|
+
color: 'fg.muted',
|
|
115
|
+
px: 1,
|
|
116
|
+
py: 1,
|
|
117
|
+
textAlign: 'center',
|
|
118
|
+
whiteSpace: 'nowrap',
|
|
119
|
+
},
|
|
120
|
+
'& td': { p: 0, textAlign: 'center' },
|
|
121
|
+
}, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", {}), GALLERY_POSES.map(pose => (_jsx("th", { scope: "col", children: GALLERY_POSE_LABELS[pose] }, pose)))] }) }), _jsx("tbody", { children: characters.map(character => (_jsxs("tr", { children: [_jsxs(Box, { as: "th", scope: "row", sx: { textAlign: 'left !important', pr: '12px !important' }, children: [_jsx(Text, { sx: { display: 'block', color: 'fg.default' }, children: character.name }), _jsx(Text, { sx: { fontWeight: 400 }, children: character.from })] }), GALLERY_POSES.map(pose => (_jsx("td", { children: _jsx(GalleryCell, { character: character, pose: pose, size: size }) }, pose)))] }, character.id))) })] }));
|
|
122
|
+
}
|
|
123
|
+
export default AssistantGalleryGrid;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the floating assistant's gallery shows (LOOP T-21 to T-27): every
|
|
3
|
+
* state a character acts (T-22), and stepped aside (T-27), with the balloon
|
|
4
|
+
* each one says — the agent's latest saying (T-23), an approval to answer,
|
|
5
|
+
* or that it is paused. Static data only: no agent, no server.
|
|
6
|
+
*
|
|
7
|
+
* @module examples/utils/assistantGallery
|
|
8
|
+
*/
|
|
9
|
+
import type { AssistantStageProps } from '../../chat/assistant/AssistantStage';
|
|
10
|
+
import { type AssistantState, type BalloonApproval } from '../../chat/assistant/state';
|
|
11
|
+
/** What the gallery poses a character in: a state, or stepped aside. */
|
|
12
|
+
export type GalleryPose = AssistantState | 'aside';
|
|
13
|
+
/** Every state, in the order an assistant lives them, then stepped aside. */
|
|
14
|
+
export declare const GALLERY_POSES: readonly ["idle", "greeting", "thinking", "working", "waiting", "speaking", "paused", "aside", "goodbye"];
|
|
15
|
+
/** How the gallery names each pose. */
|
|
16
|
+
export declare const GALLERY_POSE_LABELS: Record<GalleryPose, string>;
|
|
17
|
+
/** The state the stage acts for a pose: aside, it is idle under what covers it. */
|
|
18
|
+
export declare function stateOfPose(pose: GalleryPose): AssistantState;
|
|
19
|
+
/**
|
|
20
|
+
* What the agent said, as a conversation would hold it: each entry is read
|
|
21
|
+
* by `latestSaying`, as the floating assistant reads the chat — Markdown made
|
|
22
|
+
* plain, a long answer cut with *Open the conversation*.
|
|
23
|
+
*/
|
|
24
|
+
export declare const SAMPLE_CONVERSATIONS: readonly (readonly unknown[])[];
|
|
25
|
+
/** The saying of sample `index` (wrapping), as the balloon shows it. */
|
|
26
|
+
export declare function sampleSaying(index: number): {
|
|
27
|
+
text: string;
|
|
28
|
+
more: boolean;
|
|
29
|
+
};
|
|
30
|
+
/** The approval the gallery waits on (T-23): answered in the balloon. */
|
|
31
|
+
export declare function sampleApproval(onApprove: () => void, onDeny: () => void): BalloonApproval;
|
|
32
|
+
/**
|
|
33
|
+
* The balloon for a pose: waiting holds the approval, paused says so, and
|
|
34
|
+
* any other pose says the sample's words when asked to (`saying`).
|
|
35
|
+
*/
|
|
36
|
+
export declare function balloonForPose(pose: GalleryPose, { saying, approval, }: {
|
|
37
|
+
saying?: {
|
|
38
|
+
text: string;
|
|
39
|
+
more: boolean;
|
|
40
|
+
};
|
|
41
|
+
approval: BalloonApproval;
|
|
42
|
+
}): AssistantStageProps['balloon'];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
import { ASSISTANT_WORDS, latestSaying, } from '../../chat/assistant/state';
|
|
6
|
+
/** Every state, in the order an assistant lives them, then stepped aside. */
|
|
7
|
+
export const GALLERY_POSES = [
|
|
8
|
+
'idle',
|
|
9
|
+
'greeting',
|
|
10
|
+
'thinking',
|
|
11
|
+
'working',
|
|
12
|
+
'waiting',
|
|
13
|
+
'speaking',
|
|
14
|
+
'paused',
|
|
15
|
+
'aside',
|
|
16
|
+
'goodbye',
|
|
17
|
+
];
|
|
18
|
+
/** How the gallery names each pose. */
|
|
19
|
+
export const GALLERY_POSE_LABELS = {
|
|
20
|
+
idle: 'Idle',
|
|
21
|
+
greeting: 'Greeting',
|
|
22
|
+
thinking: 'Thinking',
|
|
23
|
+
working: 'Working',
|
|
24
|
+
waiting: 'Waiting',
|
|
25
|
+
speaking: 'Speaking',
|
|
26
|
+
paused: 'Paused',
|
|
27
|
+
aside: 'Stepped aside',
|
|
28
|
+
goodbye: 'Goodbye',
|
|
29
|
+
};
|
|
30
|
+
/** The state the stage acts for a pose: aside, it is idle under what covers it. */
|
|
31
|
+
export function stateOfPose(pose) {
|
|
32
|
+
return pose === 'aside' ? 'idle' : pose;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* What the agent said, as a conversation would hold it: each entry is read
|
|
36
|
+
* by `latestSaying`, as the floating assistant reads the chat — Markdown made
|
|
37
|
+
* plain, a long answer cut with *Open the conversation*.
|
|
38
|
+
*/
|
|
39
|
+
export const SAMPLE_CONVERSATIONS = [
|
|
40
|
+
[
|
|
41
|
+
{ id: 'u1', role: 'user', content: 'Who wrote about late deliveries?' },
|
|
42
|
+
{
|
|
43
|
+
id: 'a1',
|
|
44
|
+
role: 'assistant',
|
|
45
|
+
content: 'Three customers wrote about **late deliveries** this week: Ada, Grace and Alan.',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
[
|
|
49
|
+
{ id: 'u2', role: 'user', content: 'Is the notebook up to date?' },
|
|
50
|
+
{ id: 't2', role: 'assistant', toolName: 'readCell', content: '' },
|
|
51
|
+
{
|
|
52
|
+
id: 'a2',
|
|
53
|
+
role: 'assistant',
|
|
54
|
+
content: 'Yes — the last cell ran at 9:40 and its [chart](#chart) shows the new week.',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
{ id: 'u3', role: 'user', content: 'Summarise the quarter.' },
|
|
59
|
+
{
|
|
60
|
+
id: 'a3',
|
|
61
|
+
role: 'assistant',
|
|
62
|
+
content: 'Revenue grew 12% on the quarter, carried by the renewals in March. Churn held at 2.1%, the same as the last two quarters, and the new plan took 140 accounts in its first six weeks. Support tickets fell by a fifth after the onboarding change, and the slowest region, the north, closed half its gap with the others. The full table, with each region and month, is in the notebook.',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
];
|
|
66
|
+
/** The saying of sample `index` (wrapping), as the balloon shows it. */
|
|
67
|
+
export function sampleSaying(index) {
|
|
68
|
+
const conversation = SAMPLE_CONVERSATIONS[((index % SAMPLE_CONVERSATIONS.length) + SAMPLE_CONVERSATIONS.length) %
|
|
69
|
+
SAMPLE_CONVERSATIONS.length];
|
|
70
|
+
const saying = latestSaying(conversation);
|
|
71
|
+
if (!saying) {
|
|
72
|
+
throw new Error(`The sample conversation ${index} says nothing.`);
|
|
73
|
+
}
|
|
74
|
+
return { text: saying.text, more: saying.more };
|
|
75
|
+
}
|
|
76
|
+
/** The approval the gallery waits on (T-23): answered in the balloon. */
|
|
77
|
+
export function sampleApproval(onApprove, onDeny) {
|
|
78
|
+
return {
|
|
79
|
+
id: 'gallery-approval',
|
|
80
|
+
asks: 'send_reply',
|
|
81
|
+
why: 'Send a reply to a customer: ask me first',
|
|
82
|
+
others: 1,
|
|
83
|
+
onApprove,
|
|
84
|
+
onDeny,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The balloon for a pose: waiting holds the approval, paused says so, and
|
|
89
|
+
* any other pose says the sample's words when asked to (`saying`).
|
|
90
|
+
*/
|
|
91
|
+
export function balloonForPose(pose, { saying, approval, }) {
|
|
92
|
+
if (pose === 'waiting') {
|
|
93
|
+
return { text: ASSISTANT_WORDS.approval, approval };
|
|
94
|
+
}
|
|
95
|
+
if (pose === 'paused') {
|
|
96
|
+
return { text: ASSISTANT_WORDS.paused };
|
|
97
|
+
}
|
|
98
|
+
if (pose === 'goodbye' || pose === 'aside') {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
return saying;
|
|
102
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AssistantCharacterData } from '../../chat/assistant/formats/types';
|
|
2
|
+
/** The character's `agent.js`, as clippy.js registers one. */
|
|
3
|
+
export declare const TEST_SPRITE_AGENT_JS = "\nclippy.ready('Pixel', {\n overlayCount: 1,\n framesize: [48, 48],\n animations: {\n Idle1_1: { frames: [{ duration: 1200, images: [[0, 0]] }] },\n Thinking: { frames: [{ duration: 1200, images: [[48, 0]] }] },\n Processing: { frames: [\n { duration: 240, images: [[96, 0]] },\n { duration: 240, images: [[144, 0]] },\n ] },\n GetAttention: { frames: [{ duration: 900, images: [[192, 0]] }] },\n Sleep: { frames: [{ duration: 1600, images: [[240, 0]] }] },\n Greeting: { frames: [{ duration: 900, images: [[288, 0]] }] },\n Explain: { frames: [\n { duration: 200, images: [[336, 0]] },\n { duration: 200, images: [[384, 0]] },\n ] },\n GoodBye: { frames: [{ duration: 900, images: [[432, 0]] }] },\n },\n});\n";
|
|
4
|
+
/** Its sprite sheet, `map.png` (480×48), base64. */
|
|
5
|
+
export declare const TEST_SPRITE_MAP_PNG_BASE64 = "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";
|
|
6
|
+
/** The sprite sheet's bytes. */
|
|
7
|
+
export declare function testSpriteMapPng(): Uint8Array;
|
|
8
|
+
/** The test sprite, read as a person's clippy.js files are. */
|
|
9
|
+
export declare function readTestSpriteCharacter(): Promise<AssistantCharacterData>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* A small character of our own in the clippy.js shape (LOOP T-26), for the
|
|
7
|
+
* floating assistant's gallery: an `agent.js` and a `map.png` sprite sheet
|
|
8
|
+
* of ten 48×48 cells, read through the same reader a person's files go
|
|
9
|
+
* through (`readClippyCharacter`). It is a test sprite drawn for Datalayer —
|
|
10
|
+
* no file of Microsoft's is in the repository (§6.9).
|
|
11
|
+
*
|
|
12
|
+
* Its animations carry the names the Office characters use, so the reader's
|
|
13
|
+
* `stateAnimations` gives every state of the assistant one of its own:
|
|
14
|
+
* `Idle1_1` idles, `Thinking`, `Processing`, `GetAttention`, `Sleep`,
|
|
15
|
+
* `Greeting`, `Explain` and `GoodBye`.
|
|
16
|
+
*
|
|
17
|
+
* @module examples/utils/testSpriteCharacter
|
|
18
|
+
*/
|
|
19
|
+
import { readClippyCharacter } from '../../chat/assistant/formats/clippy';
|
|
20
|
+
/** The character's `agent.js`, as clippy.js registers one. */
|
|
21
|
+
export const TEST_SPRITE_AGENT_JS = `
|
|
22
|
+
clippy.ready('Pixel', {
|
|
23
|
+
overlayCount: 1,
|
|
24
|
+
framesize: [48, 48],
|
|
25
|
+
animations: {
|
|
26
|
+
Idle1_1: { frames: [{ duration: 1200, images: [[0, 0]] }] },
|
|
27
|
+
Thinking: { frames: [{ duration: 1200, images: [[48, 0]] }] },
|
|
28
|
+
Processing: { frames: [
|
|
29
|
+
{ duration: 240, images: [[96, 0]] },
|
|
30
|
+
{ duration: 240, images: [[144, 0]] },
|
|
31
|
+
] },
|
|
32
|
+
GetAttention: { frames: [{ duration: 900, images: [[192, 0]] }] },
|
|
33
|
+
Sleep: { frames: [{ duration: 1600, images: [[240, 0]] }] },
|
|
34
|
+
Greeting: { frames: [{ duration: 900, images: [[288, 0]] }] },
|
|
35
|
+
Explain: { frames: [
|
|
36
|
+
{ duration: 200, images: [[336, 0]] },
|
|
37
|
+
{ duration: 200, images: [[384, 0]] },
|
|
38
|
+
] },
|
|
39
|
+
GoodBye: { frames: [{ duration: 900, images: [[432, 0]] }] },
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
`;
|
|
43
|
+
/** Its sprite sheet, `map.png` (480×48), base64. */
|
|
44
|
+
export const TEST_SPRITE_MAP_PNG_BASE64 = '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';
|
|
45
|
+
/** The sprite sheet's bytes. */
|
|
46
|
+
export function testSpriteMapPng() {
|
|
47
|
+
const binary = atob(TEST_SPRITE_MAP_PNG_BASE64);
|
|
48
|
+
const bytes = new Uint8Array(binary.length);
|
|
49
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
50
|
+
bytes[index] = binary.charCodeAt(index);
|
|
51
|
+
}
|
|
52
|
+
return bytes;
|
|
53
|
+
}
|
|
54
|
+
/** The test sprite, read as a person's clippy.js files are. */
|
|
55
|
+
export function readTestSpriteCharacter() {
|
|
56
|
+
return readClippyCharacter({
|
|
57
|
+
agentJs: TEST_SPRITE_AGENT_JS,
|
|
58
|
+
mapPng: new Blob([testSpriteMapPng()], { type: 'image/png' }),
|
|
59
|
+
});
|
|
60
|
+
}
|
package/lib/specs/actions.js
CHANGED
|
@@ -644,6 +644,7 @@ export const SERVER_ACTIONS = {
|
|
|
644
644
|
export const APP_BEHAVIOURS = {
|
|
645
645
|
'customer-interview': {},
|
|
646
646
|
'data-quality': {},
|
|
647
|
+
decide: {},
|
|
647
648
|
'inbox-triage': {
|
|
648
649
|
'google-workspace.append_table_rows': 'leave_to_me',
|
|
649
650
|
'google-workspace.batch_modify_gmail_message_labels': 'do_it',
|
|
@@ -788,6 +789,7 @@ export const APP_BEHAVIOURS = {
|
|
|
788
789
|
export const APP_ESCALATIONS = {
|
|
789
790
|
'customer-interview': {},
|
|
790
791
|
'data-quality': {},
|
|
792
|
+
decide: {},
|
|
791
793
|
'inbox-triage': {
|
|
792
794
|
'google-workspace.batch_modify_gmail_message_labels': [
|
|
793
795
|
{
|
package/lib/specs/apps.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import type { AppBuilt, AppKind, AppSpec } from '../types/agentspecs';
|
|
11
11
|
export declare const CUSTOMER_INTERVIEW_APP_0_0_1: AppSpec;
|
|
12
12
|
export declare const DATA_QUALITY_APP_0_0_1: AppSpec;
|
|
13
|
+
export declare const DECIDE_APP_0_0_1: AppSpec;
|
|
13
14
|
export declare const INBOX_TRIAGE_APP_0_0_1: AppSpec;
|
|
14
15
|
export declare const MODEL_CHOICE_APP_0_0_1: AppSpec;
|
|
15
16
|
export declare const PIPELINE_REPORT_APP_0_0_1: AppSpec;
|
package/lib/specs/apps.js
CHANGED
|
@@ -269,6 +269,114 @@ export const DATA_QUALITY_APP_0_0_1 = {
|
|
|
269
269
|
banner: '',
|
|
270
270
|
setup: ["The agent 'jupyter-data-analyst:0.0.1' is not enabled."],
|
|
271
271
|
};
|
|
272
|
+
export const DECIDE_APP_0_0_1 = {
|
|
273
|
+
schema: 'loop.app/v1',
|
|
274
|
+
id: 'decide',
|
|
275
|
+
version: '0.0.1',
|
|
276
|
+
name: 'Decide',
|
|
277
|
+
kind: 'chat',
|
|
278
|
+
description: 'Answers a question about a text — a ticket, a message, a review — by asking Jev a typed decision: yes or no, one of named options, or a score, each with its confidence.',
|
|
279
|
+
owner: 'Datalayer <info@datalayer.io>',
|
|
280
|
+
agent: 'example-simple:0.0.1',
|
|
281
|
+
team: '',
|
|
282
|
+
instructions: 'Answer every question about a text by asking a typed decision with the decide tool: the text as it was given is the state, and the question is one of noul (does a statement hold: yes or no), choice (which of the options named) or score (which step of a scale, lowest first). Then say the answer in plain words with its probability or its confidence, for example "Urgent: yes (0.87)". When the question names no options for a choice or no scale for a score, ask for them rather than inventing them. When nothing was decided, say why in a sentence.',
|
|
283
|
+
model: '',
|
|
284
|
+
skills: [],
|
|
285
|
+
tools: ['decide:0.0.1'],
|
|
286
|
+
context: [],
|
|
287
|
+
contents: [],
|
|
288
|
+
connections: [],
|
|
289
|
+
rules: [
|
|
290
|
+
{
|
|
291
|
+
action: 'Ask a decision',
|
|
292
|
+
appliesTo: ['decide'],
|
|
293
|
+
behaviour: 'do_it',
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
permissions: {
|
|
297
|
+
spaces: [],
|
|
298
|
+
computer: {
|
|
299
|
+
browse: false,
|
|
300
|
+
files: false,
|
|
301
|
+
shell: false,
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
interface: {
|
|
305
|
+
layout: 'chat',
|
|
306
|
+
accent: 'sun',
|
|
307
|
+
welcome: 'Give me a text and a question about it. I ask Jev a typed decision — yes or no, a choice, or a score — and tell you the answer with its confidence.',
|
|
308
|
+
starters: [
|
|
309
|
+
{
|
|
310
|
+
label: 'Is it urgent?',
|
|
311
|
+
message: "Is this ticket urgent? 'Help! My payouts have been failing for 3 days.'",
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
label: 'Which team?',
|
|
315
|
+
message: "Which team should handle this: 'I was charged twice this month'? Billing, Tech or Sales.",
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
label: 'Score a review',
|
|
319
|
+
message: "Score how positive this review is from 1 to 5: 'Setup took an hour, but support answered fast and it works.'",
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
settings: [],
|
|
323
|
+
components: [],
|
|
324
|
+
assistant: 'wizard',
|
|
325
|
+
},
|
|
326
|
+
tests: {
|
|
327
|
+
readyAt: 0.8,
|
|
328
|
+
evalset: '',
|
|
329
|
+
cases: [
|
|
330
|
+
{
|
|
331
|
+
ask: "Is this ticket urgent? 'Help! My payouts have been failing for 3 days.'",
|
|
332
|
+
expect: 'It calls decide with a noul question and answers yes, with its probability.',
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
ask: "Which team should handle this: 'I was charged twice this month'? Billing, Tech or Sales.",
|
|
336
|
+
expect: 'It calls decide with a choice among the three and answers Billing, with its confidence.',
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
ask: 'Score this review.',
|
|
340
|
+
expect: 'It asks for the review and the scale rather than inventing them.',
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
verified: {
|
|
344
|
+
live: [],
|
|
345
|
+
recorded: [],
|
|
346
|
+
unverified: [
|
|
347
|
+
'Its tests have not been run as a set: no validation run is attached to it.',
|
|
348
|
+
],
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
record: {
|
|
352
|
+
keepFor: '30_days',
|
|
353
|
+
include: ['conversations', 'decisions'],
|
|
354
|
+
suggestTests: false,
|
|
355
|
+
retentionDays: 30,
|
|
356
|
+
},
|
|
357
|
+
checks: {
|
|
358
|
+
guards: [],
|
|
359
|
+
gates: [],
|
|
360
|
+
track: '',
|
|
361
|
+
},
|
|
362
|
+
deployment: {
|
|
363
|
+
hosted: {
|
|
364
|
+
visibility: 'private',
|
|
365
|
+
slug: '',
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
goal: '',
|
|
369
|
+
triggers: [],
|
|
370
|
+
memory: '',
|
|
371
|
+
notifications: [],
|
|
372
|
+
enabled: true,
|
|
373
|
+
tags: ['example', 'decisions', 'jev'],
|
|
374
|
+
icon: 'law',
|
|
375
|
+
emoji: '⚖️',
|
|
376
|
+
avatar: '',
|
|
377
|
+
banner: '',
|
|
378
|
+
setup: [],
|
|
379
|
+
};
|
|
272
380
|
export const INBOX_TRIAGE_APP_0_0_1 = {
|
|
273
381
|
schema: 'loop.app/v1',
|
|
274
382
|
id: 'inbox-triage',
|
|
@@ -2072,6 +2180,7 @@ export const WEB_RESEARCH_APP_0_0_1 = {
|
|
|
2072
2180
|
export const APP_CATALOGUE = {
|
|
2073
2181
|
'customer-interview': CUSTOMER_INTERVIEW_APP_0_0_1,
|
|
2074
2182
|
'data-quality': DATA_QUALITY_APP_0_0_1,
|
|
2183
|
+
decide: DECIDE_APP_0_0_1,
|
|
2075
2184
|
'inbox-triage': INBOX_TRIAGE_APP_0_0_1,
|
|
2076
2185
|
'model-choice': MODEL_CHOICE_APP_0_0_1,
|
|
2077
2186
|
'pipeline-report': PIPELINE_REPORT_APP_0_0_1,
|
|
@@ -2266,6 +2375,74 @@ export const APP_SOURCES = {
|
|
|
2266
2375
|
icon: 'filter',
|
|
2267
2376
|
emoji: '🧹',
|
|
2268
2377
|
},
|
|
2378
|
+
decide: {
|
|
2379
|
+
schema: 'loop.app/v1',
|
|
2380
|
+
id: 'decide',
|
|
2381
|
+
name: 'Decide',
|
|
2382
|
+
kind: 'chat',
|
|
2383
|
+
description: 'Answers a question about a text — a ticket, a message, a review — by asking Jev a typed decision: yes or no, one of named options, or a score, each with its confidence.',
|
|
2384
|
+
owner: 'Datalayer <info@datalayer.io>',
|
|
2385
|
+
agent: 'example-simple:0.0.1',
|
|
2386
|
+
instructions: 'Answer every question about a text by asking a typed decision with the decide tool: the text as it was given is the state, and the question is one of noul (does a statement hold: yes or no), choice (which of the options named) or score (which step of a scale, lowest first). Then say the answer in plain words with its probability or its confidence, for example "Urgent: yes (0.87)". When the question names no options for a choice or no scale for a score, ask for them rather than inventing them. When nothing was decided, say why in a sentence.',
|
|
2387
|
+
tools: ['decide:0.0.1'],
|
|
2388
|
+
rules: [
|
|
2389
|
+
{
|
|
2390
|
+
action: 'Ask a decision',
|
|
2391
|
+
applies_to: ['decide'],
|
|
2392
|
+
behaviour: 'do_it',
|
|
2393
|
+
},
|
|
2394
|
+
],
|
|
2395
|
+
interface: {
|
|
2396
|
+
accent: 'sun',
|
|
2397
|
+
welcome: 'Give me a text and a question about it. I ask Jev a typed decision — yes or no, a choice, or a score — and tell you the answer with its confidence.',
|
|
2398
|
+
starters: [
|
|
2399
|
+
{
|
|
2400
|
+
label: 'Is it urgent?',
|
|
2401
|
+
message: "Is this ticket urgent? 'Help! My payouts have been failing for 3 days.'",
|
|
2402
|
+
},
|
|
2403
|
+
{
|
|
2404
|
+
label: 'Which team?',
|
|
2405
|
+
message: "Which team should handle this: 'I was charged twice this month'? Billing, Tech or Sales.",
|
|
2406
|
+
},
|
|
2407
|
+
{
|
|
2408
|
+
label: 'Score a review',
|
|
2409
|
+
message: "Score how positive this review is from 1 to 5: 'Setup took an hour, but support answered fast and it works.'",
|
|
2410
|
+
},
|
|
2411
|
+
],
|
|
2412
|
+
assistant: 'wizard',
|
|
2413
|
+
},
|
|
2414
|
+
tests: {
|
|
2415
|
+
cases: [
|
|
2416
|
+
{
|
|
2417
|
+
ask: "Is this ticket urgent? 'Help! My payouts have been failing for 3 days.'",
|
|
2418
|
+
expect: 'It calls decide with a noul question and answers yes, with its probability.',
|
|
2419
|
+
},
|
|
2420
|
+
{
|
|
2421
|
+
ask: "Which team should handle this: 'I was charged twice this month'? Billing, Tech or Sales.",
|
|
2422
|
+
expect: 'It calls decide with a choice among the three and answers Billing, with its confidence.',
|
|
2423
|
+
},
|
|
2424
|
+
{
|
|
2425
|
+
ask: 'Score this review.',
|
|
2426
|
+
expect: 'It asks for the review and the scale rather than inventing them.',
|
|
2427
|
+
},
|
|
2428
|
+
],
|
|
2429
|
+
verified: {
|
|
2430
|
+
unverified: [
|
|
2431
|
+
'Its tests have not been run as a set: no validation run is attached to it.',
|
|
2432
|
+
],
|
|
2433
|
+
},
|
|
2434
|
+
},
|
|
2435
|
+
record: {
|
|
2436
|
+
keep_for: '30_days',
|
|
2437
|
+
include: ['conversations', 'decisions'],
|
|
2438
|
+
},
|
|
2439
|
+
deployment: {
|
|
2440
|
+
hosted: {},
|
|
2441
|
+
},
|
|
2442
|
+
tags: ['example', 'decisions', 'jev'],
|
|
2443
|
+
icon: 'law',
|
|
2444
|
+
emoji: '⚖️',
|
|
2445
|
+
},
|
|
2269
2446
|
'inbox-triage': {
|
|
2270
2447
|
schema: 'loop.app/v1',
|
|
2271
2448
|
id: 'inbox-triage',
|
|
@@ -3644,6 +3821,7 @@ export const APP_SOURCES = {
|
|
|
3644
3821
|
export const APP_BUILT = {
|
|
3645
3822
|
'customer-interview': 'python',
|
|
3646
3823
|
'data-quality': 'written',
|
|
3824
|
+
decide: 'written',
|
|
3647
3825
|
'inbox-triage': 'written',
|
|
3648
3826
|
'model-choice': 'written',
|
|
3649
3827
|
'pipeline-report': 'written',
|
|
@@ -17,5 +17,7 @@ export declare const BesideItsWork: Story;
|
|
|
17
17
|
export declare const WorkerActivity: Story;
|
|
18
18
|
/** An approval. */
|
|
19
19
|
export declare const Approval: Story;
|
|
20
|
+
/** Tool calls, each led by the mark of whoever the tool belongs to. */
|
|
21
|
+
export declare const ToolMarks: Story;
|
|
20
22
|
/** The floating assistant, in each of its states or stepped aside. */
|
|
21
23
|
export declare const Assistant: Story;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { ASSISTANT_PICTURES, AssistantScreen, ApprovalScreen, BesideWorkScreen, ConversationScreen, REFERENCE_THEMES, ReferenceTheme, WorkerActivityScreen, } from './ReferenceScreens';
|
|
2
|
+
import { ASSISTANT_PICTURES, AssistantScreen, ApprovalScreen, BesideWorkScreen, ConversationScreen, REFERENCE_THEMES, ReferenceTheme, ToolMarksScreen, WorkerActivityScreen, } from './ReferenceScreens';
|
|
3
3
|
const meta = {
|
|
4
4
|
title: 'Loop/Reference screens',
|
|
5
5
|
parameters: { layout: 'fullscreen' },
|
|
@@ -25,6 +25,8 @@ export const BesideItsWork = story(BesideWorkScreen);
|
|
|
25
25
|
export const WorkerActivity = story(WorkerActivityScreen);
|
|
26
26
|
/** An approval. */
|
|
27
27
|
export const Approval = story(ApprovalScreen);
|
|
28
|
+
/** Tool calls, each led by the mark of whoever the tool belongs to. */
|
|
29
|
+
export const ToolMarks = story(ToolMarksScreen);
|
|
28
30
|
/** The floating assistant, in each of its states or stepped aside. */
|
|
29
31
|
export const Assistant = {
|
|
30
32
|
render: ({ theme, mode, picture }) => (_jsx(ReferenceTheme, { theme: theme, mode: mode, children: _jsx(AssistantScreen, { picture: picture }) })),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { JSX, ReactNode } from 'react';
|
|
2
2
|
import { type ThemeVariant } from '@datalayer/primer-addons';
|
|
3
|
-
/** The
|
|
4
|
-
export declare const REFERENCE_SCREENS: readonly ["conversation", "beside-work", "worker-activity", "approval"];
|
|
3
|
+
/** The screens, by the name a picture and a story take. */
|
|
4
|
+
export declare const REFERENCE_SCREENS: readonly ["conversation", "beside-work", "worker-activity", "approval", "tool-marks"];
|
|
5
5
|
export type ReferenceScreen = (typeof REFERENCE_SCREENS)[number];
|
|
6
6
|
/** The floating assistant's pictures (T-27): its states, and stepped aside. */
|
|
7
7
|
export declare const ASSISTANT_PICTURES: readonly ["idle", "thinking", "working", "waiting", "paused", "speaking", "aside"];
|
|
@@ -34,6 +34,8 @@ export declare function BesideWorkScreen(): JSX.Element;
|
|
|
34
34
|
export declare function WorkerActivityScreen(): JSX.Element;
|
|
35
35
|
/** An approval: the action, the rule that asks, and the one decision. */
|
|
36
36
|
export declare function ApprovalScreen(): JSX.Element;
|
|
37
|
+
/** Tool calls, each led by the mark of whoever the tool belongs to. */
|
|
38
|
+
export declare function ToolMarksScreen(): JSX.Element;
|
|
37
39
|
export declare const REFERENCE_SCREEN_COMPONENTS: Record<ReferenceScreen, () => JSX.Element>;
|
|
38
40
|
/**
|
|
39
41
|
* The paper clip in one state, in the page's corner. `aside` puts a dialog
|
|
@@ -31,12 +31,13 @@ import { ASSISTANT_WORDS, } from '../../chat/assistant/state';
|
|
|
31
31
|
import { buildReactorFromPlugins } from '@datalayer/reactor';
|
|
32
32
|
import { AssistantCharactersPlugin, assistantCharacterNamed, } from '../../loop/plugins/assistant-characters';
|
|
33
33
|
import { OwlCharacterPlugin } from '../../examples/utils/owlCharacterPlugin';
|
|
34
|
-
/** The
|
|
34
|
+
/** The screens, by the name a picture and a story take. */
|
|
35
35
|
export const REFERENCE_SCREENS = [
|
|
36
36
|
'conversation',
|
|
37
37
|
'beside-work',
|
|
38
38
|
'worker-activity',
|
|
39
39
|
'approval',
|
|
40
|
+
'tool-marks',
|
|
40
41
|
];
|
|
41
42
|
/** The floating assistant's pictures (T-27): its states, and stepped aside. */
|
|
42
43
|
export const ASSISTANT_PICTURES = [
|
|
@@ -145,6 +146,24 @@ const APPROVAL = [
|
|
|
145
146
|
said('a2', 'assistant', 'Sending is a rule you keep: I ask first.'),
|
|
146
147
|
tool('a3', 'send_email', { to: 'ada@example.com', subject: 'Your order 1042' }, 'inProgress', { pending_approval: true, approval_id: 'approval-1' }),
|
|
147
148
|
];
|
|
149
|
+
/*
|
|
150
|
+
* Whose tools: an MCP server's (the Odoo accounting server's, its icon the
|
|
151
|
+
* Datalayer icons' Odoo), a skill's and a frontend tool set's, each call led
|
|
152
|
+
* by its mark.
|
|
153
|
+
*/
|
|
154
|
+
const TOOL_MARKS = [
|
|
155
|
+
said('t1', 'user', 'What is still open on the books, and does my notebook agree?'),
|
|
156
|
+
tool('t2', 'odoo_accounting_list_open_balances', { account: '400000' }, 'complete', { partners: 7 }),
|
|
157
|
+
tool('t3', 'run_skill_script', { skill_name: 'accounting', script: 'variance' }, 'complete', { variance: 0 }),
|
|
158
|
+
tool('t4', 'readCell', { index: 3 }, 'complete', { source: 'balances' }),
|
|
159
|
+
said('t5', 'assistant', 'Seven customers owe you, and your notebook says the same total.'),
|
|
160
|
+
];
|
|
161
|
+
const TOOL_MARKS_SERVERS = [
|
|
162
|
+
{
|
|
163
|
+
id: 'odoo-accounting',
|
|
164
|
+
tools: [{ name: 'odoo_accounting_list_open_balances' }],
|
|
165
|
+
},
|
|
166
|
+
];
|
|
148
167
|
// ---------------------------------------------------------------------------
|
|
149
168
|
// The conversation
|
|
150
169
|
// ---------------------------------------------------------------------------
|
|
@@ -157,7 +176,7 @@ const AVATARS = {
|
|
|
157
176
|
assistantAvatarBg: 'accent.emphasis',
|
|
158
177
|
};
|
|
159
178
|
/** A chat as an application shows it: its presence, its words, its composer. */
|
|
160
|
-
function Conversation({ name, face, presence, items, composer = true, }) {
|
|
179
|
+
function Conversation({ name, face, presence, items, composer = true, mcpServers, }) {
|
|
161
180
|
const endRef = useRef(null);
|
|
162
181
|
const [input, setInput] = useState('');
|
|
163
182
|
return (_jsxs(Box, { sx: {
|
|
@@ -166,7 +185,7 @@ function Conversation({ name, face, presence, items, composer = true, }) {
|
|
|
166
185
|
display: 'flex',
|
|
167
186
|
flexDirection: 'column',
|
|
168
187
|
height: '100%',
|
|
169
|
-
}, children: [_jsx(ChatBaseHeader, { title: name, brandIcon: _jsx(PresenceFace, { face: face, size: 20, state: presence }), headerContent: _jsx(PresenceLine, { state: presence }), padding: 3, messageCount: items.length, onNewChat: () => undefined, onClear: () => undefined }), _jsx(Box, { sx: { flex: 1, minHeight: 0, overflow: 'hidden' }, children: _jsx(ChatMessageList, { displayItems: items, isLoading: false, isStreaming: false, showLoadingIndicator: false, hideMessagesAfterToolUI: false, avatarConfig: AVATARS, padding: 3, emptyContent: null, messagesEndRef: endRef, onRespond: async () => undefined }) }), composer && (_jsx(InputPrompt, { input: input, setInput: setInput, isLoading: false, connectionConfirmed: true, placeholder: "Send a message", autoFocus: false, padding: 3, onSend: () => undefined, onStop: () => undefined, showTokenUsage: false, showModelSelector: false, showToolsMenu: false, showSkillsMenu: false, codemodeEnabled: false, hasConfigData: false, hasSkillsData: false, isA2AProtocol: false }))] }));
|
|
188
|
+
}, children: [_jsx(ChatBaseHeader, { title: name, brandIcon: _jsx(PresenceFace, { face: face, size: 20, state: presence }), headerContent: _jsx(PresenceLine, { state: presence }), padding: 3, messageCount: items.length, onNewChat: () => undefined, onClear: () => undefined }), _jsx(Box, { sx: { flex: 1, minHeight: 0, overflow: 'hidden' }, children: _jsx(ChatMessageList, { displayItems: items, isLoading: false, isStreaming: false, showLoadingIndicator: false, hideMessagesAfterToolUI: false, avatarConfig: AVATARS, padding: 3, emptyContent: null, messagesEndRef: endRef, onRespond: async () => undefined, mcpServers: mcpServers }) }), composer && (_jsx(InputPrompt, { input: input, setInput: setInput, isLoading: false, connectionConfirmed: true, placeholder: "Send a message", autoFocus: false, padding: 3, onSend: () => undefined, onStop: () => undefined, showTokenUsage: false, showModelSelector: false, showToolsMenu: false, showSkillsMenu: false, codemodeEnabled: false, hasConfigData: false, hasSkillsData: false, isA2AProtocol: false }))] }));
|
|
170
189
|
}
|
|
171
190
|
// ---------------------------------------------------------------------------
|
|
172
191
|
// The four screens
|
|
@@ -198,11 +217,16 @@ export function WorkerActivityScreen() {
|
|
|
198
217
|
export function ApprovalScreen() {
|
|
199
218
|
return (_jsx(Frame, { children: _jsx(Box, { sx: { flex: 1, display: 'flex', justifyContent: 'center' }, children: _jsx(Box, { sx: { width: '100%', maxWidth: 640, display: 'flex' }, children: _jsx(Conversation, { name: "Support desk", face: "\uD83E\uDD8A", presence: "waiting", items: APPROVAL }) }) }) }));
|
|
200
219
|
}
|
|
220
|
+
/** Tool calls, each led by the mark of whoever the tool belongs to. */
|
|
221
|
+
export function ToolMarksScreen() {
|
|
222
|
+
return (_jsx(Frame, { children: _jsx(Box, { sx: { flex: 1, display: 'flex', justifyContent: 'center' }, children: _jsx(Box, { sx: { width: '100%', maxWidth: 680, display: 'flex' }, children: _jsx(Conversation, { name: "Bookkeeper", face: "\uD83E\uDDEE", presence: "idle", items: TOOL_MARKS, composer: false, mcpServers: TOOL_MARKS_SERVERS }) }) }) }));
|
|
223
|
+
}
|
|
201
224
|
export const REFERENCE_SCREEN_COMPONENTS = {
|
|
202
225
|
conversation: ConversationScreen,
|
|
203
226
|
'beside-work': BesideWorkScreen,
|
|
204
227
|
'worker-activity': WorkerActivityScreen,
|
|
205
228
|
approval: ApprovalScreen,
|
|
229
|
+
'tool-marks': ToolMarksScreen,
|
|
206
230
|
};
|
|
207
231
|
// ---------------------------------------------------------------------------
|
|
208
232
|
// The floating assistant (T-27)
|
|
@@ -8,7 +8,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
* one state of the floating assistant, in one theme and one mode, chosen by
|
|
9
9
|
* the address — `html/pictures.html?screen=approval&theme=loop&mode=dark`,
|
|
10
10
|
* or `?assistant=thinking&mode=light`, or a character idle,
|
|
11
|
-
* `?character=wizard&mode=dark` (Datalayer's, or the owl a plugin contributes)
|
|
11
|
+
* `?character=wizard&mode=dark` (Datalayer's, or the owl a plugin contributes),
|
|
12
|
+
* or the gallery's grid of every character in every pose, `?gallery=grid`.
|
|
12
13
|
*
|
|
13
14
|
* It sets `data-pictures-ready` on the document once the fonts are in — Inter
|
|
14
15
|
* in its two weights, served by this page (T-04) — and two frames have
|
|
@@ -22,7 +23,9 @@ if (globals['__webpack_public_path__'] === undefined) {
|
|
|
22
23
|
}
|
|
23
24
|
import { useEffect } from 'react';
|
|
24
25
|
import { createRoot } from 'react-dom/client';
|
|
26
|
+
import { Box } from '@datalayer/primer-addons';
|
|
25
27
|
import { ASSISTANT_PICTURES, AssistantScreen, CHARACTER_PICTURES, CharacterScreen, REFERENCE_SCREEN_COMPONENTS, REFERENCE_SCREENS, REFERENCE_THEMES, ReferenceTheme, } from './ReferenceScreens';
|
|
28
|
+
import { AssistantGalleryGrid, useGalleryCharacters, } from '../../examples/utils/AssistantGalleryGrid';
|
|
26
29
|
import '../../../style/primer-primitives.css';
|
|
27
30
|
// The `loop` theme's face (LOOP T-04): Inter and its metric-matched fallback,
|
|
28
31
|
// served by this page's own Vite, as the landing serves them from its build.
|
|
@@ -35,6 +38,7 @@ const mode = params.get('mode') === 'dark' ? 'dark' : 'light';
|
|
|
35
38
|
const screen = params.get('screen');
|
|
36
39
|
const assistant = params.get('assistant');
|
|
37
40
|
const character = params.get('character');
|
|
41
|
+
const gallery = params.get('gallery') === 'grid';
|
|
38
42
|
function Ready() {
|
|
39
43
|
useEffect(() => {
|
|
40
44
|
let cancelled = false;
|
|
@@ -58,7 +62,18 @@ function Ready() {
|
|
|
58
62
|
}, []);
|
|
59
63
|
return null;
|
|
60
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* The floating assistant's gallery as a grid: every character — the test
|
|
67
|
+
* sprite read in the page included — in every pose; ready once it is read.
|
|
68
|
+
*/
|
|
69
|
+
function GalleryScreen() {
|
|
70
|
+
const { characters, ready } = useGalleryCharacters();
|
|
71
|
+
return (_jsxs(Box, { sx: { flex: 1, bg: 'canvas.default', p: 2, overflow: 'hidden' }, children: [_jsx(AssistantGalleryGrid, { characters: characters }), ready && _jsx(Ready, {})] }));
|
|
72
|
+
}
|
|
61
73
|
function Page() {
|
|
74
|
+
if (gallery) {
|
|
75
|
+
return (_jsx(ReferenceTheme, { theme: theme, mode: mode, children: _jsx(GalleryScreen, {}) }));
|
|
76
|
+
}
|
|
62
77
|
if (character && CHARACTER_PICTURES.includes(character)) {
|
|
63
78
|
return (_jsx(ReferenceTheme, { theme: theme, mode: mode, children: _jsx(CharacterScreen, { character: character, ready: _jsx(Ready, {}) }) }));
|
|
64
79
|
}
|