@datalayer/agent-runtimes 1.5.59 → 1.5.61
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-lgDCrM9e.js → ChatView-CjD1TsaE.js} +1 -1
- package/dist-embed/datalayer-app-main.js +96 -96
- package/lib/apps/apps/AppRenderer.js +8 -1
- package/lib/apps/plugins/chat/ChatView.js +3 -1
- package/lib/chat/ChatFloating.js +4 -0
- package/lib/chat/base/ChatBase.js +34 -5
- package/lib/chat/prompt/InputPrompt.js +3 -0
- package/lib/chat/prompt/stack.d.ts +22 -0
- package/lib/chat/prompt/stack.js +40 -1
- package/lib/models/PageTag.d.ts +1 -1
- package/lib/protocols/AGUIAdapter.d.ts +8 -0
- package/lib/protocols/AGUIAdapter.js +26 -0
- package/lib/types/chat.d.ts +6 -0
- package/lib/voice/ComposerVoice.js +2 -2
- package/lib/voice/HandsFree.d.ts +14 -3
- package/lib/voice/HandsFree.js +52 -8
- package/lib/voice/VoiceInput.d.ts +6 -1
- package/lib/voice/VoiceInput.js +21 -6
- package/lib/voice/chatVoice.d.ts +1 -1
- package/lib/voice/chatVoice.js +3 -1
- package/lib/voice/consent.d.ts +6 -2
- package/lib/voice/consent.js +8 -3
- package/lib/voice/handsFree.d.ts +2 -2
- package/lib/voice/handsFree.js +1 -1
- package/lib/voice/hearing.d.ts +13 -0
- package/lib/voice/hearing.js +8 -0
- package/lib/voice/index.d.ts +8 -1
- package/lib/voice/index.js +1 -0
- package/lib/voice/keptSound.d.ts +15 -0
- package/lib/voice/keptSound.js +69 -0
- package/lib/voice/sayAloud.d.ts +7 -0
- package/lib/voice/sayAloud.js +7 -0
- package/lib/voice/serverHearing.d.ts +4 -0
- package/lib/voice/serverHearing.js +20 -0
- package/lib/voice/speaker.d.ts +15 -0
- package/lib/voice/speaker.js +36 -1
- package/lib/voice/types.d.ts +12 -0
- package/lib/voice/useVoiceSpeaker.js +2 -0
- package/package.json +1 -1
|
@@ -71,6 +71,7 @@ import { keepsFeedback } from './feedback';
|
|
|
71
71
|
import { keptBeforeFirstMessage } from './kept';
|
|
72
72
|
import { ChatLanguage } from '../../chat/ChatLanguage';
|
|
73
73
|
import { chatVoiceOf } from '../../voice/chatVoice';
|
|
74
|
+
import { keptAudioDays } from '../../voice/keptSound';
|
|
74
75
|
import { defineAppRuntimePlugin } from './AppRuntimePlugins';
|
|
75
76
|
import { hasFolderModules } from './customComponents';
|
|
76
77
|
import { appPluginPair } from './pluginPair';
|
|
@@ -471,7 +472,11 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
|
|
|
471
472
|
...speech,
|
|
472
473
|
consentKey: app.id,
|
|
473
474
|
language,
|
|
474
|
-
}
|
|
475
|
+
},
|
|
476
|
+
// What is said kept as sound, as its record says (VO-63): only for
|
|
477
|
+
// a person signed in (the host gave their token), never a visitor
|
|
478
|
+
// nor an embed's.
|
|
479
|
+
visitor || !speech.token ? undefined : keptAudioDays(app.record)),
|
|
475
480
|
};
|
|
476
481
|
}
|
|
477
482
|
catch (error) {
|
|
@@ -482,6 +487,8 @@ export function AppRenderer({ app, plugins = NO_PLUGINS, instance, onPresence, o
|
|
|
482
487
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
483
488
|
}, [
|
|
484
489
|
JSON.stringify(app.interface?.voice ?? null),
|
|
490
|
+
JSON.stringify(app.record?.include ?? null),
|
|
491
|
+
visitor,
|
|
485
492
|
app.id,
|
|
486
493
|
language,
|
|
487
494
|
speech?.speechUrl,
|
|
@@ -1720,7 +1720,9 @@ export default function ChatView({ workspace }) {
|
|
|
1720
1720
|
playbackLevel: () => controlsRef.current?.voiceLevel?.() ?? 0,
|
|
1721
1721
|
// Hands-free (VO-12, VO-13): the run in progress, stopped when
|
|
1722
1722
|
// the person speaks over it, and the answer marked interrupted.
|
|
1723
|
-
running: busy,
|
|
1723
|
+
running: busy,
|
|
1724
|
+
// Spoken over, the application's session stays open (VO-46).
|
|
1725
|
+
onStopRun: () => (controlsRef.current?.interrupt ?? controlsRef.current?.stop)?.(), onInterrupted: () => controlsRef.current?.markInterrupted(), onInterimChange: setVoiceInterim,
|
|
1724
1726
|
// The menu offers when answers are heard where the
|
|
1725
1727
|
// application lets them be (VO-21, VO-30).
|
|
1726
1728
|
answers: Boolean(config?.voice?.output &&
|
package/lib/chat/ChatFloating.js
CHANGED
|
@@ -10,6 +10,7 @@ import { ButtonGlow } from './display/ButtonGlow';
|
|
|
10
10
|
import { useViewportDrag } from './useViewportDrag';
|
|
11
11
|
import { disabledChatViewModes, resolveMountPoint } from './viewModes';
|
|
12
12
|
import { AssistantStage } from './assistant/AssistantStage';
|
|
13
|
+
import { ASSISTANT_CLEARANCE_VAR } from './assistant/clearance';
|
|
13
14
|
import { decisionsAskerAt } from './assistant/decisions';
|
|
14
15
|
import { DecisionAsk } from './assistant/DecisionAsk';
|
|
15
16
|
import { useSpokenAnswers } from '../voice/useSpokenAnswers';
|
|
@@ -854,6 +855,9 @@ export function ChatFloating({ endpoint, protocol: protocolProp, useStore: useSt
|
|
|
854
855
|
: undefined;
|
|
855
856
|
/* The window itself; where it goes is decided below. */
|
|
856
857
|
const chatWindow = (_jsxs(Box, { ref: popupRef, className: className, sx: {
|
|
858
|
+
// Its own conversation stands beside the character, never under it:
|
|
859
|
+
// its prompt's controls keep their own padding (T-27).
|
|
860
|
+
...(assistantShown ? { [ASSISTANT_CLEARANCE_VAR]: 'initial' } : {}),
|
|
857
861
|
position: viewMode === 'panel' && !isMobile && (panelInFlow || dockedInMount)
|
|
858
862
|
? 'relative'
|
|
859
863
|
: 'fixed',
|
|
@@ -59,7 +59,7 @@ import { useVoiceSettings } from '../../voice/useVoiceSettings';
|
|
|
59
59
|
import { useVoiceSpeaker } from '../../voice/useVoiceSpeaker';
|
|
60
60
|
import { useSpokenAnswers } from '../../voice/useSpokenAnswers';
|
|
61
61
|
import { saidMessageId, useSaidSentence } from '../../voice/captions';
|
|
62
|
-
import { lineToSay } from '../../voice/sayAloud';
|
|
62
|
+
import { isVoiceInterrupted, lineToSay, } from '../../voice/sayAloud';
|
|
63
63
|
import { isFloatingChatViewMode } from '../viewModes';
|
|
64
64
|
import { ToolApprovalBanner, ToolApprovalDialog, } from '../tools';
|
|
65
65
|
import { initialModelId, isOffered, offeredModels as offeredModelsFor, } from './modelChoice';
|
|
@@ -2986,6 +2986,16 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2986
2986
|
*/
|
|
2987
2987
|
const [voiceSaid, setVoiceSaid] = useState('');
|
|
2988
2988
|
voiceSayRef.current = activity => {
|
|
2989
|
+
if (isVoiceInterrupted(activity)) {
|
|
2990
|
+
// Spoken over, the runtime says (VO-46): its voice stops.
|
|
2991
|
+
if (onStopSpeaking) {
|
|
2992
|
+
onStopSpeaking();
|
|
2993
|
+
}
|
|
2994
|
+
else if (ownVoice.speaker) {
|
|
2995
|
+
ownVoice.stop();
|
|
2996
|
+
}
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2989
2999
|
const line = lineToSay(activity);
|
|
2990
3000
|
if (!line) {
|
|
2991
3001
|
return;
|
|
@@ -3077,7 +3087,14 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3077
3087
|
const forwardedProps = spoken
|
|
3078
3088
|
? { ...(forwardedPropsGiven ?? {}), voice: spoken }
|
|
3079
3089
|
: forwardedPropsGiven;
|
|
3080
|
-
|
|
3090
|
+
// The message says how it was heard; its sound, sent to be kept
|
|
3091
|
+
// (VO-63), rides on the run only, never on the conversation.
|
|
3092
|
+
let heardAs;
|
|
3093
|
+
if (spoken) {
|
|
3094
|
+
const { sound: _sound, ...rest } = spoken;
|
|
3095
|
+
heardAs = rest;
|
|
3096
|
+
}
|
|
3097
|
+
const userMessage = createUserMessage(messageContent, heardAs ? { ...heardAs } : undefined);
|
|
3081
3098
|
const currentMessages = displayItems.filter((item) => !isToolCallMessage(item));
|
|
3082
3099
|
const allMessages = [...currentMessages, userMessage];
|
|
3083
3100
|
{
|
|
@@ -3251,7 +3268,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3251
3268
|
applyServerApprovalDecision,
|
|
3252
3269
|
]);
|
|
3253
3270
|
// ---- handleStop ----
|
|
3254
|
-
|
|
3271
|
+
// Spoken over (VO-46, VO-13), an application's session is told so and
|
|
3272
|
+
// stays open; Stop leaves it to be resumed.
|
|
3273
|
+
const stopTurn = useCallback((spokenOver) => {
|
|
3255
3274
|
stoppedRef.current = true;
|
|
3256
3275
|
abortControllerRef.current?.abort();
|
|
3257
3276
|
// Best-effort cancellation without tearing down adapter/session.
|
|
@@ -3262,7 +3281,10 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3262
3281
|
adapter.stopGeneration();
|
|
3263
3282
|
}
|
|
3264
3283
|
// Also tell the backend to stop (server-side cancellation).
|
|
3265
|
-
if (typeof adapter.
|
|
3284
|
+
if (spokenOver && typeof adapter.interruptSession === 'function') {
|
|
3285
|
+
void adapter.interruptSession().catch(() => { });
|
|
3286
|
+
}
|
|
3287
|
+
else if (typeof adapter.terminateSession === 'function') {
|
|
3266
3288
|
void adapter.terminateSession().catch(() => { });
|
|
3267
3289
|
}
|
|
3268
3290
|
else if (typeof adapter.terminateAgent === 'function') {
|
|
@@ -3314,6 +3336,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3314
3336
|
protocol?.authToken,
|
|
3315
3337
|
protocol?.agentId,
|
|
3316
3338
|
]);
|
|
3339
|
+
const handleStop = useCallback(() => stopTurn(false), [stopTurn]);
|
|
3340
|
+
const handleSpokenOver = useCallback(() => stopTurn(true), [stopTurn]);
|
|
3317
3341
|
/*
|
|
3318
3342
|
* How a message's display presses what it offered (RU-4,
|
|
3319
3343
|
* RU-W): with the token of the offer, as the run's press — which
|
|
@@ -3390,6 +3414,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3390
3414
|
voiceLevel: playbackLevel,
|
|
3391
3415
|
// The answer spoken over, marked for a host's own composer (VO-13).
|
|
3392
3416
|
markInterrupted,
|
|
3417
|
+
// Spoken over: the run ends, the application's session stays open (VO-46).
|
|
3418
|
+
interrupt: handleSpokenOver,
|
|
3393
3419
|
});
|
|
3394
3420
|
return () => {
|
|
3395
3421
|
onSendReady(null);
|
|
@@ -3400,6 +3426,7 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3400
3426
|
onSendMessage,
|
|
3401
3427
|
handleSend,
|
|
3402
3428
|
handleStop,
|
|
3429
|
+
handleSpokenOver,
|
|
3403
3430
|
handleNewChat,
|
|
3404
3431
|
stopSpeaking,
|
|
3405
3432
|
playbackLevel,
|
|
@@ -3808,7 +3835,9 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3808
3835
|
playbackLevel: playbackLevel,
|
|
3809
3836
|
// Hands-free (VO-12, VO-13): the run in progress, stopped when the
|
|
3810
3837
|
// person speaks over it, and the answer marked interrupted.
|
|
3811
|
-
running: isLoading,
|
|
3838
|
+
running: isLoading,
|
|
3839
|
+
// Spoken over, the application's session stays open (VO-46).
|
|
3840
|
+
onStopRun: handleSpokenOver, onInterrupted: markInterrupted, onInterimChange: setVoiceInterim,
|
|
3812
3841
|
// The menu offers when answers are heard where the application
|
|
3813
3842
|
// lets them be (VO-21, VO-30).
|
|
3814
3843
|
answers: Boolean(voice?.output && voice.output !== 'off' && voice.speechUrl), onSpoken: metadata => {
|
|
@@ -151,6 +151,9 @@ export function InputPrompt({ input, setInput, isLoading, kernelStatus, connecti
|
|
|
151
151
|
{
|
|
152
152
|
id: 'session-controls',
|
|
153
153
|
px: padding,
|
|
154
|
+
// Its right end — the voice controls — is never under
|
|
155
|
+
// the floating assistant in its corner (T-27).
|
|
156
|
+
clearsAssistant: true,
|
|
154
157
|
py: 0.5,
|
|
155
158
|
minHeight: 36,
|
|
156
159
|
bordered: true,
|
|
@@ -18,6 +18,19 @@
|
|
|
18
18
|
* @module chat/prompt/stack
|
|
19
19
|
*/
|
|
20
20
|
import type { ReactNode } from 'react';
|
|
21
|
+
/**
|
|
22
|
+
* A theme space step (`px={2}`) as a CSS length, from the theme's scale or
|
|
23
|
+
* else Primer's own.
|
|
24
|
+
*/
|
|
25
|
+
export declare function spaceLength(step: number, scale?: readonly (string | number)[]): string;
|
|
26
|
+
/**
|
|
27
|
+
* The right padding of a band that `clearsAssistant`: the floating
|
|
28
|
+
* assistant's clearance while it stands in a bottom corner (what it covers
|
|
29
|
+
* from the window's right edge, `--assistant-clearance`), so that the
|
|
30
|
+
* character is never drawn over the band's controls and never takes their
|
|
31
|
+
* clicks; else the band's own `padding`, a CSS length.
|
|
32
|
+
*/
|
|
33
|
+
export declare function clearOfAssistant(padding: string): string;
|
|
21
34
|
/** One band in a region. */
|
|
22
35
|
export type PromptStack = {
|
|
23
36
|
/** Identity, for React and for a host reasoning about which band is which. */
|
|
@@ -69,6 +82,15 @@ export type PromptStack = {
|
|
|
69
82
|
* showing the page behind it — a pale stripe down the right-hand side.
|
|
70
83
|
*/
|
|
71
84
|
layout?: 'row' | 'block';
|
|
85
|
+
/**
|
|
86
|
+
* Keep the band's right end clear of the floating assistant while it
|
|
87
|
+
* stands in a bottom corner: its right padding becomes the assistant's
|
|
88
|
+
* clearance (`--assistant-clearance`), so that the character is never
|
|
89
|
+
* drawn over the band's controls and never takes their clicks. For a band
|
|
90
|
+
* that runs to the page's right edge, as the composer's session controls
|
|
91
|
+
* do on an application's page.
|
|
92
|
+
*/
|
|
93
|
+
clearsAssistant?: boolean;
|
|
72
94
|
};
|
|
73
95
|
export type PromptStacksProps = {
|
|
74
96
|
/** The bands, top to bottom. */
|
package/lib/chat/prompt/stack.js
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useTheme } from '@primer/react';
|
|
2
3
|
import { Box } from '@datalayer/primer-addons';
|
|
4
|
+
import { ASSISTANT_CLEARANCE_VAR } from '../assistant/clearance';
|
|
5
|
+
/** Primer's space scale, for a band drawn without a theme. */
|
|
6
|
+
const PRIMER_SPACE = [
|
|
7
|
+
'0',
|
|
8
|
+
'4px',
|
|
9
|
+
'8px',
|
|
10
|
+
'16px',
|
|
11
|
+
'24px',
|
|
12
|
+
'32px',
|
|
13
|
+
'40px',
|
|
14
|
+
'48px',
|
|
15
|
+
'64px',
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* A theme space step (`px={2}`) as a CSS length, from the theme's scale or
|
|
19
|
+
* else Primer's own.
|
|
20
|
+
*/
|
|
21
|
+
export function spaceLength(step, scale = PRIMER_SPACE) {
|
|
22
|
+
const value = scale[step] ?? PRIMER_SPACE[step] ?? `${step * 8}px`;
|
|
23
|
+
return typeof value === 'number' ? `${value}px` : value;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The right padding of a band that `clearsAssistant`: the floating
|
|
27
|
+
* assistant's clearance while it stands in a bottom corner (what it covers
|
|
28
|
+
* from the window's right edge, `--assistant-clearance`), so that the
|
|
29
|
+
* character is never drawn over the band's controls and never takes their
|
|
30
|
+
* clicks; else the band's own `padding`, a CSS length.
|
|
31
|
+
*/
|
|
32
|
+
export function clearOfAssistant(padding) {
|
|
33
|
+
return `var(${ASSISTANT_CLEARANCE_VAR}, ${padding})`;
|
|
34
|
+
}
|
|
3
35
|
/**
|
|
4
36
|
* Whether a band would draw anything.
|
|
5
37
|
*
|
|
@@ -12,12 +44,19 @@ function isRendered(stack) {
|
|
|
12
44
|
/** Render a region's bands, skipping the ones with nothing to show. */
|
|
13
45
|
export function PromptStacks({ stacks, disabled = false, }) {
|
|
14
46
|
const visible = stacks.filter(isRendered);
|
|
47
|
+
const { theme } = useTheme();
|
|
15
48
|
if (visible.length === 0) {
|
|
16
49
|
return null;
|
|
17
50
|
}
|
|
18
51
|
return (_jsx(_Fragment, { children: visible.map(stack => {
|
|
19
52
|
const isDisabled = disabled || stack.disabled;
|
|
20
|
-
|
|
53
|
+
const px = stack.px ?? 2;
|
|
54
|
+
const right = Array.isArray(px) ? px[px.length - 1] : px;
|
|
55
|
+
return (_jsx(Box, { "data-prompt-stack": stack.id, "aria-disabled": isDisabled || undefined, px: px, "data-clears-assistant": stack.clearsAssistant ? '' : undefined, sx: stack.clearsAssistant
|
|
56
|
+
? {
|
|
57
|
+
paddingRight: clearOfAssistant(spaceLength(right, theme?.space)),
|
|
58
|
+
}
|
|
59
|
+
: undefined, py: stack.py ?? 1, minHeight: stack.minHeight, display: stack.layout === 'block' ? undefined : 'flex', alignItems: stack.layout === 'block' ? undefined : 'center', gap: stack.layout === 'block' ? undefined : (stack.gap ?? 2), pt: stack.pt === undefined ? undefined : stack.pt, pb: stack.pb === undefined ? undefined : stack.pb, borderTop: stack.bordered ? '1px solid' : undefined, borderColor: stack.bordered ? 'border.default' : undefined, bg: stack.subtle ? 'canvas.subtle' : undefined, opacity: isDisabled ? 0.5 : undefined, pointerEvents: isDisabled ? 'none' : undefined, children: stack.content }, stack.id));
|
|
21
60
|
}) }));
|
|
22
61
|
}
|
|
23
62
|
export default PromptStacks;
|
package/lib/models/PageTag.d.ts
CHANGED
|
@@ -15,5 +15,5 @@ export type PageTag = {
|
|
|
15
15
|
export declare const PAGE_TAGE_NONE: PageTag;
|
|
16
16
|
export declare const PAGE_TAGS: PageTag[];
|
|
17
17
|
export declare const PAGE_TAGS_BY_NAME_MAP: Map<PageTagName | undefined, PageTag>;
|
|
18
|
-
export declare const PAGE_TAGS_BY_COLOR_MAP: Map<"
|
|
18
|
+
export declare const PAGE_TAGS_BY_COLOR_MAP: Map<"blue" | "coral" | "gray" | "green" | "indigo" | "lime" | "orange" | "pink" | "purple" | "red" | "teal" | "yellow" | "default" | "lemon", PageTag>;
|
|
19
19
|
export {};
|
|
@@ -66,6 +66,14 @@ export declare class AGUIAdapter extends BaseProtocolAdapter {
|
|
|
66
66
|
* Terminate a running agent thread on the backend
|
|
67
67
|
*/
|
|
68
68
|
terminateAgent(threadId?: string): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* The person spoke over the agent (VO-46, VO-13): an application's
|
|
71
|
+
* session is told (`POST {sessions}/{uid}/interrupt`) — what runs is
|
|
72
|
+
* cancelled, its code's `@app.interrupted` runs, and it stays open for
|
|
73
|
+
* what is said next, where Stop would leave it waiting to be resumed.
|
|
74
|
+
* Another agent's run is stopped, as Stop stops it.
|
|
75
|
+
*/
|
|
76
|
+
interruptSession(threadId?: string): Promise<void>;
|
|
69
77
|
/**
|
|
70
78
|
* Stop an application's session (`POST {sessions}/{uid}/stop`): what runs
|
|
71
79
|
* is cancelled and it waits to be resumed. Its thread is the session; with
|
|
@@ -126,6 +126,32 @@ export class AGUIAdapter extends BaseProtocolAdapter {
|
|
|
126
126
|
// Ignore errors - agent may have already completed
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* The person spoke over the agent (VO-46, VO-13): an application's
|
|
131
|
+
* session is told (`POST {sessions}/{uid}/interrupt`) — what runs is
|
|
132
|
+
* cancelled, its code's `@app.interrupted` runs, and it stays open for
|
|
133
|
+
* what is said next, where Stop would leave it waiting to be resumed.
|
|
134
|
+
* Another agent's run is stopped, as Stop stops it.
|
|
135
|
+
*/
|
|
136
|
+
async interruptSession(threadId) {
|
|
137
|
+
const sessions = this.aguiConfig.appSessions;
|
|
138
|
+
const thread = threadId || this.currentThreadId;
|
|
139
|
+
if (!sessions) {
|
|
140
|
+
await this.terminateAgent(threadId);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (!thread) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const url = `${sessions.replace(/\/+$/, '')}/${encodeURIComponent(thread)}/interrupt`;
|
|
147
|
+
const response = await this.fetchOf()(url, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
headers: this.buildHeaders(),
|
|
150
|
+
});
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
throw new Error(`The session ${thread} could not be told it was spoken over (${response.status}).`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
129
155
|
/**
|
|
130
156
|
* Stop an application's session (`POST {sessions}/{uid}/stop`): what runs
|
|
131
157
|
* is cancelled and it waits to be resumed. Its thread is the session; with
|
package/lib/types/chat.d.ts
CHANGED
|
@@ -700,6 +700,12 @@ export interface ChatBaseProps {
|
|
|
700
700
|
* for a host whose composer holds the microphone.
|
|
701
701
|
*/
|
|
702
702
|
markInterrupted: () => void;
|
|
703
|
+
/**
|
|
704
|
+
* The person spoke over the run (VO-46, VO-13): it ends as Stop ends
|
|
705
|
+
* it, but an application's session is told it was spoken over and
|
|
706
|
+
* stays open for what is said next — Stop leaves it to be resumed.
|
|
707
|
+
*/
|
|
708
|
+
interrupt?: () => void;
|
|
703
709
|
} | null) => void;
|
|
704
710
|
/**
|
|
705
711
|
* Reports whether the agent's voice is speaking, for a host that draws the
|
|
@@ -55,14 +55,14 @@ export function ComposerVoice({ voice, input, setInput, onSpoken, send, disabled
|
|
|
55
55
|
if (!hearing) {
|
|
56
56
|
return null;
|
|
57
57
|
}
|
|
58
|
-
return (_jsxs("span", { "data-composer-voice": "", style: { display: 'inline-flex', alignItems: 'center', gap: 2 }, children: [_jsx(VoiceMenu, { voice: voice, answers: answers, disabled: disabled }), voice.input === 'hands_free' ? (_jsx(HandsFree, { hearing: hearing, language: voice.language, consentKey: voice.consentKey ?? 'chat', microphoneId: voice.microphoneId, turnSilenceMs: voice.turnSilenceMs, disabled: disabled, running: running, speaking: speaking, playbackLevel: playbackLevel, onStopSpeaking: onStopSpeaking, onStopRun: onStopRun, onInterrupted: onInterrupted, onInterim: (words) => {
|
|
58
|
+
return (_jsxs("span", { "data-composer-voice": "", style: { display: 'inline-flex', alignItems: 'center', gap: 2 }, children: [_jsx(VoiceMenu, { voice: voice, answers: answers, disabled: disabled }), voice.input === 'hands_free' ? (_jsx(HandsFree, { hearing: hearing, language: voice.language, consentKey: voice.consentKey ?? 'chat', keepsAudio: voice.keepsAudio, microphoneId: voice.microphoneId, turnSilenceMs: voice.turnSilenceMs, disabled: disabled, running: running, speaking: speaking, playbackLevel: playbackLevel, onStopSpeaking: onStopSpeaking, onStopRun: onStopRun, onInterrupted: onInterrupted, onInterim: (words) => {
|
|
59
59
|
setInput(words ? afterBase(words) : base.current);
|
|
60
60
|
onInterimChange?.(words !== '');
|
|
61
61
|
}, send: (text, metadata) => {
|
|
62
62
|
onInterimChange?.(false);
|
|
63
63
|
setInput(base.current);
|
|
64
64
|
send(text, metadata);
|
|
65
|
-
} })) : null, _jsx(VoiceInput, { hearing: hearing, language: voice.language, consentKey: voice.consentKey ?? 'chat', silenceMs: voice.silenceMs, microphoneId: voice.microphoneId, disabled: disabled, speaking: speaking, onStopSpeaking: onStopSpeaking, onListen: () => {
|
|
65
|
+
} })) : null, _jsx(VoiceInput, { hearing: hearing, language: voice.language, consentKey: voice.consentKey ?? 'chat', keepsAudio: voice.keepsAudio, silenceMs: voice.silenceMs, microphoneId: voice.microphoneId, disabled: disabled, speaking: speaking, onStopSpeaking: onStopSpeaking, onListen: () => {
|
|
66
66
|
base.current = input.trim();
|
|
67
67
|
onStopSpeaking?.();
|
|
68
68
|
},
|
package/lib/voice/HandsFree.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* On, the microphone stays open. The words are written in the composer as
|
|
7
7
|
* they are heard (VO-11); once the person has spoken and been silent for the
|
|
8
|
-
* turn's silence (`turnSilenceMs`,
|
|
8
|
+
* turn's silence (`turnSilenceMs`, 500 ms unless said), what was said is
|
|
9
9
|
* heard whole and **sent** — nothing waits for a click. The answer is said as
|
|
10
10
|
* it is written (hands-free implies `output: always`), and listening goes
|
|
11
11
|
* on through it: speech heard while the agent thinks or speaks is
|
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import type { JSX } from 'react';
|
|
26
26
|
import type { Hearing } from './hearing';
|
|
27
|
+
/**
|
|
28
|
+
* While the agent answers, how often the microphone is looked at for the
|
|
29
|
+
* person speaking over it, by the VAD alone (VO-52): looked at every
|
|
30
|
+
* `INTERIM_MS` with the words transcribed, barge-in waited up to 0.7 s.
|
|
31
|
+
*/
|
|
32
|
+
export declare const BARGE_IN_POLL_MS = 150;
|
|
27
33
|
import type { SpokenMetadata } from './types';
|
|
28
34
|
export interface HandsFreeProps {
|
|
29
35
|
hearing: Hearing;
|
|
@@ -31,9 +37,14 @@ export interface HandsFreeProps {
|
|
|
31
37
|
language: string;
|
|
32
38
|
/** Whose consent to the microphone is remembered: the application's id. */
|
|
33
39
|
consentKey: string;
|
|
40
|
+
/**
|
|
41
|
+
* The days the application keeps what is said as sound (VO-63): asked in
|
|
42
|
+
* the first sentence, and each turn's recording sent with its words.
|
|
43
|
+
*/
|
|
44
|
+
keepsAudio?: number;
|
|
34
45
|
/** The microphone the person chose (VO-30); the browser's own when unsaid. */
|
|
35
46
|
microphoneId?: string;
|
|
36
|
-
/** The silence after speech that ends a turn, in ms;
|
|
47
|
+
/** The silence after speech that ends a turn, in ms; 500 unless said (VO-12). */
|
|
37
48
|
turnSilenceMs?: number;
|
|
38
49
|
/** A turn, heard whole: sent at once. */
|
|
39
50
|
send: (text: string, metadata: SpokenMetadata) => void;
|
|
@@ -57,5 +68,5 @@ export interface HandsFreeProps {
|
|
|
57
68
|
onInterrupted?: () => void;
|
|
58
69
|
disabled?: boolean;
|
|
59
70
|
}
|
|
60
|
-
export declare function HandsFree({ hearing, language, consentKey, microphoneId, turnSilenceMs, send, onInterim, running, speaking, playbackLevel, onStopSpeaking, onStopRun, onInterrupted, disabled, }: HandsFreeProps): JSX.Element;
|
|
71
|
+
export declare function HandsFree({ hearing, language, consentKey, keepsAudio, microphoneId, turnSilenceMs, send, onInterim, running, speaking, playbackLevel, onStopSpeaking, onStopRun, onInterrupted, disabled, }: HandsFreeProps): JSX.Element;
|
|
61
72
|
export default HandsFree;
|
package/lib/voice/HandsFree.js
CHANGED
|
@@ -2,13 +2,24 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { Button, IconButton, Text, Tooltip } from '@primer/react';
|
|
4
4
|
import { Box } from '@datalayer/primer-addons';
|
|
5
|
-
import { openMicrophone } from './capture';
|
|
5
|
+
import { HEARING_RATE, openMicrophone } from './capture';
|
|
6
6
|
import { CONSENT_SENTENCE, SERVER_CONSENT_SENTENCE, consent, consentKeyFor, consented, } from './consent';
|
|
7
7
|
import { HANDS_FREE_WORDS, IDLE_PAUSE_MS, TURN_SILENCE_MS, bargedIn, handsFreeState, turnEnded, } from './handsFree';
|
|
8
|
+
import { keptAudioSentence, soundOf } from './keptSound';
|
|
8
9
|
import { INTERIM_MS } from './VoiceInput';
|
|
10
|
+
/**
|
|
11
|
+
* While the agent answers, how often the microphone is looked at for the
|
|
12
|
+
* person speaking over it, by the VAD alone (VO-52): looked at every
|
|
13
|
+
* `INTERIM_MS` with the words transcribed, barge-in waited up to 0.7 s.
|
|
14
|
+
*/
|
|
15
|
+
export const BARGE_IN_POLL_MS = 150;
|
|
16
|
+
/** The last of the sound looked at for barge-in: 1.5 s, so each look is cheap. */
|
|
17
|
+
const BARGE_IN_WINDOW = Math.round(HEARING_RATE * 1.5);
|
|
18
|
+
/** The least speech that is barge-in, in seconds: a short word (VO-52). */
|
|
19
|
+
const BARGE_IN_SHORTEST = 0.12;
|
|
9
20
|
/** Waits. */
|
|
10
21
|
const pause = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
11
|
-
export function HandsFree({ hearing, language, consentKey, microphoneId, turnSilenceMs = TURN_SILENCE_MS, send, onInterim, running = false, speaking = false, playbackLevel, onStopSpeaking, onStopRun, onInterrupted, disabled = false, }) {
|
|
22
|
+
export function HandsFree({ hearing, language, consentKey, keepsAudio, microphoneId, turnSilenceMs = TURN_SILENCE_MS, send, onInterim, running = false, speaking = false, playbackLevel, onStopSpeaking, onStopRun, onInterrupted, disabled = false, }) {
|
|
12
23
|
const [on, setOn] = useState(false);
|
|
13
24
|
const [paused, setPaused] = useState(false);
|
|
14
25
|
const [asking, setAsking] = useState(false);
|
|
@@ -35,7 +46,7 @@ export function HandsFree({ hearing, language, consentKey, microphoneId, turnSil
|
|
|
35
46
|
onStopRun,
|
|
36
47
|
onInterrupted,
|
|
37
48
|
};
|
|
38
|
-
const key = consentKeyFor(consentKey, hearing.where);
|
|
49
|
+
const key = consentKeyFor(consentKey, hearing.where, keepsAudio);
|
|
39
50
|
const turnOn = useCallback(() => {
|
|
40
51
|
if (!consented(key)) {
|
|
41
52
|
setAsking(true);
|
|
@@ -82,8 +93,18 @@ export function HandsFree({ hearing, language, consentKey, microphoneId, turnSil
|
|
|
82
93
|
}
|
|
83
94
|
heardWhile.current =
|
|
84
95
|
stateRef.current === 'listening' ? 'listening' : 'answering';
|
|
96
|
+
// Once the person spoke in this turn, its end is looked for often,
|
|
97
|
+
// by the VAD alone, the words between (VO-52).
|
|
98
|
+
let spokeThisTurn = false;
|
|
99
|
+
let wordsAt = 0;
|
|
85
100
|
while (alive) {
|
|
86
|
-
|
|
101
|
+
const quick = (Boolean(hearing.spokeSoFar) &&
|
|
102
|
+
(stateRef.current === 'thinking' ||
|
|
103
|
+
stateRef.current === 'speaking')) ||
|
|
104
|
+
(Boolean(hearing.quietSoFar) &&
|
|
105
|
+
spokeThisTurn &&
|
|
106
|
+
stateRef.current === 'listening');
|
|
107
|
+
await pause(quick ? BARGE_IN_POLL_MS : INTERIM_MS);
|
|
87
108
|
const open = capture.current;
|
|
88
109
|
if (!alive || !open) {
|
|
89
110
|
return;
|
|
@@ -102,11 +123,27 @@ export function HandsFree({ hearing, language, consentKey, microphoneId, turnSil
|
|
|
102
123
|
}
|
|
103
124
|
heardWhile.current = answering ? 'answering' : 'listening';
|
|
104
125
|
quietSince = Date.now();
|
|
126
|
+
spokeThisTurn = false;
|
|
105
127
|
continue;
|
|
106
128
|
}
|
|
107
129
|
let so;
|
|
108
130
|
try {
|
|
109
|
-
|
|
131
|
+
if (answering && hearing.spokeSoFar) {
|
|
132
|
+
// Barge-in only: the VAD over the last moment, nothing transcribed.
|
|
133
|
+
const sound = open.peek();
|
|
134
|
+
const spoke = await hearing.spokeSoFar(sound.subarray(Math.max(0, sound.length - BARGE_IN_WINDOW)), BARGE_IN_SHORTEST);
|
|
135
|
+
so = { text: '', spoke, silentMs: 0 };
|
|
136
|
+
}
|
|
137
|
+
else if (spokeThisTurn &&
|
|
138
|
+
hearing.quietSoFar &&
|
|
139
|
+
Date.now() - wordsAt < INTERIM_MS) {
|
|
140
|
+
// The turn's end looked for: the VAD alone, no words.
|
|
141
|
+
so = await hearing.quietSoFar(open.peek());
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
so = await hearing.hearSoFar(open.peek(), language);
|
|
145
|
+
wordsAt = Date.now();
|
|
146
|
+
}
|
|
110
147
|
}
|
|
111
148
|
catch {
|
|
112
149
|
so = undefined;
|
|
@@ -119,6 +156,9 @@ export function HandsFree({ hearing, language, consentKey, microphoneId, turnSil
|
|
|
119
156
|
}
|
|
120
157
|
if (so.spoke) {
|
|
121
158
|
quietSince = Date.now();
|
|
159
|
+
if (now === 'listening') {
|
|
160
|
+
spokeThisTurn = true;
|
|
161
|
+
}
|
|
122
162
|
}
|
|
123
163
|
const playing = latest.current.playbackLevel;
|
|
124
164
|
const levels = playing
|
|
@@ -155,7 +195,10 @@ export function HandsFree({ hearing, language, consentKey, microphoneId, turnSil
|
|
|
155
195
|
}
|
|
156
196
|
latest.current.onInterim?.('');
|
|
157
197
|
if (heard) {
|
|
158
|
-
|
|
198
|
+
// Kept as sound too, as the person agreed (VO-63).
|
|
199
|
+
latest.current.send(heard.text, keepsAudio
|
|
200
|
+
? { ...heard.metadata, sound: soundOf(audio) }
|
|
201
|
+
: heard.metadata);
|
|
159
202
|
}
|
|
160
203
|
try {
|
|
161
204
|
capture.current = await openMicrophone(microphoneId);
|
|
@@ -165,6 +208,7 @@ export function HandsFree({ hearing, language, consentKey, microphoneId, turnSil
|
|
|
165
208
|
}
|
|
166
209
|
heardWhile.current = 'listening';
|
|
167
210
|
quietSince = Date.now();
|
|
211
|
+
spokeThisTurn = false;
|
|
168
212
|
continue;
|
|
169
213
|
}
|
|
170
214
|
if (now === 'listening' && Date.now() - quietSince >= IDLE_PAUSE_MS) {
|
|
@@ -179,7 +223,7 @@ export function HandsFree({ hearing, language, consentKey, microphoneId, turnSil
|
|
|
179
223
|
capture.current = undefined;
|
|
180
224
|
};
|
|
181
225
|
// The loop reads the state through a ref: it is not restarted on it.
|
|
182
|
-
}, [on, paused, hearing, language, microphoneId, turnSilenceMs]);
|
|
226
|
+
}, [on, paused, hearing, language, microphoneId, turnSilenceMs, keepsAudio]);
|
|
183
227
|
// Esc pauses it.
|
|
184
228
|
useEffect(() => {
|
|
185
229
|
if (!on) {
|
|
@@ -215,7 +259,7 @@ export function HandsFree({ hearing, language, consentKey, microphoneId, turnSil
|
|
|
215
259
|
latest.current.onInterim?.('');
|
|
216
260
|
} }))] })) : null, problem ? (_jsx(Text, { sx: { fontSize: 0, color: 'danger.fg' }, "data-hands-free-problem": "", children: problem })) : null, asking ? (_jsxs(Box, { role: "dialog", "aria-label": "The microphone", "data-hands-free-consent": "", position: "absolute", bottom: "100%", left: 0, width: 280, p: 3, bg: "canvas.overlay", border: "1px solid", borderColor: "border.default", borderRadius: 2, boxShadow: "shadow.large", zIndex: 10, children: [_jsx(Text, { as: "p", sx: { fontSize: 1, mb: 2 }, children: hearing.where === 'server'
|
|
217
261
|
? SERVER_CONSENT_SENTENCE
|
|
218
|
-
: CONSENT_SENTENCE }), _jsxs(Box, { display: "flex", gap: 2, justifyContent: "flex-end", children: [_jsx(Button, { size: "small", onClick: () => setAsking(false), children: "Not now" }), _jsx(Button, { size: "small", variant: "primary", "data-hands-free-consent-yes": "", onClick: () => {
|
|
262
|
+
: CONSENT_SENTENCE }), keepsAudio ? (_jsx(Text, { as: "p", sx: { fontSize: 1, mb: 2 }, "data-voice-consent-kept": "", children: keptAudioSentence(keepsAudio) })) : null, _jsxs(Box, { display: "flex", gap: 2, justifyContent: "flex-end", children: [_jsx(Button, { size: "small", onClick: () => setAsking(false), children: "Not now" }), _jsx(Button, { size: "small", variant: "primary", "data-hands-free-consent-yes": "", onClick: () => {
|
|
219
263
|
consent(key);
|
|
220
264
|
setAsking(false);
|
|
221
265
|
setProblem(undefined);
|
|
@@ -48,10 +48,15 @@ export interface VoiceInputProps {
|
|
|
48
48
|
onListen?: () => void;
|
|
49
49
|
/** Whose consent is remembered: the application's id. */
|
|
50
50
|
consentKey: string;
|
|
51
|
+
/**
|
|
52
|
+
* The days the application keeps what is said as sound (VO-63): asked in
|
|
53
|
+
* the first sentence, and each turn's recording sent with its words.
|
|
54
|
+
*/
|
|
55
|
+
keepsAudio?: number;
|
|
51
56
|
disabled?: boolean;
|
|
52
57
|
/** The agent is speaking: offer to stop it. */
|
|
53
58
|
speaking?: boolean;
|
|
54
59
|
onStopSpeaking?: () => void;
|
|
55
60
|
}
|
|
56
|
-
export declare function VoiceInput({ hearing, language, onTranscript, onInterim, silenceMs, microphoneId, onListen, consentKey, disabled, speaking, onStopSpeaking, }: VoiceInputProps): JSX.Element;
|
|
61
|
+
export declare function VoiceInput({ hearing, language, onTranscript, onInterim, silenceMs, microphoneId, onListen, consentKey, keepsAudio, disabled, speaking, onStopSpeaking, }: VoiceInputProps): JSX.Element;
|
|
57
62
|
export default VoiceInput;
|
package/lib/voice/VoiceInput.js
CHANGED
|
@@ -6,6 +6,7 @@ import { MicrophoneIcon } from '@datalayer/icons-react';
|
|
|
6
6
|
import { MuteIcon } from '@primer/octicons-react';
|
|
7
7
|
import { MicrophoneRefused, openMicrophone } from './capture';
|
|
8
8
|
import { CONSENT_SENTENCE, SERVER_CONSENT_SENTENCE, consent, consentKeyFor, consented, } from './consent';
|
|
9
|
+
import { keptAudioSentence, soundOf } from './keptSound';
|
|
9
10
|
const WORDS = {
|
|
10
11
|
idle: '',
|
|
11
12
|
asking: '',
|
|
@@ -27,7 +28,7 @@ function prefersStill() {
|
|
|
27
28
|
return (typeof window !== 'undefined' &&
|
|
28
29
|
!!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches);
|
|
29
30
|
}
|
|
30
|
-
export function VoiceInput({ hearing, language, onTranscript, onInterim, silenceMs = 1500, microphoneId, onListen, consentKey, disabled = false, speaking = false, onStopSpeaking, }) {
|
|
31
|
+
export function VoiceInput({ hearing, language, onTranscript, onInterim, silenceMs = 1500, microphoneId, onListen, consentKey, keepsAudio, disabled = false, speaking = false, onStopSpeaking, }) {
|
|
31
32
|
const [state, setState] = useState('idle');
|
|
32
33
|
const [said, setSaid] = useState('');
|
|
33
34
|
const [problem, setProblem] = useState();
|
|
@@ -49,7 +50,7 @@ export function VoiceInput({ hearing, language, onTranscript, onInterim, silence
|
|
|
49
50
|
if (disabled || stateRef.current !== 'idle') {
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
|
-
if (!consented(consentKeyFor(consentKey, hearing.where))) {
|
|
53
|
+
if (!consented(consentKeyFor(consentKey, hearing.where, keepsAudio))) {
|
|
53
54
|
setState('asking');
|
|
54
55
|
return;
|
|
55
56
|
}
|
|
@@ -75,7 +76,15 @@ export function VoiceInput({ hearing, language, onTranscript, onInterim, silence
|
|
|
75
76
|
? error.message
|
|
76
77
|
: 'The microphone could not be opened.');
|
|
77
78
|
}
|
|
78
|
-
}, [
|
|
79
|
+
}, [
|
|
80
|
+
consentKey,
|
|
81
|
+
keepsAudio,
|
|
82
|
+
disabled,
|
|
83
|
+
hearing,
|
|
84
|
+
language,
|
|
85
|
+
microphoneId,
|
|
86
|
+
onListen,
|
|
87
|
+
]);
|
|
79
88
|
const stop = useCallback(async () => {
|
|
80
89
|
const open = capture.current;
|
|
81
90
|
capture.current = undefined;
|
|
@@ -99,7 +108,13 @@ export function VoiceInput({ hearing, language, onTranscript, onInterim, silence
|
|
|
99
108
|
return;
|
|
100
109
|
}
|
|
101
110
|
setSaid(`Heard: ${transcript.text}`);
|
|
102
|
-
|
|
111
|
+
// Kept as sound too, as the person agreed (VO-63).
|
|
112
|
+
onTranscript(keepsAudio
|
|
113
|
+
? {
|
|
114
|
+
...transcript,
|
|
115
|
+
metadata: { ...transcript.metadata, sound: soundOf(audio) },
|
|
116
|
+
}
|
|
117
|
+
: transcript);
|
|
103
118
|
}
|
|
104
119
|
catch (error) {
|
|
105
120
|
setState('idle');
|
|
@@ -287,8 +302,8 @@ export function VoiceInput({ hearing, language, onTranscript, onInterim, silence
|
|
|
287
302
|
}
|
|
288
303
|
} }) }), state === 'asking' && (_jsxs(Box, { role: "dialog", "aria-label": "Talk instead of typing", "data-voice-consent": "", position: "absolute", bottom: "100%", right: 0, mb: 2, width: 280, p: 3, bg: "canvas.overlay", border: "1px solid", borderColor: "border.default", borderRadius: 2, boxShadow: "shadow.large", zIndex: 10, children: [_jsx(Text, { as: "p", sx: { fontSize: 1, mb: 2 }, children: hearing.where === 'server'
|
|
289
304
|
? SERVER_CONSENT_SENTENCE
|
|
290
|
-
: CONSENT_SENTENCE }), _jsxs(Box, { display: "flex", gap: 2, justifyContent: "flex-end", children: [_jsx(Button, { size: "small", onClick: () => setState('idle'), children: "Not now" }), _jsx(Button, { size: "small", variant: "primary", "data-voice-consent-yes": "", onClick: () => {
|
|
291
|
-
consent(consentKeyFor(consentKey, hearing.where));
|
|
305
|
+
: CONSENT_SENTENCE }), keepsAudio ? (_jsx(Text, { as: "p", sx: { fontSize: 1, mb: 2 }, "data-voice-consent-kept": "", children: keptAudioSentence(keepsAudio) })) : null, _jsxs(Box, { display: "flex", gap: 2, justifyContent: "flex-end", children: [_jsx(Button, { size: "small", onClick: () => setState('idle'), children: "Not now" }), _jsx(Button, { size: "small", variant: "primary", "data-voice-consent-yes": "", onClick: () => {
|
|
306
|
+
consent(consentKeyFor(consentKey, hearing.where, keepsAudio));
|
|
292
307
|
// Asked, and said yes: listen now, without another press.
|
|
293
308
|
stateRef.current = 'idle';
|
|
294
309
|
setState('idle');
|
package/lib/voice/chatVoice.d.ts
CHANGED
|
@@ -40,7 +40,7 @@ export interface ChatVoicePlace {
|
|
|
40
40
|
* - **Speaking**: its answers said by Datalayer's speech service, on request
|
|
41
41
|
* or always (VO-20, VO-21); refused here when its host says no service.
|
|
42
42
|
*/
|
|
43
|
-
export declare function chatVoiceOf(spec: AppVoiceSpec | undefined, place: ChatVoicePlace): ChatVoice | undefined;
|
|
43
|
+
export declare function chatVoiceOf(spec: AppVoiceSpec | undefined, place: ChatVoicePlace, keepsAudio?: number): ChatVoice | undefined;
|
|
44
44
|
/**
|
|
45
45
|
* A chat's voice asked of the speech service as the chat itself speaks to
|
|
46
46
|
* its runtime (VO-37, VO-19): the token its host gave, else the chat's own —
|