@datalayer/agent-runtimes 1.5.27 → 1.5.30
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-C_NOLqwI.js +32 -0
- package/dist-embed/datalayer-app-main.js +103 -93
- package/lib/apps/apps/AppKept.js +5 -1
- package/lib/apps/apps/appspec.d.ts +0 -5
- package/lib/apps/apps/appspec.js +1 -6
- package/lib/apps/apps/checks.js +4 -9
- package/lib/apps/embed/element.js +24 -7
- package/lib/apps/embed/embedConfig.d.ts +11 -2
- package/lib/apps/embed/embedConfig.js +26 -2
- package/lib/apps/embed/visitorApp.d.ts +9 -2
- package/lib/apps/embed/visitorApp.js +9 -2
- package/lib/apps/plugins/chat/ChatView.js +6 -3
- package/lib/chat/assistant/AssistantStage.js +17 -0
- package/lib/chat/assistant/clearance.d.ts +10 -0
- package/lib/chat/assistant/clearance.js +14 -0
- package/lib/chat/base/ChatBase.js +12 -3
- package/lib/specs/appspecSchema.js +2 -2
- package/lib/specs/voices.js +89 -0
- package/lib/types/agentspecs.d.ts +1 -1
- package/lib/voice/ComposerVoice.d.ts +10 -2
- package/lib/voice/ComposerVoice.js +35 -5
- package/lib/voice/HandsFree.d.ts +55 -0
- package/lib/voice/HandsFree.js +211 -0
- package/lib/voice/VoiceInput.d.ts +2 -2
- package/lib/voice/VoiceInput.js +6 -4
- package/lib/voice/VoiceMenu.js +2 -1
- package/lib/voice/chatVoice.d.ts +6 -4
- package/lib/voice/chatVoice.js +11 -12
- package/lib/voice/consent.d.ts +8 -0
- package/lib/voice/consent.js +10 -0
- package/lib/voice/handsFree.d.ts +58 -0
- package/lib/voice/handsFree.js +48 -0
- package/lib/voice/hearing.d.ts +23 -1
- package/lib/voice/hearing.js +18 -3
- package/lib/voice/index.d.ts +21 -8
- package/lib/voice/index.js +3 -0
- package/lib/voice/serverHearing.d.ts +58 -0
- package/lib/voice/serverHearing.js +136 -0
- package/lib/voice/settings.d.ts +5 -0
- package/lib/voice/settings.js +8 -0
- package/lib/voice/speaker.d.ts +4 -3
- package/lib/voice/speaker.js +1 -2
- package/lib/voice/useVoiceSpeaker.js +0 -3
- package/package.json +2 -2
- package/scripts/voice/measure.py +74 -0
- package/dist-embed/chunks/ChatView-CUWE6I5M.js +0 -32
package/lib/apps/apps/AppKept.js
CHANGED
|
@@ -4,6 +4,7 @@ import { contribution, definePlugin, signal } from '@datalayer/reactor';
|
|
|
4
4
|
import { useContributions, useSignalValue } from '@datalayer/reactor/react';
|
|
5
5
|
import { LoopChatTurn, LoopPromptPanel } from '../core';
|
|
6
6
|
import { beforeFirstMessage } from './kept';
|
|
7
|
+
import { ASSISTANT_CLEARANCE_VAR } from '../../chat/assistant/clearance';
|
|
7
8
|
export const APP_KEPT_PLUGIN_NAME = '@datalayer/loop-plugin-app-kept';
|
|
8
9
|
/* A signal to read when no chat contributed a turn: the hook needs one. */
|
|
9
10
|
const NO_TURN = signal({ id: 0, status: 'idle' });
|
|
@@ -13,7 +14,10 @@ export function AppKept({ says }) {
|
|
|
13
14
|
if (!beforeFirstMessage(turn)) {
|
|
14
15
|
return null;
|
|
15
16
|
}
|
|
16
|
-
return (
|
|
17
|
+
return (
|
|
18
|
+
// Clear of the floating assistant in its corner, at a phone's width too
|
|
19
|
+
// (REVIEWS Q-08).
|
|
20
|
+
_jsx("div", { role: "note", style: { padding: `4px var(${ASSISTANT_CLEARANCE_VAR}, 8px) 4px 8px` }, children: _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: says }) }));
|
|
17
21
|
}
|
|
18
22
|
/**
|
|
19
23
|
* The plugin, for one application: what it keeps, under its prompt. Made
|
|
@@ -99,11 +99,6 @@ export declare const PAP_PROFILE: PapProfile;
|
|
|
99
99
|
* (`pap_profile_problem`); `undefined` when it is one Datalayer speaks.
|
|
100
100
|
*/
|
|
101
101
|
export declare function papProfileProblem(value: unknown): string | undefined;
|
|
102
|
-
/**
|
|
103
|
-
* Why `interface.voice.input: hands_free` is refused until VOICE.md VO-12 is
|
|
104
|
-
* built (decided 14 October 2026): never taken for click-to-talk.
|
|
105
|
-
*/
|
|
106
|
-
export declare const HANDS_FREE_NOT_BUILT = "hands_free is not built yet (VOICE.md VO-12): say push_to_talk \u2014 click the microphone or hold it, speak, and it stops when let go or once the person stops speaking";
|
|
107
102
|
/** An application's voice (VOICE.md VO-41): off unless said. */
|
|
108
103
|
export declare const DEFAULT_VOICE: AppVoiceSpec;
|
|
109
104
|
/** What a page's output may be drawn with (LOOP P-05), and the property of each that shows its value. */
|
package/lib/apps/apps/appspec.js
CHANGED
|
@@ -713,11 +713,6 @@ function parseSurface(data) {
|
|
|
713
713
|
composedAt: text(data.composed_at),
|
|
714
714
|
};
|
|
715
715
|
}
|
|
716
|
-
/**
|
|
717
|
-
* Why `interface.voice.input: hands_free` is refused until VOICE.md VO-12 is
|
|
718
|
-
* built (decided 14 October 2026): never taken for click-to-talk.
|
|
719
|
-
*/
|
|
720
|
-
export const HANDS_FREE_NOT_BUILT = 'hands_free is not built yet (VOICE.md VO-12): say push_to_talk — click the microphone or hold it, speak, and it stops when let go or once the person stops speaking';
|
|
721
716
|
/** An application's voice (VOICE.md VO-41): off unless said. */
|
|
722
717
|
export const DEFAULT_VOICE = {
|
|
723
718
|
enabled: false,
|
|
@@ -731,7 +726,7 @@ function parseVoice(data) {
|
|
|
731
726
|
const voice = isData(data) ? data : {};
|
|
732
727
|
return {
|
|
733
728
|
enabled: voice.enabled === true,
|
|
734
|
-
input: oneOf(voice.input, ['off', 'push_to_talk'], DEFAULT_VOICE.input),
|
|
729
|
+
input: oneOf(voice.input, ['off', 'push_to_talk', 'hands_free'], DEFAULT_VOICE.input),
|
|
735
730
|
output: oneOf(voice.output, ['off', 'on_request', 'always'], DEFAULT_VOICE.output),
|
|
736
731
|
voice: text(voice.voice),
|
|
737
732
|
language: text(voice.language),
|
package/lib/apps/apps/checks.js
CHANGED
|
@@ -39,7 +39,7 @@ import { getTeamSpec } from '../../specs/teams';
|
|
|
39
39
|
import { getBackendToolSpec } from '../../specs/backendTools';
|
|
40
40
|
import { getTrack } from '../../specs/tracks';
|
|
41
41
|
import { pluginsOffSetupNotes } from '../plugins/canvas-blocks';
|
|
42
|
-
import { APP_THEME_MODES, APP_THEME_VARIANTS,
|
|
42
|
+
import { APP_THEME_MODES, APP_THEME_VARIANTS, isAssistantCharacterId, PAGE_OUTPUT_SHOWS, parseAppspec, } from './appspec';
|
|
43
43
|
import { classesOf, splitRef, toolBehaviours } from './rules';
|
|
44
44
|
import { COMMAND_INPUT, COMMAND_NAME, MODE_ID } from './composer';
|
|
45
45
|
import { HOST_NAME, HOST_USERS, hostToolsOf } from './hostTools';
|
|
@@ -685,7 +685,7 @@ const ENUMS = {
|
|
|
685
685
|
visibility: ['private', 'invited', 'organization', 'link', 'public'],
|
|
686
686
|
mode: ['inline', 'bubble', 'panel', 'assistant'],
|
|
687
687
|
balloon: ['history', 'current'],
|
|
688
|
-
voiceInput: ['off', 'push_to_talk'],
|
|
688
|
+
voiceInput: ['off', 'push_to_talk', 'hands_free'],
|
|
689
689
|
voiceOutput: ['off', 'on_request', 'always'],
|
|
690
690
|
voiceWhere: ['auto', 'device', 'server'],
|
|
691
691
|
themeVariant: APP_THEME_VARIANTS,
|
|
@@ -843,14 +843,9 @@ export function documentShapeProblems(document) {
|
|
|
843
843
|
if (ui.assistant !== undefined && !isAssistantCharacterId(ui.assistant)) {
|
|
844
844
|
at('interface.assistant', 'names a character by the id a plugin contributes it under, lowercase words joined by a hyphen: `paperclip`, `acme-owl`');
|
|
845
845
|
}
|
|
846
|
-
// Its voice (VOICE.md VO-41
|
|
846
|
+
// Its voice (VOICE.md VO-41, VO-12).
|
|
847
847
|
mapping(ui.voice, 'interface.voice', voice => {
|
|
848
|
-
|
|
849
|
-
at('interface.voice.input', HANDS_FREE_NOT_BUILT);
|
|
850
|
-
}
|
|
851
|
-
else {
|
|
852
|
-
oneOf(voice.input, ENUMS.voiceInput, 'interface.voice.input');
|
|
853
|
-
}
|
|
848
|
+
oneOf(voice.input, ENUMS.voiceInput, 'interface.voice.input');
|
|
854
849
|
oneOf(voice.output, ENUMS.voiceOutput, 'interface.voice.output');
|
|
855
850
|
oneOf(voice.where, ENUMS.voiceWhere, 'interface.voice.where');
|
|
856
851
|
});
|
|
@@ -70,7 +70,7 @@ import { parseAppspec } from '../apps/appspec';
|
|
|
70
70
|
import { readAppspecYaml } from '../apps/yaml';
|
|
71
71
|
import { AppEmbed, embedAssistantCharacter } from './AppEmbed';
|
|
72
72
|
import { WINDOW_NOT_YET } from './embedSession';
|
|
73
|
-
import { EMBED_HOST_VARIABLES, EMBED_OBSERVED_ATTRIBUTES, EMBED_TAG, EmbedAttributeError, adoptPropertiesSetEarly, embedLookOf, inlineHeightOf, languageOf, resumeOf, runnable, } from './embedConfig';
|
|
73
|
+
import { EMBED_HOST_VARIABLES, EMBED_OBSERVED_ATTRIBUTES, EMBED_TAG, EMBED_TOKEN_NAMES_NO_DEPLOYMENT, EmbedAttributeError, adoptPropertiesSetEarly, embedLookOf, embedTokenDeployment, inlineHeightOf, languageOf, resumeOf, runnable, } from './embedConfig';
|
|
74
74
|
import { embedShadowCss } from './embedTheme';
|
|
75
75
|
import { DEFAULT_API, readVisitorApp } from './visitorApp';
|
|
76
76
|
export { runnable };
|
|
@@ -164,9 +164,15 @@ export async function readEmbeddedApp({ api, origin, app, token, fetcher = fetch
|
|
|
164
164
|
return { kind: 'frame', src: frameSrcOf(origin, app, token) };
|
|
165
165
|
}
|
|
166
166
|
const spec = runnable(parseAppspec(definition.spec));
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
if (spec.kind === 'decision') {
|
|
168
|
+
return { kind: 'frame', src: frameSrcOf(origin, app, token) };
|
|
169
|
+
}
|
|
170
|
+
// The visit runs the deployment its token names, and no other (VOICE.md
|
|
171
|
+
// decision 16): a token naming none is refused before anything is drawn.
|
|
172
|
+
if (!embedTokenDeployment(token)) {
|
|
173
|
+
return { kind: 'said', text: EMBED_TOKEN_NAMES_NO_DEPLOYMENT };
|
|
174
|
+
}
|
|
175
|
+
return { kind: 'spec', app: spec };
|
|
170
176
|
}
|
|
171
177
|
/** The script that is running, when it is a script tag. */
|
|
172
178
|
function currentScriptUrl() {
|
|
@@ -377,7 +383,7 @@ export function defineDatalayerAppElement(options = {}) {
|
|
|
377
383
|
// A public application, as a visitor without an account (D-07):
|
|
378
384
|
// an example kept warm or an address open to visitors, run on the
|
|
379
385
|
// visitors' runtime; a decision framed; a refusal said.
|
|
380
|
-
const visitor = await readVisitorApp({ api: this.
|
|
386
|
+
const visitor = await readVisitorApp({ api: this.agents(), app });
|
|
381
387
|
if (visitor.kind === 'decision') {
|
|
382
388
|
return { kind: 'frame', src: frameSrcOf(this.origin(), visitor.app) };
|
|
383
389
|
}
|
|
@@ -402,11 +408,22 @@ export function defineDatalayerAppElement(options = {}) {
|
|
|
402
408
|
}
|
|
403
409
|
return this.spoken;
|
|
404
410
|
}
|
|
405
|
-
/**
|
|
411
|
+
/**
|
|
412
|
+
* ai-agents, where a visitor's address is read and beside which the
|
|
413
|
+
* visitors' runtime and ai-inference answer: the `api` attribute, else
|
|
414
|
+
* the platform's, else r1's. Not Spacer's address (`api()`), which lives
|
|
415
|
+
* on another plane.
|
|
416
|
+
*/
|
|
417
|
+
agents() {
|
|
418
|
+
return (this.getAttribute('api') ||
|
|
419
|
+
coreStore.getState().configuration?.aiAgentsUrl ||
|
|
420
|
+
DEFAULT_API).replace(/\/+$/, '');
|
|
421
|
+
}
|
|
422
|
+
/** Spacer, where an application is read with its embed token: the `api` attribute, else the platform's, else prod1's. */
|
|
406
423
|
api() {
|
|
407
424
|
return (this.getAttribute('api') ||
|
|
408
425
|
coreStore.getState().configuration?.spacerUrl ||
|
|
409
|
-
|
|
426
|
+
'https://prod1.datalayer.run').replace(/\/+$/, '');
|
|
410
427
|
}
|
|
411
428
|
async load() {
|
|
412
429
|
const turn = ++this.loading;
|
|
@@ -25,20 +25,29 @@ export declare const EMBED_STYLESHEET_PATH = "/embed/datalayer-app.css";
|
|
|
25
25
|
export declare const EMBED_ATTRIBUTES: {
|
|
26
26
|
readonly app: "The application’s id on Datalayer.";
|
|
27
27
|
readonly origin: "Where Datalayer is; the origin the script was loaded from by default.";
|
|
28
|
-
readonly token: "An embed token for an application that is not public, issued to your server for each visit.";
|
|
28
|
+
readonly token: "An embed token for an application that is not public, issued to your server for each visit; it names the embedded deployment the visit runs, which nothing else on the page names.";
|
|
29
29
|
readonly spec: "The address of an Appspec (YAML or JSON) to run, in place of `app`.";
|
|
30
30
|
readonly mode: "inline, bubble, panel or assistant; the Appspec’s `deployment.embedded.mode` by default, then inline.";
|
|
31
31
|
readonly accent: "green, rose, sky, lime, sun or violet; the Appspec’s `interface.accent` by default, else the theme’s own colours.";
|
|
32
32
|
readonly theme: "light, dark or auto (the visitor’s system); auto by default.";
|
|
33
33
|
readonly font: "A CSS font family for the application’s text; Inter, then the system’s sans-serif, by default.";
|
|
34
34
|
readonly server: "An agent-runtimes server the application’s agent runs on; a Datalayer runtime by default.";
|
|
35
|
-
readonly api: "Datalayer’s API
|
|
35
|
+
readonly api: "Datalayer’s API: where an application is read with its token (Spacer), and where a visitor’s address is read (ai-agents); the platform’s by default.";
|
|
36
36
|
readonly height: "The height of an inline application, in pixels; 640 by default.";
|
|
37
37
|
readonly resume: "true or false: whether a visit’s conversation is picked up again after the page reloads, kept in the page’s storage; true by default.";
|
|
38
38
|
readonly language: "The language the visitor reads the application in, as BCP 47 tags it (fr, pt-BR): its translation into it, and the chat’s own words; the visitor’s browser’s by default.";
|
|
39
39
|
readonly 'user-token': "A token your server signed with the deployment’s secret, naming your user (HS256: sub, name, exp at most an hour away): what an application that says user: signed opens its session with.";
|
|
40
40
|
};
|
|
41
41
|
export type EmbedAttribute = keyof typeof EMBED_ATTRIBUTES;
|
|
42
|
+
/**
|
|
43
|
+
* The deployment an embed token names (VOICE.md decision 16): the embedded
|
|
44
|
+
* deployment its owner's server asked it for. Read without vouching for it —
|
|
45
|
+
* the services that take the token verify it — and the only place the
|
|
46
|
+
* element learns its deployment: it takes no attribute for one.
|
|
47
|
+
*/
|
|
48
|
+
export declare function embedTokenDeployment(token: string): string | undefined;
|
|
49
|
+
/** What an embed token naming no deployment is told (VOICE.md decision 16). */
|
|
50
|
+
export declare const EMBED_TOKEN_NAMES_NO_DEPLOYMENT = "datalayer-app: the embed token names no deployment. Your server asks for one naming the application\u2019s embedded deployment: POST \u2026/api/spacer/v1/apps/<id>/embed-token with {\"deployment\": \"<its uid>\"}.";
|
|
42
51
|
/** Every attribute the element watches. */
|
|
43
52
|
export declare const EMBED_OBSERVED_ATTRIBUTES: EmbedAttribute[];
|
|
44
53
|
/**
|
|
@@ -16,19 +16,43 @@ export const EMBED_STYLESHEET_PATH = '/embed/datalayer-app.css';
|
|
|
16
16
|
export const EMBED_ATTRIBUTES = {
|
|
17
17
|
app: 'The application’s id on Datalayer.',
|
|
18
18
|
origin: 'Where Datalayer is; the origin the script was loaded from by default.',
|
|
19
|
-
token: 'An embed token for an application that is not public, issued to your server for each visit.',
|
|
19
|
+
token: 'An embed token for an application that is not public, issued to your server for each visit; it names the embedded deployment the visit runs, which nothing else on the page names.',
|
|
20
20
|
spec: 'The address of an Appspec (YAML or JSON) to run, in place of `app`.',
|
|
21
21
|
mode: 'inline, bubble, panel or assistant; the Appspec’s `deployment.embedded.mode` by default, then inline.',
|
|
22
22
|
accent: 'green, rose, sky, lime, sun or violet; the Appspec’s `interface.accent` by default, else the theme’s own colours.',
|
|
23
23
|
theme: 'light, dark or auto (the visitor’s system); auto by default.',
|
|
24
24
|
font: 'A CSS font family for the application’s text; Inter, then the system’s sans-serif, by default.',
|
|
25
25
|
server: 'An agent-runtimes server the application’s agent runs on; a Datalayer runtime by default.',
|
|
26
|
-
api: 'Datalayer’s API
|
|
26
|
+
api: 'Datalayer’s API: where an application is read with its token (Spacer), and where a visitor’s address is read (ai-agents); the platform’s by default.',
|
|
27
27
|
height: 'The height of an inline application, in pixels; 640 by default.',
|
|
28
28
|
resume: 'true or false: whether a visit’s conversation is picked up again after the page reloads, kept in the page’s storage; true by default.',
|
|
29
29
|
language: 'The language the visitor reads the application in, as BCP 47 tags it (fr, pt-BR): its translation into it, and the chat’s own words; the visitor’s browser’s by default.',
|
|
30
30
|
'user-token': 'A token your server signed with the deployment’s secret, naming your user (HS256: sub, name, exp at most an hour away): what an application that says user: signed opens its session with.',
|
|
31
31
|
};
|
|
32
|
+
/**
|
|
33
|
+
* The deployment an embed token names (VOICE.md decision 16): the embedded
|
|
34
|
+
* deployment its owner's server asked it for. Read without vouching for it —
|
|
35
|
+
* the services that take the token verify it — and the only place the
|
|
36
|
+
* element learns its deployment: it takes no attribute for one.
|
|
37
|
+
*/
|
|
38
|
+
export function embedTokenDeployment(token) {
|
|
39
|
+
const payload = token.split('.')[1];
|
|
40
|
+
if (!payload) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
|
|
45
|
+
const claims = JSON.parse(atob(base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=')));
|
|
46
|
+
return typeof claims.deployment_uid === 'string' && claims.deployment_uid
|
|
47
|
+
? claims.deployment_uid
|
|
48
|
+
: undefined;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** What an embed token naming no deployment is told (VOICE.md decision 16). */
|
|
55
|
+
export const EMBED_TOKEN_NAMES_NO_DEPLOYMENT = 'datalayer-app: the embed token names no deployment. Your server asks for one naming the application’s embedded deployment: POST …/api/spacer/v1/apps/<id>/embed-token with {"deployment": "<its uid>"}.';
|
|
32
56
|
/** Every attribute the element watches. */
|
|
33
57
|
export const EMBED_OBSERVED_ATTRIBUTES = Object.keys(EMBED_ATTRIBUTES);
|
|
34
58
|
/**
|
|
@@ -42,8 +42,15 @@ export declare const VISITORS_PATH = "/api/runtime-visitors";
|
|
|
42
42
|
export declare const VISITOR_EXAMPLES: readonly string[];
|
|
43
43
|
/** The key of an application at its address, as a visitor's token names it. */
|
|
44
44
|
export declare const AT_PREFIX = "at:";
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Datalayer's services for a visitor, where the element names none (`api`)
|
|
47
|
+
* and the platform's configuration says none: ai-agents, where an address is
|
|
48
|
+
* read, and beside it the visitors' runtime and ai-inference, which mints the
|
|
49
|
+
* visitor's token. Not Spacer's address, which reads an application with its
|
|
50
|
+
* embed token: the two live on different planes (ai-agents on r1, Spacer on
|
|
51
|
+
* prod1), found 15 October 2026 on datalayer.ai's embed.
|
|
52
|
+
*/
|
|
53
|
+
export declare const DEFAULT_API = "https://r1.datalayer.run";
|
|
47
54
|
/** An application's id on Datalayer: a ULID. */
|
|
48
55
|
export declare const isAppUid: (value: string) => boolean;
|
|
49
56
|
/** What `app` with no token resolves to. */
|
|
@@ -14,8 +14,15 @@ export const VISITOR_EXAMPLES = [
|
|
|
14
14
|
];
|
|
15
15
|
/** The key of an application at its address, as a visitor's token names it. */
|
|
16
16
|
export const AT_PREFIX = 'at:';
|
|
17
|
-
/**
|
|
18
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Datalayer's services for a visitor, where the element names none (`api`)
|
|
19
|
+
* and the platform's configuration says none: ai-agents, where an address is
|
|
20
|
+
* read, and beside it the visitors' runtime and ai-inference, which mints the
|
|
21
|
+
* visitor's token. Not Spacer's address, which reads an application with its
|
|
22
|
+
* embed token: the two live on different planes (ai-agents on r1, Spacer on
|
|
23
|
+
* prod1), found 15 October 2026 on datalayer.ai's embed.
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_API = 'https://r1.datalayer.run';
|
|
19
26
|
/** An application's id on Datalayer: a ULID. */
|
|
20
27
|
export const isAppUid = (value) => /^[0-9A-HJKMNP-TV-Z]{26}$/.test(value);
|
|
21
28
|
/** An address, as ai-agents spells one. */
|
|
@@ -1321,9 +1321,9 @@ export default function ChatView({ workspace }) {
|
|
|
1321
1321
|
/*
|
|
1322
1322
|
* Its voice asked of the speech service as this chat speaks to its runtime
|
|
1323
1323
|
* (VOICE.md VO-37, VO-19): the host's token, else this one — a visitor's,
|
|
1324
|
-
* an embed's or the person's
|
|
1324
|
+
* an embed's or the person's; an embed's names its deployment (decision 16).
|
|
1325
1325
|
*/
|
|
1326
|
-
const askedVoice = useMemo(() => voiceAskedAs(config?.voice, iamToken || undefined
|
|
1326
|
+
const askedVoice = useMemo(() => voiceAskedAs(config?.voice, iamToken || undefined), [config?.voice, iamToken]);
|
|
1327
1327
|
/*
|
|
1328
1328
|
* A deployment's runtime, and the visitors', answer only the application's
|
|
1329
1329
|
* own routes (OwnRoutesOnly, PAP_DATALAYER's release gates): its chat calls
|
|
@@ -1700,7 +1700,10 @@ export default function ChatView({ workspace }) {
|
|
|
1700
1700
|
footerExtras: promptActions.length > 0 || (voice && voice.input !== 'off') ? (_jsxs(_Fragment, { children: [promptActions.length > 0 ? (_jsx(ReactorSlot, { slot: LoopSlots.promptAction, props: { workspace } })) : null, voice && voice.input !== 'off' ? (_jsx(ComposerVoice, { voice: voice, input: draft, setInput: setDraft, disabled: chatDisabled,
|
|
1701
1701
|
// The chat's own speaker, never a second one: the microphone
|
|
1702
1702
|
// opening stops it, and *Stop speaking* while it speaks.
|
|
1703
|
-
speaking: speaking, onStopSpeaking: () => controlsRef.current?.stopSpeaking?.(),
|
|
1703
|
+
speaking: speaking, onStopSpeaking: () => controlsRef.current?.stopSpeaking?.(),
|
|
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,
|
|
1704
1707
|
// The menu offers when answers are heard where the
|
|
1705
1708
|
// application lets them be (VO-21, VO-30).
|
|
1706
1709
|
answers: Boolean(config?.voice?.output &&
|
|
@@ -7,6 +7,7 @@ import { Dialog, Text } from '@primer/react';
|
|
|
7
7
|
import { assistantCharacter } from './characters';
|
|
8
8
|
import { BalloonHistoryLarge, SpeechBalloon, } from './SpeechBalloon';
|
|
9
9
|
import { conversationHeaderText } from './BalloonParts';
|
|
10
|
+
import { ASSISTANT_CLEARANCE_VAR } from './clearance';
|
|
10
11
|
import { AssistantContextMenu, LONG_PRESS_MS, opensContextMenu, } from './AssistantContextMenu';
|
|
11
12
|
import { BalloonExpandContext, ExpandedVisual, focusExpanded, } from './BalloonVisual';
|
|
12
13
|
import { SpriteCharacter } from './SpriteCharacter';
|
|
@@ -405,6 +406,22 @@ export function AssistantStage({ character, state, size = 88, place, stageRef, o
|
|
|
405
406
|
telling ||
|
|
406
407
|
detailing !== null ||
|
|
407
408
|
stayPut);
|
|
409
|
+
// What it covers in its corner, for a line written under a prompt there
|
|
410
|
+
// (REVIEWS Q-08).
|
|
411
|
+
const cornerRight = place.left === undefined && place.top === undefined
|
|
412
|
+
? pixels(place.right)
|
|
413
|
+
: undefined;
|
|
414
|
+
const cornerBottom = pixels(place.bottom);
|
|
415
|
+
useEffect(() => {
|
|
416
|
+
if (aside || cornerRight === undefined || cornerBottom === undefined) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const root = document.documentElement;
|
|
420
|
+
root.style.setProperty(ASSISTANT_CLEARANCE_VAR, `${cornerRight + size}px`);
|
|
421
|
+
return () => {
|
|
422
|
+
root.style.removeProperty(ASSISTANT_CLEARANCE_VAR);
|
|
423
|
+
};
|
|
424
|
+
}, [aside, cornerRight, cornerBottom, size]);
|
|
408
425
|
useMouth(stageRef, mouthLevel, state === 'speaking');
|
|
409
426
|
// A decision being asked, or its answer, keeps the balloon up.
|
|
410
427
|
const [deciding, setDeciding] = useState(false);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the floating assistant covers from the right edge of the window while
|
|
3
|
+
* it stands in a bottom corner, as a CSS length on the document: a line
|
|
4
|
+
* written under a prompt there (what an application keeps, R-31) leaves it
|
|
5
|
+
* free, so that the character never stands over its words at a phone's width
|
|
6
|
+
* (REVIEWS Q-08). Unset when it stands anywhere else, steps aside or is gone.
|
|
7
|
+
*
|
|
8
|
+
* @module chat/assistant/clearance
|
|
9
|
+
*/
|
|
10
|
+
export declare const ASSISTANT_CLEARANCE_VAR = "--assistant-clearance";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* What the floating assistant covers from the right edge of the window while
|
|
7
|
+
* it stands in a bottom corner, as a CSS length on the document: a line
|
|
8
|
+
* written under a prompt there (what an application keeps, R-31) leaves it
|
|
9
|
+
* free, so that the character never stands over its words at a phone's width
|
|
10
|
+
* (REVIEWS Q-08). Unset when it stands anywhere else, steps aside or is gone.
|
|
11
|
+
*
|
|
12
|
+
* @module chat/assistant/clearance
|
|
13
|
+
*/
|
|
14
|
+
export const ASSISTANT_CLEARANCE_VAR = '--assistant-clearance';
|
|
@@ -2968,6 +2968,8 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
2968
2968
|
const ownVoice = useVoiceSpeaker(settled, !hostSpeaks);
|
|
2969
2969
|
useSpokenAnswers(displayItems, isLoading, !hostSpeaks && settled?.output === 'always', ownVoice.speaker);
|
|
2970
2970
|
const speakingNow = voiceSpeaking || ownVoice.state.speaking;
|
|
2971
|
+
// Answers spoken over, hands-free (VOICE.md VO-13): marked *interrupted*.
|
|
2972
|
+
const [interruptedIds, setInterruptedIds] = useState(() => new Set());
|
|
2971
2973
|
const stopSpeaking = onStopSpeaking ?? (ownVoice.speaker ? ownVoice.stop : undefined);
|
|
2972
2974
|
const readAnswerAloud = onReadAloud ?? (ownVoice.speaker ? ownVoice.read : undefined);
|
|
2973
2975
|
const handleSend = useCallback(async (messageOverride,
|
|
@@ -3724,10 +3726,10 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3724
3726
|
.reverse()
|
|
3725
3727
|
.find((item) => !isToolCallMessage(item) && item.role === 'assistant')?.id
|
|
3726
3728
|
: undefined;
|
|
3727
|
-
const readAloudFooter = readAnswerAloud
|
|
3729
|
+
const readAloudFooter = readAnswerAloud || interruptedIds.size
|
|
3728
3730
|
? (message) => {
|
|
3729
3731
|
const text = getMessageText(message);
|
|
3730
|
-
return readsAloud(message, text, writingId) ? (_jsx(ReadAloud, { onRead: () => readAnswerAloud(text) })) : null;
|
|
3732
|
+
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] }));
|
|
3731
3733
|
}
|
|
3732
3734
|
: undefined;
|
|
3733
3735
|
const messagesContent = children ? (children) : (_jsxs(Box, { display: "flex", flexDirection: "column", minHeight: 0,
|
|
@@ -3745,7 +3747,14 @@ showToolApprovalBanner = true, pendingApprovals: pendingApprovalsProp, onApprove
|
|
|
3745
3747
|
// so every chat with a headless notebook gets the same transcript.
|
|
3746
3748
|
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] }));
|
|
3747
3749
|
// The microphone, in the composer's footer (VO-10, VO-11).
|
|
3748
|
-
const voiceControl = settled && settled.input !== 'off' ? (_jsx(ComposerVoice, { voice: settled, input: input, setInput: setInput, disabled: disabled || launching, speaking: speakingNow, onStopSpeaking: stopSpeaking,
|
|
3750
|
+
const voiceControl = settled && settled.input !== 'off' ? (_jsx(ComposerVoice, { voice: settled, input: input, setInput: setInput, disabled: disabled || launching, speaking: speakingNow, onStopSpeaking: stopSpeaking,
|
|
3751
|
+
// Hands-free (VO-12, VO-13): the run in progress, stopped when the
|
|
3752
|
+
// person speaks over it, and the answer marked interrupted.
|
|
3753
|
+
running: isLoading, onStopRun: handleStop, onInterrupted: () => {
|
|
3754
|
+
if (writingId) {
|
|
3755
|
+
setInterruptedIds(ids => new Set([...ids, writingId]));
|
|
3756
|
+
}
|
|
3757
|
+
}, onInterimChange: setVoiceInterim,
|
|
3749
3758
|
// The menu offers when answers are heard where the application
|
|
3750
3759
|
// lets them be (VO-21, VO-30).
|
|
3751
3760
|
answers: Boolean(voice?.output && voice.output !== 'off' && voice.speechUrl), onSpoken: metadata => {
|
|
@@ -2522,8 +2522,8 @@ export const APPSPEC_SCHEMA = {
|
|
|
2522
2522
|
type: 'string',
|
|
2523
2523
|
},
|
|
2524
2524
|
VoiceInput: {
|
|
2525
|
-
description: 'How a person talks to the application (VOICE.md VO-10).\n\n`hands_free
|
|
2526
|
-
enum: ['off', 'push_to_talk'],
|
|
2525
|
+
description: 'How a person talks to the application (VOICE.md VO-10, VO-12).\n\n`push_to_talk`: the microphone in the composer, clicked or held.\n`hands_free`: a conversation by voice — the composer offers *Talk with\nit*; a turn ends after a short silence and is sent, the answer is said,\nand listening goes on; speaking over the agent stops its voice (VO-13).\nHands-free implies answers said as they are written (`output: always`).',
|
|
2526
|
+
enum: ['off', 'push_to_talk', 'hands_free'],
|
|
2527
2527
|
title: 'VoiceInput',
|
|
2528
2528
|
type: 'string',
|
|
2529
2529
|
},
|
package/lib/specs/voices.js
CHANGED
|
@@ -328,6 +328,95 @@ export const SPEECH_MODEL_CATALOGUE = {
|
|
|
328
328
|
},
|
|
329
329
|
],
|
|
330
330
|
},
|
|
331
|
+
'whisper-large-v3-turbo': {
|
|
332
|
+
id: 'whisper-large-v3-turbo',
|
|
333
|
+
version: '0.0.1',
|
|
334
|
+
name: 'Whisper Large v3 Turbo',
|
|
335
|
+
description: "Speech to text on Datalayer's servers, on CPU: OpenAI's Whisper large-v3-turbo (809M parameters, 99 languages), converted to CTranslate2 in half precision and run by faster-whisper, its weights quantized to 8 bits when loaded. A recording of up to a minute at a time; the language is detected when the page does not say it.",
|
|
336
|
+
provider: 'openai',
|
|
337
|
+
task: 'stt',
|
|
338
|
+
engine: 'faster-whisper',
|
|
339
|
+
dtype: 'float16',
|
|
340
|
+
languages: ['en', 'fr', 'es', 'it', 'pt', 'de', 'nl', 'ja', 'zh', 'hi'],
|
|
341
|
+
where: ['server'],
|
|
342
|
+
streaming: false,
|
|
343
|
+
licence: {
|
|
344
|
+
weights: 'MIT',
|
|
345
|
+
code: 'MIT',
|
|
346
|
+
},
|
|
347
|
+
attribution: '',
|
|
348
|
+
upstream: 'https://huggingface.co/openai/whisper-large-v3-turbo',
|
|
349
|
+
revision: '0a363e9161cbc7ed1431c9597a8ceaf0c4f78fcf',
|
|
350
|
+
files: [
|
|
351
|
+
{
|
|
352
|
+
path: 'config.json',
|
|
353
|
+
sha256: 'b0253ea6c0d3bea6b1e19e91a02acfd3b53f4467362efcb5a3e6b16c9b3a9b7e',
|
|
354
|
+
size: 2263,
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
path: 'preprocessor_config.json',
|
|
358
|
+
sha256: '7ccc62c6f2765af1f3b46c00c9b5894426835a05021c8b9c01eecb6dfb542711',
|
|
359
|
+
size: 340,
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
path: 'tokenizer.json',
|
|
363
|
+
sha256: '297b13372ac43916285644fb9687add3cc62ee2a1adb60da3dc25cc94c1871fd',
|
|
364
|
+
size: 2710337,
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
path: 'vocabulary.json',
|
|
368
|
+
sha256: 'c69260f2ab26d659b7c398f9a2b2b48ed0df16c3b47d7326782fd9cba71690c1',
|
|
369
|
+
size: 1068114,
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
path: 'model.bin',
|
|
373
|
+
sha256: 'e76620f83d5f5b69efd3d87e3dc180c1bd21df9fbebacfd4335e5e1efcc018da',
|
|
374
|
+
size: 1617884929,
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
'whisper-small': {
|
|
379
|
+
id: 'whisper-small',
|
|
380
|
+
version: '0.0.1',
|
|
381
|
+
name: 'Whisper Small',
|
|
382
|
+
description: "Speech to text on Datalayer's servers for short spoken turns, on CPU: OpenAI's Whisper small (244M parameters, 99 languages), converted to CTranslate2 and run by faster-whisper, its weights quantized to 8 bits when loaded. About four times faster than large-v3-turbo; on the fixtures it hears English as well (3.3% against 2.4%) and French less well (16.1% against 2.6%). Recordings longer than a turn go to large-v3-turbo.",
|
|
383
|
+
provider: 'openai',
|
|
384
|
+
task: 'stt',
|
|
385
|
+
engine: 'faster-whisper',
|
|
386
|
+
dtype: 'float16',
|
|
387
|
+
languages: ['en', 'fr', 'es', 'it', 'pt', 'de', 'nl', 'ja', 'zh', 'hi'],
|
|
388
|
+
where: ['server'],
|
|
389
|
+
streaming: false,
|
|
390
|
+
licence: {
|
|
391
|
+
weights: 'MIT',
|
|
392
|
+
code: 'MIT',
|
|
393
|
+
},
|
|
394
|
+
attribution: '',
|
|
395
|
+
upstream: 'https://huggingface.co/openai/whisper-small',
|
|
396
|
+
revision: '536b0662742c02347bc0e980a01041f333bce120',
|
|
397
|
+
files: [
|
|
398
|
+
{
|
|
399
|
+
path: 'config.json',
|
|
400
|
+
sha256: 'b55496ac7940a7ae47d2c01eab40edfd8701feec1229d9cce3b40014383fb828',
|
|
401
|
+
size: 2370,
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
path: 'tokenizer.json',
|
|
405
|
+
sha256: 'fb7b63191e9bb045082c79fd742a3106a12c99513ab30df4a0d47fa6cb6fd0ab',
|
|
406
|
+
size: 2203239,
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
path: 'vocabulary.txt',
|
|
410
|
+
sha256: '34ce3fe1c5041027b3f8d42912270993f986dbc4bb34cf27f951e34a1e453913',
|
|
411
|
+
size: 459861,
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
path: 'model.bin',
|
|
415
|
+
sha256: '3e305921506d8872816023e4c273e75d2419fb89b24da97b4fe7bce14170d671',
|
|
416
|
+
size: 483546902,
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
},
|
|
331
420
|
};
|
|
332
421
|
/** Whether a voice speaks a language: `fr` and `fr-FR` are spoken by a `fr-FR` voice. */
|
|
333
422
|
export function voiceSpeaks(voice, language) {
|
|
@@ -958,7 +958,7 @@ export interface AppPageSpec {
|
|
|
958
958
|
export interface AppVoiceSpec {
|
|
959
959
|
enabled: boolean;
|
|
960
960
|
/** `hands_free` (VO-12) is refused until it is built. */
|
|
961
|
-
input: 'off' | 'push_to_talk';
|
|
961
|
+
input: 'off' | 'push_to_talk' | 'hands_free';
|
|
962
962
|
output: 'off' | 'on_request' | 'always';
|
|
963
963
|
voice: string;
|
|
964
964
|
language: string;
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* person to edit and send — or sent at once with *Send what I say*. While
|
|
8
8
|
* the words are still being heard the host is told (`onInterimChange`), to
|
|
9
9
|
* grey them until they are final (VO-11). Beside the microphone, the
|
|
10
|
-
* person's voice menu (`VoiceMenu`, VO-30).
|
|
10
|
+
* person's voice menu (`VoiceMenu`, VO-30). Where the application's voice
|
|
11
|
+
* says `hands_free`, *Talk with it* too (`HandsFree`, VO-12, VO-13): a turn
|
|
12
|
+
* is sent by itself and the agent's answer may be spoken over.
|
|
11
13
|
*
|
|
12
14
|
* @module voice/ComposerVoice
|
|
13
15
|
*/
|
|
@@ -29,6 +31,12 @@ export interface ComposerVoiceProps {
|
|
|
29
31
|
speaking?: boolean;
|
|
30
32
|
/** Stops the agent's voice, also when the microphone opens (VO-13). */
|
|
31
33
|
onStopSpeaking?: () => void;
|
|
34
|
+
/** The agent is answering: a run is in progress (hands-free, VO-12). */
|
|
35
|
+
running?: boolean;
|
|
36
|
+
/** Stops the run while the answer is still being written (barge-in, VO-13). */
|
|
37
|
+
onStopRun?: () => void;
|
|
38
|
+
/** The answer being written was spoken over: marked in the conversation (VO-13). */
|
|
39
|
+
onInterrupted?: () => void;
|
|
32
40
|
/**
|
|
33
41
|
* The words in the box are still being heard (`true`), or final
|
|
34
42
|
* (`false`): the host greys them until they are (VO-11).
|
|
@@ -42,5 +50,5 @@ export interface ComposerVoiceProps {
|
|
|
42
50
|
}
|
|
43
51
|
/** Whether something rides on a run as a spoken message's metadata (VO-27). */
|
|
44
52
|
export declare function spokenOf(value: unknown): SpokenMetadata | undefined;
|
|
45
|
-
export declare function ComposerVoice({ voice, input, setInput, onSpoken, send, disabled, speaking, onStopSpeaking, onInterimChange, answers, }: ComposerVoiceProps): JSX.Element | null;
|
|
53
|
+
export declare function ComposerVoice({ voice, input, setInput, onSpoken, send, disabled, speaking, onStopSpeaking, running, onStopRun, onInterrupted, onInterimChange, answers, }: ComposerVoiceProps): JSX.Element | null;
|
|
46
54
|
export default ComposerVoice;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
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
|
+
import { HandsFree } from './HandsFree';
|
|
5
|
+
import { ServerHearing, hearingPlace } from './serverHearing';
|
|
4
6
|
import { VoiceInput } from './VoiceInput';
|
|
5
7
|
import { VoiceMenu } from './VoiceMenu';
|
|
6
8
|
/** Whether something rides on a run as a spoken message's metadata (VO-27). */
|
|
@@ -13,17 +15,45 @@ export function spokenOf(value) {
|
|
|
13
15
|
? said
|
|
14
16
|
: undefined;
|
|
15
17
|
}
|
|
16
|
-
export function ComposerVoice({ voice, input, setInput, onSpoken, send, disabled = false, speaking = false, onStopSpeaking, onInterimChange, answers = false, }) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
export function ComposerVoice({ voice, input, setInput, onSpoken, send, disabled = false, speaking = false, onStopSpeaking, running = false, onStopRun, onInterrupted, onInterimChange, answers = false, }) {
|
|
19
|
+
// In the browser, or on Datalayer's servers where the application, the
|
|
20
|
+
// language or the person's detection asks for it (§5, VO-14, VO-34).
|
|
21
|
+
const place = hearingPlace(voice);
|
|
22
|
+
const hearing = useMemo(() => {
|
|
23
|
+
if (voice.input === 'off') {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const device = new DeviceHearing(voice.modelsUrl, voice.engines);
|
|
27
|
+
return place === 'server' && voice.speechUrl
|
|
28
|
+
? new ServerHearing(device, {
|
|
29
|
+
speechUrl: voice.speechUrl,
|
|
30
|
+
token: voice.token,
|
|
31
|
+
detect: voice.detect,
|
|
32
|
+
})
|
|
33
|
+
: device;
|
|
34
|
+
}, [
|
|
35
|
+
voice.input,
|
|
36
|
+
voice.modelsUrl,
|
|
37
|
+
voice.engines,
|
|
38
|
+
place,
|
|
39
|
+
voice.speechUrl,
|
|
40
|
+
voice.token,
|
|
41
|
+
voice.detect,
|
|
42
|
+
]);
|
|
20
43
|
// What the box held when the microphone opened: the words heard go after it.
|
|
21
44
|
const base = useRef('');
|
|
22
45
|
const afterBase = (words) => [base.current, words].filter(Boolean).join(' ');
|
|
23
46
|
if (!hearing) {
|
|
24
47
|
return null;
|
|
25
48
|
}
|
|
26
|
-
return (_jsxs("span", { "data-composer-voice": "", style: { display: 'inline-flex', alignItems: 'center', gap: 2 }, children: [_jsx(VoiceMenu, { voice: voice, answers: answers, disabled: disabled }), _jsx(
|
|
49
|
+
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, onStopSpeaking: onStopSpeaking, onStopRun: onStopRun, onInterrupted: onInterrupted, onInterim: (words) => {
|
|
50
|
+
setInput(words ? afterBase(words) : base.current);
|
|
51
|
+
onInterimChange?.(words !== '');
|
|
52
|
+
}, send: (text, metadata) => {
|
|
53
|
+
onInterimChange?.(false);
|
|
54
|
+
setInput(base.current);
|
|
55
|
+
send(text, metadata);
|
|
56
|
+
} })) : 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: () => {
|
|
27
57
|
base.current = input.trim();
|
|
28
58
|
onStopSpeaking?.();
|
|
29
59
|
},
|