@datalayer/agent-runtimes 1.3.62 → 1.3.64
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/ChatFloating.d.ts +9 -1
- package/lib/chat/ChatFloating.js +69 -18
- package/lib/chat/assistant/AssistantStage.d.ts +14 -1
- package/lib/chat/assistant/AssistantStage.js +53 -1
- package/lib/chat/assistant/SpriteCharacter.js +1 -1
- package/lib/chat/assistant/characters.js +1 -5
- package/lib/chat/assistant/state.d.ts +7 -1
- package/lib/chat/assistant/state.js +3 -2
- package/lib/chat/base/ChatBase.js +32 -4
- package/lib/chat/messages/ChatMessageList.d.ts +0 -6
- package/lib/chat/messages/ChatMessageList.js +8 -2
- package/lib/config/AgentConfiguration.js +0 -6
- package/lib/examples/AgentA2ATeamExample.js +19 -2
- package/lib/examples/ChatAssistantExample.d.ts +3 -1
- package/lib/examples/ChatAssistantExample.js +75 -7
- package/lib/examples/ChatAssistantGalleryExample.d.ts +3 -2
- package/lib/examples/ChatAssistantGalleryExample.js +11 -6
- package/lib/examples/DecksAgent.js +4 -2
- package/lib/examples/LoopShellExample.js +4 -2
- package/lib/examples/VoiceChatExample.d.ts +20 -0
- package/lib/examples/VoiceChatExample.js +64 -0
- package/lib/examples/example-selector.js +1 -0
- package/lib/examples/main.js +12 -7
- package/lib/examples/utils/clippyJsCharacters.d.ts +18 -0
- package/lib/examples/utils/clippyJsCharacters.js +129 -0
- package/lib/loop/apps/appspec.d.ts +3 -1
- package/lib/loop/apps/appspec.js +31 -0
- package/lib/loop/apps/checks.js +1 -1
- package/lib/protocols/VercelAIAdapter.js +5 -0
- package/lib/specs/apps.js +112 -0
- package/lib/specs/appspecSchema.js +65 -0
- package/lib/specs/index.d.ts +1 -0
- package/lib/specs/index.js +1 -0
- package/lib/specs/voices.d.ts +72 -0
- package/lib/specs/voices.js +344 -0
- package/lib/types/agents.d.ts +1 -1
- package/lib/types/agentspecs.d.ts +16 -0
- package/lib/types/chat.d.ts +11 -0
- package/lib/voice/VoiceInput.d.ts +35 -0
- package/lib/voice/VoiceInput.js +233 -0
- package/lib/voice/capture.d.ts +30 -0
- package/lib/voice/capture.js +113 -0
- package/lib/voice/consent.d.ts +6 -0
- package/lib/voice/consent.js +34 -0
- package/lib/voice/hearing.d.ts +37 -0
- package/lib/voice/hearing.js +168 -0
- package/lib/voice/index.d.ts +47 -0
- package/lib/voice/index.js +13 -0
- package/lib/voice/pinned.d.ts +28 -0
- package/lib/voice/pinned.js +75 -0
- package/lib/voice/sentences.d.ts +26 -0
- package/lib/voice/sentences.js +71 -0
- package/lib/voice/speaker.d.ts +66 -0
- package/lib/voice/speaker.js +168 -0
- package/lib/voice/types.d.ts +92 -0
- package/lib/voice/types.js +5 -0
- package/lib/voice/useSpokenAnswers.d.ts +12 -0
- package/lib/voice/useSpokenAnswers.js +90 -0
- package/package.json +7 -3
- package/scripts/codegen/generate_voices.py +279 -0
- package/scripts/voice/measure.py +244 -0
- package/scripts/voice/pin_store.py +159 -0
- package/scripts/voice/serve_store.py +70 -0
- package/scripts/voice/transcribe.mjs +58 -0
|
@@ -10,9 +10,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
*
|
|
11
11
|
* The characters offered are what the enabled plugins contribute to
|
|
12
12
|
* `loop.assistant.character` (T-24): Datalayer's four, and an owl from a
|
|
13
|
-
* small example plugin (`utils/owlCharacterPlugin`).
|
|
13
|
+
* small example plugin (`utils/owlCharacterPlugin`). Pixel, the test sprite,
|
|
14
|
+
* is read through the clippy.js reader, as a character a person brings is,
|
|
15
|
+
* and so are the characters clippy.js publishes, fetched when picked.
|
|
14
16
|
*/
|
|
15
|
-
import { useState } from 'react';
|
|
17
|
+
import { useEffect, useRef, useState } from 'react';
|
|
16
18
|
import { buildReactorFromPlugins } from '@datalayer/reactor';
|
|
17
19
|
import { Button, Heading, Text } from '@primer/react';
|
|
18
20
|
import { Box } from '@datalayer/primer-addons';
|
|
@@ -20,6 +22,8 @@ import { ThemedProvider } from './utils/themedProvider';
|
|
|
20
22
|
import { ChatFloating } from '../chat';
|
|
21
23
|
import { AssistantCharactersPlugin, assistantCharacterNamed, assistantCharactersOf, } from '../loop/plugins/assistant-characters';
|
|
22
24
|
import { OwlCharacterPlugin } from './utils/owlCharacterPlugin';
|
|
25
|
+
import { readTestSpriteCharacter } from './utils/testSpriteCharacter';
|
|
26
|
+
import { CLIPPY_JS_CHARACTERS, CLIPPY_JS_VERSION, readClippyJsCharacter, } from './utils/clippyJsCharacters';
|
|
23
27
|
import { readAcsCharacter, readClippyCharacter, } from '../chat/assistant/formats';
|
|
24
28
|
/**
|
|
25
29
|
* The agent-runtimes server the assistant talks to: `?agentRuntimesUrl=`
|
|
@@ -69,10 +73,72 @@ const ChatAssistantExample = () => {
|
|
|
69
73
|
? assistantCharacterNamed(reactor, character)
|
|
70
74
|
: character;
|
|
71
75
|
const [loadError, setLoadError] = useState();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
// Pixel, the test sprite, read once through the clippy.js reader.
|
|
77
|
+
const [pixel, setPixel] = useState();
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
let cancelled = false;
|
|
80
|
+
readTestSpriteCharacter().then(read => {
|
|
81
|
+
if (!cancelled) {
|
|
82
|
+
setPixel(read);
|
|
83
|
+
}
|
|
84
|
+
}, error => {
|
|
85
|
+
if (!cancelled) {
|
|
86
|
+
setLoadError(error instanceof Error ? error.message : String(error));
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
return () => {
|
|
90
|
+
cancelled = true;
|
|
91
|
+
};
|
|
92
|
+
}, []);
|
|
93
|
+
// The object URL lives as long as the page does.
|
|
94
|
+
useEffect(() => () => {
|
|
95
|
+
if (pixel) {
|
|
96
|
+
URL.revokeObjectURL(pixel.sprite);
|
|
97
|
+
}
|
|
98
|
+
}, [pixel]);
|
|
99
|
+
// The clippy.js characters, each fetched the first time it is picked.
|
|
100
|
+
const [clippyJs, setClippyJs] = useState({});
|
|
101
|
+
const [fetching, setFetching] = useState();
|
|
102
|
+
const fetched = useRef([]);
|
|
103
|
+
useEffect(() => () => {
|
|
104
|
+
for (const read of fetched.current) {
|
|
105
|
+
URL.revokeObjectURL(read.sprite);
|
|
106
|
+
}
|
|
107
|
+
}, []);
|
|
108
|
+
const pickClippyJs = async (name) => {
|
|
109
|
+
const known = clippyJs[name];
|
|
110
|
+
if (known) {
|
|
111
|
+
setCharacter(known);
|
|
112
|
+
setLoadError(undefined);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
setFetching(name);
|
|
116
|
+
try {
|
|
117
|
+
const read = await readClippyJsCharacter(name);
|
|
118
|
+
fetched.current.push(read);
|
|
119
|
+
setClippyJs(previous => ({ ...previous, [name]: read }));
|
|
120
|
+
setCharacter(read);
|
|
121
|
+
setLoadError(undefined);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
setLoadError(error instanceof Error ? error.message : String(error));
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
setFetching(undefined);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const isClippyJs = typeof character !== 'string' &&
|
|
131
|
+
Object.values(clippyJs).includes(character);
|
|
132
|
+
return (_jsx(ThemedProvider, { children: _jsxs(Box, { sx: { minHeight: '100vh', bg: 'canvas.default', p: 4 }, children: [_jsxs(Box, { sx: { maxWidth: 720, mx: 'auto' }, children: [_jsx(Heading, { as: "h1", sx: { mb: 2 }, children: "Chat Assistant" }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', mb: 3 }, children: "The chat as a character on the page. It greets you, acts out what the agent is doing, and says the agent\u2019s words in a balloon while the conversation is closed. Click it to talk, drag it to move it, hover it to send it away." }), _jsxs(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: [CATALOGUE.map(option => (_jsx(Button, { variant: option.id === character ? 'primary' : 'default', onClick: () => {
|
|
133
|
+
setCharacter(option.id);
|
|
134
|
+
setLoadError(undefined);
|
|
135
|
+
}, "aria-pressed": option.id === character, "data-assistant-character": option.id, children: option.name }, option.id))), pixel && (_jsxs(Button, { variant: character === pixel ? 'primary' : 'default', onClick: () => {
|
|
136
|
+
setCharacter(pixel);
|
|
137
|
+
setLoadError(undefined);
|
|
138
|
+
}, "aria-pressed": character === pixel, "data-assistant-character": "sprite", children: [pixel.name, " (sprite)"] }))] }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "The clippy.js characters" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["Fetched when picked from the", ' ', _jsxs("code", { children: ["clippyjs@", CLIPPY_JS_VERSION] }), " package on jsDelivr and read as data by the same reader. They are not in this repository: clippy.js\u2019s licence covers its code only, and the characters are Microsoft\u2019s."] }), _jsx(Box, { sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: CLIPPY_JS_CHARACTERS.map(name => {
|
|
139
|
+
const picked = clippyJs[name] !== undefined && character === clippyJs[name];
|
|
140
|
+
return (_jsx(Button, { variant: picked ? 'primary' : 'default', onClick: () => void pickClippyJs(name), disabled: fetching !== undefined, "aria-pressed": picked, "data-assistant-character": `clippy-js-${name.toLowerCase()}`, children: fetching === name ? `${name}…` : name }, name));
|
|
141
|
+
}) })] }), _jsxs(Box, { as: "section", sx: { mt: 4 }, children: [_jsx(Heading, { as: "h2", sx: { fontSize: 2, mb: 1 }, children: "A character you bring" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 2 }, children: ["The characters of Microsoft Office \u2014 Clippy, Merlin, Links and the others \u2014 are Microsoft\u2019s. Load only a character file you have the right to use: a Microsoft Agent ", _jsx("code", { children: ".acs" }), " file, or a clippy.js character\u2019s ", _jsx("code", { children: "agent.js" }), " and map image. It is read in this page and sent nowhere."] }), _jsx("input", { type: "file", multiple: true, accept: ".acs,.js,.png,.gif,.webp,.jpg,.jpeg", "aria-label": "Character files", onChange: async (event) => {
|
|
76
142
|
const files = Array.from(event.target.files ?? []);
|
|
77
143
|
if (!files.length) {
|
|
78
144
|
return;
|
|
@@ -84,7 +150,9 @@ const ChatAssistantExample = () => {
|
|
|
84
150
|
catch (error) {
|
|
85
151
|
setLoadError(error instanceof Error ? error.message : String(error));
|
|
86
152
|
}
|
|
87
|
-
} }), loadError && (_jsx(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mt: 2 }, children: loadError })), typeof character !== 'string' &&
|
|
153
|
+
} }), loadError && (_jsx(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mt: 2 }, children: loadError })), typeof character !== 'string' &&
|
|
154
|
+
character !== pixel &&
|
|
155
|
+
!isClippyJs && (_jsxs(Text, { as: "p", sx: { mt: 2 }, children: ["Playing ", character.name, "."] }))] })] }), _jsx(ChatFloating, { defaultViewMode: "assistant", assistantCharacter: drawn, protocol: "vercel-ai", endpoint: `${SERVER}/api/v1/vercel-ai/assistant`, title: "Assistant", description: "Hello! Ask me anything about this page.", position: "bottom-right", useStore: false,
|
|
88
156
|
// Ask a decision beside the composer: Jev, through this server.
|
|
89
157
|
decisions: { serverUrl: SERVER } }, typeof character === 'string' ? character : character.sprite)] }) }));
|
|
90
158
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Every representation of the floating assistant, as a gallery (LOOP T-21
|
|
3
3
|
* to T-27): each character — Datalayer's four, the owl an example plugin
|
|
4
|
-
* contributes to `loop.assistant.character`,
|
|
5
|
-
* the clippy.js reader
|
|
4
|
+
* contributes to `loop.assistant.character`, a test sprite read through
|
|
5
|
+
* the clippy.js reader, and, when asked, the characters clippy.js publishes,
|
|
6
|
+
* fetched from jsDelivr — in each state it acts, stepped aside, with its
|
|
6
7
|
* balloon, in light and dark, moving or still.
|
|
7
8
|
*
|
|
8
9
|
* Static data, no agent: it opens without a server or an account.
|
|
@@ -6,8 +6,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
6
6
|
/**
|
|
7
7
|
* Every representation of the floating assistant, as a gallery (LOOP T-21
|
|
8
8
|
* to T-27): each character — Datalayer's four, the owl an example plugin
|
|
9
|
-
* contributes to `loop.assistant.character`,
|
|
10
|
-
* the clippy.js reader
|
|
9
|
+
* contributes to `loop.assistant.character`, a test sprite read through
|
|
10
|
+
* the clippy.js reader, and, when asked, the characters clippy.js publishes,
|
|
11
|
+
* fetched from jsDelivr — in each state it acts, stepped aside, with its
|
|
11
12
|
* balloon, in light and dark, moving or still.
|
|
12
13
|
*
|
|
13
14
|
* Static data, no agent: it opens without a server or an account.
|
|
@@ -21,6 +22,7 @@ import { AssistantStage } from '../chat/assistant/AssistantStage';
|
|
|
21
22
|
import { useViewportDrag } from '../chat/useViewportDrag';
|
|
22
23
|
import { AssistantGalleryGrid, GalleryObstacle, Still, useGalleryCharacters, } from './utils/AssistantGalleryGrid';
|
|
23
24
|
import { GALLERY_POSES, GALLERY_POSE_LABELS, balloonForPose, sampleApproval, sampleSaying, stateOfPose, } from './utils/assistantGallery';
|
|
25
|
+
import { CLIPPY_JS_VERSION, useClippyJsCharacters, } from './utils/clippyJsCharacters';
|
|
24
26
|
const STAGE_SIZE = 96;
|
|
25
27
|
const PANEL_HEIGHT = 420;
|
|
26
28
|
const LEAVE_MS = 700;
|
|
@@ -98,7 +100,10 @@ function InModes({ modes, stacked = false, children, }) {
|
|
|
98
100
|
}, children: shown.map(mode => (_jsx(Box, { sx: { flex: '1 1 320px', minWidth: 0 }, children: _jsx(DatalayerThemeProvider, { colorMode: mode, theme: config.primerTheme, themeStyles: config.themeStyles, children: children(mode) }) }, mode))) }));
|
|
99
101
|
}
|
|
100
102
|
const ChatAssistantGalleryExample = () => {
|
|
101
|
-
const { characters, problem } = useGalleryCharacters();
|
|
103
|
+
const { characters: ours, problem } = useGalleryCharacters();
|
|
104
|
+
const [withClippyJs, setWithClippyJs] = useState(false);
|
|
105
|
+
const clippyJs = useClippyJsCharacters(withClippyJs);
|
|
106
|
+
const characters = [...ours, ...clippyJs.characters];
|
|
102
107
|
const [characterId, setCharacterId] = useState('paperclip');
|
|
103
108
|
const [pose, setPose] = useState('idle');
|
|
104
109
|
const [view, setView] = useState('stage');
|
|
@@ -142,7 +147,7 @@ const ChatAssistantGalleryExample = () => {
|
|
|
142
147
|
bg: 'canvas.default',
|
|
143
148
|
color: 'fg.default',
|
|
144
149
|
p: 4,
|
|
145
|
-
}, children: _jsxs(Box, { sx: { maxWidth: 1200, mx: 'auto' }, children: [_jsx(Heading, { as: "h1", sx: { mb: 2 }, children: "Chat Assistant gallery" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 3, maxWidth: 760 }, children: ["Every character in every state the floating assistant acts, with its balloon, in light and dark. Datalayer\u2019s four and the owl are what the enabled plugins contribute to", ' ', _jsx("code", { children: "loop.assistant.character" }), "; Pixel is a test sprite of our own, read through the clippy.js reader. No agent is behind it: the words are a sample conversation."] }), _jsxs(Box, { as: "section", "aria-label": "Gallery controls", sx: {
|
|
150
|
+
}, children: _jsxs(Box, { sx: { maxWidth: 1200, mx: 'auto' }, children: [_jsx(Heading, { as: "h1", sx: { mb: 2 }, children: "Chat Assistant gallery" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 3, maxWidth: 760 }, children: ["Every character in every state the floating assistant acts, with its balloon, in light and dark. Datalayer\u2019s four and the owl are what the enabled plugins contribute to", ' ', _jsx("code", { children: "loop.assistant.character" }), "; Pixel is a test sprite of our own, read through the clippy.js reader. No agent is behind it: the words are a sample conversation."] }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 3, maxWidth: 760 }, children: ["The characters clippy.js publishes \u2014 Clippy, Merlin, Links, Rover and the others \u2014 are loaded on request from the", ' ', _jsxs("code", { children: ["clippyjs@", CLIPPY_JS_VERSION] }), " package on jsDelivr (about 15 MB) and read by the same reader. They are not in this repository: clippy.js\u2019s licence covers its code only, and the characters are Microsoft\u2019s."] }), _jsxs(Box, { as: "section", "aria-label": "Gallery controls", sx: {
|
|
146
151
|
display: 'grid',
|
|
147
152
|
gap: 3,
|
|
148
153
|
mb: 4,
|
|
@@ -160,7 +165,7 @@ const ChatAssistantGalleryExample = () => {
|
|
|
160
165
|
? 'Side by side'
|
|
161
166
|
: option === 'light'
|
|
162
167
|
? 'Light'
|
|
163
|
-
: 'Dark' }, option))) }), _jsxs(FormControl, { children: [_jsx(Checkbox, { checked: still, onChange: event => setStill(event.target.checked) }), _jsx(FormControl.Label, { children: "Reduced motion" })] }), _jsx(Button, { onClick: () => setReplay(replay + 1), children: "Replay the motions" })] }), view === 'stage' && (_jsxs(_Fragment, { children: [_jsx(Box, { role: "group", "aria-label": "Character", sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: characters.map(option => (_jsx(Button, { size: "small", variant: option.id === character.id ? 'primary' : 'default', "aria-pressed": option.id === character.id, "data-gallery-character": option.id, onClick: () => setCharacterId(option.id), children: option.name }, option.id))) }), _jsx(Box, { role: "group", "aria-label": "State", sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: GALLERY_POSES.map(option => (_jsx(Button, { size: "small", variant: option === pose ? 'primary' : 'default', "aria-pressed": option === pose, "data-gallery-pose": option, onClick: () => {
|
|
168
|
+
: 'Dark' }, option))) }), _jsxs(FormControl, { children: [_jsx(Checkbox, { checked: still, onChange: event => setStill(event.target.checked) }), _jsx(FormControl.Label, { children: "Reduced motion" })] }), _jsxs(FormControl, { children: [_jsx(Checkbox, { checked: withClippyJs, onChange: event => setWithClippyJs(event.target.checked), "data-gallery-clippy-js": "" }), _jsxs(FormControl.Label, { children: ["clippy.js characters", clippyJs.loading ? ' (loading…)' : ''] })] }), _jsx(Button, { onClick: () => setReplay(replay + 1), children: "Replay the motions" })] }), view === 'stage' && (_jsxs(_Fragment, { children: [_jsx(Box, { role: "group", "aria-label": "Character", sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: characters.map(option => (_jsx(Button, { size: "small", variant: option.id === character.id ? 'primary' : 'default', "aria-pressed": option.id === character.id, "data-gallery-character": option.id, onClick: () => setCharacterId(option.id), children: option.name }, option.id))) }), _jsx(Box, { role: "group", "aria-label": "State", sx: { display: 'flex', gap: 2, flexWrap: 'wrap' }, children: GALLERY_POSES.map(option => (_jsx(Button, { size: "small", variant: option === pose ? 'primary' : 'default', "aria-pressed": option === pose, "data-gallery-pose": option, onClick: () => {
|
|
164
169
|
setPose(option);
|
|
165
170
|
setDecision(undefined);
|
|
166
171
|
setReplay(replay + 1);
|
|
@@ -169,7 +174,7 @@ const ChatAssistantGalleryExample = () => {
|
|
|
169
174
|
gap: 2,
|
|
170
175
|
flexWrap: 'wrap',
|
|
171
176
|
alignItems: 'center',
|
|
172
|
-
}, children: [_jsx(Button, { onClick: say, "data-gallery-say": "", children: "Say something" }), saying !== undefined && (_jsx(Button, { variant: "invisible", onClick: () => setSaying(undefined), children: "Hush" })), presence === 'away' ? (_jsx(Button, { onClick: callBack, children: "Call it back" })) : (_jsx(Button, { onClick: () => sendAway('page'), disabled: presence !== 'here', "data-gallery-send-away": "", children: "Send it away" })), _jsx(Text, { sx: { color: 'fg.muted', fontSize: 1 }, children: "Drag the character to move it; hover it for its own send-away menu." }), decision && (_jsx(Text, { role: "status", sx: { fontSize: 1 }, children: decision }))] })] }))] }), problem && (_jsxs(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mb: 3 }, children: ["The test sprite could not be read: ", problem] })), _jsx(Still, { still: still, children: view === 'stage' ? (_jsx(InModes, { modes: modes, children: mode => (_jsx(ModePanel, { mode: mode, character: character, pose: pose, presence: presence, balloon: balloon, insist: insist, onToggle: () => saying === undefined ? say() : setSaying(undefined), onDismiss: sendAway, onCallBack: callBack, away: away, replay: replay })) })) : (_jsx(InModes, { modes: modes, stacked: true, children: mode => (_jsx(Box, { "data-gallery-mode": mode, sx: {
|
|
177
|
+
}, children: [_jsx(Button, { onClick: say, "data-gallery-say": "", children: "Say something" }), saying !== undefined && (_jsx(Button, { variant: "invisible", onClick: () => setSaying(undefined), children: "Hush" })), presence === 'away' ? (_jsx(Button, { onClick: callBack, children: "Call it back" })) : (_jsx(Button, { onClick: () => sendAway('page'), disabled: presence !== 'here', "data-gallery-send-away": "", children: "Send it away" })), _jsx(Text, { sx: { color: 'fg.muted', fontSize: 1 }, children: "Drag the character to move it; hover it for its own send-away menu." }), decision && (_jsx(Text, { role: "status", sx: { fontSize: 1 }, children: decision }))] })] }))] }), problem && (_jsxs(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mb: 3 }, children: ["The test sprite could not be read: ", problem] })), clippyJs.problem && (_jsxs(Text, { as: "p", role: "alert", sx: { color: 'danger.fg', mb: 3 }, children: ["A clippy.js character could not be read: ", clippyJs.problem] })), _jsx(Still, { still: still, children: view === 'stage' ? (_jsx(InModes, { modes: modes, children: mode => (_jsx(ModePanel, { mode: mode, character: character, pose: pose, presence: presence, balloon: balloon, insist: insist, onToggle: () => saying === undefined ? say() : setSaying(undefined), onDismiss: sendAway, onCallBack: callBack, away: away, replay: replay })) })) : (_jsx(InModes, { modes: modes, stacked: true, children: mode => (_jsx(Box, { "data-gallery-mode": mode, sx: {
|
|
173
178
|
bg: 'canvas.default',
|
|
174
179
|
border: '1px solid',
|
|
175
180
|
borderColor: 'border.default',
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box } from '@datalayer/primer-addons';
|
|
3
3
|
import { ThemedProvider } from './utils/themedProvider';
|
|
4
|
-
import { registerDecks } from '@datalayer/decks';
|
|
5
|
-
import { exampleDecks } from '@datalayer/decks/examples';
|
|
4
|
+
import { registerDeckComponents, registerDecks } from '@datalayer/decks';
|
|
5
|
+
import { exampleDeckComponents, exampleDecks } from '@datalayer/decks/examples';
|
|
6
6
|
import { LoopEmbed } from '../loop';
|
|
7
7
|
import { DeckViewPlugin, LoopDecksPlugin } from '../loop/plugins/decks';
|
|
8
8
|
// The package's example decks, so the catalog has something to open and the
|
|
9
9
|
// agent's suggestions point at decks that exist. Once, at module load.
|
|
10
10
|
registerDecks(exampleDecks);
|
|
11
|
+
// And the components their slides name (AgentsHero, LiveAppearance…).
|
|
12
|
+
registerDeckComponents(exampleDeckComponents);
|
|
11
13
|
// Module-level, so the embed builds its reactor once rather than on every
|
|
12
14
|
// render of this component.
|
|
13
15
|
const DECK_PLUGINS = [LoopDecksPlugin, DeckViewPlugin];
|
|
@@ -3,8 +3,8 @@ import { useMemo } from 'react';
|
|
|
3
3
|
import { Box } from '@primer/react';
|
|
4
4
|
import { QueryClientProvider } from '@tanstack/react-query';
|
|
5
5
|
import { useReactor } from '@datalayer/reactor/react';
|
|
6
|
-
import { registerDecks } from '@datalayer/decks';
|
|
7
|
-
import { exampleDecks } from '@datalayer/decks/examples';
|
|
6
|
+
import { registerDeckComponents, registerDecks } from '@datalayer/decks';
|
|
7
|
+
import { exampleDeckComponents, exampleDecks } from '@datalayer/decks/examples';
|
|
8
8
|
import { buildLoopReactor, LoopWorkspace } from '../loop/shell';
|
|
9
9
|
import { DeckViewPlugin, LoopDecksPlugin } from '../loop/plugins/decks';
|
|
10
10
|
import { loopPlugins } from '../loop/presets';
|
|
@@ -18,6 +18,8 @@ const FRONT_DOOR = 'jupyter-data-analyst';
|
|
|
18
18
|
// The package's example decks, so the Decks member has something to open and
|
|
19
19
|
// change before it has made one. Once, at module load.
|
|
20
20
|
registerDecks(exampleDecks);
|
|
21
|
+
// And the components their slides name (AgentsHero, LiveAppearance…).
|
|
22
|
+
registerDeckComponents(exampleDeckComponents);
|
|
21
23
|
export function LoopShellExample({ serverUrl = resolveExampleAgentRuntimesUrl('local'), agentId = FRONT_DOOR, defaultEditor = 'none', inheritTheme = false, }) {
|
|
22
24
|
// Built once: rebuilding would restart every plugin on each render.
|
|
23
25
|
const reactor = useMemo(() => buildLoopReactor([
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A voice chat with the floating assistant (VOICE.md V1).
|
|
3
|
+
*
|
|
4
|
+
* Hold the microphone (or `Ctrl`+`Space`), speak, let go: what you said is
|
|
5
|
+
* heard in this page — Moonshine for English, Whisper for French, Silero VAD
|
|
6
|
+
* keeping only the speech — and put in the composer, marked as said. Send
|
|
7
|
+
* it, and the answer is read aloud as it is written, sentence by sentence,
|
|
8
|
+
* by Datalayer's speech service (Kokoro, on ai-agents), the assistant's
|
|
9
|
+
* mouth moving with the sound and its balloon showing the sentence said.
|
|
10
|
+
*
|
|
11
|
+
* Three places, each named in the address when it is not this machine's:
|
|
12
|
+
* - `?agentRuntimesUrl=` — the agent-runtimes server and its `assistant`
|
|
13
|
+
* agent (http://127.0.0.1:8765);
|
|
14
|
+
* - `?voiceModelsUrl=` — the browser's models, as pinned
|
|
15
|
+
* (`scripts/voice/serve_store.py`, http://127.0.0.1:8770);
|
|
16
|
+
* - `?speechUrl=` — ai-agents' speech service (http://127.0.0.1:4401).
|
|
17
|
+
*/
|
|
18
|
+
import React from 'react';
|
|
19
|
+
declare const VoiceChatExample: React.FC;
|
|
20
|
+
export default VoiceChatExample;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } 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
|
+
* A voice chat with the floating assistant (VOICE.md V1).
|
|
8
|
+
*
|
|
9
|
+
* Hold the microphone (or `Ctrl`+`Space`), speak, let go: what you said is
|
|
10
|
+
* heard in this page — Moonshine for English, Whisper for French, Silero VAD
|
|
11
|
+
* keeping only the speech — and put in the composer, marked as said. Send
|
|
12
|
+
* it, and the answer is read aloud as it is written, sentence by sentence,
|
|
13
|
+
* by Datalayer's speech service (Kokoro, on ai-agents), the assistant's
|
|
14
|
+
* mouth moving with the sound and its balloon showing the sentence said.
|
|
15
|
+
*
|
|
16
|
+
* Three places, each named in the address when it is not this machine's:
|
|
17
|
+
* - `?agentRuntimesUrl=` — the agent-runtimes server and its `assistant`
|
|
18
|
+
* agent (http://127.0.0.1:8765);
|
|
19
|
+
* - `?voiceModelsUrl=` — the browser's models, as pinned
|
|
20
|
+
* (`scripts/voice/serve_store.py`, http://127.0.0.1:8770);
|
|
21
|
+
* - `?speechUrl=` — ai-agents' speech service (http://127.0.0.1:4401).
|
|
22
|
+
*/
|
|
23
|
+
import { useMemo, useState } from 'react';
|
|
24
|
+
import { Heading, Link, SegmentedControl, Text } from '@primer/react';
|
|
25
|
+
import { Box } from '@datalayer/primer-addons';
|
|
26
|
+
import { useSimpleAuthStore } from '@datalayer/core/lib/views/otel';
|
|
27
|
+
import { ThemedProvider } from './utils/themedProvider';
|
|
28
|
+
import { ChatFloating } from '../chat';
|
|
29
|
+
import { VOICE_CATALOGUE, voiceFor } from '../specs/voices';
|
|
30
|
+
function fromAddress(name, otherwise) {
|
|
31
|
+
return ((typeof window === 'undefined'
|
|
32
|
+
? null
|
|
33
|
+
: new URLSearchParams(window.location.search).get(name)) || otherwise).replace(/\/+$/, '');
|
|
34
|
+
}
|
|
35
|
+
const SERVER = fromAddress('agentRuntimesUrl', 'http://127.0.0.1:8765');
|
|
36
|
+
const MODELS = fromAddress('voiceModelsUrl', 'http://127.0.0.1:8770');
|
|
37
|
+
const SPEECH = fromAddress('speechUrl', 'http://127.0.0.1:4401');
|
|
38
|
+
/** The packages that hear, loaded the first time the microphone is used. */
|
|
39
|
+
const ENGINES = {
|
|
40
|
+
transformers: () => import('@huggingface/transformers'),
|
|
41
|
+
vad: () => import('@ricky0123/vad-web'),
|
|
42
|
+
};
|
|
43
|
+
const LANGUAGES = [
|
|
44
|
+
{ tag: 'en-US', label: 'English' },
|
|
45
|
+
{ tag: 'fr-FR', label: 'Français' },
|
|
46
|
+
];
|
|
47
|
+
const VoiceChatExample = () => {
|
|
48
|
+
const { token } = useSimpleAuthStore();
|
|
49
|
+
const [language, setLanguage] = useState('en-US');
|
|
50
|
+
const voiceSpec = voiceFor(language) ?? VOICE_CATALOGUE['kokoro-af-heart'];
|
|
51
|
+
const voice = useMemo(() => ({
|
|
52
|
+
language,
|
|
53
|
+
input: 'push_to_talk',
|
|
54
|
+
output: 'always',
|
|
55
|
+
voice: voiceSpec.id,
|
|
56
|
+
modelsUrl: MODELS,
|
|
57
|
+
engines: ENGINES,
|
|
58
|
+
speechUrl: SPEECH,
|
|
59
|
+
token: token || undefined,
|
|
60
|
+
consentKey: 'voice-chat-example',
|
|
61
|
+
}), [language, voiceSpec.id, token]);
|
|
62
|
+
return (_jsx(ThemedProvider, { children: _jsxs(Box, { sx: { minHeight: '100vh', bg: 'canvas.default', p: 4 }, children: [_jsxs(Box, { sx: { maxWidth: 720, mx: 'auto' }, children: [_jsx(Heading, { as: "h1", sx: { mb: 2 }, children: "Voice chat" }), _jsxs(Text, { as: "p", sx: { color: 'fg.muted', mb: 3 }, children: ["Talk to the assistant instead of typing: hold the microphone in its composer, or ", _jsx("kbd", { children: "Ctrl" }), "+", _jsx("kbd", { children: "Space" }), ", speak, and let go. What you say is heard in this page and never leaves it; the words go in the message box for you to send. Its answers are read aloud as they are written, by Datalayer\u2019s speech service, and not kept.", ' ', _jsx("kbd", { children: "Esc" }), " stops its voice."] }), _jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 3, mb: 3 }, children: [_jsx(SegmentedControl, { "aria-label": "Language", children: LANGUAGES.map(option => (_jsx(SegmentedControl.Button, { selected: language === option.tag, onClick: () => setLanguage(option.tag), "data-voice-language": option.tag, children: option.label }, option.tag))) }), _jsxs(Text, { sx: { color: 'fg.muted', fontSize: 1 }, children: ["Voice: ", voiceSpec.name] })] }), voiceSpec.attribution && (_jsx(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted', mb: 3 }, children: voiceSpec.attribution })), _jsxs(Text, { as: "p", sx: { fontSize: 0, color: 'fg.muted' }, children: ["Models from ", MODELS, ", speech from ", SPEECH, ", the agent at ", SERVER, ". See", ' ', _jsx(Link, { href: "https://agent-runtimes.datalayer.tech/chat/voice", children: "Voice" }), ' ', "for how to run each."] })] }), _jsx(ChatFloating, { defaultViewMode: "assistant", assistantCharacter: "paperclip", protocol: "vercel-ai", endpoint: `${SERVER}/api/v1/vercel-ai/assistant`, title: "Assistant", description: "Hello! Hold the microphone and ask me anything.", position: "bottom-right", useStore: false, voice: voice })] }) }));
|
|
63
|
+
};
|
|
64
|
+
export default VoiceChatExample;
|
|
@@ -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('VoiceChatExample', () => import('./VoiceChatExample'), 'A voice chat with the floating assistant: push-to-talk heard in the page, answers read aloud by the speech service, the mouth moving with the sound.'),
|
|
91
92
|
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.'),
|
|
92
93
|
makeEntry('ChatCustomExample', () => import('./ChatCustomExample'), 'Custom chat experience composition example.'),
|
|
93
94
|
makeEntry('ChatExample', () => import('./ChatExample'), 'Baseline chat integration example.'),
|
package/lib/examples/main.js
CHANGED
|
@@ -47,7 +47,7 @@ const DEFAULT_LOCAL_JUPYTER_SERVER_URL = 'http://localhost:8888/api/jupyter-serv
|
|
|
47
47
|
const DEFAULT_LOCAL_JUPYTER_SERVER_TOKEN = '60c1661cc408f978c309d04157af55c9588ff9557c9380e4fb50785750703da6';
|
|
48
48
|
const DEFAULT_CLOUD_RUNTIME_ENVIRONMENT = 'ai-agents-env';
|
|
49
49
|
const EXAMPLE_GROUP_ORDER = [
|
|
50
|
-
'
|
|
50
|
+
'Apps',
|
|
51
51
|
'A2UI',
|
|
52
52
|
'A2A',
|
|
53
53
|
'AG-UI',
|
|
@@ -70,6 +70,9 @@ const ANONYMOUS_EXAMPLES = new Set([
|
|
|
70
70
|
// An application beside its computer, on the local agent-runtimes server:
|
|
71
71
|
// nothing allocated on Datalayer, and the machine itself needs no token.
|
|
72
72
|
'LoopAppComputerExample',
|
|
73
|
+
// The Sales and Accounting team: Sales in the page on a visitor's trial
|
|
74
|
+
// key, Accounting on the local server, which answers the machine itself.
|
|
75
|
+
'AgentA2ATeamExample',
|
|
73
76
|
// Decide, on the local agent-runtimes server as above: Jev is asked
|
|
74
77
|
// through the server's ai-inference, with the server's own key.
|
|
75
78
|
'AgentDecideExample',
|
|
@@ -88,14 +91,15 @@ const ANONYMOUS_EXAMPLES = new Set([
|
|
|
88
91
|
'ChatAssistantExample',
|
|
89
92
|
// Every character in every state, from static data: no agent at all.
|
|
90
93
|
'ChatAssistantGalleryExample',
|
|
94
|
+
// Voice: heard in the page, the agent on the local server, the speech
|
|
95
|
+
// service named in the address.
|
|
96
|
+
'VoiceChatExample',
|
|
91
97
|
]);
|
|
92
98
|
const getExampleGroup = (id) => {
|
|
93
99
|
if (id === 'AgentspecsExample' ||
|
|
94
|
-
id
|
|
95
|
-
id === 'LoopWorkspaceExample' ||
|
|
96
|
-
id === 'LoopShellExample' ||
|
|
100
|
+
id.startsWith('Loop') ||
|
|
97
101
|
id === 'DecksAgent') {
|
|
98
|
-
return '
|
|
102
|
+
return 'Apps';
|
|
99
103
|
}
|
|
100
104
|
if (id.startsWith('A2Ui'))
|
|
101
105
|
return 'A2UI';
|
|
@@ -110,7 +114,7 @@ const getExampleGroup = (id) => {
|
|
|
110
114
|
// runtime: checkpoints, hooks, memory, guardrails…
|
|
111
115
|
if (id.startsWith('Agent'))
|
|
112
116
|
return 'Capabilities';
|
|
113
|
-
if (id.startsWith('Chat'))
|
|
117
|
+
if (id.startsWith('Chat') || id === 'VoiceChatExample')
|
|
114
118
|
return 'Chat';
|
|
115
119
|
// The document examples: the ones on the Lexical editor, and the page
|
|
116
120
|
// with a document on it.
|
|
@@ -1072,13 +1076,14 @@ const ExampleAppThemed = ({ selectedExample, isChangingExample, error, ExampleCo
|
|
|
1072
1076
|
}
|
|
1073
1077
|
for (const [groupName, examples] of groups) {
|
|
1074
1078
|
examples.sort((left, right) => {
|
|
1075
|
-
if (groupName === '
|
|
1079
|
+
if (groupName === 'Apps') {
|
|
1076
1080
|
// The shells first, most naked first; then the strategy that drives a
|
|
1077
1081
|
// notebook; then the library of specs behind them all.
|
|
1078
1082
|
const LOOP_ORDER = [
|
|
1079
1083
|
'LoopShellExample',
|
|
1080
1084
|
'LoopWorkspaceExample',
|
|
1081
1085
|
'LoopStrategyExample',
|
|
1086
|
+
'LoopAppComputerExample',
|
|
1082
1087
|
'AgentspecsExample',
|
|
1083
1088
|
];
|
|
1084
1089
|
const loopOrder = (id) => {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AssistantCharacterData } from '../../chat/assistant/formats/types';
|
|
2
|
+
import type { GalleryCharacter } from './AssistantGalleryGrid';
|
|
3
|
+
/** The version of `clippyjs` the characters are read from. */
|
|
4
|
+
export declare const CLIPPY_JS_VERSION = "0.1.0";
|
|
5
|
+
/** The characters the package publishes: their names, and their folders. */
|
|
6
|
+
export declare const CLIPPY_JS_CHARACTERS: readonly ["Clippy", "Merlin", "Links", "Rover", "Genie", "Genius", "F1", "Peedy", "Rocky", "Bonzi"];
|
|
7
|
+
export type ClippyJsCharacterName = (typeof CLIPPY_JS_CHARACTERS)[number];
|
|
8
|
+
/** One clippy.js character, fetched and read. */
|
|
9
|
+
export declare function readClippyJsCharacter(name: ClippyJsCharacterName): Promise<AssistantCharacterData>;
|
|
10
|
+
/**
|
|
11
|
+
* The clippy.js characters as the gallery shows them, fetched once `enabled`
|
|
12
|
+
* and kept while it stays so; each as soon as it is read.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useClippyJsCharacters(enabled: boolean): {
|
|
15
|
+
characters: readonly GalleryCharacter[];
|
|
16
|
+
loading: boolean;
|
|
17
|
+
problem?: string;
|
|
18
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The characters clippy.js publishes — Clippy, Merlin, Links, Rover and the
|
|
7
|
+
* others — loaded into the floating assistant's gallery from the `clippyjs`
|
|
8
|
+
* package on jsDelivr, at the reader's request, and read through the same
|
|
9
|
+
* clippy.js reader a person's files go through.
|
|
10
|
+
*
|
|
11
|
+
* Nothing of them is in this repository: clippy.js's licence covers its code
|
|
12
|
+
* only, and the characters, their names and their drawings are Microsoft's
|
|
13
|
+
* (§6.9). The page fetches them when asked and keeps them in memory.
|
|
14
|
+
*
|
|
15
|
+
* The package ships each character as ES modules: `agent.mjs` exports the
|
|
16
|
+
* character's data, `map.mjs` its sprite sheet as a data URL. Both are read as
|
|
17
|
+
* text, never run: the data is rewritten as the `clippy.ready(...)` call the
|
|
18
|
+
* reader expects. Sounds are left out.
|
|
19
|
+
*
|
|
20
|
+
* @module examples/utils/clippyJsCharacters
|
|
21
|
+
*/
|
|
22
|
+
import { useEffect, useState } from 'react';
|
|
23
|
+
import { readClippyCharacter } from '../../chat/assistant/formats/clippy';
|
|
24
|
+
/** The version of `clippyjs` the characters are read from. */
|
|
25
|
+
export const CLIPPY_JS_VERSION = '0.1.0';
|
|
26
|
+
const BASE = `https://cdn.jsdelivr.net/npm/clippyjs@${CLIPPY_JS_VERSION}/dist/agents`;
|
|
27
|
+
/** The characters the package publishes: their names, and their folders. */
|
|
28
|
+
export const CLIPPY_JS_CHARACTERS = [
|
|
29
|
+
'Clippy',
|
|
30
|
+
'Merlin',
|
|
31
|
+
'Links',
|
|
32
|
+
'Rover',
|
|
33
|
+
'Genie',
|
|
34
|
+
'Genius',
|
|
35
|
+
'F1',
|
|
36
|
+
'Peedy',
|
|
37
|
+
'Rocky',
|
|
38
|
+
'Bonzi',
|
|
39
|
+
];
|
|
40
|
+
async function fetchText(url) {
|
|
41
|
+
const response = await fetch(url);
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
throw new Error(`${url} answered ${response.status}.`);
|
|
44
|
+
}
|
|
45
|
+
return response.text();
|
|
46
|
+
}
|
|
47
|
+
/** The value a module exports by default: `var x = <value>;\nexport …`. */
|
|
48
|
+
function defaultExportText(module, what) {
|
|
49
|
+
const match = /^var [A-Za-z_$][\w$]* = ([\s\S]*?);\s*export\s*\{/m.exec(module);
|
|
50
|
+
if (!match) {
|
|
51
|
+
throw new Error(`${what} does not export its value as clippy.js 0.1 does.`);
|
|
52
|
+
}
|
|
53
|
+
return match[1];
|
|
54
|
+
}
|
|
55
|
+
/** One clippy.js character, fetched and read. */
|
|
56
|
+
export async function readClippyJsCharacter(name) {
|
|
57
|
+
const folder = `${BASE}/${name.toLowerCase()}`;
|
|
58
|
+
const [agentModule, mapModule] = await Promise.all([
|
|
59
|
+
fetchText(`${folder}/agent.mjs`),
|
|
60
|
+
fetchText(`${folder}/map.mjs`),
|
|
61
|
+
]);
|
|
62
|
+
const data = defaultExportText(agentModule, `${name}'s agent.mjs`);
|
|
63
|
+
const mapUrl = defaultExportText(mapModule, `${name}'s map.mjs`);
|
|
64
|
+
if (!/^"data:image\/(png|gif|webp|jpeg);base64,[A-Za-z0-9+/=]+"$/.test(mapUrl)) {
|
|
65
|
+
throw new Error(`${name}'s map.mjs does not hold an image.`);
|
|
66
|
+
}
|
|
67
|
+
const mapPng = await (await fetch(mapUrl.slice(1, -1))).blob();
|
|
68
|
+
return readClippyCharacter({
|
|
69
|
+
agentJs: `clippy.ready(${JSON.stringify(name)}, ${data});`,
|
|
70
|
+
mapPng,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The clippy.js characters as the gallery shows them, fetched once `enabled`
|
|
75
|
+
* and kept while it stays so; each as soon as it is read.
|
|
76
|
+
*/
|
|
77
|
+
export function useClippyJsCharacters(enabled) {
|
|
78
|
+
const [read, setRead] = useState([]);
|
|
79
|
+
const [pending, setPending] = useState(0);
|
|
80
|
+
const [problem, setProblem] = useState();
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!enabled) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
let cancelled = false;
|
|
86
|
+
const loaded = [];
|
|
87
|
+
setRead([]);
|
|
88
|
+
setProblem(undefined);
|
|
89
|
+
setPending(CLIPPY_JS_CHARACTERS.length);
|
|
90
|
+
for (const name of CLIPPY_JS_CHARACTERS) {
|
|
91
|
+
readClippyJsCharacter(name).then(character => {
|
|
92
|
+
if (cancelled) {
|
|
93
|
+
URL.revokeObjectURL(character.sprite);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
loaded.push(character);
|
|
97
|
+
setRead(previous => [
|
|
98
|
+
...previous,
|
|
99
|
+
{
|
|
100
|
+
id: `clippy-js-${name.toLowerCase()}`,
|
|
101
|
+
name: character.name,
|
|
102
|
+
character,
|
|
103
|
+
from: `clippy.js ${CLIPPY_JS_VERSION}`,
|
|
104
|
+
},
|
|
105
|
+
].sort((a, b) => CLIPPY_JS_CHARACTERS.indexOf(a.name) -
|
|
106
|
+
CLIPPY_JS_CHARACTERS.indexOf(b.name)));
|
|
107
|
+
setPending(count => count - 1);
|
|
108
|
+
}, error => {
|
|
109
|
+
if (cancelled) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
setProblem(`${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
113
|
+
setPending(count => count - 1);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// The object URLs live as long as the characters are shown.
|
|
117
|
+
return () => {
|
|
118
|
+
cancelled = true;
|
|
119
|
+
for (const character of loaded) {
|
|
120
|
+
URL.revokeObjectURL(character.sprite);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}, [enabled]);
|
|
124
|
+
return {
|
|
125
|
+
characters: enabled ? read : [],
|
|
126
|
+
loading: enabled && pending > 0,
|
|
127
|
+
problem: enabled ? problem : undefined,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*
|
|
25
25
|
* @module loop/apps/appspec
|
|
26
26
|
*/
|
|
27
|
-
import type { AppAccent, AppAssistantCharacter, AppEmbedMode, AppKind, AppLayout, AppSpec } from '../../types/agentspecs';
|
|
27
|
+
import type { AppAccent, AppAssistantCharacter, AppEmbedMode, AppVoiceSpec, AppKind, AppLayout, AppSpec } from '../../types/agentspecs';
|
|
28
28
|
/** The version of the spec itself. */
|
|
29
29
|
export declare const APP_SCHEMA = "loop.app/v1";
|
|
30
30
|
/** The face of an application that has not chosen one. */
|
|
@@ -51,6 +51,8 @@ type Data = Record<string, unknown>;
|
|
|
51
51
|
export declare function retentionDays(keepFor: string): number;
|
|
52
52
|
/** An application with nothing said: what *Start blank* opens. */
|
|
53
53
|
export declare function emptyAppspec(kind?: AppKind): AppSpec;
|
|
54
|
+
/** An application's voice (VOICE.md VO-41): off unless said. */
|
|
55
|
+
export declare const DEFAULT_VOICE: AppVoiceSpec;
|
|
54
56
|
/** What reading a document gives: the application, and what the document got wrong. */
|
|
55
57
|
export interface ParsedAppspec {
|
|
56
58
|
app: AppSpec;
|
package/lib/loop/apps/appspec.js
CHANGED
|
@@ -214,6 +214,26 @@ function parseSurface(data) {
|
|
|
214
214
|
composedAt: text(data.composed_at),
|
|
215
215
|
};
|
|
216
216
|
}
|
|
217
|
+
/** An application's voice (VOICE.md VO-41): off unless said. */
|
|
218
|
+
export const DEFAULT_VOICE = {
|
|
219
|
+
enabled: false,
|
|
220
|
+
input: 'push_to_talk',
|
|
221
|
+
output: 'on_request',
|
|
222
|
+
voice: '',
|
|
223
|
+
language: '',
|
|
224
|
+
where: 'auto',
|
|
225
|
+
};
|
|
226
|
+
function parseVoice(data) {
|
|
227
|
+
const voice = isData(data) ? data : {};
|
|
228
|
+
return {
|
|
229
|
+
enabled: voice.enabled === true,
|
|
230
|
+
input: oneOf(voice.input, ['off', 'push_to_talk', 'hands_free'], DEFAULT_VOICE.input),
|
|
231
|
+
output: oneOf(voice.output, ['off', 'on_request', 'always'], DEFAULT_VOICE.output),
|
|
232
|
+
voice: text(voice.voice),
|
|
233
|
+
language: text(voice.language),
|
|
234
|
+
where: oneOf(voice.where, ['auto', 'device', 'server'], DEFAULT_VOICE.where),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
217
237
|
function parseInterface(data, kind) {
|
|
218
238
|
const parsed = {
|
|
219
239
|
layout: oneOf(data.layout, ['chat', 'page', 'split'], DEFAULT_LAYOUTS[kind]),
|
|
@@ -225,6 +245,7 @@ function parseInterface(data, kind) {
|
|
|
225
245
|
})),
|
|
226
246
|
settings: records(data.settings).map(parseSetting),
|
|
227
247
|
components: texts(data.components),
|
|
248
|
+
voice: parseVoice(data.voice),
|
|
228
249
|
};
|
|
229
250
|
if (isData(data.surface)) {
|
|
230
251
|
parsed.surface = parseSurface(data.surface);
|
|
@@ -493,6 +514,16 @@ function dumpInterface(spec, kind) {
|
|
|
493
514
|
if (spec.assistant) {
|
|
494
515
|
writer.data.assistant = spec.assistant;
|
|
495
516
|
}
|
|
517
|
+
// Its voice, when it says one (VO-41): a spec made before voice has none.
|
|
518
|
+
if (spec.voice) {
|
|
519
|
+
writer.part('voice', new Writer()
|
|
520
|
+
.value('enabled', spec.voice.enabled, DEFAULT_VOICE.enabled)
|
|
521
|
+
.value('input', spec.voice.input, DEFAULT_VOICE.input)
|
|
522
|
+
.value('output', spec.voice.output, DEFAULT_VOICE.output)
|
|
523
|
+
.text('voice', spec.voice.voice)
|
|
524
|
+
.text('language', spec.voice.language)
|
|
525
|
+
.value('where', spec.voice.where, DEFAULT_VOICE.where).data);
|
|
526
|
+
}
|
|
496
527
|
return writer.data;
|
|
497
528
|
}
|
|
498
529
|
function dumpDecision(decision) {
|