@oshara/voice-sdk 0.1.0
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/README.md +198 -0
- package/dist/appearance-CNWT8x1G.cjs +2 -0
- package/dist/appearance-CNWT8x1G.cjs.map +1 -0
- package/dist/appearance-i6QBkpCk.js +650 -0
- package/dist/appearance-i6QBkpCk.js.map +1 -0
- package/dist/consent-CK9VXNPa.js +54 -0
- package/dist/consent-CK9VXNPa.js.map +1 -0
- package/dist/consent-D7QNSkQD.cjs +2 -0
- package/dist/consent-D7QNSkQD.cjs.map +1 -0
- package/dist/core/analytics.d.ts +30 -0
- package/dist/core/appearance.d.ts +113 -0
- package/dist/core/audioSettings.d.ts +69 -0
- package/dist/core/consent.d.ts +17 -0
- package/dist/core/createVoiceAgent.d.ts +79 -0
- package/dist/core/events.d.ts +103 -0
- package/dist/core/formController.d.ts +28 -0
- package/dist/core/forms.d.ts +235 -0
- package/dist/core/index.d.ts +29 -0
- package/dist/core/prevContext.d.ts +26 -0
- package/dist/core/transport.d.ts +30 -0
- package/dist/core/types.d.ts +49 -0
- package/dist/core/voice.d.ts +79 -0
- package/dist/createVoiceAgent-BM3HODS6.js +1058 -0
- package/dist/createVoiceAgent-BM3HODS6.js.map +1 -0
- package/dist/createVoiceAgent-CJWxWzz6.cjs +4 -0
- package/dist/createVoiceAgent-CJWxWzz6.cjs.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.d.ts +60 -0
- package/dist/react.cjs +2 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.js +115 -0
- package/dist/react.js.map +1 -0
- package/dist/styles.css +1838 -0
- package/dist/ui/index.d.ts +21 -0
- package/dist/ui/ui.d.ts +165 -0
- package/dist/ui.cjs +284 -0
- package/dist/ui.cjs.map +1 -0
- package/dist/ui.js +1153 -0
- package/dist/ui.js.map +1 -0
- package/package.json +67 -0
- package/src/core/analytics.ts +111 -0
- package/src/core/appearance.ts +464 -0
- package/src/core/audioSettings.ts +180 -0
- package/src/core/consent.ts +78 -0
- package/src/core/createVoiceAgent.ts +280 -0
- package/src/core/events.ts +120 -0
- package/src/core/formController.ts +317 -0
- package/src/core/forms.ts +861 -0
- package/src/core/index.ts +121 -0
- package/src/core/prevContext.ts +153 -0
- package/src/core/transport.ts +118 -0
- package/src/core/types.ts +66 -0
- package/src/core/voice.ts +1179 -0
- package/src/react/index.ts +238 -0
- package/src/ui/index.ts +507 -0
- package/src/ui/styles.css +1838 -0
- package/src/ui/ui.ts +1672 -0
- package/src/vite-env.d.ts +10 -0
package/src/ui/index.ts
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @oshara/voice-sdk/ui — the prebuilt widget UI.
|
|
3
|
+
*
|
|
4
|
+
* `mountVoiceUI(client, opts)` builds the shadow-DOM widget (FAB + panel +
|
|
5
|
+
* call/form screens + audio drawer), subscribes it to the headless client's
|
|
6
|
+
* events, and wires DOM interactions back to the client's methods. It's the
|
|
7
|
+
* exact UI the embeddable widget ships — now an optional layer over the core.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { VoiceAgentClient } from "../core";
|
|
11
|
+
import { saveConsent } from "../core";
|
|
12
|
+
import type { AudioStateSnapshot } from "../core";
|
|
13
|
+
import {
|
|
14
|
+
applyAppearance,
|
|
15
|
+
appendOrUpdateTranscriptSegment,
|
|
16
|
+
buildUI,
|
|
17
|
+
clearFieldErrors,
|
|
18
|
+
clearTranscript,
|
|
19
|
+
populateAudioDeviceSelect,
|
|
20
|
+
readFormValues,
|
|
21
|
+
renderForm,
|
|
22
|
+
setAgentStatusLine,
|
|
23
|
+
setAudioDrawerOpen,
|
|
24
|
+
setAudioMeterLevel,
|
|
25
|
+
setCallStatus,
|
|
26
|
+
setCallTimer,
|
|
27
|
+
setFieldErrors,
|
|
28
|
+
setFormBusy,
|
|
29
|
+
setFormCallControlsVisible,
|
|
30
|
+
setFormError,
|
|
31
|
+
setFormSuccess,
|
|
32
|
+
setFormTranscriptVisible,
|
|
33
|
+
setMuteVisual,
|
|
34
|
+
setOrbState,
|
|
35
|
+
setScreen,
|
|
36
|
+
showAgentTransition,
|
|
37
|
+
Screen,
|
|
38
|
+
UIRefs,
|
|
39
|
+
} from "./ui";
|
|
40
|
+
|
|
41
|
+
export interface MountVoiceUIOptions {
|
|
42
|
+
/** Where to mount. Default document.body. */
|
|
43
|
+
target?: HTMLElement;
|
|
44
|
+
/** Fill the parent container instead of floating (hides FAB). */
|
|
45
|
+
inline?: boolean;
|
|
46
|
+
/** Open the panel on mount. */
|
|
47
|
+
openChat?: boolean;
|
|
48
|
+
/** Hide the panel close button. */
|
|
49
|
+
closeButtonHide?: boolean;
|
|
50
|
+
/** Host element id. Default "voice-agent-widget-root". */
|
|
51
|
+
rootId?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface VoiceUIHandle {
|
|
55
|
+
destroy: () => void;
|
|
56
|
+
/** The shadow-DOM refs, for advanced host integrations. */
|
|
57
|
+
refs: UIRefs;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function mountVoiceUI(
|
|
61
|
+
client: VoiceAgentClient,
|
|
62
|
+
opts: MountVoiceUIOptions = {},
|
|
63
|
+
): VoiceUIHandle {
|
|
64
|
+
const parent = opts.target ?? document.body;
|
|
65
|
+
const refs = buildUI(opts.rootId ?? "voice-agent-widget-root", {
|
|
66
|
+
inline: opts.inline,
|
|
67
|
+
parent,
|
|
68
|
+
closeButtonHide: opts.closeButtonHide,
|
|
69
|
+
});
|
|
70
|
+
applyAppearance(refs, client.getAppearance());
|
|
71
|
+
setScreen(refs, "welcome");
|
|
72
|
+
|
|
73
|
+
const audioCaps = client.getAudioCapabilities();
|
|
74
|
+
const unsubs: Array<() => void> = [];
|
|
75
|
+
const timers: number[] = [];
|
|
76
|
+
let panelOpen = false;
|
|
77
|
+
let formOpen = false;
|
|
78
|
+
let audioDrawerOpen = false;
|
|
79
|
+
let audioStatsTimer: number | null = null;
|
|
80
|
+
let audioMeterCtx: AudioContext | null = null;
|
|
81
|
+
let audioMeterRaf: number | null = null;
|
|
82
|
+
|
|
83
|
+
const trackedSetInterval = (handler: () => void, ms: number): number => {
|
|
84
|
+
const id = window.setInterval(handler, ms);
|
|
85
|
+
timers.push(id);
|
|
86
|
+
return id;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const togglePanel = (show: boolean) => {
|
|
90
|
+
panelOpen = show;
|
|
91
|
+
refs.panel.style.display = show ? "flex" : "none";
|
|
92
|
+
};
|
|
93
|
+
const ensurePanelOpen = () => {
|
|
94
|
+
if (!panelOpen) togglePanel(true);
|
|
95
|
+
};
|
|
96
|
+
const returnTo = (): Screen => (client.isActive() ? "call" : "welcome");
|
|
97
|
+
|
|
98
|
+
// ── event subscriptions (replace the controller's old direct UI calls) ──
|
|
99
|
+
unsubs.push(
|
|
100
|
+
client.on("appearance", (a) => applyAppearance(refs, a)),
|
|
101
|
+
client.on("state", ({ orb, statusLabel }) => {
|
|
102
|
+
setOrbState(refs, orb);
|
|
103
|
+
setAgentStatusLine(refs, orb === "thinking" ? statusLabel : null);
|
|
104
|
+
}),
|
|
105
|
+
client.on("call:status", ({ status }) => setCallStatus(refs, status)),
|
|
106
|
+
client.on("call:timer", ({ remainingMs }) => setCallTimer(refs, remainingMs)),
|
|
107
|
+
client.on("mute", ({ muted }) => setMuteVisual(refs, muted)),
|
|
108
|
+
client.on("controls", ({ canStart, canMute, canEnd }) => {
|
|
109
|
+
refs.startBtn.disabled = !canStart;
|
|
110
|
+
refs.muteBtn.disabled = !canMute;
|
|
111
|
+
refs.endBtn.disabled = !canEnd;
|
|
112
|
+
}),
|
|
113
|
+
client.on("connection", ({ phase }) => {
|
|
114
|
+
if (phase === "connecting") {
|
|
115
|
+
if (!formOpen) setScreen(refs, "call");
|
|
116
|
+
} else if (phase === "disconnected" || phase === "failed") {
|
|
117
|
+
if (!formOpen) setScreen(refs, "welcome");
|
|
118
|
+
}
|
|
119
|
+
}),
|
|
120
|
+
client.on("transcript", ({ role, segmentId, text, isFinal }) =>
|
|
121
|
+
appendOrUpdateTranscriptSegment(refs, role, segmentId, text, isFinal),
|
|
122
|
+
),
|
|
123
|
+
client.on("transcript:clear", () => clearTranscript(refs)),
|
|
124
|
+
client.on("agent:handoff", ({ agentName }) =>
|
|
125
|
+
showAgentTransition(refs, agentName),
|
|
126
|
+
),
|
|
127
|
+
client.on("audio", (state: AudioStateSnapshot) =>
|
|
128
|
+
syncDrawerToState(refs, state, audioCaps),
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// ── forms ──
|
|
133
|
+
const renderActiveForm = () => {
|
|
134
|
+
const active = client.getActiveForm();
|
|
135
|
+
if (!active) return;
|
|
136
|
+
renderForm(refs, {
|
|
137
|
+
definition: active.definition,
|
|
138
|
+
values: active.values,
|
|
139
|
+
stepIndex: active.stepIndex,
|
|
140
|
+
cancelLabel: client.isActive() ? "Back to call" : "Cancel",
|
|
141
|
+
onFieldChange: () => client.updateFormValues(readFormValues(refs)),
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
unsubs.push(
|
|
146
|
+
client.on("form:show", ({ inCall, transcriptionEnabled }) => {
|
|
147
|
+
formOpen = true;
|
|
148
|
+
ensurePanelOpen();
|
|
149
|
+
setScreen(refs, "form");
|
|
150
|
+
setFormCallControlsVisible(refs, inCall);
|
|
151
|
+
setFormTranscriptVisible(refs, inCall && transcriptionEnabled);
|
|
152
|
+
renderActiveForm();
|
|
153
|
+
}),
|
|
154
|
+
client.on("form:update", () => renderActiveForm()),
|
|
155
|
+
client.on("form:validation", ({ errors }) => {
|
|
156
|
+
setFormError(refs, "");
|
|
157
|
+
setFieldErrors(refs, errors);
|
|
158
|
+
}),
|
|
159
|
+
client.on("form:submitting", () => {
|
|
160
|
+
setFormError(refs, "");
|
|
161
|
+
setFormSuccess(refs, "");
|
|
162
|
+
clearFieldErrors(refs);
|
|
163
|
+
setFormBusy(refs, true);
|
|
164
|
+
}),
|
|
165
|
+
client.on("form:submitted", ({ successMessage }) => {
|
|
166
|
+
setFormSuccess(refs, successMessage);
|
|
167
|
+
setFormBusy(refs, false);
|
|
168
|
+
}),
|
|
169
|
+
client.on("form:error", ({ message }) => {
|
|
170
|
+
setFormError(refs, message);
|
|
171
|
+
setFormBusy(refs, false);
|
|
172
|
+
}),
|
|
173
|
+
client.on("form:close", () => {
|
|
174
|
+
formOpen = false;
|
|
175
|
+
setFormCallControlsVisible(refs, false);
|
|
176
|
+
setFormTranscriptVisible(refs, false);
|
|
177
|
+
setScreen(refs, returnTo());
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Form buttons → client (capture on-screen values before submit/step).
|
|
182
|
+
const captureValues = () => client.updateFormValues(readFormValues(refs));
|
|
183
|
+
refs.formBackBtn.addEventListener("click", () => client.closeForm());
|
|
184
|
+
refs.formCancelBtn.addEventListener("click", () => client.closeForm());
|
|
185
|
+
refs.formStepBackBtn.addEventListener("click", () => {
|
|
186
|
+
captureValues();
|
|
187
|
+
client.stepForm("back");
|
|
188
|
+
});
|
|
189
|
+
refs.formSubmitBtn.addEventListener("click", () => {
|
|
190
|
+
captureValues();
|
|
191
|
+
client.submitForm();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ── audio drawer + meters + stats (ported from the widget) ──
|
|
195
|
+
const refreshAudioDevices = async () => {
|
|
196
|
+
try {
|
|
197
|
+
const { inputs, outputs } = await client.enumerateAudioDevices();
|
|
198
|
+
const state = client.getAudioState();
|
|
199
|
+
populateAudioDeviceSelect(
|
|
200
|
+
refs.audioMicSelect,
|
|
201
|
+
inputs,
|
|
202
|
+
state.prefs.micDeviceId,
|
|
203
|
+
"System default",
|
|
204
|
+
);
|
|
205
|
+
populateAudioDeviceSelect(
|
|
206
|
+
refs.audioSpeakerSelect,
|
|
207
|
+
outputs,
|
|
208
|
+
state.prefs.speakerDeviceId,
|
|
209
|
+
"System default",
|
|
210
|
+
);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
// eslint-disable-next-line no-console
|
|
213
|
+
console.warn("[voice-agent] device enumeration failed:", err);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const stopMeter = () => {
|
|
218
|
+
if (audioMeterRaf !== null) {
|
|
219
|
+
cancelAnimationFrame(audioMeterRaf);
|
|
220
|
+
audioMeterRaf = null;
|
|
221
|
+
}
|
|
222
|
+
if (audioMeterCtx) {
|
|
223
|
+
void audioMeterCtx.close().catch(() => undefined);
|
|
224
|
+
audioMeterCtx = null;
|
|
225
|
+
}
|
|
226
|
+
setAudioMeterLevel(refs, 0);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const startMeter = async () => {
|
|
230
|
+
stopMeter();
|
|
231
|
+
if (!client.isActive()) return;
|
|
232
|
+
try {
|
|
233
|
+
const state = client.getAudioState();
|
|
234
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
235
|
+
audio: state.prefs.micDeviceId
|
|
236
|
+
? { deviceId: { exact: state.prefs.micDeviceId } }
|
|
237
|
+
: true,
|
|
238
|
+
video: false,
|
|
239
|
+
});
|
|
240
|
+
const ctx = new (window.AudioContext ||
|
|
241
|
+
(window as unknown as { webkitAudioContext: typeof AudioContext })
|
|
242
|
+
.webkitAudioContext)();
|
|
243
|
+
audioMeterCtx = ctx;
|
|
244
|
+
const source = ctx.createMediaStreamSource(stream);
|
|
245
|
+
const analyser = ctx.createAnalyser();
|
|
246
|
+
analyser.fftSize = 1024;
|
|
247
|
+
source.connect(analyser);
|
|
248
|
+
const buffer = new Float32Array(analyser.fftSize);
|
|
249
|
+
const tick = () => {
|
|
250
|
+
if (!audioMeterCtx) return;
|
|
251
|
+
analyser.getFloatTimeDomainData(buffer);
|
|
252
|
+
let sum = 0;
|
|
253
|
+
for (let i = 0; i < buffer.length; i++) sum += buffer[i] * buffer[i];
|
|
254
|
+
const rms = Math.sqrt(sum / buffer.length);
|
|
255
|
+
const db = 20 * Math.log10(Math.max(rms, 1e-6));
|
|
256
|
+
const norm = Math.max(0, Math.min(1, (db + 50) / 50));
|
|
257
|
+
setAudioMeterLevel(refs, norm);
|
|
258
|
+
audioMeterRaf = requestAnimationFrame(tick);
|
|
259
|
+
};
|
|
260
|
+
audioMeterRaf = requestAnimationFrame(tick);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
// eslint-disable-next-line no-console
|
|
263
|
+
console.warn("[voice-agent] mic-level meter unavailable:", err);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const stopAudioStatsPoll = () => {
|
|
268
|
+
if (audioStatsTimer !== null) {
|
|
269
|
+
window.clearInterval(audioStatsTimer);
|
|
270
|
+
audioStatsTimer = null;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const startAudioStatsPoll = () => {
|
|
275
|
+
stopAudioStatsPoll();
|
|
276
|
+
const tick = async () => {
|
|
277
|
+
const stats = await client.getAudioStats();
|
|
278
|
+
if (!stats) {
|
|
279
|
+
refs.audioDiagLoss.textContent = "—";
|
|
280
|
+
refs.audioDiagJitter.textContent = "—";
|
|
281
|
+
refs.audioDiagRtt.textContent = "—";
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
refs.audioDiagLoss.textContent = String(stats.packetsLost);
|
|
285
|
+
refs.audioDiagJitter.textContent = `${stats.jitter.toFixed(1)} ms`;
|
|
286
|
+
refs.audioDiagRtt.textContent = stats.roundTripTime
|
|
287
|
+
? `${stats.roundTripTime.toFixed(0)} ms`
|
|
288
|
+
: "—";
|
|
289
|
+
};
|
|
290
|
+
void tick();
|
|
291
|
+
audioStatsTimer = trackedSetInterval(() => void tick(), 2000);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const openAudioDrawer = async () => {
|
|
295
|
+
audioDrawerOpen = true;
|
|
296
|
+
setAudioDrawerOpen(refs, true);
|
|
297
|
+
syncDrawerToState(refs, client.getAudioState(), audioCaps);
|
|
298
|
+
await refreshAudioDevices();
|
|
299
|
+
await startMeter();
|
|
300
|
+
startAudioStatsPoll();
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const closeAudioDrawer = () => {
|
|
304
|
+
audioDrawerOpen = false;
|
|
305
|
+
setAudioDrawerOpen(refs, false);
|
|
306
|
+
stopMeter();
|
|
307
|
+
stopAudioStatsPoll();
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// Auto-close the drawer when the call ends.
|
|
311
|
+
trackedSetInterval(() => {
|
|
312
|
+
if (audioDrawerOpen && !client.isActive()) closeAudioDrawer();
|
|
313
|
+
}, 1000);
|
|
314
|
+
|
|
315
|
+
// ── top-level DOM wiring ──
|
|
316
|
+
refs.fab.addEventListener("click", () => {
|
|
317
|
+
if (!panelOpen) client.trackEvent("bubble_clicked");
|
|
318
|
+
togglePanel(!panelOpen);
|
|
319
|
+
});
|
|
320
|
+
refs.closeBtn.addEventListener("click", () => {
|
|
321
|
+
if (client.isActive()) void client.end();
|
|
322
|
+
client.closeForm();
|
|
323
|
+
togglePanel(false);
|
|
324
|
+
});
|
|
325
|
+
if (opts.openChat || opts.inline) ensurePanelOpen();
|
|
326
|
+
|
|
327
|
+
refs.startBtn.addEventListener("click", () => {
|
|
328
|
+
const termsUrl = refs.appearance.terms_url;
|
|
329
|
+
if (termsUrl) saveConsent(client.agentSlug || "default", termsUrl);
|
|
330
|
+
void client.start();
|
|
331
|
+
});
|
|
332
|
+
refs.endBtn.addEventListener("click", () => void client.end());
|
|
333
|
+
refs.muteBtn.addEventListener("click", () => void client.toggleMute());
|
|
334
|
+
refs.formMuteBtn.addEventListener("click", () => void client.toggleMute());
|
|
335
|
+
refs.formEndBtn.addEventListener("click", () => {
|
|
336
|
+
client.closeForm();
|
|
337
|
+
void client.end();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
refs.settingsBtn.addEventListener("click", () => {
|
|
341
|
+
if (!client.isActive()) return;
|
|
342
|
+
if (audioDrawerOpen) closeAudioDrawer();
|
|
343
|
+
else void openAudioDrawer();
|
|
344
|
+
});
|
|
345
|
+
refs.audioDrawerClose.addEventListener("click", closeAudioDrawer);
|
|
346
|
+
|
|
347
|
+
refs.audioMicSelect.addEventListener("change", () => {
|
|
348
|
+
void client.updateAudioSettings({ micDeviceId: refs.audioMicSelect.value });
|
|
349
|
+
void startMeter();
|
|
350
|
+
});
|
|
351
|
+
refs.audioSpeakerSelect.addEventListener("change", () => {
|
|
352
|
+
void client.updateAudioSettings({
|
|
353
|
+
speakerDeviceId: refs.audioSpeakerSelect.value,
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
refs.audioVolume.addEventListener("input", () => {
|
|
357
|
+
const value = Number(refs.audioVolume.value);
|
|
358
|
+
refs.audioVolumeValue.textContent = `${value}%`;
|
|
359
|
+
void client.updateAudioSettings({ outputVolume: value });
|
|
360
|
+
});
|
|
361
|
+
refs.audioNcEngine.addEventListener("change", () => {
|
|
362
|
+
const engine = refs.audioNcEngine.value as "off" | "krisp" | "deepfilter";
|
|
363
|
+
void client.updateAudioSettings({ noiseFilter: engine });
|
|
364
|
+
refs.audioDfStrengthRow.hidden = engine !== "deepfilter";
|
|
365
|
+
});
|
|
366
|
+
refs.audioDfStrength.addEventListener("input", () => {
|
|
367
|
+
const value = Number(refs.audioDfStrength.value);
|
|
368
|
+
refs.audioDfStrengthValue.textContent = String(value);
|
|
369
|
+
void client.updateAudioSettings({ deepFilterStrength: value });
|
|
370
|
+
});
|
|
371
|
+
refs.audioTogAec.addEventListener("change", () =>
|
|
372
|
+
void client.updateAudioSettings({ echoCancellation: refs.audioTogAec.checked }),
|
|
373
|
+
);
|
|
374
|
+
refs.audioTogNs.addEventListener("change", () =>
|
|
375
|
+
void client.updateAudioSettings({ noiseSuppression: refs.audioTogNs.checked }),
|
|
376
|
+
);
|
|
377
|
+
refs.audioTogAgc.addEventListener("change", () =>
|
|
378
|
+
void client.updateAudioSettings({ autoGainControl: refs.audioTogAgc.checked }),
|
|
379
|
+
);
|
|
380
|
+
refs.audioTogVi.addEventListener("change", () =>
|
|
381
|
+
void client.updateAudioSettings({ voiceIsolation: refs.audioTogVi.checked }),
|
|
382
|
+
);
|
|
383
|
+
refs.audioTogHp.addEventListener("change", () =>
|
|
384
|
+
void client.updateAudioSettings({ headphonesMode: refs.audioTogHp.checked }),
|
|
385
|
+
);
|
|
386
|
+
refs.audioTogTranscription.addEventListener("change", () =>
|
|
387
|
+
void client.updateAudioSettings({
|
|
388
|
+
transcriptionEnabled: refs.audioTogTranscription.checked,
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
391
|
+
refs.audioTogTextInput.addEventListener("change", () =>
|
|
392
|
+
void client.updateAudioSettings({
|
|
393
|
+
textInputEnabled: refs.audioTogTextInput.checked,
|
|
394
|
+
}),
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
// Gear / text-input visibility tied to call state + prefs.
|
|
398
|
+
trackedSetInterval(() => {
|
|
399
|
+
const active = client.isActive();
|
|
400
|
+
refs.settingsBtn.hidden = !active || !refs.appearance.show_audio_settings;
|
|
401
|
+
const showText = active && client.getAudioState().prefs.textInputEnabled;
|
|
402
|
+
refs.textInputRow.hidden = !showText;
|
|
403
|
+
refs.textSendBtn.disabled = !showText;
|
|
404
|
+
}, 500);
|
|
405
|
+
|
|
406
|
+
refs.textInput.addEventListener("input", () => {
|
|
407
|
+
refs.textInput.style.height = "auto";
|
|
408
|
+
refs.textInput.style.height = `${refs.textInput.scrollHeight}px`;
|
|
409
|
+
});
|
|
410
|
+
const sendTextMessage = () => {
|
|
411
|
+
const text = refs.textInput.value.trim();
|
|
412
|
+
if (!text || !client.isActive()) return;
|
|
413
|
+
refs.textInput.value = "";
|
|
414
|
+
refs.textInput.style.height = "auto";
|
|
415
|
+
void client.sendText(text);
|
|
416
|
+
};
|
|
417
|
+
refs.textSendBtn.addEventListener("click", sendTextMessage);
|
|
418
|
+
refs.textInput.addEventListener("keydown", (e) => {
|
|
419
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
420
|
+
e.preventDefault();
|
|
421
|
+
sendTextMessage();
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const destroy = () => {
|
|
426
|
+
for (const u of unsubs.splice(0)) {
|
|
427
|
+
try {
|
|
428
|
+
u();
|
|
429
|
+
} catch {
|
|
430
|
+
/* best-effort */
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
for (const id of timers.splice(0)) window.clearInterval(id);
|
|
434
|
+
stopMeter();
|
|
435
|
+
stopAudioStatsPoll();
|
|
436
|
+
refs.host.remove();
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
return { destroy, refs };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ── audio drawer sync (ported from widget.ts) ──
|
|
443
|
+
function syncDrawerToState(
|
|
444
|
+
refs: UIRefs,
|
|
445
|
+
state: AudioStateSnapshot,
|
|
446
|
+
caps: { setSinkIdSupported: boolean; voiceIsolationSupported: boolean },
|
|
447
|
+
) {
|
|
448
|
+
const { prefs, applied, noiseFilter } = state;
|
|
449
|
+
|
|
450
|
+
refs.audioNcEngine.value = prefs.noiseFilter;
|
|
451
|
+
refs.audioDfStrength.value = String(prefs.deepFilterStrength);
|
|
452
|
+
refs.audioDfStrengthValue.textContent = String(prefs.deepFilterStrength);
|
|
453
|
+
refs.audioDfStrengthRow.hidden = prefs.noiseFilter !== "deepfilter";
|
|
454
|
+
|
|
455
|
+
refs.audioTogAec.checked = prefs.echoCancellation;
|
|
456
|
+
refs.audioTogNs.checked = prefs.noiseSuppression;
|
|
457
|
+
refs.audioTogAgc.checked = prefs.autoGainControl;
|
|
458
|
+
refs.audioTogVi.checked = prefs.voiceIsolation;
|
|
459
|
+
refs.audioTogHp.checked = prefs.headphonesMode;
|
|
460
|
+
refs.audioTogTranscription.checked = prefs.transcriptionEnabled;
|
|
461
|
+
refs.audioTogTextInput.checked = prefs.textInputEnabled;
|
|
462
|
+
refs.transcript.style.display = prefs.transcriptionEnabled ? "flex" : "none";
|
|
463
|
+
if (prefs.transcriptionEnabled) {
|
|
464
|
+
refs.panel.classList.add("with-transcript");
|
|
465
|
+
} else {
|
|
466
|
+
refs.panel.classList.remove("with-transcript");
|
|
467
|
+
}
|
|
468
|
+
refs.audioVolume.value = String(prefs.outputVolume);
|
|
469
|
+
refs.audioVolumeValue.textContent = `${prefs.outputVolume}%`;
|
|
470
|
+
|
|
471
|
+
refs.audioTogViRow.style.display = caps.voiceIsolationSupported ? "" : "none";
|
|
472
|
+
refs.audioSpeakerRow.style.display = caps.setSinkIdSupported ? "" : "none";
|
|
473
|
+
|
|
474
|
+
refs.audioDiagEngine.textContent = formatEngineStatus(noiseFilter);
|
|
475
|
+
refs.audioDiagAec.textContent = boolLabel(applied.echoCancellation);
|
|
476
|
+
refs.audioDiagNs.textContent = boolLabel(applied.noiseSuppression);
|
|
477
|
+
refs.audioDiagAgc.textContent = boolLabel(applied.autoGainControl);
|
|
478
|
+
refs.audioDiagVi.textContent =
|
|
479
|
+
applied.voiceIsolation === undefined
|
|
480
|
+
? "Unsupported"
|
|
481
|
+
: applied.voiceIsolation
|
|
482
|
+
? "On"
|
|
483
|
+
: "Off";
|
|
484
|
+
refs.audioDiagSr.textContent =
|
|
485
|
+
applied.sampleRate !== undefined ? `${applied.sampleRate} Hz` : "—";
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function boolLabel(v: boolean | undefined): string {
|
|
489
|
+
if (v === undefined) return "—";
|
|
490
|
+
return v ? "On" : "Off";
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function formatEngineStatus(nf: AudioStateSnapshot["noiseFilter"]): string {
|
|
494
|
+
const engineLabel =
|
|
495
|
+
nf.engine === "krisp"
|
|
496
|
+
? "Krisp"
|
|
497
|
+
: nf.engine === "deepfilter"
|
|
498
|
+
? "DeepFilterNet3"
|
|
499
|
+
: "Off";
|
|
500
|
+
if (nf.engine === "off") return "Off";
|
|
501
|
+
if (nf.status === "active") return `${engineLabel} (active)`;
|
|
502
|
+
if (nf.status === "unsupported") return `${engineLabel} (unsupported)`;
|
|
503
|
+
if (nf.status === "failed") return `${engineLabel} (failed)`;
|
|
504
|
+
return engineLabel;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export type { UIRefs } from "./ui";
|