@datalayer/agent-runtimes 1.5.44 → 1.5.45
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/dist-embed/chunks/{ChatView-kCmBbnDa.js → ChatView-Dy-IgGFL.js} +1 -1
- package/dist-embed/datalayer-app-main.js +97 -92
- package/lib/apps/plugins/chat/ChatView.js +2 -2
- package/lib/chat/ChatFloating.js +1 -1
- package/lib/chat/base/ChatBase.js +23 -8
- package/lib/chat/messages/ChatMessageList.d.ts +7 -1
- package/lib/chat/messages/ChatMessageList.js +8 -2
- package/lib/protocols/VercelAIAdapter.js +11 -0
- package/lib/specs/voices.js +62 -0
- package/lib/types/chat.d.ts +10 -0
- package/lib/voice/ComposerVoice.js +12 -3
- package/lib/voice/captions.d.ts +31 -0
- package/lib/voice/captions.js +156 -0
- package/lib/voice/handsFree.d.ts +12 -0
- package/lib/voice/handsFree.js +18 -0
- package/lib/voice/serverHearing.d.ts +29 -0
- package/lib/voice/serverHearing.js +78 -2
- package/package.json +1 -1
- package/scripts/voice/measure.py +77 -17
|
@@ -1702,8 +1702,8 @@ export default function ChatView({ workspace }) {
|
|
|
1702
1702
|
// opening stops it, and *Stop speaking* while it speaks.
|
|
1703
1703
|
speaking: speaking, onStopSpeaking: () => controlsRef.current?.stopSpeaking?.(),
|
|
1704
1704
|
// Hands-free (VO-12, VO-13): the run in progress, stopped when
|
|
1705
|
-
// the person speaks over it.
|
|
1706
|
-
running: busy, onStopRun: () => controlsRef.current?.stop?.(), onInterimChange: setVoiceInterim,
|
|
1705
|
+
// the person speaks over it, and the answer marked interrupted.
|
|
1706
|
+
running: busy, onStopRun: () => controlsRef.current?.stop?.(), onInterrupted: () => controlsRef.current?.markInterrupted(), onInterimChange: setVoiceInterim,
|
|
1707
1707
|
// The menu offers when answers are heard where the
|
|
1708
1708
|
// application lets them be (VO-21, VO-30).
|
|
1709
1709
|
answers: Boolean(config?.voice?.output &&
|
package/lib/chat/ChatFloating.js
CHANGED
|
@@ -1030,7 +1030,7 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
1030
1030
|
...poweredByProps,
|
|
1031
1031
|
}, description: description, onStateUpdate: onStateUpdate, onNewChat: onNewChat, suggestions: suggestions, submitOnSuggestionClick: submitOnSuggestionClick, hideMessagesAfterToolUI: hideMessagesAfterToolUI, avatarConfig: {
|
|
1032
1032
|
showAvatars: true,
|
|
1033
|
-
}, 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 ?? suggestionPrompt, inspector: inspector, 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, onReadAloud: speaker ? readAnswerAloud : undefined, ...panelProps, ...assistantChat, onLoadingChange: handleLoadingChange, onDisplayItemsChange: handleDisplayItemsChange, onSendReady: handleSendReady, children: conversationBalloon && currentOpen ? (
|
|
1033
|
+
}, 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 ?? suggestionPrompt, inspector: inspector, showInformation: showInformation, onInformationClick: onInformationClick, onToolCallStart: onToolCallStart, onToolCallComplete: onToolCallComplete, showToolApprovalBanner: showToolApprovalBanner, pendingApprovals: pendingApprovals, onApproveApproval: onApproveApproval, onRejectApproval: onRejectApproval, voice: voice, voiceSpeaking: speakerState.speaking, voiceSentence: speakerState.sentence, onStopSpeaking: speaker ? stopSpeaking : undefined, onReadAloud: speaker ? readAnswerAloud : undefined, ...panelProps, ...assistantChat, onLoadingChange: handleLoadingChange, onDisplayItemsChange: handleDisplayItemsChange, onSendReady: handleSendReady, children: conversationBalloon && currentOpen ? (
|
|
1034
1034
|
// Current: the one thing said or done now, over the composer.
|
|
1035
1035
|
_jsx(Box, { px: 3, pt: 3, pb: 2, overflow: "hidden", children: _jsx(CurrentBalloonBody, { text: toolSaid ? undefined : (currentWords ?? description), fullText: toolSaid ? undefined : currentWhole, tool: toolSaid, busy: atWork, speaking: answering && atWork, waiting: atWork && !answering }) })) : (children) }))] }));
|
|
1036
1036
|
/*
|
|
@@ -52,11 +52,13 @@ import { EphemeralSurfaceControl } from '../EphemeralSurfaceControl';
|
|
|
52
52
|
import { ChatMessageList, } from '../messages/ChatMessageList';
|
|
53
53
|
import { InputPrompt } from '../prompt/InputPrompt';
|
|
54
54
|
import { ComposerVoice, spokenOf } from '../../voice/ComposerVoice';
|
|
55
|
+
import { answerSpokenOver } from '../../voice/handsFree';
|
|
55
56
|
import { ReadAloud, readsAloud } from '../../voice/ReadAloud';
|
|
56
57
|
import { settledVoice } from '../../voice/chatVoice';
|
|
57
58
|
import { useVoiceSettings } from '../../voice/useVoiceSettings';
|
|
58
59
|
import { useVoiceSpeaker } from '../../voice/useVoiceSpeaker';
|
|
59
60
|
import { useSpokenAnswers } from '../../voice/useSpokenAnswers';
|
|
61
|
+
import { saidMessageId, useSaidSentence } from '../../voice/captions';
|
|
60
62
|
import { lineToSay } from '../../voice/sayAloud';
|
|
61
63
|
import { isFloatingChatViewMode } from '../viewModes';
|
|
62
64
|
import { ToolApprovalBanner, ToolApprovalDialog, } from '../tools';
|
|
@@ -424,7 +426,7 @@ function ChatBaseInner({ title, subtitle, showHeader = false, showTokenUsage = t
|
|
|
424
426
|
// Mode selection
|
|
425
427
|
useStore: useStoreMode = true, protocol: protocolRaw, onSendMessage, onSendReady, onLoadingChange, onSpeakingChange, onContextSnapshot, enableStreaming = false,
|
|
426
428
|
// Extended props
|
|
427
|
-
brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderHeader, showTurnFooter = true, notebookToolSurfacesId, footerContent, welcome, trailingContent, voice, voiceSpeaking = false, onStopSpeaking, onReadAloud, 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,
|
|
429
|
+
brandIcon, avatarConfig, headerButtons, showPoweredBy = false, poweredByProps, emptyState, renderHeader, showTurnFooter = true, notebookToolSurfacesId, footerContent, welcome, trailingContent, voice, voiceSpeaking = false, voiceSentence, onStopSpeaking, onReadAloud, 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,
|
|
428
430
|
// Tool invocation hooks
|
|
429
431
|
onToolCallStart, onToolCallComplete, inspector: inspectorGiven,
|
|
430
432
|
// Identity/Authorization props
|
|
@@ -2974,6 +2976,10 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2974
2976
|
const ownVoice = useVoiceSpeaker(settled, !hostSpeaks);
|
|
2975
2977
|
useSpokenAnswers(displayItems, isLoading, !hostSpeaks && settled?.output === 'always', ownVoice.speaker);
|
|
2976
2978
|
const speakingNow = voiceSpeaking || ownVoice.state.speaking;
|
|
2979
|
+
// The sentence being said, marked in the conversation (VO-65).
|
|
2980
|
+
const sentenceSaid = hostSpeaks ? voiceSentence : ownVoice.state.sentence;
|
|
2981
|
+
const saidConversationRef = useRef(null);
|
|
2982
|
+
useSaidSentence(saidConversationRef, sentenceSaid, displayItems);
|
|
2977
2983
|
/*
|
|
2978
2984
|
* A line the agent wants heard, not written (VO-35): a caption while the
|
|
2979
2985
|
* run goes on, said by the speaker where answers are said by themselves.
|
|
@@ -3001,6 +3007,14 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3001
3007
|
}, [isLoading]);
|
|
3002
3008
|
// Answers spoken over, hands-free (VOICE.md VO-13): marked *interrupted*.
|
|
3003
3009
|
const [interruptedIds, setInterruptedIds] = useState(() => new Set());
|
|
3010
|
+
// The conversation as shown, read when the person speaks over an answer.
|
|
3011
|
+
const shownItemsRef = useRef([]);
|
|
3012
|
+
const markInterrupted = useCallback(() => {
|
|
3013
|
+
const spokenOver = answerSpokenOver(shownItemsRef.current.filter((item) => !isToolCallMessage(item)));
|
|
3014
|
+
if (spokenOver) {
|
|
3015
|
+
setInterruptedIds(ids => new Set([...ids, spokenOver]));
|
|
3016
|
+
}
|
|
3017
|
+
}, []);
|
|
3004
3018
|
const stopSpeaking = onStopSpeaking ?? (ownVoice.speaker ? ownVoice.stop : undefined);
|
|
3005
3019
|
const readAnswerAloud = onReadAloud ?? (ownVoice.speaker ? ownVoice.read : undefined);
|
|
3006
3020
|
const handleSend = useCallback(async (messageOverride,
|
|
@@ -3370,6 +3384,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3370
3384
|
thread: () => threadIdRef.current,
|
|
3371
3385
|
// Its speaker, for a host whose composer holds the microphone (VO-10).
|
|
3372
3386
|
stopSpeaking,
|
|
3387
|
+
// The answer spoken over, marked for a host's own composer (VO-13).
|
|
3388
|
+
markInterrupted,
|
|
3373
3389
|
});
|
|
3374
3390
|
return () => {
|
|
3375
3391
|
onSendReady(null);
|
|
@@ -3382,6 +3398,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3382
3398
|
handleStop,
|
|
3383
3399
|
handleNewChat,
|
|
3384
3400
|
stopSpeaking,
|
|
3401
|
+
markInterrupted,
|
|
3385
3402
|
]);
|
|
3386
3403
|
// Streaming state, for a host that draws the prompt.
|
|
3387
3404
|
useEffect(() => {
|
|
@@ -3748,6 +3765,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3748
3765
|
...displayItems,
|
|
3749
3766
|
]
|
|
3750
3767
|
: displayItems;
|
|
3768
|
+
shownItemsRef.current = welcomedItems;
|
|
3769
|
+
const saidId = saidMessageId(welcomedItems, sentenceSaid);
|
|
3751
3770
|
/*
|
|
3752
3771
|
* *Read aloud* under each answer the agent has finished (VOICE.md VO-21),
|
|
3753
3772
|
* when the host reads answers aloud on request.
|
|
@@ -3763,7 +3782,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3763
3782
|
return (_jsxs(_Fragment, { children: [interruptedIds.has(message.id) ? (_jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', fontStyle: 'italic' }, "data-interrupted": "", children: "interrupted" })) : null, readAnswerAloud && readsAloud(message, text, writingId) ? (_jsx(ReadAloud, { onRead: () => readAnswerAloud(text) })) : null] }));
|
|
3764
3783
|
}
|
|
3765
3784
|
: undefined;
|
|
3766
|
-
const messagesContent = children ? (children) : (_jsxs(Box, { display: "flex", flexDirection: "column", minHeight: 0,
|
|
3785
|
+
const messagesContent = children ? (children) : (_jsxs(Box, { ref: saidConversationRef, display: "flex", flexDirection: "column", minHeight: 0,
|
|
3767
3786
|
/*
|
|
3768
3787
|
A reading column, not a full-bleed sheet.
|
|
3769
3788
|
|
|
@@ -3776,16 +3795,12 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3776
3795
|
// Tool calls are drawn by the page's Reactor plugins; a named
|
|
3777
3796
|
// notebook's cell surfaces stand in for the default row of its tools,
|
|
3778
3797
|
// so every chat with a headless notebook gets the same transcript.
|
|
3779
|
-
notebookToolSurfacesId: notebookToolSurfacesId, approvalConfig: approvalConfig, messagesEndRef: messagesEndRef, onRespond: handleRespond, showTurnFooters: showTurnFooter, renderMessageFooter: readAloudFooter, 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] }));
|
|
3798
|
+
notebookToolSurfacesId: notebookToolSurfacesId, approvalConfig: approvalConfig, messagesEndRef: messagesEndRef, onRespond: handleRespond, showTurnFooters: showTurnFooter, renderMessageFooter: readAloudFooter, saidMessageId: saidId, 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] }));
|
|
3780
3799
|
// The microphone, in the composer's footer (VO-10, VO-11).
|
|
3781
3800
|
const voiceControl = settled && settled.input !== 'off' ? (_jsx(ComposerVoice, { voice: settled, input: input, setInput: setInput, disabled: disabled || launching, speaking: speakingNow, onStopSpeaking: stopSpeaking,
|
|
3782
3801
|
// Hands-free (VO-12, VO-13): the run in progress, stopped when the
|
|
3783
3802
|
// person speaks over it, and the answer marked interrupted.
|
|
3784
|
-
running: isLoading, onStopRun: handleStop, onInterrupted:
|
|
3785
|
-
if (writingId) {
|
|
3786
|
-
setInterruptedIds(ids => new Set([...ids, writingId]));
|
|
3787
|
-
}
|
|
3788
|
-
}, onInterimChange: setVoiceInterim,
|
|
3803
|
+
running: isLoading, onStopRun: handleStop, onInterrupted: markInterrupted, onInterimChange: setVoiceInterim,
|
|
3789
3804
|
// The menu offers when answers are heard where the application
|
|
3790
3805
|
// lets them be (VO-21, VO-30).
|
|
3791
3806
|
answers: Boolean(voice?.output && voice.output !== 'off' && voice.speechUrl), onSpoken: metadata => {
|
|
@@ -78,6 +78,12 @@ export interface ChatMessageListProps {
|
|
|
78
78
|
* `note`, which has no footer of its own.
|
|
79
79
|
*/
|
|
80
80
|
renderMessageFooter?: (message: ChatMessage) => ReactNode;
|
|
81
|
+
/**
|
|
82
|
+
* The answer a sentence is being said from (VOICE.md VO-65): its row is
|
|
83
|
+
* marked `aria-current` and `data-voice-saying`, with a bar beside it —
|
|
84
|
+
* the sentence itself is underlined by `useSaidSentence`.
|
|
85
|
+
*/
|
|
86
|
+
saidMessageId?: string;
|
|
81
87
|
/**
|
|
82
88
|
* The MCP servers the agent has, with the tools each serves: a tool call is
|
|
83
89
|
* drawn with the marks of the server it belongs to. Skills, frontend tools
|
|
@@ -109,5 +115,5 @@ export interface SubagentChatPanelProps {
|
|
|
109
115
|
* approval decisions are sent over the shared monitoring socket.
|
|
110
116
|
*/
|
|
111
117
|
export declare function SubagentChatPanel({ toolCallId, subagentName: subagentNameHint, height, avatarConfig, }: SubagentChatPanelProps): React.ReactElement | null;
|
|
112
|
-
export declare function ChatMessageList({ displayItems, actOn, upload, isLoading, isStreaming, showLoadingIndicator, hideMessagesAfterToolUI, avatarConfig, padding, notebookToolSurfacesId, emptyContent, messagesEndRef, onRespond, approvalConfig, showTurnFooters, agentUsage, onRemoveItems, renderMessageFooter, mcpServers, density, }: ChatMessageListProps): import("react").JSX.Element;
|
|
118
|
+
export declare function ChatMessageList({ displayItems, actOn, upload, isLoading, isStreaming, showLoadingIndicator, hideMessagesAfterToolUI, avatarConfig, padding, notebookToolSurfacesId, emptyContent, messagesEndRef, onRespond, approvalConfig, showTurnFooters, agentUsage, onRemoveItems, renderMessageFooter, saidMessageId, mcpServers, density, }: ChatMessageListProps): import("react").JSX.Element;
|
|
113
119
|
export {};
|
|
@@ -366,7 +366,7 @@ function DefaultToolCallRenderer({ item, onRespond, mcpServers, density, }) {
|
|
|
366
366
|
// ---------------------------------------------------------------------------
|
|
367
367
|
// Component
|
|
368
368
|
// ---------------------------------------------------------------------------
|
|
369
|
-
export function ChatMessageList({ displayItems, actOn, upload, isLoading, isStreaming, showLoadingIndicator, hideMessagesAfterToolUI, avatarConfig, padding, notebookToolSurfacesId, emptyContent, messagesEndRef, onRespond, approvalConfig, showTurnFooters = false, agentUsage, onRemoveItems, renderMessageFooter, mcpServers, density = 'comfortable', }) {
|
|
369
|
+
export function ChatMessageList({ displayItems, actOn, upload, isLoading, isStreaming, showLoadingIndicator, hideMessagesAfterToolUI, avatarConfig, padding, notebookToolSurfacesId, emptyContent, messagesEndRef, onRespond, approvalConfig, showTurnFooters = false, agentUsage, onRemoveItems, renderMessageFooter, saidMessageId, mcpServers, density = 'comfortable', }) {
|
|
370
370
|
/*
|
|
371
371
|
* Who draws a tool call: the Reactor plugins of the page, when it has a
|
|
372
372
|
* platform. Each plugin binds its own tools (`ToolBindings`) and renders its
|
|
@@ -541,7 +541,13 @@ export function ChatMessageList({ displayItems, actOn, upload, isLoading, isStre
|
|
|
541
541
|
}
|
|
542
542
|
// The host's footer, asked once: a row only when it draws something.
|
|
543
543
|
const messageFooter = renderMessageFooter?.(message);
|
|
544
|
-
|
|
544
|
+
// The answer being said (VO-65): marked by a bar and for assistive tech.
|
|
545
|
+
const said = saidMessageId !== undefined && message.id === saidMessageId;
|
|
546
|
+
return (_jsx(Box, { "data-chat-message": message.role, "data-voice-saying": said ? '' : undefined, "aria-current": said ? 'true' : undefined, sx: said
|
|
547
|
+
? {
|
|
548
|
+
boxShadow: 'inset 3px 0 0 var(--borderColor-accent-emphasis, #0969da)',
|
|
549
|
+
}
|
|
550
|
+
: undefined, display: "flex", flexDirection: "column", alignItems: isUser ? 'flex-end' : 'flex-start', px: padding, py: compact ? '2px' : 1, children: _jsxs(Box, { display: "flex", gap: 2, flexDirection: isUser ? 'row-reverse' : 'row', alignItems: "flex-start",
|
|
545
551
|
/*
|
|
546
552
|
Full width, always.
|
|
547
553
|
|
|
@@ -627,6 +627,17 @@ export class VercelAIAdapter extends BaseProtocolAdapter {
|
|
|
627
627
|
});
|
|
628
628
|
}
|
|
629
629
|
}
|
|
630
|
+
else if (typeof event.type === 'string' &&
|
|
631
|
+
event.type.startsWith('data-')) {
|
|
632
|
+
// What the server says besides the conversation's text, as
|
|
633
|
+
// AG-UI's custom events say it: `data-voice.say`, a line the
|
|
634
|
+
// agent says aloud, and `data-voice.settings` (VOICE.md VO-36).
|
|
635
|
+
this.emit({
|
|
636
|
+
type: 'activity',
|
|
637
|
+
activity: { type: event.type, data: event.data },
|
|
638
|
+
timestamp: new Date(),
|
|
639
|
+
});
|
|
640
|
+
}
|
|
630
641
|
}
|
|
631
642
|
catch (parseError) {
|
|
632
643
|
console.error('[VercelAIAdapter] Failed to parse SSE data:', parseError);
|
package/lib/specs/voices.js
CHANGED
|
@@ -100,6 +100,68 @@ export const SPEECH_MODEL_CATALOGUE = {
|
|
|
100
100
|
},
|
|
101
101
|
],
|
|
102
102
|
},
|
|
103
|
+
'moonshine-base-en-server': {
|
|
104
|
+
id: 'moonshine-base-en-server',
|
|
105
|
+
version: '0.0.1',
|
|
106
|
+
name: 'Moonshine Base (English, server)',
|
|
107
|
+
description: "Speech to text on Datalayer's servers, in English: Useful Sensors' Moonshine Base (61M parameters), the same 8-bit ONNX export the browser runs, run by onnxruntime on the CPU with a greedy decoder. Its encoder is as long as the speech, with no 30-second window: measured beside Whisper small for short spoken turns (decision 18).",
|
|
108
|
+
provider: 'usefulsensors',
|
|
109
|
+
task: 'stt',
|
|
110
|
+
engine: 'onnxruntime',
|
|
111
|
+
dtype: 'q8',
|
|
112
|
+
languages: ['en'],
|
|
113
|
+
where: ['server'],
|
|
114
|
+
streaming: false,
|
|
115
|
+
licence: {
|
|
116
|
+
weights: 'MIT',
|
|
117
|
+
code: 'MIT',
|
|
118
|
+
},
|
|
119
|
+
attribution: '',
|
|
120
|
+
upstream: 'https://huggingface.co/UsefulSensors/moonshine-base',
|
|
121
|
+
revision: 'b1e9b6aae3c3c7298f10c3798393fdf38e8fbbad',
|
|
122
|
+
files: [
|
|
123
|
+
{
|
|
124
|
+
path: 'config.json',
|
|
125
|
+
sha256: 'fab7241d1e9fc6c2370c4c6dfb5da79bb54d67ed9ab6b507ac51d29d2abe01d1',
|
|
126
|
+
size: 922,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
path: 'generation_config.json',
|
|
130
|
+
sha256: 'f9b3f711b57be7def2e50a8942f64f36ee0a55fad5b84ff93a687b6c5bcc1d44',
|
|
131
|
+
size: 147,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
path: 'preprocessor_config.json',
|
|
135
|
+
sha256: 'fa43a7017ef85cd1d0fba0d9aae77c8adb16990ae6f11115631f41ec5d8aa679',
|
|
136
|
+
size: 128,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
path: 'special_tokens_map.json',
|
|
140
|
+
sha256: 'ca3d163bab055381827226140568f3bef7eaac187cebd76878e0b63e9e442356',
|
|
141
|
+
size: 3,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
path: 'tokenizer.json',
|
|
145
|
+
sha256: '7b913404bdd039af4756783218af4440bc07fb7d6d8258d677e34f95b3ec416f',
|
|
146
|
+
size: 3761754,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
path: 'tokenizer_config.json',
|
|
150
|
+
sha256: 'edaee394565d428ea98a663ae7209cdcfeefc5585c42d7a570ff7c986df2cd15',
|
|
151
|
+
size: 135735,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
path: 'onnx/encoder_model_quantized.onnx',
|
|
155
|
+
sha256: '1dd9ab0a7f987113d30affcba5a068d11c8f90fa0223caa3e491ade431ad9751',
|
|
156
|
+
size: 20513063,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
path: 'onnx/decoder_model_merged_quantized.onnx',
|
|
160
|
+
sha256: 'cc9f3cd6698a369c6008b41aa60aa3fb3322e7f03c9bdf19d8e6b7200afca4f3',
|
|
161
|
+
size: 42498870,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
},
|
|
103
165
|
'moonshine-base-en': {
|
|
104
166
|
id: 'moonshine-base-en',
|
|
105
167
|
version: '0.0.1',
|
package/lib/types/chat.d.ts
CHANGED
|
@@ -689,6 +689,11 @@ export interface ChatBaseProps {
|
|
|
689
689
|
* Absent while its answers are not heard.
|
|
690
690
|
*/
|
|
691
691
|
stopSpeaking?: () => void;
|
|
692
|
+
/**
|
|
693
|
+
* Marks the answer the person spoke over *interrupted* (VOICE.md
|
|
694
|
+
* VO-13), for a host whose composer holds the microphone.
|
|
695
|
+
*/
|
|
696
|
+
markInterrupted: () => void;
|
|
692
697
|
} | null) => void;
|
|
693
698
|
/**
|
|
694
699
|
* Reports whether the agent's voice is speaking, for a host that draws the
|
|
@@ -1013,6 +1018,11 @@ export interface ChatBaseProps {
|
|
|
1013
1018
|
voice?: ChatVoice;
|
|
1014
1019
|
/** The agent is speaking: the composer offers *Stop speaking* (`Esc`). */
|
|
1015
1020
|
voiceSpeaking?: boolean;
|
|
1021
|
+
/**
|
|
1022
|
+
* The sentence the host's speaker is saying, marked in the conversation
|
|
1023
|
+
* (VOICE.md VO-65); a chat that speaks for itself marks its own.
|
|
1024
|
+
*/
|
|
1025
|
+
voiceSentence?: string;
|
|
1016
1026
|
/** Stops the agent's voice: the composer's *Stop speaking*, and the microphone opening (VO-13). */
|
|
1017
1027
|
onStopSpeaking?: () => void;
|
|
1018
1028
|
/**
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useMemo, useRef } from 'react';
|
|
3
3
|
import { DeviceHearing } from './hearing';
|
|
4
4
|
import { HandsFree } from './HandsFree';
|
|
5
|
-
import { ServerHearing, hearingPlace } from './serverHearing';
|
|
5
|
+
import { AutoHearing, ServerHearing, hearingPlace } from './serverHearing';
|
|
6
6
|
import { VoiceInput } from './VoiceInput';
|
|
7
7
|
import { VoiceMenu } from './VoiceMenu';
|
|
8
8
|
/** Whether something rides on a run as a spoken message's metadata (VO-27). */
|
|
@@ -24,13 +24,20 @@ export function ComposerVoice({ voice, input, setInput, onSpoken, send, disabled
|
|
|
24
24
|
return undefined;
|
|
25
25
|
}
|
|
26
26
|
const device = new DeviceHearing(voice.modelsUrl, voice.engines);
|
|
27
|
-
|
|
27
|
+
const server = voice.speechUrl
|
|
28
28
|
? new ServerHearing(device, {
|
|
29
29
|
speechUrl: voice.speechUrl,
|
|
30
30
|
token: voice.token,
|
|
31
31
|
detect: voice.detect,
|
|
32
32
|
})
|
|
33
|
-
:
|
|
33
|
+
: undefined;
|
|
34
|
+
// `where: auto` (§5): the device when it hears the language and its model
|
|
35
|
+
// loads, the server otherwise — and back to the device when the server
|
|
36
|
+
// refuses (a visitor's day spent).
|
|
37
|
+
if (server && !voice.detect && (voice.where ?? 'auto') === 'auto') {
|
|
38
|
+
return new AutoHearing(device, server, voice.language);
|
|
39
|
+
}
|
|
40
|
+
return place === 'server' && server ? server : device;
|
|
34
41
|
}, [
|
|
35
42
|
voice.input,
|
|
36
43
|
voice.modelsUrl,
|
|
@@ -39,6 +46,8 @@ export function ComposerVoice({ voice, input, setInput, onSpoken, send, disabled
|
|
|
39
46
|
voice.speechUrl,
|
|
40
47
|
voice.token,
|
|
41
48
|
voice.detect,
|
|
49
|
+
voice.where,
|
|
50
|
+
voice.language,
|
|
42
51
|
]);
|
|
43
52
|
// What the box held when the microphone opened: the words heard go after it.
|
|
44
53
|
const base = useRef('');
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Captions always (VOICE.md VO-65): the sentence being said is marked in the
|
|
3
|
+
* open conversation, not by colour alone. The answer that holds it is marked
|
|
4
|
+
* `aria-current` and `data-voice-saying` with a bar beside it; the sentence
|
|
5
|
+
* itself is underlined in place through the CSS Custom Highlight API, which
|
|
6
|
+
* draws over the text without touching the DOM React owns. A browser without
|
|
7
|
+
* that API keeps the answer's mark.
|
|
8
|
+
*
|
|
9
|
+
* @module voice/captions
|
|
10
|
+
*/
|
|
11
|
+
import { type RefObject } from 'react';
|
|
12
|
+
/** The name the sentence's highlight is registered under. */
|
|
13
|
+
export declare const SAID_HIGHLIGHT = "voice-said";
|
|
14
|
+
/**
|
|
15
|
+
* The id of the answer a sentence is being said from: the newest answer of
|
|
16
|
+
* the agent whose words hold it, or none.
|
|
17
|
+
*/
|
|
18
|
+
export declare function saidMessageId(items: readonly unknown[], sentence: string | undefined): string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Where a sentence is in what an element shows, as a `Range` over its text
|
|
21
|
+
* — across the bold, the links and the line breaks it is drawn with — or
|
|
22
|
+
* none when the element does not show it as said (a code block said as *a
|
|
23
|
+
* code block*).
|
|
24
|
+
*/
|
|
25
|
+
export declare function sentenceRange(root: Node, sentence: string): Range | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Underlines `sentence` in the answer marked `data-voice-saying` under
|
|
28
|
+
* `root`, for as long as it is said; the mark goes when it ends. Drawn
|
|
29
|
+
* again whenever `version` changes (the answer still being written).
|
|
30
|
+
*/
|
|
31
|
+
export declare function useSaidSentence(root: RefObject<HTMLElement | null>, sentence: string | undefined, version: unknown): void;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Captions always (VOICE.md VO-65): the sentence being said is marked in the
|
|
7
|
+
* open conversation, not by colour alone. The answer that holds it is marked
|
|
8
|
+
* `aria-current` and `data-voice-saying` with a bar beside it; the sentence
|
|
9
|
+
* itself is underlined in place through the CSS Custom Highlight API, which
|
|
10
|
+
* draws over the text without touching the DOM React owns. A browser without
|
|
11
|
+
* that API keeps the answer's mark.
|
|
12
|
+
*
|
|
13
|
+
* @module voice/captions
|
|
14
|
+
*/
|
|
15
|
+
import { useEffect } from 'react';
|
|
16
|
+
import { plainWords } from './sentences';
|
|
17
|
+
import { answerText } from './useSpokenAnswers';
|
|
18
|
+
/** The name the sentence's highlight is registered under. */
|
|
19
|
+
export const SAID_HIGHLIGHT = 'voice-said';
|
|
20
|
+
/** What the sentence being said looks like: underlined, thick, offset. */
|
|
21
|
+
const SAID_STYLE = `::highlight(${SAID_HIGHLIGHT}) {
|
|
22
|
+
text-decoration: underline;
|
|
23
|
+
text-decoration-thickness: 2px;
|
|
24
|
+
text-underline-offset: 3px;
|
|
25
|
+
background-color: var(--bgColor-accent-muted, rgba(84, 174, 255, 0.2));
|
|
26
|
+
}`;
|
|
27
|
+
/** Words compared as words: spaces collapsed, case kept. */
|
|
28
|
+
function squeezed(text) {
|
|
29
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The id of the answer a sentence is being said from: the newest answer of
|
|
33
|
+
* the agent whose words hold it, or none.
|
|
34
|
+
*/
|
|
35
|
+
export function saidMessageId(items, sentence) {
|
|
36
|
+
const said = squeezed(sentence ?? '');
|
|
37
|
+
if (!said) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
41
|
+
const text = answerText(items[index]);
|
|
42
|
+
if (text !== undefined && squeezed(plainWords(text)).includes(said)) {
|
|
43
|
+
const id = items[index].id;
|
|
44
|
+
return typeof id === 'string' ? id : undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Where a sentence is in what an element shows, as a `Range` over its text
|
|
51
|
+
* — across the bold, the links and the line breaks it is drawn with — or
|
|
52
|
+
* none when the element does not show it as said (a code block said as *a
|
|
53
|
+
* code block*).
|
|
54
|
+
*/
|
|
55
|
+
export function sentenceRange(root, sentence) {
|
|
56
|
+
const said = squeezed(sentence);
|
|
57
|
+
if (!said || !root.ownerDocument) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const document = root.ownerDocument;
|
|
61
|
+
const walker = document.createTreeWalker(root, 4 /* NodeFilter.SHOW_TEXT */);
|
|
62
|
+
// The text, spaces collapsed, and where each of its characters came from.
|
|
63
|
+
let text = '';
|
|
64
|
+
const from = [];
|
|
65
|
+
let space = true;
|
|
66
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
67
|
+
const value = node.nodeValue ?? '';
|
|
68
|
+
for (let offset = 0; offset < value.length; offset += 1) {
|
|
69
|
+
const char = value[offset];
|
|
70
|
+
if (/\s/.test(char)) {
|
|
71
|
+
if (space) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
space = true;
|
|
75
|
+
text += ' ';
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
space = false;
|
|
79
|
+
text += char;
|
|
80
|
+
}
|
|
81
|
+
from.push({ node, offset });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const start = text.indexOf(said);
|
|
85
|
+
if (start < 0) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
const first = from[start];
|
|
89
|
+
const last = from[start + said.length - 1];
|
|
90
|
+
const range = document.createRange();
|
|
91
|
+
range.setStart(first.node, first.offset);
|
|
92
|
+
range.setEnd(last.node, last.offset + 1);
|
|
93
|
+
return range;
|
|
94
|
+
}
|
|
95
|
+
function registry() {
|
|
96
|
+
const css = globalThis.CSS;
|
|
97
|
+
return css?.highlights;
|
|
98
|
+
}
|
|
99
|
+
function highlightOf(range) {
|
|
100
|
+
const Highlight = globalThis.Highlight;
|
|
101
|
+
return Highlight ? new Highlight(range) : undefined;
|
|
102
|
+
}
|
|
103
|
+
let styled = false;
|
|
104
|
+
function ensureStyle(document) {
|
|
105
|
+
if (styled || document.getElementById('datalayer-voice-said')) {
|
|
106
|
+
styled = true;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const style = document.createElement('style');
|
|
110
|
+
style.id = 'datalayer-voice-said';
|
|
111
|
+
style.textContent = SAID_STYLE;
|
|
112
|
+
document.head.appendChild(style);
|
|
113
|
+
styled = true;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Underlines `sentence` in the answer marked `data-voice-saying` under
|
|
117
|
+
* `root`, for as long as it is said; the mark goes when it ends. Drawn
|
|
118
|
+
* again whenever `version` changes (the answer still being written).
|
|
119
|
+
*/
|
|
120
|
+
export function useSaidSentence(root, sentence, version) {
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
const highlights = registry();
|
|
123
|
+
const element = root.current?.querySelector('[data-voice-saying]');
|
|
124
|
+
if (!highlights || !element || !sentence) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const underline = () => {
|
|
128
|
+
const range = sentenceRange(element, sentence);
|
|
129
|
+
const highlight = range ? highlightOf(range) : undefined;
|
|
130
|
+
if (!highlight) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
ensureStyle(element.ownerDocument);
|
|
134
|
+
highlights.set(SAID_HIGHLIGHT, highlight);
|
|
135
|
+
return true;
|
|
136
|
+
};
|
|
137
|
+
// The answer's Markdown may be drawn after this effect: wait for it.
|
|
138
|
+
let watcher;
|
|
139
|
+
if (!underline() && typeof MutationObserver !== 'undefined') {
|
|
140
|
+
watcher = new MutationObserver(() => {
|
|
141
|
+
if (underline()) {
|
|
142
|
+
watcher?.disconnect();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
watcher.observe(element, {
|
|
146
|
+
childList: true,
|
|
147
|
+
subtree: true,
|
|
148
|
+
characterData: true,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return () => {
|
|
152
|
+
watcher?.disconnect();
|
|
153
|
+
highlights.delete(SAID_HIGHLIGHT);
|
|
154
|
+
};
|
|
155
|
+
}, [root, sentence, version]);
|
|
156
|
+
}
|
package/lib/voice/handsFree.d.ts
CHANGED
|
@@ -56,3 +56,15 @@ export declare function turnEnded(heard: HeardSoFarLike, turnSilenceMs?: number)
|
|
|
56
56
|
* answer's echo — the browser's echo cancellation keeps that out.
|
|
57
57
|
*/
|
|
58
58
|
export declare function bargedIn(state: HandsFreeState, heard: HeardSoFarLike): boolean;
|
|
59
|
+
/** A message as the conversation holds it: enough to find the answer spoken over. */
|
|
60
|
+
export interface ConversationItemLike {
|
|
61
|
+
id: string;
|
|
62
|
+
role?: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The answer the person spoke over (VO-13): the agent's last message after
|
|
66
|
+
* the person's last one — still being written, or written and being said.
|
|
67
|
+
* `undefined` while the agent has not answered the turn yet: there is
|
|
68
|
+
* nothing to mark.
|
|
69
|
+
*/
|
|
70
|
+
export declare function answerSpokenOver(items: readonly ConversationItemLike[]): string | undefined;
|
package/lib/voice/handsFree.js
CHANGED
|
@@ -46,3 +46,21 @@ export function turnEnded(heard, turnSilenceMs = TURN_SILENCE_MS) {
|
|
|
46
46
|
export function bargedIn(state, heard) {
|
|
47
47
|
return (state === 'speaking' || state === 'thinking') && heard.spoke;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* The answer the person spoke over (VO-13): the agent's last message after
|
|
51
|
+
* the person's last one — still being written, or written and being said.
|
|
52
|
+
* `undefined` while the agent has not answered the turn yet: there is
|
|
53
|
+
* nothing to mark.
|
|
54
|
+
*/
|
|
55
|
+
export function answerSpokenOver(items) {
|
|
56
|
+
for (let i = items.length - 1; i >= 0; i -= 1) {
|
|
57
|
+
const role = items[i].role;
|
|
58
|
+
if (role === 'user') {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
if (role === 'assistant') {
|
|
62
|
+
return items[i].id;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
@@ -12,6 +12,9 @@ export declare const STREAM_FINAL_MS = 60000;
|
|
|
12
12
|
export declare const SERVER_HEARING_MODEL = "whisper-large-v3-turbo";
|
|
13
13
|
/** Where a chat's voice hears: in the browser, or on Datalayer's servers. */
|
|
14
14
|
export type HearingPlace = 'device' | 'server';
|
|
15
|
+
/** The speech service refused a press, in its own sentence (a limit, a visitor's day). */
|
|
16
|
+
export declare class ServerRefused extends Error {
|
|
17
|
+
}
|
|
15
18
|
/** What decides where a chat's voice hears. */
|
|
16
19
|
export interface HearingAsked {
|
|
17
20
|
language: string;
|
|
@@ -88,3 +91,29 @@ export declare class ServerHearing implements Hearing {
|
|
|
88
91
|
hearSoFar(audio: Float32Array, language?: string): Promise<HeardSoFar>;
|
|
89
92
|
hear(audio: Float32Array, language: string): Promise<Transcript | undefined>;
|
|
90
93
|
}
|
|
94
|
+
/** Said when the device's model did not load and the server will hear instead. */
|
|
95
|
+
export declare const MOVED_TO_SERVER = "This device could not load its model: what you say will be heard on Datalayer's servers instead. Press the microphone again to agree.";
|
|
96
|
+
/**
|
|
97
|
+
* `where: auto` (§5): each press heard on the device when a model there
|
|
98
|
+
* hears the language and loads, on the speech service otherwise.
|
|
99
|
+
*
|
|
100
|
+
* The device's model failing to load moves the hearing to the server, and
|
|
101
|
+
* the press is refused in a sentence so that the server's own consent is
|
|
102
|
+
* asked at the next one. The server refusing a press — a limit, a visitor's
|
|
103
|
+
* day spent — moves it back to the device when a model there hears the
|
|
104
|
+
* language, and the press is heard there: the sound then stays in the page.
|
|
105
|
+
*/
|
|
106
|
+
export declare class AutoHearing implements Hearing {
|
|
107
|
+
private readonly device;
|
|
108
|
+
private readonly server;
|
|
109
|
+
private readonly hearsHere;
|
|
110
|
+
private place;
|
|
111
|
+
/** The device's model did not load: it is not tried again on this page. */
|
|
112
|
+
private deviceBroken;
|
|
113
|
+
constructor(device: Hearing, server: Hearing, language: string, hearsHere?: (language: string) => boolean);
|
|
114
|
+
get where(): HearingPlace;
|
|
115
|
+
private get current();
|
|
116
|
+
ready(language: string, progress?: HearingProgress): Promise<void>;
|
|
117
|
+
hearSoFar(audio: Float32Array, language: string): Promise<HeardSoFar>;
|
|
118
|
+
hear(audio: Float32Array, language: string): Promise<Transcript | undefined>;
|
|
119
|
+
}
|