@datalayer/agent-runtimes 1.3.63 → 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 +48 -1
- 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/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 +7 -1
- package/lib/loop/apps/appspec.d.ts +3 -1
- package/lib/loop/apps/appspec.js +31 -0
- 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/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
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* @module chat/ChatFloating
|
|
17
17
|
*/
|
|
18
18
|
import React from 'react';
|
|
19
|
+
import type { ChatVoice } from '../voice';
|
|
19
20
|
import { type AssistantCharacter } from './assistant/characters';
|
|
20
21
|
import type { AssistantCharacterData } from './assistant/formats/types';
|
|
21
22
|
import { type AssistantSaying } from './assistant/state';
|
|
@@ -149,10 +150,17 @@ export interface ChatFloatingProps extends ChatCommonProps {
|
|
|
149
150
|
* own props (`protocol`, `suggestions`, …) are then not read.
|
|
150
151
|
*/
|
|
151
152
|
conversation?: FloatingConversation;
|
|
153
|
+
/**
|
|
154
|
+
* Its voice (VOICE.md V1): push-to-talk in the composer, heard on the
|
|
155
|
+
* device; with `output: 'always'`, its answers said by Datalayer's speech
|
|
156
|
+
* service as they are written, the assistant's mouth moving with the sound
|
|
157
|
+
* and its balloon showing the sentence being said.
|
|
158
|
+
*/
|
|
159
|
+
voice?: ChatVoice;
|
|
152
160
|
}
|
|
153
161
|
/**
|
|
154
162
|
* ChatFloating component
|
|
155
163
|
* A floating chat window built on ChatBase
|
|
156
164
|
*/
|
|
157
|
-
export declare function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode, title, description, position, defaultOpen, width, height, showHeader, showButton, showNewChatButton, showClearButton, showSettingsButton, enableKeyboardShortcuts, toggleShortcut, showPoweredBy, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose, escapeToClose, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip, brandColor, buttonGlow, offset, animationDuration, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick, hideMessagesAfterToolUI, defaultViewMode, onViewModeChange, sidebarMountPoint, showPanelBackdrop, availableModels, showModelSelector, showToolsMenu, showSkillsMenu, showTokenUsage, showContextRing, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching, launchingMessage, assistantCharacter, conversation, decisions, }: ChatFloatingProps): React.JSX.Element;
|
|
165
|
+
export declare function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode, title, description, position, defaultOpen, width, height, showHeader, showButton, showNewChatButton, showClearButton, showSettingsButton, enableKeyboardShortcuts, toggleShortcut, showPoweredBy, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose, escapeToClose, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip, brandColor, buttonGlow, offset, animationDuration, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick, hideMessagesAfterToolUI, defaultViewMode, onViewModeChange, sidebarMountPoint, showPanelBackdrop, availableModels, showModelSelector, showToolsMenu, showSkillsMenu, showTokenUsage, showContextRing, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching, launchingMessage, assistantCharacter, conversation, decisions, voice, }: ChatFloatingProps): React.JSX.Element;
|
|
158
166
|
export default ChatFloating;
|
package/lib/chat/ChatFloating.js
CHANGED
|
@@ -33,6 +33,8 @@ import { disabledChatViewModes, resolveMountPoint } from './viewModes';
|
|
|
33
33
|
import { AssistantStage } from './assistant/AssistantStage';
|
|
34
34
|
import { decisionsAskerAt } from './assistant/decisions';
|
|
35
35
|
import { DecisionAsk } from './assistant/DecisionAsk';
|
|
36
|
+
import { ServerSpeaker } from '../voice/speaker';
|
|
37
|
+
import { useSpokenAnswers } from '../voice/useSpokenAnswers';
|
|
36
38
|
import { BalloonApprovalMessage, CONVERSATION_BALLOON_WIDTH, ConversationBalloonClose, balloonTailAt, conversationBalloonHeight, conversationBalloonSx, peekLine, } from './assistant/ConversationBalloon';
|
|
37
39
|
import { SpeechBalloon } from './assistant/SpeechBalloon';
|
|
38
40
|
import { DEFAULT_ASSISTANT_CHARACTER, } from './assistant/characters';
|
|
@@ -66,7 +68,7 @@ const clamp = (value, max) => Math.max(8, Math.min(value, Math.max(8, max)));
|
|
|
66
68
|
* ChatFloating component
|
|
67
69
|
* A floating chat window built on ChatBase
|
|
68
70
|
*/
|
|
69
|
-
export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode = true, title = 'Chat', description = 'Start a conversation with the AI agent.', position = 'bottom-right', defaultOpen = false, width = 400, height = 550, showHeader = true, showButton = true, showNewChatButton = true, showClearButton = true, showSettingsButton = false, enableKeyboardShortcuts = true, toggleShortcut = '/', showPoweredBy = true, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose = false, escapeToClose = true, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip = 'Chat with AI', brandColor, buttonGlow = true, offset = 20, animationDuration = 200, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, defaultViewMode = 'floating', onViewModeChange, sidebarMountPoint = null, showPanelBackdrop = false, availableModels, showModelSelector = false, showToolsMenu = false, showSkillsMenu = false, showTokenUsage = true, showContextRing = false, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation = false, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching = false, launchingMessage, assistantCharacter = DEFAULT_ASSISTANT_CHARACTER, conversation, decisions, }) {
|
|
71
|
+
export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useStoreMode = true, title = 'Chat', description = 'Start a conversation with the AI agent.', position = 'bottom-right', defaultOpen = false, width = 400, height = 550, showHeader = true, showButton = true, showNewChatButton = true, showClearButton = true, showSettingsButton = false, enableKeyboardShortcuts = true, toggleShortcut = '/', showPoweredBy = true, promptVariant, mentionableAgents, poweredByProps, clickOutsideToClose = false, escapeToClose = true, className, onSettingsClick, onNewChat, onOpen, onClose, onStateUpdate, children, brandIcon, buttonIcon, buttonTooltip = 'Chat with AI', brandColor, buttonGlow = true, offset = 20, animationDuration = 200, renderToolResult, frontendTools, initialState: _initialState, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, defaultViewMode = 'floating', onViewModeChange, sidebarMountPoint = null, showPanelBackdrop = false, availableModels, showModelSelector = false, showToolsMenu = false, showSkillsMenu = false, showTokenUsage = true, showContextRing = false, themeVariant, themeOverrides, colorMode, runtimeId, historyEndpoint, authToken, historyAuthToken, pendingPrompt, showInformation = false, onInformationClick, onToolCallStart, onToolCallComplete, showToolApprovalBanner, pendingApprovals, onApproveApproval, onRejectApproval, panelProps, launching = false, launchingMessage, assistantCharacter = DEFAULT_ASSISTANT_CHARACTER, conversation, decisions, voice, }) {
|
|
70
72
|
// Store-based state
|
|
71
73
|
const storeIsOpen = useChatOpen();
|
|
72
74
|
const storeMessages = useChatMessages();
|
|
@@ -129,6 +131,47 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
129
131
|
pendingApproval: false,
|
|
130
132
|
});
|
|
131
133
|
const [ownAnswering, setAnswering] = useState(false);
|
|
134
|
+
/*
|
|
135
|
+
* Its voice (VOICE.md V1): the answers said by the speech service, as
|
|
136
|
+
* they are written (VO-20), the state following the sound (VO-22).
|
|
137
|
+
*/
|
|
138
|
+
const [voiceItems, setVoiceItems] = useState([]);
|
|
139
|
+
const [speakerState, setSpeakerState] = useState({
|
|
140
|
+
speaking: false,
|
|
141
|
+
});
|
|
142
|
+
const speaker = useMemo(() => voice && voice.output === 'always' && voice.speechUrl
|
|
143
|
+
? new ServerSpeaker({
|
|
144
|
+
speechUrl: voice.speechUrl,
|
|
145
|
+
voice: voice.voice,
|
|
146
|
+
language: voice.language,
|
|
147
|
+
token: voice.token,
|
|
148
|
+
onState: setSpeakerState,
|
|
149
|
+
})
|
|
150
|
+
: undefined, [voice?.output, voice?.speechUrl]);
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
speaker?.update({
|
|
153
|
+
voice: voice?.voice,
|
|
154
|
+
language: voice?.language,
|
|
155
|
+
token: voice?.token,
|
|
156
|
+
});
|
|
157
|
+
}, [speaker, voice?.voice, voice?.language, voice?.token]);
|
|
158
|
+
useEffect(() => () => speaker?.close(), [speaker]);
|
|
159
|
+
// A browser plays only what a gesture allowed: the first one does.
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (!speaker) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const unlock = () => speaker.unlock();
|
|
165
|
+
window.addEventListener('pointerdown', unlock, { once: true });
|
|
166
|
+
window.addEventListener('keydown', unlock, { once: true });
|
|
167
|
+
return () => {
|
|
168
|
+
window.removeEventListener('pointerdown', unlock);
|
|
169
|
+
window.removeEventListener('keydown', unlock);
|
|
170
|
+
};
|
|
171
|
+
}, [speaker]);
|
|
172
|
+
useSpokenAnswers(voiceItems, chatBusy, !!speaker, speaker);
|
|
173
|
+
const stopSpeaking = useCallback(() => speaker?.stop(), [speaker]);
|
|
174
|
+
const mouthLevel = useMemo(() => (speaker ? () => speaker.level() : undefined), [speaker]);
|
|
132
175
|
const [arriving, setArriving] = useState(false);
|
|
133
176
|
const [leaving, setLeaving] = useState(false);
|
|
134
177
|
const [assistantAway, setAssistantAway] = useState(keptAway);
|
|
@@ -173,6 +216,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
173
216
|
arriving,
|
|
174
217
|
leaving,
|
|
175
218
|
speaking: answering,
|
|
219
|
+
voicing: speakerState.speaking,
|
|
176
220
|
});
|
|
177
221
|
const unheard = !!saying && saying.id !== heardId;
|
|
178
222
|
/*
|
|
@@ -202,21 +246,27 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
202
246
|
: undefined, [decisions?.serverUrl, decisions?.token]);
|
|
203
247
|
const assistantBalloon = assistantState === 'paused'
|
|
204
248
|
? { text: ASSISTANT_WORDS.paused }
|
|
205
|
-
:
|
|
206
|
-
?
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
249
|
+
: speakerState.sentence
|
|
250
|
+
? // Heard: the sentence being said is the one in the balloon (VO-26).
|
|
251
|
+
{ text: speakerState.sentence }
|
|
252
|
+
: speakerState.refused
|
|
253
|
+
? { text: speakerState.refused }
|
|
254
|
+
: balloonApproval
|
|
255
|
+
? { text: ASSISTANT_WORDS.approval, approval: balloonApproval }
|
|
256
|
+
: assistantState === 'waiting'
|
|
257
|
+
? { text: ASSISTANT_WORDS.waiting }
|
|
258
|
+
: unheard && saying
|
|
259
|
+
? {
|
|
260
|
+
// A peek: the first words; the rest is in the conversation.
|
|
261
|
+
...peekLine(saying.text),
|
|
262
|
+
onDismiss: () => {
|
|
263
|
+
setHeardId(saying.id);
|
|
264
|
+
setFreshSaying(false);
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
: { text: description };
|
|
268
|
+
const balloonInsists = !!speakerState.sentence ||
|
|
269
|
+
assistantState === 'greeting' ||
|
|
220
270
|
assistantState === 'waiting' ||
|
|
221
271
|
!!balloonApproval ||
|
|
222
272
|
(unheard && (assistantState === 'speaking' || freshSaying));
|
|
@@ -233,6 +283,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
233
283
|
? previous
|
|
234
284
|
: tool);
|
|
235
285
|
setAnswering(newestIsAnswer(items));
|
|
286
|
+
setVoiceItems(items);
|
|
236
287
|
const said = latestSaying(items);
|
|
237
288
|
setSaying(previous => previous?.id === said?.id && previous?.text === said?.text
|
|
238
289
|
? previous
|
|
@@ -906,7 +957,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
906
957
|
...poweredByProps,
|
|
907
958
|
}, renderToolResult: renderToolResult, description: description, onStateUpdate: onStateUpdate, onNewChat: onNewChat, suggestions: suggestions, submitOnSuggestionClick: submitOnSuggestionClick, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: {
|
|
908
959
|
showAvatars: true,
|
|
909
|
-
}, placeholder: "Type a message...", backgroundColor: "canvas.subtle", frontendTools: frontendTools, showModelSelector: showModelSelector, availableModels: availableModels, showToolsMenu: showToolsMenu, showSkillsMenu: showSkillsMenu, showTokenUsage: showTokenUsage, showContextRing: showContextRing, runtimeId: runtimeId, historyEndpoint: historyEndpoint, historyAuthToken: historyAuthToken, pendingPrompt: pendingPrompt, showInformation: showInformation, onInformationClick: onInformationClick, onToolCallStart: onToolCallStart, onToolCallComplete: onToolCallComplete, showToolApprovalBanner: showToolApprovalBanner, pendingApprovals: pendingApprovals, onApproveApproval: onApproveApproval, onRejectApproval: onRejectApproval, ...panelProps, ...assistantChat, onLoadingChange: handleLoadingChange, onDisplayItemsChange: handleDisplayItemsChange, children: children }))] }));
|
|
960
|
+
}, placeholder: "Type a message...", backgroundColor: "canvas.subtle", frontendTools: frontendTools, showModelSelector: showModelSelector, availableModels: availableModels, showToolsMenu: showToolsMenu, showSkillsMenu: showSkillsMenu, showTokenUsage: showTokenUsage, showContextRing: showContextRing, runtimeId: runtimeId, historyEndpoint: historyEndpoint, historyAuthToken: historyAuthToken, pendingPrompt: pendingPrompt, showInformation: showInformation, onInformationClick: onInformationClick, onToolCallStart: onToolCallStart, onToolCallComplete: onToolCallComplete, showToolApprovalBanner: showToolApprovalBanner, pendingApprovals: pendingApprovals, onApproveApproval: onApproveApproval, onRejectApproval: onRejectApproval, voice: voice, voiceSpeaking: speakerState.speaking, onStopSpeaking: speaker ? stopSpeaking : undefined, ...panelProps, ...assistantChat, onLoadingChange: handleLoadingChange, onDisplayItemsChange: handleDisplayItemsChange, children: children }))] }));
|
|
910
961
|
/*
|
|
911
962
|
* The floating popup's button. Words not yet heard make it blink for the
|
|
912
963
|
* person's attention, until the chat is opened; hovered, they show in a
|
|
@@ -954,7 +1005,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
954
1005
|
left: px(stageDrag.position.left),
|
|
955
1006
|
top: px(stageDrag.position.top),
|
|
956
1007
|
}
|
|
957
|
-
: getPositionStyles(), stageRef: stageRef, onDragStart: stageDrag.onHandlePointerDown, open: isOpen, onToggle: handleToggle, balloon: assistantBalloon, insist: balloonInsists, onDismiss: dismissAssistant, ownRef: popupRef })), showButton && !isOpen && !assistantShown && (_jsx(Box, { sx: {
|
|
1008
|
+
: getPositionStyles(), stageRef: stageRef, onDragStart: stageDrag.onHandlePointerDown, open: isOpen, onToggle: handleToggle, balloon: assistantBalloon, insist: balloonInsists, onDismiss: dismissAssistant, ownRef: popupRef, mouthLevel: mouthLevel })), showButton && !isOpen && !assistantShown && (_jsx(Box, { sx: {
|
|
958
1009
|
...getPositionStyles(),
|
|
959
1010
|
}, children: _jsxs(Box, { sx: {
|
|
960
1011
|
position: 'relative',
|
|
@@ -94,6 +94,19 @@ export interface AssistantStageProps {
|
|
|
94
94
|
* decision*, and stays while its form or its answer is on screen.
|
|
95
95
|
*/
|
|
96
96
|
decide?: DecisionAsker;
|
|
97
|
+
/**
|
|
98
|
+
* How loud its voice is now, between 0 and 1, while it is heard (VOICE.md
|
|
99
|
+
* VO-23): the mouth opens with the sound, in three openings, instead of
|
|
100
|
+
* the talking loop. Still, for a reader who asks for reduced motion.
|
|
101
|
+
*/
|
|
102
|
+
mouthLevel?: () => number;
|
|
97
103
|
}
|
|
98
|
-
|
|
104
|
+
/** The openings of the mouth, by level: shut, a little, half, wide. */
|
|
105
|
+
export declare function mouthOpening(level: number): 0 | 1 | 2 | 3;
|
|
106
|
+
/**
|
|
107
|
+
* Moves the mouth with the voice: the opening is written on the stage as
|
|
108
|
+
* `data-assistant-mouth`, each frame, without drawing the character again.
|
|
109
|
+
*/
|
|
110
|
+
export declare function useMouth(stageRef: RefObject<HTMLElement | null>, mouthLevel: (() => number) | undefined, speaking: boolean): void;
|
|
111
|
+
export declare function AssistantStage({ character, state, size, place, stageRef, onDragStart, open, onToggle, balloon, insist, onDismiss, ownRef, decide, mouthLevel, }: AssistantStageProps): JSX.Element;
|
|
99
112
|
export default AssistantStage;
|
|
@@ -180,7 +180,36 @@ export function useKeepClear(stageRef, own, paused) {
|
|
|
180
180
|
}, [stageRef, own, paused]);
|
|
181
181
|
return obstructed ? 'obstacle' : pointerBusy ? 'pointer' : undefined;
|
|
182
182
|
}
|
|
183
|
-
|
|
183
|
+
/** The openings of the mouth, by level: shut, a little, half, wide. */
|
|
184
|
+
export function mouthOpening(level) {
|
|
185
|
+
return level < 0.08 ? 0 : level < 0.3 ? 1 : level < 0.6 ? 2 : 3;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Moves the mouth with the voice: the opening is written on the stage as
|
|
189
|
+
* `data-assistant-mouth`, each frame, without drawing the character again.
|
|
190
|
+
*/
|
|
191
|
+
export function useMouth(stageRef, mouthLevel, speaking) {
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
const stage = stageRef.current;
|
|
194
|
+
const still = typeof window !== 'undefined' &&
|
|
195
|
+
!!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
196
|
+
if (!stage || !mouthLevel || !speaking || still) {
|
|
197
|
+
stage?.removeAttribute('data-assistant-mouth');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
let frame = 0;
|
|
201
|
+
const move = () => {
|
|
202
|
+
stage.setAttribute('data-assistant-mouth', String(mouthOpening(mouthLevel())));
|
|
203
|
+
frame = requestAnimationFrame(move);
|
|
204
|
+
};
|
|
205
|
+
frame = requestAnimationFrame(move);
|
|
206
|
+
return () => {
|
|
207
|
+
cancelAnimationFrame(frame);
|
|
208
|
+
stage.removeAttribute('data-assistant-mouth');
|
|
209
|
+
};
|
|
210
|
+
}, [stageRef, mouthLevel, speaking]);
|
|
211
|
+
}
|
|
212
|
+
export function AssistantStage({ character, state, size = 88, place, stageRef, onDragStart, open, onToggle, balloon, insist = false, onDismiss, ownRef, decide, mouthLevel, }) {
|
|
184
213
|
// A shipped one by id, a drawing contributed by a plugin (T-24), or a
|
|
185
214
|
// character read from a file (T-26).
|
|
186
215
|
const shipped = typeof character === 'string'
|
|
@@ -199,6 +228,7 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
|
|
|
199
228
|
// Where the press began: a press that moves is a drag, not a click.
|
|
200
229
|
const pressedAt = useRef(null);
|
|
201
230
|
const aside = useKeepClear(stageRef, ownRef, menuOpen);
|
|
231
|
+
useMouth(stageRef, mouthLevel, state === 'speaking');
|
|
202
232
|
// A decision being asked, or its answer, keeps the balloon up.
|
|
203
233
|
const [deciding, setDeciding] = useState(false);
|
|
204
234
|
const showBalloon = !aside && !open && !!balloon && (hovered || insist || deciding);
|
|
@@ -261,6 +291,23 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
|
|
|
261
291
|
'&[data-assistant-state="speaking"] .assistant-body': {
|
|
262
292
|
animation: 'assistantFloat 1.6s ease-in-out infinite',
|
|
263
293
|
},
|
|
294
|
+
// Heard (VO-23): the mouth opens with the sound, not on a loop.
|
|
295
|
+
'&[data-assistant-mouth] .assistant-mouth': {
|
|
296
|
+
animation: 'none',
|
|
297
|
+
transition: 'transform 60ms linear',
|
|
298
|
+
},
|
|
299
|
+
'&[data-assistant-mouth="0"] .assistant-mouth': {
|
|
300
|
+
transform: 'scaleY(1)',
|
|
301
|
+
},
|
|
302
|
+
'&[data-assistant-mouth="1"] .assistant-mouth': {
|
|
303
|
+
transform: 'scaleY(1.5)',
|
|
304
|
+
},
|
|
305
|
+
'&[data-assistant-mouth="2"] .assistant-mouth': {
|
|
306
|
+
transform: 'scaleY(2.1)',
|
|
307
|
+
},
|
|
308
|
+
'&[data-assistant-mouth="3"] .assistant-mouth': {
|
|
309
|
+
transform: 'scaleY(2.8)',
|
|
310
|
+
},
|
|
264
311
|
// Paused (R-17): it dozes — its eyes shut, its breath slow, a little
|
|
265
312
|
// greyed — and, still, it is asleep: the shut eyes are not a motion.
|
|
266
313
|
'&[data-assistant-state="paused"] .assistant-body': {
|
|
@@ -17,6 +17,11 @@ export interface AssistantMoment {
|
|
|
17
17
|
leaving?: boolean;
|
|
18
18
|
/** The answer is arriving: words are being written. */
|
|
19
19
|
speaking?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Its answer is being heard: sound is playing (VOICE.md VO-22). It speaks
|
|
22
|
+
* while the audio plays, even after the text has ended.
|
|
23
|
+
*/
|
|
24
|
+
voicing?: boolean;
|
|
20
25
|
}
|
|
21
26
|
/**
|
|
22
27
|
* The state to act, from the application's presence and the moment.
|
|
@@ -24,7 +29,8 @@ export interface AssistantMoment {
|
|
|
24
29
|
* Leaving wins, being short; then paused — a paused application neither
|
|
25
30
|
* greets nor speaks, it dozes; then arriving; then waiting for the person,
|
|
26
31
|
* which must never be missed; then speaking over thinking, since words
|
|
27
|
-
* arriving are what the person watches
|
|
32
|
+
* arriving are what the person watches — and while its voice is heard, even
|
|
33
|
+
* once the words have all arrived; then the presence as it is.
|
|
28
34
|
*/
|
|
29
35
|
export declare function assistantStateOf(presence: PresenceState, moment?: AssistantMoment): AssistantState;
|
|
30
36
|
/**
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* Leaving wins, being short; then paused — a paused application neither
|
|
9
9
|
* greets nor speaks, it dozes; then arriving; then waiting for the person,
|
|
10
10
|
* which must never be missed; then speaking over thinking, since words
|
|
11
|
-
* arriving are what the person watches
|
|
11
|
+
* arriving are what the person watches — and while its voice is heard, even
|
|
12
|
+
* once the words have all arrived; then the presence as it is.
|
|
12
13
|
*/
|
|
13
14
|
export function assistantStateOf(presence, moment = {}) {
|
|
14
15
|
if (moment.leaving) {
|
|
@@ -23,7 +24,7 @@ export function assistantStateOf(presence, moment = {}) {
|
|
|
23
24
|
if (presence === 'waiting') {
|
|
24
25
|
return 'waiting';
|
|
25
26
|
}
|
|
26
|
-
if (moment.speaking && presence !== 'idle') {
|
|
27
|
+
if ((moment.speaking && presence !== 'idle') || moment.voicing) {
|
|
27
28
|
return 'speaking';
|
|
28
29
|
}
|
|
29
30
|
return presence;
|
|
@@ -45,6 +45,8 @@ import { PoweredByTag } from '../display/PoweredByTag';
|
|
|
45
45
|
import { EphemeralSurfaceControl } from '../EphemeralSurfaceControl';
|
|
46
46
|
import { ChatMessageList, } from '../messages/ChatMessageList';
|
|
47
47
|
import { InputPrompt } from '../prompt/InputPrompt';
|
|
48
|
+
import { DeviceHearing } from '../../voice/hearing';
|
|
49
|
+
import { VoiceInput } from '../../voice/VoiceInput';
|
|
48
50
|
import { isFloatingChatViewMode } from '../viewModes';
|
|
49
51
|
import { ToolApprovalBanner, ToolApprovalDialog, } from '../tools';
|
|
50
52
|
import { EphemeralNotebook } from '../notebook/EphemeralNotebook';
|
|
@@ -509,7 +511,7 @@ function ChatBaseInner({ title, subtitle, showHeader = false, showTokenUsage = t
|
|
|
509
511
|
// Mode selection
|
|
510
512
|
useStore: useStoreMode = true, protocol: protocolRaw, onSendMessage, onSendReady, onLoadingChange, onContextSnapshot, enableStreaming = false,
|
|
511
513
|
// Extended props
|
|
512
|
-
brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, renderHeader, showTurnFooter = true, notebookToolSurfacesId, footerContent, welcome, trailingContent, showInformation = false, onInformationClick, headerContent, children, borderRadius, backgroundColor, border, boxShadow, compact = false, placeholder, description = 'Start a conversation with the AI agent.', onStateUpdate, onNewChat, onClear, onMessagesChange, onItemsChange, onDisplayItemsChange, autoFocus = false, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, focusTrigger, frontendTools: frontendToolsProp, enableEphemeralNotebook = false, initialEphemeralNotebookOpen = true, onEphemeralNotebookOpenChange, enableEphemeralDocument = false, initialEphemeralSurfaceMode, onEphemeralSurfaceModeChange, collapsed = false, onExpandFromCollapsed, ephemeralNotebookToolbar, ephemeralNotebookToolbarExtraItems, ephemeralDocumentToolbarExtraItems, ephemeralNotebookCollaborationProvider, ephemeralNotebookCollaborationDocumentId, ephemeralDocumentCollaboration, ephemeralRuntimeOverride,
|
|
514
|
+
brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderToolResult, renderHeader, showTurnFooter = true, notebookToolSurfacesId, footerContent, welcome, trailingContent, voice, voiceSpeaking = false, onStopSpeaking, showInformation = false, onInformationClick, headerContent, children, borderRadius, backgroundColor, border, boxShadow, compact = false, placeholder, description = 'Start a conversation with the AI agent.', onStateUpdate, onNewChat, onClear, onMessagesChange, onItemsChange, onDisplayItemsChange, autoFocus = false, suggestions, submitOnSuggestionClick = true, hideMessagesAfterToolUI = false, focusTrigger, frontendTools: frontendToolsProp, enableEphemeralNotebook = false, initialEphemeralNotebookOpen = true, onEphemeralNotebookOpenChange, enableEphemeralDocument = false, initialEphemeralSurfaceMode, onEphemeralSurfaceModeChange, collapsed = false, onExpandFromCollapsed, ephemeralNotebookToolbar, ephemeralNotebookToolbarExtraItems, ephemeralDocumentToolbarExtraItems, ephemeralNotebookCollaborationProvider, ephemeralNotebookCollaborationDocumentId, ephemeralDocumentCollaboration, ephemeralRuntimeOverride,
|
|
513
515
|
// Tool invocation hooks
|
|
514
516
|
onToolCallStart, onToolCallComplete,
|
|
515
517
|
// Identity/Authorization props
|
|
@@ -2819,10 +2821,17 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2819
2821
|
// ========================================================================
|
|
2820
2822
|
// handleSend
|
|
2821
2823
|
// ========================================================================
|
|
2824
|
+
// Voice (VOICE.md V1): what was said and put in the composer, until sent.
|
|
2825
|
+
const spokenRef = useRef(undefined);
|
|
2826
|
+
const hearing = useMemo(() => voice && voice.input !== 'off'
|
|
2827
|
+
? new DeviceHearing(voice.modelsUrl, voice.engines)
|
|
2828
|
+
: undefined, [voice?.input, voice?.modelsUrl, voice?.engines]);
|
|
2822
2829
|
const handleSend = useCallback(async (messageOverride,
|
|
2823
2830
|
// AG-UI's: what goes with the run besides the conversation — an
|
|
2824
2831
|
// application's page says there what it did (LOOP R-04).
|
|
2825
|
-
|
|
2832
|
+
forwardedPropsGiven,
|
|
2833
|
+
// How the message was heard, when it was said (VOICE.md VO-27).
|
|
2834
|
+
spokenGiven) => {
|
|
2826
2835
|
const messageContent = (messageOverride ?? input).trim();
|
|
2827
2836
|
if (!messageContent || isLoading)
|
|
2828
2837
|
return;
|
|
@@ -2831,7 +2840,15 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2831
2840
|
stoppedRef.current = false;
|
|
2832
2841
|
suppressAssistantTextForToolOnlyRef.current =
|
|
2833
2842
|
isToolCallOnlyPrompt(messageContent);
|
|
2834
|
-
|
|
2843
|
+
// Said, then sent: by the microphone at once, or from the composer it
|
|
2844
|
+
// was put in — the words may have been edited, it was still said.
|
|
2845
|
+
const spoken = spokenGiven ??
|
|
2846
|
+
(messageOverride === undefined ? spokenRef.current : undefined);
|
|
2847
|
+
spokenRef.current = undefined;
|
|
2848
|
+
const forwardedProps = spoken
|
|
2849
|
+
? { ...(forwardedPropsGiven ?? {}), voice: spoken }
|
|
2850
|
+
: forwardedPropsGiven;
|
|
2851
|
+
const userMessage = createUserMessage(messageContent, spoken ? { ...spoken } : undefined);
|
|
2835
2852
|
const currentMessages = displayItems.filter((item) => !isToolCallMessage(item));
|
|
2836
2853
|
const allMessages = [...currentMessages, userMessage];
|
|
2837
2854
|
setDisplayItems(prev => [...prev, userMessage]);
|
|
@@ -3513,7 +3530,18 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3513
3530
|
mx: 'auto',
|
|
3514
3531
|
bg: 'canvas.default',
|
|
3515
3532
|
}, children: [_jsx(ChatMessageList, { displayItems: welcomedItems, isLoading: isLoading, isStreaming: isStreaming, showLoadingIndicator: showLoadingIndicator, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: defaultAvatarConfig, padding: padding, renderToolResult: effectiveRenderToolResult, approvalConfig: approvalConfig, messagesEndRef: messagesEndRef, onRespond: handleRespond, showTurnFooters: showTurnFooter, agentUsage: agentUsage, onRemoveItems: handleRemoveItems, mcpServers: filteredMcpServers, emptyContent: launching ? null : (_jsx(ChatEmptyState, { emptyState: emptyState, brandIcon: brandIcon, description: resolvedDescription, suggestions: resolvedSuggestions, submitOnSuggestionClick: submitOnSuggestionClick, onSuggestionSubmit: handleSuggestionSubmit, onSuggestionFill: handleSuggestionFill })) }), trailingContent] }));
|
|
3516
|
-
|
|
3533
|
+
// The microphone, in the composer's footer (VO-10).
|
|
3534
|
+
const voiceControl = hearing && voice ? (_jsx(VoiceInput, { hearing: hearing, language: voice.language, consentKey: voice.consentKey ?? 'chat', disabled: disabled || launching, speaking: voiceSpeaking, onStopSpeaking: onStopSpeaking, onListen: onStopSpeaking, onTranscript: (transcript) => {
|
|
3535
|
+
if (voice.sendWhatISay) {
|
|
3536
|
+
void handleSend(transcript.text, undefined, transcript.metadata);
|
|
3537
|
+
return;
|
|
3538
|
+
}
|
|
3539
|
+
spokenRef.current = transcript.metadata;
|
|
3540
|
+
setInput(input.trim()
|
|
3541
|
+
? `${input.trim()} ${transcript.text}`
|
|
3542
|
+
: transcript.text);
|
|
3543
|
+
} })) : undefined;
|
|
3544
|
+
const inputToolbar = showInput ? (_jsx(InputPrompt, { footerExtras: voiceControl, input: input, setInput: setInput, isLoading: isLoading, kernelStatus: liveKernelStatus, connectionConfirmed: connectionConfirmed, placeholder: placeholder,
|
|
3517
3545
|
/*
|
|
3518
3546
|
The same openers the empty state offers, typed into the box they would
|
|
3519
3547
|
be typed into.
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MessageList — Renders the scrollable list of chat messages, tool calls,
|
|
3
|
-
* and the typing indicator.
|
|
4
|
-
*
|
|
5
|
-
* @module chat/messages/MessageList
|
|
6
|
-
*/
|
|
7
1
|
import { type ReactNode, type RefObject } from 'react';
|
|
8
2
|
import { type MarkedMcpServer } from '../marks';
|
|
9
3
|
import type { DisplayItem, AvatarConfig, RenderToolResult } from '../../types/chat';
|
|
@@ -9,6 +9,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
9
9
|
*
|
|
10
10
|
* @module chat/messages/MessageList
|
|
11
11
|
*/
|
|
12
|
+
import { MicrophoneIcon } from '@datalayer/icons-react';
|
|
12
13
|
import { Fragment, useState, useCallback, useEffect, useMemo, useRef, } from 'react';
|
|
13
14
|
import { Text, Spinner, Link, Label, RelativeTime } from '@primer/react';
|
|
14
15
|
import { ArrowRightIcon, BroadcastIcon, ClockIcon, DependabotIcon, PersonIcon, SkipIcon, ToolsIcon, XCircleFillIcon, } from '@primer/octicons-react';
|
|
@@ -708,11 +709,16 @@ export function ChatMessageList({ displayItems, isLoading, isStreaming, showLoad
|
|
|
708
709
|
// the agent reads — see `formSubmission`.
|
|
709
710
|
const text = getMessageText(message);
|
|
710
711
|
const submission = parseFormSubmission(text);
|
|
711
|
-
return submission ? (_jsx(FormSubmissionMessage, { submission: submission })) : (
|
|
712
|
+
return submission ? (_jsx(FormSubmissionMessage, { submission: submission })) : (_jsxs(Text, { sx: {
|
|
712
713
|
fontSize: 1,
|
|
713
714
|
whiteSpace: 'pre-wrap',
|
|
714
715
|
wordBreak: 'break-word',
|
|
715
|
-
}, children:
|
|
716
|
+
}, children: [message.metadata?.input === 'voice' ? (_jsx(Box, { as: "span", "data-spoken": "", title: "Said aloud", "aria-label": "Said aloud:", sx: {
|
|
717
|
+
display: 'inline-flex',
|
|
718
|
+
verticalAlign: 'text-bottom',
|
|
719
|
+
mr: 1,
|
|
720
|
+
opacity: 0.85,
|
|
721
|
+
}, children: _jsx(MicrophoneIcon, { size: 14, "aria-hidden": "true" }) })) : null, text] }));
|
|
716
722
|
})()) : message.live && !getMessageText(message) ? (_jsx(TypingDots, { size: 6 })) : (_jsx(Box, { sx: streamdownMarkdownStyles, children: _jsx(Streamdown, { children: normalizeAssistantMarkdown(getMessageText(message) || (isStreaming ? '...' : '')) }) }))] }), renderMessageFooter ? (_jsx(Box, { sx: {
|
|
717
723
|
display: 'flex',
|
|
718
724
|
gap: 2,
|
|
@@ -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
|
@@ -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,6 +91,9 @@ 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' ||
|
|
@@ -108,7 +114,7 @@ const getExampleGroup = (id) => {
|
|
|
108
114
|
// runtime: checkpoints, hooks, memory, guardrails…
|
|
109
115
|
if (id.startsWith('Agent'))
|
|
110
116
|
return 'Capabilities';
|
|
111
|
-
if (id.startsWith('Chat'))
|
|
117
|
+
if (id.startsWith('Chat') || id === 'VoiceChatExample')
|
|
112
118
|
return 'Chat';
|
|
113
119
|
// The document examples: the ones on the Lexical editor, and the page
|
|
114
120
|
// with a document on it.
|
|
@@ -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) {
|