@acorex-platform/extension-ai-voice-shell-client 0.0.21
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/fesm2022/acorex-platform-extension-ai-voice-shell-client-contracts.mjs +164 -0
- package/fesm2022/acorex-platform-extension-ai-voice-shell-client-contracts.mjs.map +1 -0
- package/fesm2022/acorex-platform-extension-ai-voice-shell-client.mjs +1026 -0
- package/fesm2022/acorex-platform-extension-ai-voice-shell-client.mjs.map +1 -0
- package/package.json +28 -0
- package/types/acorex-platform-extension-ai-voice-shell-client-contracts.d.ts +186 -0
- package/types/acorex-platform-extension-ai-voice-shell-client.d.ts +176 -0
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { inject, DestroyRef, ViewEncapsulation, ChangeDetectionStrategy, Component, Injectable, NgModule, InjectionToken, signal, PLATFORM_ID, computed } from '@angular/core';
|
|
3
|
+
import { AXPSearchCommandProvider, AXP_SEARCH_PROVIDER, AXPFileStorageService } from '@acorex-platform/framework-client/common';
|
|
4
|
+
import * as i1$1 from '@acorex-platform/framework-client/core';
|
|
5
|
+
import { AXPKeyboardShortcutRegistry, AXPComponentSlotModule } from '@acorex-platform/framework-client/core';
|
|
6
|
+
import { AXPCommandExecutor, provideCommandSetups, AXPCommandService } from '@acorex-platform/framework-client/runtime';
|
|
7
|
+
import { AXMAiManagementModule, AXPAiChatToolRunContextService, AXPAiPlatformRuntimeContextBuilder, AXPAiManagerService, persistAiChatAttachmentImage, AIMANAGEMENT_CHAT_TRANSCRIBE_SPEECH_COMMAND_KEY, AIMANAGEMENT_CHAT_SYNTHESIZE_SPEECH_COMMAND_KEY, createBrowserPlayableTtsBlobUrl } from '@acorex-platform/module-ai-management-client';
|
|
8
|
+
import { AXE_AI_VOICE_SHELL_OPEN_COMMAND, AXE_ASSESSMENT_QUESTIONNAIRE_VOICE_CAPABILITY_ID } from '@acorex-platform/extension-ai-voice-shell-client/contracts';
|
|
9
|
+
import { AXPKeyboardShortcutPriority, runtimeCommandPayloadData } from '@acorex-platform/framework-shared/core';
|
|
10
|
+
import * as i1 from '@acorex/components/button';
|
|
11
|
+
import { AXButtonModule } from '@acorex/components/button';
|
|
12
|
+
import * as i2 from '@acorex/components/decorators';
|
|
13
|
+
import { AXDecoratorModule } from '@acorex/components/decorators';
|
|
14
|
+
import * as i3 from '@acorex/core/translation';
|
|
15
|
+
import { AXTranslationModule, AXTranslationService } from '@acorex/core/translation';
|
|
16
|
+
import * as i2$1 from '@angular/common';
|
|
17
|
+
import { AsyncPipe, isPlatformBrowser, CommonModule } from '@angular/common';
|
|
18
|
+
import { AXPopupService } from '@acorex/components/popup';
|
|
19
|
+
import { RootConfig } from '@acorex-platform/module-ai-management-client/contracts';
|
|
20
|
+
import { AXPEntityService } from '@acorex-platform/framework-client/layout/entity';
|
|
21
|
+
import { first } from 'lodash';
|
|
22
|
+
import { AXBasePageComponent } from '@acorex/components/page';
|
|
23
|
+
|
|
24
|
+
//#region ----- Imports -----
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region ----- Constants -----
|
|
27
|
+
/** Dedicated chord — Ctrl+K remains global search. */
|
|
28
|
+
const AXE_AI_VOICE_SHELL_SHORTCUT = 'ctrl+shift+space';
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region ----- Component -----
|
|
31
|
+
/**
|
|
32
|
+
* Header mic next to the conversation AI robot. Opens the voice command shell.
|
|
33
|
+
*/
|
|
34
|
+
class AXEAiVoiceShellHeaderSlotComponent {
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region ----- Lifecycle -----
|
|
37
|
+
constructor() {
|
|
38
|
+
//#region ----- Services & Dependencies -----
|
|
39
|
+
this.commandExecutor = inject(AXPCommandExecutor);
|
|
40
|
+
this.shortcutRegistry = inject(AXPKeyboardShortcutRegistry);
|
|
41
|
+
this.destroyRef = inject(DestroyRef);
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region ----- Computed Properties -----
|
|
44
|
+
this.shortcutDisplay = this.shortcutRegistry.formatDisplayKeys([AXE_AI_VOICE_SHELL_SHORTCUT]);
|
|
45
|
+
this.shortcutRegistry.register({
|
|
46
|
+
owner: this.destroyRef,
|
|
47
|
+
id: 'ai-voice-shell',
|
|
48
|
+
priority: AXPKeyboardShortcutPriority.Global,
|
|
49
|
+
group: '@general:keyboard-shortcuts.groups.global',
|
|
50
|
+
shortcuts: [
|
|
51
|
+
{
|
|
52
|
+
keys: [AXE_AI_VOICE_SHELL_SHORTCUT],
|
|
53
|
+
title: '@general:keyboard-shortcuts.actions.open-voice-command',
|
|
54
|
+
allowInEditableFields: true,
|
|
55
|
+
handler: () => {
|
|
56
|
+
void this.openVoiceCommand();
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region ----- Event Handlers -----
|
|
64
|
+
async openVoiceCommand() {
|
|
65
|
+
await this.commandExecutor.execute(AXE_AI_VOICE_SHELL_OPEN_COMMAND, {});
|
|
66
|
+
}
|
|
67
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiVoiceShellHeaderSlotComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
68
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.8", type: AXEAiVoiceShellHeaderSlotComponent, isStandalone: true, selector: "axe-ai-voice-shell-header-slot", ngImport: i0, template: "<ax-button\n [iconOnly]=\"true\"\n class=\"relative\"\n color=\"primary\"\n [title]=\"\n ('@extensions-ai-voice-shell:header.open-title' | translate: { params: { shortcut: shortcutDisplay } } | async) ??\n ''\n \"\n [attr.aria-label]=\"\n ('@extensions-ai-voice-shell:header.open-title' | translate: { params: { shortcut: shortcutDisplay } } | async) ??\n ''\n \"\n (onClick)=\"openVoiceCommand()\"\n>\n <ax-icon>\n <i class=\"fa-regular fa-microphone\"></i>\n </ax-icon>\n</ax-button>\n", styles: ["axe-ai-voice-shell-header-slot{position:relative}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"], dependencies: [{ kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i1.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "component", type: i2.AXDecoratorIconComponent, selector: "ax-icon", inputs: ["icon"] }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "pipe", type: i3.AXTranslatorPipe, name: "translate" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
69
|
+
}
|
|
70
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiVoiceShellHeaderSlotComponent, decorators: [{
|
|
71
|
+
type: Component,
|
|
72
|
+
args: [{ selector: 'axe-ai-voice-shell-header-slot', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, imports: [AXButtonModule, AXDecoratorModule, AXTranslationModule, AsyncPipe], template: "<ax-button\n [iconOnly]=\"true\"\n class=\"relative\"\n color=\"primary\"\n [title]=\"\n ('@extensions-ai-voice-shell:header.open-title' | translate: { params: { shortcut: shortcutDisplay } } | async) ??\n ''\n \"\n [attr.aria-label]=\"\n ('@extensions-ai-voice-shell:header.open-title' | translate: { params: { shortcut: shortcutDisplay } } | async) ??\n ''\n \"\n (onClick)=\"openVoiceCommand()\"\n>\n <ax-icon>\n <i class=\"fa-regular fa-microphone\"></i>\n </ax-icon>\n</ax-button>\n", styles: ["axe-ai-voice-shell-header-slot{position:relative}\n/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n"] }]
|
|
73
|
+
}], ctorParameters: () => [] });
|
|
74
|
+
|
|
75
|
+
//#region ----- Imports -----
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region ----- Search -----
|
|
78
|
+
class AXEAiVoiceShellSearchCommandProvider extends AXPSearchCommandProvider {
|
|
79
|
+
constructor() {
|
|
80
|
+
super(...arguments);
|
|
81
|
+
this.commands = [
|
|
82
|
+
{
|
|
83
|
+
id: 'open-voice-command-shell',
|
|
84
|
+
group: 'command',
|
|
85
|
+
title: '@extensions-ai-voice-shell:search.open.title',
|
|
86
|
+
icon: 'fa-light fa-microphone',
|
|
87
|
+
description: '@extensions-ai-voice-shell:search.open.description',
|
|
88
|
+
command: {
|
|
89
|
+
name: AXE_AI_VOICE_SHELL_OPEN_COMMAND,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiVoiceShellSearchCommandProvider, deps: null, target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
95
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiVoiceShellSearchCommandProvider }); }
|
|
96
|
+
}
|
|
97
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiVoiceShellSearchCommandProvider, decorators: [{
|
|
98
|
+
type: Injectable
|
|
99
|
+
}] });
|
|
100
|
+
|
|
101
|
+
//#region ----- Imports -----
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region ----- Module -----
|
|
104
|
+
/**
|
|
105
|
+
* Central voice AI command shell (void dialog + capability registry + HTTP/SSE transport).
|
|
106
|
+
* Register next to {@link AXMAiManagementModule}. Domain modules register {@link AXP_VOICE_CAPABILITY_PROVIDER}.
|
|
107
|
+
*/
|
|
108
|
+
class AXEAiVoiceShellExtensionModule {
|
|
109
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiVoiceShellExtensionModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|
|
110
|
+
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.0.8", ngImport: i0, type: AXEAiVoiceShellExtensionModule, imports: [AXMAiManagementModule, i1$1.AXPComponentSlotModule] }); }
|
|
111
|
+
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiVoiceShellExtensionModule, providers: [
|
|
112
|
+
{
|
|
113
|
+
provide: AXP_SEARCH_PROVIDER,
|
|
114
|
+
useClass: AXEAiVoiceShellSearchCommandProvider,
|
|
115
|
+
multi: true,
|
|
116
|
+
},
|
|
117
|
+
provideCommandSetups([
|
|
118
|
+
{
|
|
119
|
+
key: AXE_AI_VOICE_SHELL_OPEN_COMMAND,
|
|
120
|
+
command: () => Promise.resolve().then(function () { return openVoiceCommandShell_command; }).then((c) => c.AXEAiVoiceShellOpenCommand),
|
|
121
|
+
},
|
|
122
|
+
]),
|
|
123
|
+
], imports: [AXMAiManagementModule,
|
|
124
|
+
AXPComponentSlotModule.forChild({
|
|
125
|
+
'root-header-end': [
|
|
126
|
+
{
|
|
127
|
+
// Immediately after conversation mini-chat (55) so mic + robot sit as one AI cluster.
|
|
128
|
+
priority: 56,
|
|
129
|
+
name: 'ai-voice-shell',
|
|
130
|
+
component: AXEAiVoiceShellHeaderSlotComponent,
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
})] }); }
|
|
134
|
+
}
|
|
135
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiVoiceShellExtensionModule, decorators: [{
|
|
136
|
+
type: NgModule,
|
|
137
|
+
args: [{
|
|
138
|
+
imports: [
|
|
139
|
+
AXMAiManagementModule,
|
|
140
|
+
AXPComponentSlotModule.forChild({
|
|
141
|
+
'root-header-end': [
|
|
142
|
+
{
|
|
143
|
+
// Immediately after conversation mini-chat (55) so mic + robot sit as one AI cluster.
|
|
144
|
+
priority: 56,
|
|
145
|
+
name: 'ai-voice-shell',
|
|
146
|
+
component: AXEAiVoiceShellHeaderSlotComponent,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
}),
|
|
150
|
+
],
|
|
151
|
+
providers: [
|
|
152
|
+
{
|
|
153
|
+
provide: AXP_SEARCH_PROVIDER,
|
|
154
|
+
useClass: AXEAiVoiceShellSearchCommandProvider,
|
|
155
|
+
multi: true,
|
|
156
|
+
},
|
|
157
|
+
provideCommandSetups([
|
|
158
|
+
{
|
|
159
|
+
key: AXE_AI_VOICE_SHELL_OPEN_COMMAND,
|
|
160
|
+
command: () => Promise.resolve().then(function () { return openVoiceCommandShell_command; }).then((c) => c.AXEAiVoiceShellOpenCommand),
|
|
161
|
+
},
|
|
162
|
+
]),
|
|
163
|
+
],
|
|
164
|
+
}]
|
|
165
|
+
}] });
|
|
166
|
+
|
|
167
|
+
//#region ----- Imports -----
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region ----- Tokens -----
|
|
170
|
+
/** Multi-provider token for domain voice capabilities (assessment, later others). */
|
|
171
|
+
const AXP_VOICE_CAPABILITY_PROVIDER = new InjectionToken('AXP_VOICE_CAPABILITY_PROVIDER');
|
|
172
|
+
//#endregion
|
|
173
|
+
|
|
174
|
+
//#region ----- Imports -----
|
|
175
|
+
//#endregion
|
|
176
|
+
//#region ----- Registry -----
|
|
177
|
+
/**
|
|
178
|
+
* Discovers module-owned {@link AXPVoiceCapabilityProvider} implementations.
|
|
179
|
+
* The shell never imports domain internals.
|
|
180
|
+
*/
|
|
181
|
+
class AXPVoiceCapabilityRegistry {
|
|
182
|
+
constructor() {
|
|
183
|
+
//#region ----- Services & Dependencies -----
|
|
184
|
+
this.providers = inject(AXP_VOICE_CAPABILITY_PROVIDER, { optional: true }) ?? [];
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
//#region ----- Public API -----
|
|
188
|
+
list() {
|
|
189
|
+
const raw = this.providers;
|
|
190
|
+
return Array.isArray(raw) ? raw : raw ? [raw] : [];
|
|
191
|
+
}
|
|
192
|
+
getById(id) {
|
|
193
|
+
const trimmed = id.trim();
|
|
194
|
+
if (!trimmed) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
return this.list().find((provider) => provider.id === trimmed);
|
|
198
|
+
}
|
|
199
|
+
async findForIntent(intent) {
|
|
200
|
+
for (const provider of this.list()) {
|
|
201
|
+
if (await provider.canHandle(intent)) {
|
|
202
|
+
return provider;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPVoiceCapabilityRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
208
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPVoiceCapabilityRegistry, providedIn: 'root' }); }
|
|
209
|
+
}
|
|
210
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPVoiceCapabilityRegistry, decorators: [{
|
|
211
|
+
type: Injectable,
|
|
212
|
+
args: [{ providedIn: 'root' }]
|
|
213
|
+
}] });
|
|
214
|
+
|
|
215
|
+
//#region ----- Imports -----
|
|
216
|
+
//#endregion
|
|
217
|
+
//#region ----- Constants -----
|
|
218
|
+
const SPEECH_TO_TEXT_AGENT_NAME = 'speech-to-text';
|
|
219
|
+
const TEXT_TO_SPEECH_AGENT_NAME = 'text-to-speech';
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region ----- Transport -----
|
|
222
|
+
/**
|
|
223
|
+
* v1 interaction transport: existing AI HTTP commands + assist `run` (SSE under the hood).
|
|
224
|
+
*/
|
|
225
|
+
class AXPHttpSseVoiceTransport {
|
|
226
|
+
constructor() {
|
|
227
|
+
//#region ----- Services & Dependencies -----
|
|
228
|
+
this.id = 'http-sse';
|
|
229
|
+
this.commandService = inject(AXPCommandService);
|
|
230
|
+
this.fileStorage = inject(AXPFileStorageService);
|
|
231
|
+
this.entityService = inject(AXPEntityService);
|
|
232
|
+
this.chatToolRunContext = inject(AXPAiChatToolRunContextService);
|
|
233
|
+
this.runtimeContextBuilder = inject(AXPAiPlatformRuntimeContextBuilder);
|
|
234
|
+
this.aiManager = inject(AXPAiManagerService, { optional: true });
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region ----- Public API -----
|
|
238
|
+
async transcribe(audio, options) {
|
|
239
|
+
const aiManager = this.requireAiManager();
|
|
240
|
+
const assist = await aiManager.getAssistForChat();
|
|
241
|
+
const assistId = assist.id?.trim();
|
|
242
|
+
if (!assistId) {
|
|
243
|
+
throw new Error('No AI assist is configured for speech transcription.');
|
|
244
|
+
}
|
|
245
|
+
const chatTransport = (await aiManager.getAssistPrimaryChatTransport(assistId)) ?? 'openai';
|
|
246
|
+
const agentId = await this.findAgentIdByName(SPEECH_TO_TEXT_AGENT_NAME);
|
|
247
|
+
const mimeType = audio.type?.trim() || 'audio/webm';
|
|
248
|
+
const file = new File([audio], `voice-${Date.now()}.webm`, { type: mimeType });
|
|
249
|
+
const { fileId } = await persistAiChatAttachmentImage(this.fileStorage, file, globalThis.crypto.randomUUID());
|
|
250
|
+
const language = options?.language?.trim() ||
|
|
251
|
+
this.localeToTranscriptionLanguage(await this.resolveUiLocale());
|
|
252
|
+
this.chatToolRunContext.setContext({
|
|
253
|
+
assistId,
|
|
254
|
+
chatTransport,
|
|
255
|
+
...(agentId ? { agentId } : {}),
|
|
256
|
+
});
|
|
257
|
+
try {
|
|
258
|
+
const result = await this.commandService.execute(AIMANAGEMENT_CHAT_TRANSCRIBE_SPEECH_COMMAND_KEY, {
|
|
259
|
+
fileId,
|
|
260
|
+
assistId,
|
|
261
|
+
...(agentId ? { agentId } : {}),
|
|
262
|
+
...(language ? { language } : {}),
|
|
263
|
+
});
|
|
264
|
+
const data = runtimeCommandPayloadData(result);
|
|
265
|
+
const text = typeof data?.text === 'string' ? data.text.trim() : '';
|
|
266
|
+
if (!result?.success || !text) {
|
|
267
|
+
throw new Error(this.commandFailureI18nKey(result, '@extensions-ai-voice-shell:errors.transcribe-failed'));
|
|
268
|
+
}
|
|
269
|
+
return { text };
|
|
270
|
+
}
|
|
271
|
+
finally {
|
|
272
|
+
this.chatToolRunContext.setContext(null);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async synthesize(text) {
|
|
276
|
+
const trimmed = text.trim();
|
|
277
|
+
if (!trimmed) {
|
|
278
|
+
return { text: '' };
|
|
279
|
+
}
|
|
280
|
+
const aiManager = this.requireAiManager();
|
|
281
|
+
const assist = await aiManager.getAssistForChat();
|
|
282
|
+
const assistId = assist.id?.trim();
|
|
283
|
+
if (!assistId) {
|
|
284
|
+
throw new Error('No AI assist is configured for speech synthesis.');
|
|
285
|
+
}
|
|
286
|
+
const chatTransport = (await aiManager.getAssistPrimaryChatTransport(assistId)) ?? 'openai';
|
|
287
|
+
const agentId = await this.findAgentIdByName(TEXT_TO_SPEECH_AGENT_NAME);
|
|
288
|
+
this.chatToolRunContext.setContext({
|
|
289
|
+
assistId,
|
|
290
|
+
chatTransport,
|
|
291
|
+
...(agentId ? { agentId } : {}),
|
|
292
|
+
});
|
|
293
|
+
try {
|
|
294
|
+
const result = await this.commandService.executeRemote(AIMANAGEMENT_CHAT_SYNTHESIZE_SPEECH_COMMAND_KEY, {
|
|
295
|
+
text: trimmed,
|
|
296
|
+
assistId,
|
|
297
|
+
...(agentId ? { agentId } : {}),
|
|
298
|
+
});
|
|
299
|
+
const data = runtimeCommandPayloadData(result);
|
|
300
|
+
const fileId = typeof data?.fileId === 'string' ? data.fileId.trim() : '';
|
|
301
|
+
if (!result?.success || !fileId) {
|
|
302
|
+
throw new Error(this.commandFailureI18nKey(result, '@extensions-ai-voice-shell:errors.assist-failed'));
|
|
303
|
+
}
|
|
304
|
+
const info = await this.fileStorage.getInfo(fileId);
|
|
305
|
+
const binary = await this.fileStorage.download(fileId);
|
|
306
|
+
const objectUrl = await createBrowserPlayableTtsBlobUrl(binary, info?.mimeType ?? binary.type, info?.name);
|
|
307
|
+
return { fileId, objectUrl, text: typeof data?.text === 'string' ? data.text : trimmed };
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
this.chatToolRunContext.setContext(null);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async runAssist(prompt, context) {
|
|
314
|
+
const aiManager = this.requireAiManager();
|
|
315
|
+
const result = await aiManager.run({
|
|
316
|
+
prompt,
|
|
317
|
+
context: {
|
|
318
|
+
systemInstruction: 'You are the platform voice command assistant. Prefer AI-compatible tools. Keep spoken replies short. Confirm before destructive or submit actions.',
|
|
319
|
+
structuredContext: context,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
return { text: result.text?.trim() ?? '' };
|
|
323
|
+
}
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region ----- Private helpers -----
|
|
326
|
+
requireAiManager() {
|
|
327
|
+
if (!this.aiManager) {
|
|
328
|
+
throw new Error('AI manager service is not available.');
|
|
329
|
+
}
|
|
330
|
+
return this.aiManager;
|
|
331
|
+
}
|
|
332
|
+
async findAgentIdByName(name) {
|
|
333
|
+
const list = await this.entityService
|
|
334
|
+
.withEntity(RootConfig.module.name, RootConfig.entities.agent.name)
|
|
335
|
+
.data()
|
|
336
|
+
.query({
|
|
337
|
+
skip: 0,
|
|
338
|
+
take: 1,
|
|
339
|
+
filter: {
|
|
340
|
+
field: 'name',
|
|
341
|
+
operator: { type: 'equal' },
|
|
342
|
+
value: name,
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
return first(list.items)?.id;
|
|
346
|
+
}
|
|
347
|
+
async resolveUiLocale() {
|
|
348
|
+
const ctx = await this.runtimeContextBuilder.build();
|
|
349
|
+
const locale = ctx['locale'];
|
|
350
|
+
return typeof locale === 'string' && locale.trim() ? locale.trim() : undefined;
|
|
351
|
+
}
|
|
352
|
+
commandFailureI18nKey(result, fallbackKey) {
|
|
353
|
+
const text = typeof result?.message?.text === 'string' ? result.message.text.trim() : '';
|
|
354
|
+
return text.startsWith('@') ? text : fallbackKey;
|
|
355
|
+
}
|
|
356
|
+
localeToTranscriptionLanguage(locale) {
|
|
357
|
+
const primary = locale?.trim().split('-')[0]?.toLowerCase();
|
|
358
|
+
return primary || undefined;
|
|
359
|
+
}
|
|
360
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPHttpSseVoiceTransport, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
361
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPHttpSseVoiceTransport, providedIn: 'root' }); }
|
|
362
|
+
}
|
|
363
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPHttpSseVoiceTransport, decorators: [{
|
|
364
|
+
type: Injectable,
|
|
365
|
+
args: [{ providedIn: 'root' }]
|
|
366
|
+
}] });
|
|
367
|
+
|
|
368
|
+
//#region ----- Browser speech -----
|
|
369
|
+
function canUseBrowserSpeechSynthesis() {
|
|
370
|
+
return typeof window !== 'undefined' && typeof window.speechSynthesis?.speak === 'function';
|
|
371
|
+
}
|
|
372
|
+
/** Unlock speech synthesis during a user gesture so later replies can be spoken. */
|
|
373
|
+
function primeBrowserSpeechSynthesis() {
|
|
374
|
+
if (!canUseBrowserSpeechSynthesis()) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
window.speechSynthesis.cancel();
|
|
379
|
+
const silent = new SpeechSynthesisUtterance(' ');
|
|
380
|
+
silent.volume = 0;
|
|
381
|
+
silent.rate = 10;
|
|
382
|
+
window.speechSynthesis.speak(silent);
|
|
383
|
+
window.speechSynthesis.cancel();
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
// Ignore browsers that reject the silent prime utterance.
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function cancelBrowserSpeechSynthesis() {
|
|
390
|
+
if (!canUseBrowserSpeechSynthesis()) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
window.speechSynthesis.cancel();
|
|
394
|
+
}
|
|
395
|
+
function speakWithBrowserSpeechSynthesis(text) {
|
|
396
|
+
const trimmed = text.trim();
|
|
397
|
+
if (!trimmed || !canUseBrowserSpeechSynthesis()) {
|
|
398
|
+
return Promise.resolve(false);
|
|
399
|
+
}
|
|
400
|
+
return new Promise((resolve) => {
|
|
401
|
+
try {
|
|
402
|
+
window.speechSynthesis.cancel();
|
|
403
|
+
const utterance = new SpeechSynthesisUtterance(trimmed);
|
|
404
|
+
const done = (ok) => {
|
|
405
|
+
utterance.onend = null;
|
|
406
|
+
utterance.onerror = null;
|
|
407
|
+
resolve(ok);
|
|
408
|
+
};
|
|
409
|
+
utterance.onend = () => done(true);
|
|
410
|
+
utterance.onerror = () => done(false);
|
|
411
|
+
window.speechSynthesis.speak(utterance);
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
resolve(false);
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
//#endregion
|
|
419
|
+
|
|
420
|
+
//#region ----- Imports -----
|
|
421
|
+
//#endregion
|
|
422
|
+
//#region ----- Constants -----
|
|
423
|
+
const INTERVIEW_INTENT_RE = /\b(fill|inspect|inspection|questionnaire|assessment|survey|checklist|فرم|پرسشنامه|بازرسی)\b/i;
|
|
424
|
+
//#endregion
|
|
425
|
+
//#region ----- Service -----
|
|
426
|
+
/**
|
|
427
|
+
* Session coordinator for the central voice void dialog.
|
|
428
|
+
* Does not import assessment internals — only the capability registry and transport.
|
|
429
|
+
*/
|
|
430
|
+
class AXPVoiceCommandShellService {
|
|
431
|
+
constructor() {
|
|
432
|
+
//#region ----- Services & Dependencies -----
|
|
433
|
+
this.popupService = inject(AXPopupService);
|
|
434
|
+
this.translation = inject(AXTranslationService);
|
|
435
|
+
this.registry = inject(AXPVoiceCapabilityRegistry);
|
|
436
|
+
this.transport = inject(AXPHttpSseVoiceTransport);
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region ----- State Signals -----
|
|
439
|
+
this.uiState = signal('idle', /* @ts-ignore */
|
|
440
|
+
...(ngDevMode ? [{ debugName: "uiState" }] : /* istanbul ignore next */ []));
|
|
441
|
+
this.mode = signal('command', /* @ts-ignore */
|
|
442
|
+
...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
443
|
+
this.statusText = signal('', /* @ts-ignore */
|
|
444
|
+
...(ngDevMode ? [{ debugName: "statusText" }] : /* istanbul ignore next */ []));
|
|
445
|
+
this.lastTranscript = signal('', /* @ts-ignore */
|
|
446
|
+
...(ngDevMode ? [{ debugName: "lastTranscript" }] : /* istanbul ignore next */ []));
|
|
447
|
+
this.lastSpoken = signal('', /* @ts-ignore */
|
|
448
|
+
...(ngDevMode ? [{ debugName: "lastSpoken" }] : /* istanbul ignore next */ []));
|
|
449
|
+
this.errorMessage = signal('', /* @ts-ignore */
|
|
450
|
+
...(ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []));
|
|
451
|
+
this.confirmRequest = signal(null, /* @ts-ignore */
|
|
452
|
+
...(ngDevMode ? [{ debugName: "confirmRequest" }] : /* istanbul ignore next */ []));
|
|
453
|
+
this.turns = signal([], /* @ts-ignore */
|
|
454
|
+
...(ngDevMode ? [{ debugName: "turns" }] : /* istanbul ignore next */ []));
|
|
455
|
+
this.interviewProgress = signal(null, /* @ts-ignore */
|
|
456
|
+
...(ngDevMode ? [{ debugName: "interviewProgress" }] : /* istanbul ignore next */ []));
|
|
457
|
+
//#endregion
|
|
458
|
+
//#region ----- Session -----
|
|
459
|
+
this.activeCapability = null;
|
|
460
|
+
this.activeSessionId = null;
|
|
461
|
+
this.pendingSpokenConfirm = null;
|
|
462
|
+
this.currentAudio = null;
|
|
463
|
+
this.currentObjectUrl = null;
|
|
464
|
+
this.openPromise = null;
|
|
465
|
+
}
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region ----- Public API -----
|
|
468
|
+
async open(options) {
|
|
469
|
+
if (this.openPromise) {
|
|
470
|
+
return this.openPromise;
|
|
471
|
+
}
|
|
472
|
+
this.resetSession();
|
|
473
|
+
this.statusText.set(await this.translation.translateAsync('@extensions-ai-voice-shell:states.idle'));
|
|
474
|
+
const title = await this.translation.translateAsync('@extensions-ai-voice-shell:dialog.title');
|
|
475
|
+
const { AXEAiVoiceVoidDialogComponent } = await Promise.resolve().then(function () { return voiceVoidDialog_component; });
|
|
476
|
+
const popupClosed = this.popupService
|
|
477
|
+
.open(AXEAiVoiceVoidDialogComponent, {
|
|
478
|
+
title,
|
|
479
|
+
size: 'sm',
|
|
480
|
+
header: true,
|
|
481
|
+
closeButton: true,
|
|
482
|
+
maximizable: false,
|
|
483
|
+
draggable: true,
|
|
484
|
+
hasBackdrop: true,
|
|
485
|
+
closeOnBackdropClick: true,
|
|
486
|
+
closeOnEscape: true,
|
|
487
|
+
panelClass: ['axe-ai-voice-shell-popup'],
|
|
488
|
+
})
|
|
489
|
+
.then(() => undefined);
|
|
490
|
+
this.openPromise = popupClosed.finally(() => {
|
|
491
|
+
this.openPromise = null;
|
|
492
|
+
this.stopPlayback();
|
|
493
|
+
this.resetSession();
|
|
494
|
+
});
|
|
495
|
+
if (options?.capabilityId || options?.startInput) {
|
|
496
|
+
void this.beginCapabilitySession(options);
|
|
497
|
+
}
|
|
498
|
+
else if (options?.initialText?.trim()) {
|
|
499
|
+
void this.handleUserText(options.initialText.trim());
|
|
500
|
+
}
|
|
501
|
+
return this.openPromise;
|
|
502
|
+
}
|
|
503
|
+
async enterListening() {
|
|
504
|
+
this.stopPlayback();
|
|
505
|
+
this.uiState.set('listening');
|
|
506
|
+
this.statusText.set(await this.translation.translateAsync('@extensions-ai-voice-shell:states.listening'));
|
|
507
|
+
this.errorMessage.set('');
|
|
508
|
+
}
|
|
509
|
+
async cancelListening() {
|
|
510
|
+
this.uiState.set('idle');
|
|
511
|
+
this.statusText.set(await this.translation.translateAsync('@extensions-ai-voice-shell:states.idle'));
|
|
512
|
+
}
|
|
513
|
+
async reportError(key, detail) {
|
|
514
|
+
await this.setError(key, detail);
|
|
515
|
+
}
|
|
516
|
+
async submitRecording(audio) {
|
|
517
|
+
this.uiState.set('thinking');
|
|
518
|
+
this.statusText.set(await this.translation.translateAsync('@extensions-ai-voice-shell:states.thinking'));
|
|
519
|
+
try {
|
|
520
|
+
const { text } = await this.transport.transcribe(audio);
|
|
521
|
+
if (!text.trim()) {
|
|
522
|
+
await this.setError('@extensions-ai-voice-shell:errors.empty-transcript');
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
await this.handleUserText(text.trim());
|
|
526
|
+
}
|
|
527
|
+
catch (err) {
|
|
528
|
+
await this.setError('@extensions-ai-voice-shell:errors.transcribe-failed', err instanceof Error ? err.message : undefined);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async handleUserText(text) {
|
|
532
|
+
const trimmed = text.trim();
|
|
533
|
+
if (!trimmed) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
this.lastTranscript.set(trimmed);
|
|
537
|
+
this.appendTurn('user', trimmed);
|
|
538
|
+
this.uiState.set('thinking');
|
|
539
|
+
this.statusText.set(await this.translation.translateAsync('@extensions-ai-voice-shell:states.thinking'));
|
|
540
|
+
this.errorMessage.set('');
|
|
541
|
+
if (this.mode() === 'confirm' && this.pendingSpokenConfirm) {
|
|
542
|
+
await this.handleConfirmUtterance(trimmed);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (this.mode() === 'interview' && this.activeCapability && this.activeSessionId) {
|
|
546
|
+
await this.applyInterviewUtterance(trimmed);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const intent = { text: trimmed };
|
|
550
|
+
const capability = this.registry.getById(AXE_ASSESSMENT_QUESTIONNAIRE_VOICE_CAPABILITY_ID) && INTERVIEW_INTENT_RE.test(trimmed)
|
|
551
|
+
? await this.registry.findForIntent(intent)
|
|
552
|
+
: await this.registry.findForIntent(intent);
|
|
553
|
+
if (capability) {
|
|
554
|
+
await this.beginCapabilitySession({ capabilityId: capability.id, startInput: { text: trimmed } });
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
await this.runCommandAssist(trimmed);
|
|
558
|
+
}
|
|
559
|
+
async confirmYes() {
|
|
560
|
+
if (this.pendingSpokenConfirm?.kind === 'submit') {
|
|
561
|
+
await this.submitInterview();
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
this.confirmRequest.set(null);
|
|
565
|
+
this.mode.set(this.activeSessionId ? 'interview' : 'command');
|
|
566
|
+
this.uiState.set('idle');
|
|
567
|
+
}
|
|
568
|
+
async confirmNo() {
|
|
569
|
+
this.pendingSpokenConfirm = null;
|
|
570
|
+
this.confirmRequest.set(null);
|
|
571
|
+
if (this.activeSessionId) {
|
|
572
|
+
this.mode.set('interview');
|
|
573
|
+
const stay = await this.translation.translateAsync('@extensions-ai-voice-shell:interview.submit-cancelled');
|
|
574
|
+
await this.speak(stay);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
this.mode.set('command');
|
|
578
|
+
this.uiState.set('idle');
|
|
579
|
+
this.statusText.set(await this.translation.translateAsync('@extensions-ai-voice-shell:states.idle'));
|
|
580
|
+
}
|
|
581
|
+
async saveInterviewDraft() {
|
|
582
|
+
if (!this.activeCapability?.saveDraft || !this.activeSessionId) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
this.uiState.set('thinking');
|
|
586
|
+
try {
|
|
587
|
+
const result = await this.activeCapability.saveDraft(this.activeSessionId);
|
|
588
|
+
const spoken = result && typeof result === 'object' && 'spokenMessage' in result && result.spokenMessage
|
|
589
|
+
? result.spokenMessage
|
|
590
|
+
: await this.translation.translateAsync('@extensions-ai-voice-shell:interview.draft-saved');
|
|
591
|
+
await this.speak(spoken);
|
|
592
|
+
}
|
|
593
|
+
catch (err) {
|
|
594
|
+
await this.setError('@extensions-ai-voice-shell:errors.save-failed', err instanceof Error ? err.message : undefined);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
async cancelInterview() {
|
|
598
|
+
if (this.activeCapability?.cancel && this.activeSessionId) {
|
|
599
|
+
await this.activeCapability.cancel(this.activeSessionId);
|
|
600
|
+
}
|
|
601
|
+
this.resetSession();
|
|
602
|
+
this.statusText.set(await this.translation.translateAsync('@extensions-ai-voice-shell:states.idle'));
|
|
603
|
+
}
|
|
604
|
+
stopPlayback() {
|
|
605
|
+
if (this.currentAudio) {
|
|
606
|
+
this.currentAudio.pause();
|
|
607
|
+
this.currentAudio = null;
|
|
608
|
+
}
|
|
609
|
+
if (this.currentObjectUrl) {
|
|
610
|
+
URL.revokeObjectURL(this.currentObjectUrl);
|
|
611
|
+
this.currentObjectUrl = null;
|
|
612
|
+
}
|
|
613
|
+
cancelBrowserSpeechSynthesis();
|
|
614
|
+
}
|
|
615
|
+
//#endregion
|
|
616
|
+
//#region ----- Interview / command -----
|
|
617
|
+
async beginCapabilitySession(options) {
|
|
618
|
+
const startInput = options.startInput;
|
|
619
|
+
const startText = startInput != null && typeof startInput === 'object' && 'text' in startInput
|
|
620
|
+
? String(startInput.text ?? '')
|
|
621
|
+
: '';
|
|
622
|
+
const capability = options.capabilityId
|
|
623
|
+
? this.registry.getById(options.capabilityId)
|
|
624
|
+
: await this.registry.findForIntent({
|
|
625
|
+
text: startText || undefined,
|
|
626
|
+
context: startInput != null && typeof startInput === 'object'
|
|
627
|
+
? startInput
|
|
628
|
+
: undefined,
|
|
629
|
+
});
|
|
630
|
+
if (!capability) {
|
|
631
|
+
await this.setError('@extensions-ai-voice-shell:errors.no-capability');
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
this.uiState.set('thinking');
|
|
635
|
+
try {
|
|
636
|
+
const started = await capability.startSession(options.startInput ?? {});
|
|
637
|
+
this.activeCapability = capability;
|
|
638
|
+
this.activeSessionId = started.sessionId;
|
|
639
|
+
this.mode.set('interview');
|
|
640
|
+
if (started.spokenPrompt) {
|
|
641
|
+
await this.speak(started.spokenPrompt);
|
|
642
|
+
}
|
|
643
|
+
if (capability.nextPrompt) {
|
|
644
|
+
await this.speakNextPrompt();
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch (err) {
|
|
648
|
+
await this.setError('@extensions-ai-voice-shell:errors.start-failed', err instanceof Error ? err.message : undefined);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async speakNextPrompt() {
|
|
652
|
+
if (!this.activeCapability?.nextPrompt || !this.activeSessionId) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const next = await this.activeCapability.nextPrompt(this.activeSessionId);
|
|
656
|
+
await this.handleNextPrompt(next);
|
|
657
|
+
}
|
|
658
|
+
async handleNextPrompt(next) {
|
|
659
|
+
if ('done' in next && next.done) {
|
|
660
|
+
this.interviewProgress.set(null);
|
|
661
|
+
await this.enterSubmitConfirm(next.spokenPrompt);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if ('spokenPrompt' in next) {
|
|
665
|
+
if ('progress' in next && next.progress) {
|
|
666
|
+
this.interviewProgress.set(next.progress);
|
|
667
|
+
}
|
|
668
|
+
await this.speak(next.spokenPrompt);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async applyInterviewUtterance(text) {
|
|
672
|
+
if (!this.activeCapability?.applyInput || !this.activeSessionId) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
try {
|
|
676
|
+
const result = await this.activeCapability.applyInput(this.activeSessionId, {
|
|
677
|
+
text,
|
|
678
|
+
});
|
|
679
|
+
if (!result.ok) {
|
|
680
|
+
await this.speak(result.spokenMessage ??
|
|
681
|
+
(await this.translation.translateAsync('@extensions-ai-voice-shell:interview.apply-failed')));
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (result.next) {
|
|
685
|
+
await this.handleNextPrompt(result.next);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
await this.speakNextPrompt();
|
|
689
|
+
}
|
|
690
|
+
catch (err) {
|
|
691
|
+
await this.setError('@extensions-ai-voice-shell:errors.apply-failed', err instanceof Error ? err.message : undefined);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
async enterSubmitConfirm(spoken) {
|
|
695
|
+
this.mode.set('confirm');
|
|
696
|
+
this.pendingSpokenConfirm = { kind: 'submit' };
|
|
697
|
+
const prompt = spoken?.trim() ||
|
|
698
|
+
(await this.translation.translateAsync('@extensions-ai-voice-shell:interview.submit-confirm'));
|
|
699
|
+
this.confirmRequest.set({
|
|
700
|
+
prompt,
|
|
701
|
+
confirmLabelKey: '@extensions-ai-voice-shell:actions.submit',
|
|
702
|
+
cancelLabelKey: '@extensions-ai-voice-shell:actions.keep-editing',
|
|
703
|
+
});
|
|
704
|
+
await this.speak(prompt);
|
|
705
|
+
}
|
|
706
|
+
async handleConfirmUtterance(text) {
|
|
707
|
+
const normalized = text.trim().toLowerCase();
|
|
708
|
+
const yes = /^(yes|y|ok|okay|sure|submit|confirm|بله|آره|باشه|تایید)$/i.test(normalized);
|
|
709
|
+
const no = /^(no|n|cancel|stop|نه|خیر|لغو)$/i.test(normalized);
|
|
710
|
+
if (yes) {
|
|
711
|
+
await this.confirmYes();
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (no) {
|
|
715
|
+
await this.confirmNo();
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
await this.speak(await this.translation.translateAsync('@extensions-ai-voice-shell:interview.confirm-repeat'));
|
|
719
|
+
}
|
|
720
|
+
async submitInterview() {
|
|
721
|
+
if (!this.activeCapability?.submit || !this.activeSessionId) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
this.uiState.set('thinking');
|
|
725
|
+
try {
|
|
726
|
+
const result = await this.activeCapability.submit(this.activeSessionId);
|
|
727
|
+
this.pendingSpokenConfirm = null;
|
|
728
|
+
this.confirmRequest.set(null);
|
|
729
|
+
if (!result.ok) {
|
|
730
|
+
const spoken = result.spokenMessage ||
|
|
731
|
+
(await this.translation.translateAsync('@extensions-ai-voice-shell:interview.submit-failed'));
|
|
732
|
+
await this.speak(spoken);
|
|
733
|
+
this.mode.set('interview');
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
const spoken = result.spokenMessage ||
|
|
737
|
+
(await this.translation.translateAsync('@extensions-ai-voice-shell:interview.submitted'));
|
|
738
|
+
await this.speak(spoken);
|
|
739
|
+
this.resetSession();
|
|
740
|
+
}
|
|
741
|
+
catch (err) {
|
|
742
|
+
await this.setError('@extensions-ai-voice-shell:errors.submit-failed', err instanceof Error ? err.message : undefined);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
async runCommandAssist(text) {
|
|
746
|
+
try {
|
|
747
|
+
const { text: reply } = await this.transport.runAssist(text, {
|
|
748
|
+
voiceShell: true,
|
|
749
|
+
mode: 'command',
|
|
750
|
+
});
|
|
751
|
+
const spoken = reply.trim() || (await this.translation.translateAsync('@extensions-ai-voice-shell:command.empty-reply'));
|
|
752
|
+
await this.speak(spoken);
|
|
753
|
+
}
|
|
754
|
+
catch (err) {
|
|
755
|
+
await this.setError('@extensions-ai-voice-shell:errors.assist-failed', err instanceof Error ? err.message : undefined);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
async speak(text) {
|
|
759
|
+
const trimmed = text.trim();
|
|
760
|
+
if (!trimmed) {
|
|
761
|
+
this.uiState.set('idle');
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
this.lastSpoken.set(trimmed);
|
|
765
|
+
this.appendTurn('assistant', trimmed);
|
|
766
|
+
this.uiState.set('speaking');
|
|
767
|
+
this.statusText.set(await this.translation.translateAsync('@extensions-ai-voice-shell:states.speaking'));
|
|
768
|
+
this.stopPlayback();
|
|
769
|
+
try {
|
|
770
|
+
const tts = await this.transport.synthesize(trimmed);
|
|
771
|
+
if (tts.objectUrl) {
|
|
772
|
+
this.currentObjectUrl = tts.objectUrl;
|
|
773
|
+
const played = await this.playObjectUrl(tts.objectUrl);
|
|
774
|
+
if (played) {
|
|
775
|
+
await this.finishSpeaking();
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
catch {
|
|
781
|
+
// Fall through to browser speech when server TTS is unavailable.
|
|
782
|
+
}
|
|
783
|
+
await speakWithBrowserSpeechSynthesis(trimmed);
|
|
784
|
+
await this.finishSpeaking();
|
|
785
|
+
}
|
|
786
|
+
async finishSpeaking() {
|
|
787
|
+
if (this.mode() === 'confirm') {
|
|
788
|
+
this.uiState.set('confirm');
|
|
789
|
+
this.statusText.set(await this.translation.translateAsync('@extensions-ai-voice-shell:states.confirm'));
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
this.uiState.set('idle');
|
|
793
|
+
this.statusText.set(await this.translation.translateAsync('@extensions-ai-voice-shell:states.idle'));
|
|
794
|
+
}
|
|
795
|
+
playObjectUrl(url) {
|
|
796
|
+
return new Promise((resolve) => {
|
|
797
|
+
const audio = new Audio(url);
|
|
798
|
+
this.currentAudio = audio;
|
|
799
|
+
const finish = (ok) => {
|
|
800
|
+
audio.removeEventListener('ended', onEnded);
|
|
801
|
+
audio.removeEventListener('error', onError);
|
|
802
|
+
resolve(ok);
|
|
803
|
+
};
|
|
804
|
+
const onEnded = () => finish(true);
|
|
805
|
+
const onError = () => finish(false);
|
|
806
|
+
audio.addEventListener('ended', onEnded);
|
|
807
|
+
audio.addEventListener('error', onError);
|
|
808
|
+
void audio.play().catch(() => finish(false));
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
async setError(key, detail) {
|
|
812
|
+
const messageKey = detail?.startsWith('@') ? detail : key;
|
|
813
|
+
this.errorMessage.set(await this.translation.translateAsync(messageKey));
|
|
814
|
+
this.uiState.set('error');
|
|
815
|
+
this.statusText.set(await this.translation.translateAsync('@extensions-ai-voice-shell:states.error'));
|
|
816
|
+
}
|
|
817
|
+
appendTurn(role, text) {
|
|
818
|
+
this.turns.update((current) => [...current, { role, text }]);
|
|
819
|
+
}
|
|
820
|
+
resetSession() {
|
|
821
|
+
this.activeCapability = null;
|
|
822
|
+
this.activeSessionId = null;
|
|
823
|
+
this.pendingSpokenConfirm = null;
|
|
824
|
+
this.confirmRequest.set(null);
|
|
825
|
+
this.interviewProgress.set(null);
|
|
826
|
+
this.mode.set('command');
|
|
827
|
+
this.uiState.set('idle');
|
|
828
|
+
this.statusText.set('');
|
|
829
|
+
this.lastTranscript.set('');
|
|
830
|
+
this.lastSpoken.set('');
|
|
831
|
+
this.errorMessage.set('');
|
|
832
|
+
}
|
|
833
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPVoiceCommandShellService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
834
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPVoiceCommandShellService, providedIn: 'root' }); }
|
|
835
|
+
}
|
|
836
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXPVoiceCommandShellService, decorators: [{
|
|
837
|
+
type: Injectable,
|
|
838
|
+
args: [{ providedIn: 'root' }]
|
|
839
|
+
}] });
|
|
840
|
+
|
|
841
|
+
//#endregion
|
|
842
|
+
//#region ----- Command -----
|
|
843
|
+
class AXEAiVoiceShellOpenCommand {
|
|
844
|
+
constructor() {
|
|
845
|
+
//#region ----- Services & Dependencies -----
|
|
846
|
+
this.shell = inject(AXPVoiceCommandShellService);
|
|
847
|
+
}
|
|
848
|
+
//#endregion
|
|
849
|
+
//#region ----- Command execution -----
|
|
850
|
+
async execute(input) {
|
|
851
|
+
const startInput = input?.caseId || input?.sessionId
|
|
852
|
+
? { caseId: input.caseId, sessionId: input.sessionId }
|
|
853
|
+
: input?.initialText
|
|
854
|
+
? { text: input.initialText }
|
|
855
|
+
: undefined;
|
|
856
|
+
const options = {
|
|
857
|
+
initialText: input?.initialText,
|
|
858
|
+
capabilityId: input?.capabilityId,
|
|
859
|
+
startInput,
|
|
860
|
+
};
|
|
861
|
+
await this.shell.open(options);
|
|
862
|
+
return { success: true, kind: 'object', data: undefined };
|
|
863
|
+
}
|
|
864
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiVoiceShellOpenCommand, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
865
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiVoiceShellOpenCommand }); }
|
|
866
|
+
}
|
|
867
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiVoiceShellOpenCommand, decorators: [{
|
|
868
|
+
type: Injectable
|
|
869
|
+
}] });
|
|
870
|
+
|
|
871
|
+
var openVoiceCommandShell_command = /*#__PURE__*/Object.freeze({
|
|
872
|
+
__proto__: null,
|
|
873
|
+
AXEAiVoiceShellOpenCommand: AXEAiVoiceShellOpenCommand
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
//#region ----- Imports -----
|
|
877
|
+
//#endregion
|
|
878
|
+
//#region ----- Component -----
|
|
879
|
+
class AXEAiVoiceVoidDialogComponent extends AXBasePageComponent {
|
|
880
|
+
//#endregion
|
|
881
|
+
//#region ----- Lifecycle -----
|
|
882
|
+
constructor() {
|
|
883
|
+
super();
|
|
884
|
+
//#region ----- Services & Dependencies -----
|
|
885
|
+
this.shell = inject(AXPVoiceCommandShellService);
|
|
886
|
+
this.platformId = inject(PLATFORM_ID);
|
|
887
|
+
this.destroyRef = inject(DestroyRef);
|
|
888
|
+
this.audioChunks = [];
|
|
889
|
+
//#endregion
|
|
890
|
+
//#region ----- Computed Properties -----
|
|
891
|
+
this.orbClass = computed(() => `voice-void-orb voice-void-orb--${this.shell.uiState()}`, /* @ts-ignore */
|
|
892
|
+
...(ngDevMode ? [{ debugName: "orbClass" }] : /* istanbul ignore next */ []));
|
|
893
|
+
this.orbWrapClass = computed(() => `voice-void-orb-wrap voice-void-orb-wrap--${this.shell.uiState()}`, /* @ts-ignore */
|
|
894
|
+
...(ngDevMode ? [{ debugName: "orbWrapClass" }] : /* istanbul ignore next */ []));
|
|
895
|
+
this.canListen = computed(() => {
|
|
896
|
+
const state = this.shell.uiState();
|
|
897
|
+
return state === 'idle' || state === 'error' || state === 'confirm';
|
|
898
|
+
}, /* @ts-ignore */
|
|
899
|
+
...(ngDevMode ? [{ debugName: "canListen" }] : /* istanbul ignore next */ []));
|
|
900
|
+
this.isListening = computed(() => this.shell.uiState() === 'listening', /* @ts-ignore */
|
|
901
|
+
...(ngDevMode ? [{ debugName: "isListening" }] : /* istanbul ignore next */ []));
|
|
902
|
+
this.isSpeaking = computed(() => this.shell.uiState() === 'speaking', /* @ts-ignore */
|
|
903
|
+
...(ngDevMode ? [{ debugName: "isSpeaking" }] : /* istanbul ignore next */ []));
|
|
904
|
+
this.showConfirm = computed(() => this.shell.uiState() === 'confirm' && this.shell.confirmRequest() != null, /* @ts-ignore */
|
|
905
|
+
...(ngDevMode ? [{ debugName: "showConfirm" }] : /* istanbul ignore next */ []));
|
|
906
|
+
this.progressLabel = computed(() => {
|
|
907
|
+
const progress = this.shell.interviewProgress();
|
|
908
|
+
if (!progress || progress.totalVisible <= 0) {
|
|
909
|
+
return '';
|
|
910
|
+
}
|
|
911
|
+
return `${progress.answered}/${progress.totalVisible}`;
|
|
912
|
+
}, /* @ts-ignore */
|
|
913
|
+
...(ngDevMode ? [{ debugName: "progressLabel" }] : /* istanbul ignore next */ []));
|
|
914
|
+
this.destroyRef.onDestroy(() => {
|
|
915
|
+
this.stopTracks();
|
|
916
|
+
cancelBrowserSpeechSynthesis();
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
//#endregion
|
|
920
|
+
//#region ----- Event Handlers -----
|
|
921
|
+
async onMicClick() {
|
|
922
|
+
if (isPlatformBrowser(this.platformId)) {
|
|
923
|
+
primeBrowserSpeechSynthesis();
|
|
924
|
+
}
|
|
925
|
+
if (this.isListening()) {
|
|
926
|
+
await this.stopAndSubmit();
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
if (!this.canListen()) {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
await this.startRecording();
|
|
933
|
+
}
|
|
934
|
+
async onCancelListen() {
|
|
935
|
+
this.stopTracks();
|
|
936
|
+
this.audioChunks = [];
|
|
937
|
+
await this.shell.cancelListening();
|
|
938
|
+
}
|
|
939
|
+
async onConfirmYes() {
|
|
940
|
+
await this.shell.confirmYes();
|
|
941
|
+
}
|
|
942
|
+
async onConfirmNo() {
|
|
943
|
+
await this.shell.confirmNo();
|
|
944
|
+
}
|
|
945
|
+
async onSaveDraft() {
|
|
946
|
+
await this.shell.saveInterviewDraft();
|
|
947
|
+
}
|
|
948
|
+
async onCancelInterview() {
|
|
949
|
+
await this.shell.cancelInterview();
|
|
950
|
+
}
|
|
951
|
+
//#endregion
|
|
952
|
+
//#region ----- Recording -----
|
|
953
|
+
async startRecording() {
|
|
954
|
+
if (!isPlatformBrowser(this.platformId) || !navigator.mediaDevices?.getUserMedia) {
|
|
955
|
+
await this.shell.reportError('@extensions-ai-voice-shell:errors.mic-unavailable');
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
try {
|
|
959
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
960
|
+
this.activeStream = stream;
|
|
961
|
+
this.audioChunks = [];
|
|
962
|
+
const recorder = new MediaRecorder(stream);
|
|
963
|
+
this.mediaRecorder = recorder;
|
|
964
|
+
recorder.ondataavailable = (event) => {
|
|
965
|
+
if (event.data.size > 0) {
|
|
966
|
+
this.audioChunks.push(event.data);
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
recorder.start(250);
|
|
970
|
+
await this.shell.enterListening();
|
|
971
|
+
}
|
|
972
|
+
catch {
|
|
973
|
+
this.stopTracks();
|
|
974
|
+
await this.shell.reportError('@extensions-ai-voice-shell:errors.mic-denied');
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
async stopAndSubmit() {
|
|
978
|
+
const recorder = this.mediaRecorder;
|
|
979
|
+
if (!recorder || recorder.state !== 'recording') {
|
|
980
|
+
this.stopTracks();
|
|
981
|
+
await this.shell.cancelListening();
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
const blob = await new Promise((resolve) => {
|
|
985
|
+
recorder.onstop = () => {
|
|
986
|
+
const mime = recorder.mimeType?.trim() || this.audioChunks[0]?.type?.trim() || 'audio/webm';
|
|
987
|
+
const audioBlob = new Blob(this.audioChunks, { type: mime });
|
|
988
|
+
resolve(audioBlob.size > 0 ? audioBlob : null);
|
|
989
|
+
};
|
|
990
|
+
recorder.requestData();
|
|
991
|
+
recorder.stop();
|
|
992
|
+
});
|
|
993
|
+
this.stopTracks();
|
|
994
|
+
if (!blob) {
|
|
995
|
+
await this.shell.cancelListening();
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
await this.shell.submitRecording(blob);
|
|
999
|
+
}
|
|
1000
|
+
stopTracks() {
|
|
1001
|
+
if (this.mediaRecorder?.state === 'recording') {
|
|
1002
|
+
this.mediaRecorder.stop();
|
|
1003
|
+
}
|
|
1004
|
+
this.mediaRecorder = undefined;
|
|
1005
|
+
this.activeStream?.getTracks().forEach((track) => track.stop());
|
|
1006
|
+
this.activeStream = undefined;
|
|
1007
|
+
}
|
|
1008
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiVoiceVoidDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1009
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: AXEAiVoiceVoidDialogComponent, isStandalone: true, selector: "axe-ai-voice-void-dialog", usesInheritance: true, ngImport: i0, template: "<div class=\"voice-void\">\n <div class=\"voice-void-stage\">\n <!-- ----- Listening / thinking / speaking orb ----- -->\n <div [class]=\"orbWrapClass()\">\n <span class=\"voice-void-ring voice-void-ring--1\" aria-hidden=\"true\"></span>\n <span class=\"voice-void-ring voice-void-ring--2\" aria-hidden=\"true\"></span>\n <span class=\"voice-void-ring voice-void-ring--3\" aria-hidden=\"true\"></span>\n <button\n type=\"button\"\n [class]=\"orbClass()\"\n (click)=\"onMicClick()\"\n [disabled]=\"!canListen() && !isListening()\"\n [attr.aria-label]=\"'@extensions-ai-voice-shell:actions.tap-to-talk' | translate | async\"\n >\n @if (isSpeaking()) {\n <!-- ----- AI is speaking ----- -->\n <i class=\"fa-regular fa-volume-high\"></i>\n }\n <!-- ----- Tap-to-talk / user speaking ----- -->\n @else {\n <i class=\"fa-regular fa-microphone\"></i>\n }\n </button>\n @if (isListening() || isSpeaking()) {\n <!-- ----- Waveform bars: user listen vs AI speak ----- -->\n <span class=\"voice-void-eq\" aria-hidden=\"true\">\n <span></span>\n <span></span>\n <span></span>\n <span></span>\n <span></span>\n </span>\n }\n </div>\n\n <p class=\"voice-void-status\">{{ shell.statusText() }}</p>\n\n @if (progressLabel()) {\n <!-- ----- Interview progress counter ----- -->\n <p class=\"voice-void-progress\">{{ progressLabel() }}</p>\n }\n\n @if (shell.lastSpoken()) {\n <!-- ----- Last spoken assistant text ----- -->\n <p class=\"voice-void-spoken\">{{ shell.lastSpoken() }}</p>\n }\n\n @if (shell.lastTranscript()) {\n <!-- ----- Last user transcript ----- -->\n <p class=\"voice-void-transcript\">{{ shell.lastTranscript() }}</p>\n }\n\n @if (shell.errorMessage()) {\n <!-- ----- Spoken / visible error ----- -->\n <p class=\"voice-void-error\">{{ shell.errorMessage() }}</p>\n }\n </div>\n\n @if (showConfirm()) {\n <!-- ----- Confirm chips for submit / destructive actions ----- -->\n <div class=\"voice-void-confirm\">\n <p class=\"voice-void-confirm-prompt\">{{ shell.confirmRequest()?.prompt }}</p>\n <div class=\"voice-void-confirm-actions\">\n <ax-button\n [text]=\"'@extensions-ai-voice-shell:actions.submit' | translate | async\"\n color=\"primary\"\n (onClick)=\"onConfirmYes()\"\n ></ax-button>\n <ax-button\n [text]=\"'@extensions-ai-voice-shell:actions.keep-editing' | translate | async\"\n [look]=\"'ghost'\"\n (onClick)=\"onConfirmNo()\"\n ></ax-button>\n </div>\n </div>\n }\n\n <!-- ----- Secondary interview / listen actions ----- -->\n <div class=\"voice-void-toolbar\">\n @if (isListening()) {\n <!-- ----- Stop listening without sending ----- -->\n <ax-button\n [text]=\"'@extensions-ai-voice-shell:actions.cancel-listen' | translate | async\"\n [look]=\"'ghost'\"\n (onClick)=\"onCancelListen()\"\n ></ax-button>\n }\n <!-- ----- Interview session actions ----- -->\n @if (shell.mode() === 'interview') {\n <!-- ----- Draft save and cancel while filling by voice ----- -->\n <ax-button\n [text]=\"'@extensions-ai-voice-shell:actions.save-draft' | translate | async\"\n [look]=\"'outline'\"\n (onClick)=\"onSaveDraft()\"\n ></ax-button>\n <ax-button\n [text]=\"'@extensions-ai-voice-shell:actions.cancel-interview' | translate | async\"\n [look]=\"'ghost'\"\n (onClick)=\"onCancelInterview()\"\n ></ax-button>\n }\n </div>\n\n <p class=\"voice-void-hint\">{{ '@extensions-ai-voice-shell:dialog.hint' | translate | async }}</p>\n</div>\n", styles: [":host,axe-ai-voice-void-dialog{display:block;height:auto}.axe-ai-voice-shell-popup .ax-popup-content,.axe-ai-voice-shell-popup .ax-popup-body{overflow:visible}.voice-void{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:16rem;padding:1.25rem 1rem .75rem;gap:1rem}.voice-void-stage{display:flex;flex-direction:column;align-items:center;gap:.75rem;max-width:22rem;text-align:center}.voice-void-orb-wrap{position:relative;display:flex;align-items:center;justify-content:center;width:9.5rem;height:9.5rem}.voice-void-ring{position:absolute;inset:1.1rem;border-radius:50%;pointer-events:none;opacity:0}.voice-void-orb{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;width:6.25rem;height:6.25rem;border:none;border-radius:50%;background:rgb(var(--ax-sys-color-primary-500, 59 130 246));color:rgb(var(--ax-sys-color-on-primary, 255 255 255));font-size:1.75rem;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease,opacity .2s ease,background-color .2s ease}.voice-void-orb:disabled{cursor:default;opacity:.92}.voice-void-orb--idle{box-shadow:0 .5rem 1.25rem rgb(var(--ax-sys-color-primary-500, 59 130 246) / .28)}.voice-void-orb--listening{animation:voice-void-user-pulse 1.05s ease-in-out infinite}.voice-void-orb-wrap--listening .voice-void-ring{border:2px solid rgb(var(--ax-sys-color-primary-400, 96 165 250))}.voice-void-orb-wrap--listening .voice-void-ring--1{animation:voice-void-user-ripple 1.5s ease-out infinite}.voice-void-orb-wrap--listening .voice-void-ring--2{animation:voice-void-user-ripple 1.5s ease-out .28s infinite}.voice-void-orb-wrap--listening .voice-void-ring--3{animation:voice-void-user-ripple 1.5s ease-out .56s infinite}.voice-void-orb--thinking{opacity:.8;animation:voice-void-breathe 1.8s ease-in-out infinite}.voice-void-orb--speaking{background:rgb(var(--ax-sys-color-primary-700, 29 78 216));animation:voice-void-ai-glow 1.2s ease-in-out infinite}.voice-void-orb-wrap--speaking .voice-void-ring{border:2px solid rgb(var(--ax-sys-color-primary-300, 147 197 253) / .7)}.voice-void-orb-wrap--speaking .voice-void-ring--1{animation:voice-void-ai-orbit 2.4s linear infinite}.voice-void-orb-wrap--speaking .voice-void-ring--2{inset:.55rem;animation:voice-void-ai-orbit 3.2s linear infinite reverse}.voice-void-orb-wrap--speaking .voice-void-ring--3{display:none}.voice-void-orb--confirm,.voice-void-orb--error{background:rgb(var(--ax-sys-color-warning-500, 217 119 6))}.voice-void-eq{position:absolute;z-index:2;bottom:.85rem;display:none;align-items:flex-end;gap:.18rem;height:1.1rem;pointer-events:none}.voice-void-orb-wrap--listening .voice-void-eq,.voice-void-orb-wrap--speaking .voice-void-eq{display:flex}.voice-void-eq span{display:block;width:.18rem;border-radius:999px;background:rgb(var(--ax-sys-color-on-primary, 255 255 255));transform-origin:bottom center}.voice-void-orb-wrap--listening .voice-void-eq span:nth-child(1),.voice-void-orb-wrap--speaking .voice-void-eq span:nth-child(1){animation:voice-void-eq-bar .7s ease-in-out infinite}.voice-void-orb-wrap--listening .voice-void-eq span:nth-child(2),.voice-void-orb-wrap--speaking .voice-void-eq span:nth-child(2){animation:voice-void-eq-bar .7s ease-in-out .1s infinite}.voice-void-orb-wrap--listening .voice-void-eq span:nth-child(3),.voice-void-orb-wrap--speaking .voice-void-eq span:nth-child(3){animation:voice-void-eq-bar .7s ease-in-out .18s infinite}.voice-void-orb-wrap--listening .voice-void-eq span:nth-child(4),.voice-void-orb-wrap--speaking .voice-void-eq span:nth-child(4){animation:voice-void-eq-bar .7s ease-in-out .08s infinite}.voice-void-orb-wrap--listening .voice-void-eq span:nth-child(5),.voice-void-orb-wrap--speaking .voice-void-eq span:nth-child(5){animation:voice-void-eq-bar .7s ease-in-out .22s infinite}.voice-void-orb-wrap--speaking .voice-void-eq span{animation-duration:.95s}.voice-void-status{margin:0;font-weight:600}.voice-void-progress,.voice-void-spoken,.voice-void-transcript,.voice-void-error,.voice-void-hint,.voice-void-confirm-prompt{margin:0}.voice-void-spoken,.voice-void-transcript{max-height:4.5rem;overflow:auto;font-size:.875rem}.voice-void-spoken,.voice-void-transcript,.voice-void-hint{color:rgb(var(--ax-sys-color-on-surface) / .8)}.voice-void-error{color:rgb(var(--ax-sys-color-danger, 185 28 28))}.voice-void-confirm,.voice-void-toolbar{display:flex;flex-wrap:wrap;gap:.75rem;justify-content:center;align-items:center}.voice-void-confirm{flex-direction:column}.voice-void-hint{font-size:.8125rem}@keyframes voice-void-user-pulse{0%,to{transform:scale(1);box-shadow:0 0 rgb(var(--ax-sys-color-primary-500, 59 130 246) / .4)}50%{transform:scale(1.06);box-shadow:0 0 0 .65rem rgb(var(--ax-sys-color-primary-500, 59 130 246) / .12)}}@keyframes voice-void-user-ripple{0%{transform:scale(.92);opacity:.55}to{transform:scale(1.55);opacity:0}}@keyframes voice-void-ai-glow{0%,to{transform:scale(1);box-shadow:0 0 0 .35rem rgb(var(--ax-sys-color-primary-300, 147 197 253) / .35),0 0 1.75rem rgb(var(--ax-sys-color-primary-500, 59 130 246) / .55)}50%{transform:scale(1.04);box-shadow:0 0 0 .7rem rgb(var(--ax-sys-color-primary-300, 147 197 253) / .18),0 0 2.25rem rgb(var(--ax-sys-color-primary-500, 59 130 246) / .75)}}@keyframes voice-void-ai-orbit{0%{transform:scale(1) rotate(0);opacity:.45;border-style:dashed}50%{opacity:.85}to{transform:scale(1.12) rotate(360deg);opacity:.45;border-style:dashed}}@keyframes voice-void-breathe{0%,to{transform:scale(1);opacity:.8}50%{transform:scale(1.06);opacity:1}}@keyframes voice-void-eq-bar{0%,to{height:.28rem}40%{height:.95rem}70%{height:.45rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: AXButtonModule }, { kind: "component", type: i1.AXButtonComponent, selector: "ax-button", inputs: ["disabled", "size", "tabIndex", "color", "look", "text", "toggleable", "selected", "iconOnly", "type", "loadingText"], outputs: ["onBlur", "onFocus", "onClick", "selectedChange", "toggleableChange", "lookChange", "colorChange", "disabledChange", "loadingTextChange"] }, { kind: "ngmodule", type: AXDecoratorModule }, { kind: "ngmodule", type: AXTranslationModule }, { kind: "pipe", type: i2$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i3.AXTranslatorPipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
|
|
1010
|
+
}
|
|
1011
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: AXEAiVoiceVoidDialogComponent, decorators: [{
|
|
1012
|
+
type: Component,
|
|
1013
|
+
args: [{ selector: 'axe-ai-voice-void-dialog', changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, AXButtonModule, AXDecoratorModule, AXTranslationModule], encapsulation: ViewEncapsulation.None, template: "<div class=\"voice-void\">\n <div class=\"voice-void-stage\">\n <!-- ----- Listening / thinking / speaking orb ----- -->\n <div [class]=\"orbWrapClass()\">\n <span class=\"voice-void-ring voice-void-ring--1\" aria-hidden=\"true\"></span>\n <span class=\"voice-void-ring voice-void-ring--2\" aria-hidden=\"true\"></span>\n <span class=\"voice-void-ring voice-void-ring--3\" aria-hidden=\"true\"></span>\n <button\n type=\"button\"\n [class]=\"orbClass()\"\n (click)=\"onMicClick()\"\n [disabled]=\"!canListen() && !isListening()\"\n [attr.aria-label]=\"'@extensions-ai-voice-shell:actions.tap-to-talk' | translate | async\"\n >\n @if (isSpeaking()) {\n <!-- ----- AI is speaking ----- -->\n <i class=\"fa-regular fa-volume-high\"></i>\n }\n <!-- ----- Tap-to-talk / user speaking ----- -->\n @else {\n <i class=\"fa-regular fa-microphone\"></i>\n }\n </button>\n @if (isListening() || isSpeaking()) {\n <!-- ----- Waveform bars: user listen vs AI speak ----- -->\n <span class=\"voice-void-eq\" aria-hidden=\"true\">\n <span></span>\n <span></span>\n <span></span>\n <span></span>\n <span></span>\n </span>\n }\n </div>\n\n <p class=\"voice-void-status\">{{ shell.statusText() }}</p>\n\n @if (progressLabel()) {\n <!-- ----- Interview progress counter ----- -->\n <p class=\"voice-void-progress\">{{ progressLabel() }}</p>\n }\n\n @if (shell.lastSpoken()) {\n <!-- ----- Last spoken assistant text ----- -->\n <p class=\"voice-void-spoken\">{{ shell.lastSpoken() }}</p>\n }\n\n @if (shell.lastTranscript()) {\n <!-- ----- Last user transcript ----- -->\n <p class=\"voice-void-transcript\">{{ shell.lastTranscript() }}</p>\n }\n\n @if (shell.errorMessage()) {\n <!-- ----- Spoken / visible error ----- -->\n <p class=\"voice-void-error\">{{ shell.errorMessage() }}</p>\n }\n </div>\n\n @if (showConfirm()) {\n <!-- ----- Confirm chips for submit / destructive actions ----- -->\n <div class=\"voice-void-confirm\">\n <p class=\"voice-void-confirm-prompt\">{{ shell.confirmRequest()?.prompt }}</p>\n <div class=\"voice-void-confirm-actions\">\n <ax-button\n [text]=\"'@extensions-ai-voice-shell:actions.submit' | translate | async\"\n color=\"primary\"\n (onClick)=\"onConfirmYes()\"\n ></ax-button>\n <ax-button\n [text]=\"'@extensions-ai-voice-shell:actions.keep-editing' | translate | async\"\n [look]=\"'ghost'\"\n (onClick)=\"onConfirmNo()\"\n ></ax-button>\n </div>\n </div>\n }\n\n <!-- ----- Secondary interview / listen actions ----- -->\n <div class=\"voice-void-toolbar\">\n @if (isListening()) {\n <!-- ----- Stop listening without sending ----- -->\n <ax-button\n [text]=\"'@extensions-ai-voice-shell:actions.cancel-listen' | translate | async\"\n [look]=\"'ghost'\"\n (onClick)=\"onCancelListen()\"\n ></ax-button>\n }\n <!-- ----- Interview session actions ----- -->\n @if (shell.mode() === 'interview') {\n <!-- ----- Draft save and cancel while filling by voice ----- -->\n <ax-button\n [text]=\"'@extensions-ai-voice-shell:actions.save-draft' | translate | async\"\n [look]=\"'outline'\"\n (onClick)=\"onSaveDraft()\"\n ></ax-button>\n <ax-button\n [text]=\"'@extensions-ai-voice-shell:actions.cancel-interview' | translate | async\"\n [look]=\"'ghost'\"\n (onClick)=\"onCancelInterview()\"\n ></ax-button>\n }\n </div>\n\n <p class=\"voice-void-hint\">{{ '@extensions-ai-voice-shell:dialog.hint' | translate | async }}</p>\n</div>\n", styles: [":host,axe-ai-voice-void-dialog{display:block;height:auto}.axe-ai-voice-shell-popup .ax-popup-content,.axe-ai-voice-shell-popup .ax-popup-body{overflow:visible}.voice-void{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:16rem;padding:1.25rem 1rem .75rem;gap:1rem}.voice-void-stage{display:flex;flex-direction:column;align-items:center;gap:.75rem;max-width:22rem;text-align:center}.voice-void-orb-wrap{position:relative;display:flex;align-items:center;justify-content:center;width:9.5rem;height:9.5rem}.voice-void-ring{position:absolute;inset:1.1rem;border-radius:50%;pointer-events:none;opacity:0}.voice-void-orb{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;width:6.25rem;height:6.25rem;border:none;border-radius:50%;background:rgb(var(--ax-sys-color-primary-500, 59 130 246));color:rgb(var(--ax-sys-color-on-primary, 255 255 255));font-size:1.75rem;cursor:pointer;transition:transform .2s ease,box-shadow .2s ease,opacity .2s ease,background-color .2s ease}.voice-void-orb:disabled{cursor:default;opacity:.92}.voice-void-orb--idle{box-shadow:0 .5rem 1.25rem rgb(var(--ax-sys-color-primary-500, 59 130 246) / .28)}.voice-void-orb--listening{animation:voice-void-user-pulse 1.05s ease-in-out infinite}.voice-void-orb-wrap--listening .voice-void-ring{border:2px solid rgb(var(--ax-sys-color-primary-400, 96 165 250))}.voice-void-orb-wrap--listening .voice-void-ring--1{animation:voice-void-user-ripple 1.5s ease-out infinite}.voice-void-orb-wrap--listening .voice-void-ring--2{animation:voice-void-user-ripple 1.5s ease-out .28s infinite}.voice-void-orb-wrap--listening .voice-void-ring--3{animation:voice-void-user-ripple 1.5s ease-out .56s infinite}.voice-void-orb--thinking{opacity:.8;animation:voice-void-breathe 1.8s ease-in-out infinite}.voice-void-orb--speaking{background:rgb(var(--ax-sys-color-primary-700, 29 78 216));animation:voice-void-ai-glow 1.2s ease-in-out infinite}.voice-void-orb-wrap--speaking .voice-void-ring{border:2px solid rgb(var(--ax-sys-color-primary-300, 147 197 253) / .7)}.voice-void-orb-wrap--speaking .voice-void-ring--1{animation:voice-void-ai-orbit 2.4s linear infinite}.voice-void-orb-wrap--speaking .voice-void-ring--2{inset:.55rem;animation:voice-void-ai-orbit 3.2s linear infinite reverse}.voice-void-orb-wrap--speaking .voice-void-ring--3{display:none}.voice-void-orb--confirm,.voice-void-orb--error{background:rgb(var(--ax-sys-color-warning-500, 217 119 6))}.voice-void-eq{position:absolute;z-index:2;bottom:.85rem;display:none;align-items:flex-end;gap:.18rem;height:1.1rem;pointer-events:none}.voice-void-orb-wrap--listening .voice-void-eq,.voice-void-orb-wrap--speaking .voice-void-eq{display:flex}.voice-void-eq span{display:block;width:.18rem;border-radius:999px;background:rgb(var(--ax-sys-color-on-primary, 255 255 255));transform-origin:bottom center}.voice-void-orb-wrap--listening .voice-void-eq span:nth-child(1),.voice-void-orb-wrap--speaking .voice-void-eq span:nth-child(1){animation:voice-void-eq-bar .7s ease-in-out infinite}.voice-void-orb-wrap--listening .voice-void-eq span:nth-child(2),.voice-void-orb-wrap--speaking .voice-void-eq span:nth-child(2){animation:voice-void-eq-bar .7s ease-in-out .1s infinite}.voice-void-orb-wrap--listening .voice-void-eq span:nth-child(3),.voice-void-orb-wrap--speaking .voice-void-eq span:nth-child(3){animation:voice-void-eq-bar .7s ease-in-out .18s infinite}.voice-void-orb-wrap--listening .voice-void-eq span:nth-child(4),.voice-void-orb-wrap--speaking .voice-void-eq span:nth-child(4){animation:voice-void-eq-bar .7s ease-in-out .08s infinite}.voice-void-orb-wrap--listening .voice-void-eq span:nth-child(5),.voice-void-orb-wrap--speaking .voice-void-eq span:nth-child(5){animation:voice-void-eq-bar .7s ease-in-out .22s infinite}.voice-void-orb-wrap--speaking .voice-void-eq span{animation-duration:.95s}.voice-void-status{margin:0;font-weight:600}.voice-void-progress,.voice-void-spoken,.voice-void-transcript,.voice-void-error,.voice-void-hint,.voice-void-confirm-prompt{margin:0}.voice-void-spoken,.voice-void-transcript{max-height:4.5rem;overflow:auto;font-size:.875rem}.voice-void-spoken,.voice-void-transcript,.voice-void-hint{color:rgb(var(--ax-sys-color-on-surface) / .8)}.voice-void-error{color:rgb(var(--ax-sys-color-danger, 185 28 28))}.voice-void-confirm,.voice-void-toolbar{display:flex;flex-wrap:wrap;gap:.75rem;justify-content:center;align-items:center}.voice-void-confirm{flex-direction:column}.voice-void-hint{font-size:.8125rem}@keyframes voice-void-user-pulse{0%,to{transform:scale(1);box-shadow:0 0 rgb(var(--ax-sys-color-primary-500, 59 130 246) / .4)}50%{transform:scale(1.06);box-shadow:0 0 0 .65rem rgb(var(--ax-sys-color-primary-500, 59 130 246) / .12)}}@keyframes voice-void-user-ripple{0%{transform:scale(.92);opacity:.55}to{transform:scale(1.55);opacity:0}}@keyframes voice-void-ai-glow{0%,to{transform:scale(1);box-shadow:0 0 0 .35rem rgb(var(--ax-sys-color-primary-300, 147 197 253) / .35),0 0 1.75rem rgb(var(--ax-sys-color-primary-500, 59 130 246) / .55)}50%{transform:scale(1.04);box-shadow:0 0 0 .7rem rgb(var(--ax-sys-color-primary-300, 147 197 253) / .18),0 0 2.25rem rgb(var(--ax-sys-color-primary-500, 59 130 246) / .75)}}@keyframes voice-void-ai-orbit{0%{transform:scale(1) rotate(0);opacity:.45;border-style:dashed}50%{opacity:.85}to{transform:scale(1.12) rotate(360deg);opacity:.45;border-style:dashed}}@keyframes voice-void-breathe{0%,to{transform:scale(1);opacity:.8}50%{transform:scale(1.06);opacity:1}}@keyframes voice-void-eq-bar{0%,to{height:.28rem}40%{height:.95rem}70%{height:.45rem}}\n"] }]
|
|
1014
|
+
}], ctorParameters: () => [] });
|
|
1015
|
+
|
|
1016
|
+
var voiceVoidDialog_component = /*#__PURE__*/Object.freeze({
|
|
1017
|
+
__proto__: null,
|
|
1018
|
+
AXEAiVoiceVoidDialogComponent: AXEAiVoiceVoidDialogComponent
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Generated bundle index. Do not edit.
|
|
1023
|
+
*/
|
|
1024
|
+
|
|
1025
|
+
export { AXEAiVoiceShellExtensionModule, AXEAiVoiceShellHeaderSlotComponent, AXEAiVoiceShellOpenCommand, AXEAiVoiceVoidDialogComponent, AXE_AI_VOICE_SHELL_SHORTCUT, AXPHttpSseVoiceTransport, AXPVoiceCapabilityRegistry, AXPVoiceCommandShellService, AXP_VOICE_CAPABILITY_PROVIDER };
|
|
1026
|
+
//# sourceMappingURL=acorex-platform-extension-ai-voice-shell-client.mjs.map
|