@hubtalk-ai/web-sdk 0.2.0 → 0.3.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/dist/widget.js ADDED
@@ -0,0 +1,787 @@
1
+ import { ConnectionError as nt, ConnectionErrorReason as Y, MediaDeviceFailure as F, Room as it, RoomEvent as b, ConnectionState as q, ParticipantKind as ot, Track as G, DisconnectReason as X } from "livekit-client";
2
+ class st {
3
+ constructor(t) {
4
+ if (t) {
5
+ this.element = t, this.owned = !1;
6
+ return;
7
+ }
8
+ this.element = document.createElement("div"), this.element.style.display = "none", document.body.appendChild(this.element), this.owned = !0;
9
+ }
10
+ attach(t) {
11
+ this.element.appendChild(t.attach());
12
+ }
13
+ detach(t) {
14
+ t.detach().forEach((e) => e.remove());
15
+ }
16
+ clear() {
17
+ this.element.replaceChildren();
18
+ }
19
+ destroy() {
20
+ this.clear(), this.owned && this.element.remove();
21
+ }
22
+ }
23
+ class at {
24
+ constructor() {
25
+ this.listeners = /* @__PURE__ */ new Map();
26
+ }
27
+ on(t, e) {
28
+ let n = this.listeners.get(t);
29
+ n || (n = /* @__PURE__ */ new Set(), this.listeners.set(t, n)), n.add(e);
30
+ }
31
+ off(t, e) {
32
+ var n;
33
+ (n = this.listeners.get(t)) == null || n.delete(e);
34
+ }
35
+ once(t, e) {
36
+ const n = (i) => {
37
+ this.off(t, n), e(i);
38
+ };
39
+ this.on(t, n);
40
+ }
41
+ emit(t, e) {
42
+ const n = this.listeners.get(t);
43
+ if (n)
44
+ for (const i of [...n])
45
+ i(e);
46
+ }
47
+ clear() {
48
+ this.listeners.clear();
49
+ }
50
+ }
51
+ const rt = {
52
+ AUDIO_PLAYBACK_BLOCKED: "Tap Enable sound to hear the agent",
53
+ INVALID_PARAMS: "accessToken и url обязательны",
54
+ WEBRTC_UNSUPPORTED: "браузер/контекст не поддерживает WebRTC (нужен HTTPS)",
55
+ MICROPHONE_PERMISSION_DENIED: "доступ к микрофону запрещён пользователем",
56
+ MICROPHONE_UNAVAILABLE: "микрофон недоступен или занят другим приложением",
57
+ TOKEN_EXPIRED: "срок действия токена истёк",
58
+ TOKEN_INVALID: "сервер отклонил токен",
59
+ CONNECTION_FAILED: "не удалось установить соединение",
60
+ CONNECTION_LOST: "соединение потеряно, реконнект не восстановил его",
61
+ ALREADY_IN_CALL: "звонок уже активен",
62
+ NOT_IN_CALL: "нет активного звонка",
63
+ SERVER_ERROR: "внутренняя ошибка сервиса сигналинга"
64
+ };
65
+ class ct extends Error {
66
+ constructor(t, e, n) {
67
+ super(rt[t]), this.name = "VoiceCallError", this.code = t, this.state = e, this.cause = n;
68
+ }
69
+ }
70
+ function dt(a) {
71
+ if (a instanceof nt)
72
+ switch (a.reason) {
73
+ case Y.NotAllowed:
74
+ return "TOKEN_INVALID";
75
+ case Y.ServerUnreachable:
76
+ case Y.Timeout:
77
+ case Y.WebSocket:
78
+ return "CONNECTION_FAILED";
79
+ default:
80
+ return "SERVER_ERROR";
81
+ }
82
+ return "SERVER_ERROR";
83
+ }
84
+ function lt(a) {
85
+ return F.getFailure(a) === F.PermissionDenied ? "MICROPHONE_PERMISSION_DENIED" : "MICROPHONE_UNAVAILABLE";
86
+ }
87
+ function ht(a) {
88
+ const t = F.getFailure(a);
89
+ return t === F.PermissionDenied ? "MICROPHONE_PERMISSION_DENIED" : t === F.NotFound || t === F.DeviceInUse || a instanceof DOMException && [
90
+ "AbortError",
91
+ "OverconstrainedError",
92
+ "SecurityError",
93
+ "InvalidStateError",
94
+ "NotSupportedError"
95
+ ].includes(a.name) ? "MICROPHONE_UNAVAILABLE" : "SERVER_ERROR";
96
+ }
97
+ const Q = "[redacted]", ut = /^[\w-]+\.[\w-]+\.[\w-]+$/, pt = /token/i;
98
+ function j(a) {
99
+ if (typeof a == "string")
100
+ return ut.test(a) ? Q : a;
101
+ if (Array.isArray(a))
102
+ return a.map(j);
103
+ if (a && typeof a == "object") {
104
+ const t = {};
105
+ for (const [e, n] of Object.entries(a))
106
+ t[e] = pt.test(e) ? Q : j(n);
107
+ return t;
108
+ }
109
+ return a;
110
+ }
111
+ function ft(a, t) {
112
+ if (a === "silent")
113
+ return { error: () => {
114
+ }, debug: () => {
115
+ } };
116
+ const e = t ?? {
117
+ error: (n, ...i) => console.error(n, ...i),
118
+ debug: (n, ...i) => console.debug(n, ...i)
119
+ };
120
+ return {
121
+ error: (n, ...i) => e.error(n, ...i.map(j)),
122
+ debug: (n, ...i) => {
123
+ a === "debug" && e.debug(n, ...i.map(j));
124
+ }
125
+ };
126
+ }
127
+ function gt(a = !0) {
128
+ var t;
129
+ return typeof window > "u" || !window.isSecureContext || typeof RTCPeerConnection > "u" ? !1 : a ? typeof navigator < "u" && !!((t = navigator.mediaDevices) != null && t.getUserMedia) : !0;
130
+ }
131
+ function mt(a) {
132
+ const t = a.split(".")[1];
133
+ if (!t) return null;
134
+ try {
135
+ const e = t.replace(/-/g, "+").replace(/_/g, "/"), n = decodeURIComponent(
136
+ Array.from(atob(e)).map((o) => "%" + o.charCodeAt(0).toString(16).padStart(2, "0")).join("")
137
+ ), i = JSON.parse(n);
138
+ return typeof i.exp == "number" ? i.exp : null;
139
+ } catch {
140
+ return null;
141
+ }
142
+ }
143
+ const Et = "lk.agent.state", bt = /* @__PURE__ */ new Set(["initializing", "listening", "thinking", "speaking"]), tt = "lk.publish_on_behalf", yt = /* @__PURE__ */ new Set([
144
+ X.ROOM_DELETED,
145
+ X.PARTICIPANT_REMOVED,
146
+ X.SERVER_SHUTDOWN,
147
+ X.ROOM_CLOSED
148
+ ]), J = /* @__PURE__ */ new Set(["connecting", "connected", "reconnecting"]), et = "lk.transcription", Ct = "lk.chat", St = "lk.segment_id", At = "lk.transcription_final", Tt = 1e3;
149
+ class wt {
150
+ constructor(t = {}) {
151
+ this.emitter = new at(), this.connectionState = "idle", this.agentState = "unknown", this.room = null, this.callId = null, this.playAudio = !0, this.agentVideoTrack = null, this.agentVideoElements = /* @__PURE__ */ new Set(), this.microphoneEnabled = !1, this.microphoneOperation = Promise.resolve(), this.stoppingLocally = !1, this.reconnectSeen = !1, this.destroyed = !1, this.roomCleanup = [], this.audio = new st(t.audioContainer), this.log = ft(t.logLevel ?? "error", t.logger), this.connectTimeoutMs = t.connectTimeoutMs;
152
+ }
153
+ getState() {
154
+ return this.connectionState;
155
+ }
156
+ getAgentState() {
157
+ return this.agentState;
158
+ }
159
+ getCallId() {
160
+ return this.callId;
161
+ }
162
+ on(t, e) {
163
+ this.emitter.on(t, e);
164
+ }
165
+ off(t, e) {
166
+ this.emitter.off(t, e);
167
+ }
168
+ once(t, e) {
169
+ this.emitter.once(t, e);
170
+ }
171
+ /**
172
+ * `options.audio === false` — chat-only call: микрофон не публикуется,
173
+ * аудио агента не подписывается (см. connectRoom). Текст в обе стороны
174
+ * идёт как обычно — `sendText` и событие `transcript`.
175
+ *
176
+ * Аудио агента при этом ПОДПИСЫВАЕТСЯ и просто не проигрывается — это не
177
+ * расточительность, а единственный работающий вариант с livekit-agents
178
+ * 1.6.4: `_ParticipantAudioOutput.capture_frame` ждёт `_subscribed_fut`
179
+ * (room_io/_output.py:92), который закрывается только по
180
+ * `publication.wait_for_subscription()`. Без подписчика первый же кадр
181
+ * TTS блокируется навсегда, а вместе с ним умирает и текст: транскрипт
182
+ * агента идёт через TranscriptSynchronizer, привязанный к тому же
183
+ * аудио-выходу (room_io.py:163-166). Наблюдалось как «агент мигает
184
+ * thinking -> listening и молчит», при том что эхо реплик посетителя
185
+ * приходило — у него своя, несинхронизированная ветка вывода.
186
+ *
187
+ * ponytail: платит трафиком и TTS за то, что никто не слышит. Убирается
188
+ * только на стороне платформы — RoomOutputOptions(audio_enabled=False) по
189
+ * флагу звонка, тогда текст пойдёт сразу и синтеза не будет вовсе.
190
+ */
191
+ async startCall(t, e = {}) {
192
+ if (this.destroyed) throw new Error("VoiceClient has been destroyed");
193
+ if (J.has(this.connectionState))
194
+ throw this.raiseError("ALREADY_IN_CALL", this.connectionState);
195
+ const n = e.audio !== !1;
196
+ this.playAudio = n;
197
+ const { accessToken: i, url: o } = this.validateStartParams(t, n);
198
+ this.stoppingLocally = !1, this.reconnectSeen = !1, this.setConnectionState("connecting");
199
+ const r = new it();
200
+ if (this.room = r, this.wireRoomEvents(r), await this.connectRoom(r, o, i), this.stoppingLocally || this.destroyed || this.room !== r) {
201
+ await this.abandonConnectingCall(r);
202
+ return;
203
+ }
204
+ if (n && await this.publishMicrophone(r), this.stoppingLocally || this.destroyed || this.room !== r) {
205
+ await this.abandonConnectingCall(r);
206
+ return;
207
+ }
208
+ if (n)
209
+ try {
210
+ await r.startAudio();
211
+ } catch {
212
+ this.room === r && !this.destroyed && this.emitter.emit("audioPlaybackBlocked", {});
213
+ }
214
+ if (this.stoppingLocally || this.destroyed || this.room !== r) {
215
+ await this.abandonConnectingCall(r);
216
+ return;
217
+ }
218
+ this.callId = r.name, this.setConnectionState("connected"), this.syncAgentState(r), this.emitter.emit("callStarted", { callId: this.callId });
219
+ }
220
+ validateStartParams(t, e) {
221
+ const n = t.accessToken ?? t.access_token, i = t.url;
222
+ if (!n || !i)
223
+ throw this.raiseError("INVALID_PARAMS", "error");
224
+ if (!gt(e))
225
+ throw this.raiseError("WEBRTC_UNSUPPORTED", "error");
226
+ const o = mt(n);
227
+ if (o !== null && o * 1e3 <= Date.now())
228
+ throw this.raiseError("TOKEN_EXPIRED", "error");
229
+ return { accessToken: n, url: i };
230
+ }
231
+ async connectRoom(t, e, n) {
232
+ try {
233
+ await t.connect(
234
+ e,
235
+ n,
236
+ this.connectTimeoutMs === void 0 ? void 0 : {
237
+ peerConnectionTimeout: this.connectTimeoutMs,
238
+ websocketTimeout: this.connectTimeoutMs
239
+ }
240
+ );
241
+ } catch (i) {
242
+ if (this.destroyed || this.room !== t) return;
243
+ throw this.teardownRoom(), this.raiseError(dt(i), "error", i);
244
+ }
245
+ }
246
+ // stopCall() пришёл во время connecting (AC4/§1.2) — сворачиваем попытку
247
+ // без callStarted/error вместо продолжения к публикации микрофона
248
+ async abandonConnectingCall(t) {
249
+ await t.disconnect().catch(() => {
250
+ }), this.room === t && this.teardownRoom(), !this.destroyed && !this.room && this.setConnectionState("disconnected");
251
+ }
252
+ async publishMicrophone(t) {
253
+ try {
254
+ await t.localParticipant.setMicrophoneEnabled(!0), this.room === t && !this.destroyed && this.confirmMicrophone(t.localParticipant.isMicrophoneEnabled);
255
+ } catch (e) {
256
+ if (this.destroyed || this.room !== t) return;
257
+ const n = lt(e);
258
+ throw await t.disconnect().catch(() => {
259
+ }), this.teardownRoom(), this.raiseError(n, "error", e);
260
+ }
261
+ }
262
+ isMicrophoneEnabled() {
263
+ return this.microphoneEnabled;
264
+ }
265
+ async setMicrophoneEnabled(t) {
266
+ const e = this.room;
267
+ if (!e || !this.playAudio || this.connectionState !== "connected")
268
+ throw this.raiseError("NOT_IN_CALL", this.connectionState);
269
+ const n = this.microphoneOperation.then(async () => {
270
+ if (!e || this.room !== e || !this.playAudio || this.connectionState !== "connected")
271
+ throw this.raiseError("NOT_IN_CALL", this.connectionState);
272
+ try {
273
+ await e.localParticipant.setMicrophoneEnabled(t);
274
+ } catch (i) {
275
+ throw this.raiseError(ht(i), this.connectionState, i);
276
+ }
277
+ this.room === e && !this.destroyed && this.confirmMicrophone(e.localParticipant.isMicrophoneEnabled);
278
+ });
279
+ return this.microphoneOperation = n.catch(() => {
280
+ }), n;
281
+ }
282
+ async resumeAudio() {
283
+ const t = this.room;
284
+ if (!t || !this.playAudio || this.connectionState !== "connected")
285
+ throw this.raiseError("NOT_IN_CALL", this.connectionState);
286
+ try {
287
+ await t.startAudio();
288
+ } catch (e) {
289
+ throw this.room === t && !this.destroyed && this.emitter.emit("audioPlaybackBlocked", {}), this.raiseError("AUDIO_PLAYBACK_BLOCKED", this.connectionState, e);
290
+ }
291
+ }
292
+ /**
293
+ * VPT-857 §1.3: attach the avatar worker's current and any later video
294
+ * track to `element`; the returned function detaches. Outside a call it
295
+ * attaches nothing and returns a no-op detach.
296
+ */
297
+ attachAgentVideo(t) {
298
+ return !this.room || this.destroyed ? () => {
299
+ } : (this.agentVideoElements.add(t), this.agentVideoTrack && this.agentVideoTrack.attach(t), () => {
300
+ var e;
301
+ this.agentVideoElements.delete(t), (e = this.agentVideoTrack) == null || e.detach(t);
302
+ });
303
+ }
304
+ confirmMicrophone(t) {
305
+ this.microphoneEnabled !== t && (this.microphoneEnabled = t, this.emitter.emit("microphoneStateChanged", { enabled: t }));
306
+ }
307
+ /**
308
+ * Отправляет текстовую реплику посетителя в разговор (VPT-108 §2.1).
309
+ * Своё сообщение в чат SDK не кладёт — его вернёт агент событием
310
+ * `transcript` с role: 'user' (§2.4): один источник правды и один порядок.
311
+ */
312
+ async sendText(t) {
313
+ const e = this.room;
314
+ if (!e || !J.has(this.connectionState))
315
+ throw this.raiseError("NOT_IN_CALL", this.connectionState);
316
+ const n = t.trim().slice(0, Tt);
317
+ if (n)
318
+ try {
319
+ await e.localParticipant.sendText(n, { topic: Ct });
320
+ } catch (i) {
321
+ throw this.raiseError("SERVER_ERROR", this.connectionState, i);
322
+ }
323
+ }
324
+ async stopCall() {
325
+ this.stoppingLocally = !0;
326
+ const t = this.room;
327
+ !t || !J.has(this.connectionState) || this.connectionState !== "connecting" && await t.disconnect().catch((e) => this.log.debug("[voice-client] disconnect", e));
328
+ }
329
+ destroy() {
330
+ this.stopCall(), this.playAudio = !0, this.destroyed = !0, this.teardownRoom(), this.emitter.clear(), this.audio.destroy(), this.agentVideoElements.clear(), this.connectionState = "idle", this.agentState = "unknown", this.callId = null;
331
+ }
332
+ wireRoomEvents(t) {
333
+ const e = (d, y, p) => {
334
+ this.log.debug("[voice-client] remote track subscribed", {
335
+ participant: p.identity,
336
+ participantID: p.sid,
337
+ trackID: d.sid,
338
+ streamTrackID: d.mediaStreamTrack.id,
339
+ kind: d.kind,
340
+ source: y.source,
341
+ muted: y.isMuted
342
+ }), d.kind === G.Kind.Audio && this.playAudio && this.audio.attach(d), d.kind === G.Kind.Video && this.syncAgentVideo(t);
343
+ }, n = (d, y, p) => {
344
+ this.log.debug("[voice-client] remote track unsubscribed", {
345
+ participant: p.identity,
346
+ participantID: p.sid,
347
+ trackID: d.sid,
348
+ kind: d.kind,
349
+ source: y.source
350
+ }), d.kind === G.Kind.Audio && this.audio.detach(d), d.kind === G.Kind.Video && this.syncAgentVideo(t);
351
+ }, i = (d, y, p) => {
352
+ this.log.debug("[voice-client] remote track subscription failed", {
353
+ participant: y.identity,
354
+ participantID: y.sid,
355
+ trackID: d,
356
+ reason: p
357
+ });
358
+ }, o = (d) => {
359
+ this.log.debug("[voice-client] remote participant connected", {
360
+ room: t.name,
361
+ participant: d.identity,
362
+ participantID: d.sid
363
+ }), !this.destroyed && this.syncAgentState(t);
364
+ }, r = () => {
365
+ this.destroyed || (this.syncAgentState(t), this.syncAgentVideo(t));
366
+ }, g = (d) => {
367
+ this.destroyed || this.applyLkConnectionState(d);
368
+ }, m = (d) => {
369
+ this.log.debug("[voice-client] room disconnected", {
370
+ room: t.name,
371
+ reason: d,
372
+ stoppingLocally: this.stoppingLocally
373
+ }), !this.destroyed && this.handleDisconnected(d);
374
+ };
375
+ t.registerTextStreamHandler(et, (d, y) => {
376
+ if (this.destroyed) return;
377
+ const p = y.identity === t.localParticipant.identity ? "user" : "agent";
378
+ this.readTranscriptSegment(d, p, t);
379
+ });
380
+ const S = () => {
381
+ this.playAudio && !t.canPlaybackAudio && !this.destroyed && this.emitter.emit("audioPlaybackBlocked", {});
382
+ };
383
+ t.on(b.AudioPlaybackStatusChanged, S), t.on(b.TrackSubscribed, e), t.on(b.TrackUnsubscribed, n), t.on(b.TrackSubscriptionFailed, i), t.on(b.ParticipantAttributesChanged, r), t.on(b.ParticipantConnected, o), t.on(b.ParticipantDisconnected, r), t.on(b.ConnectionStateChanged, g), t.on(b.Disconnected, m), this.roomCleanup = [
384
+ () => t.off(b.AudioPlaybackStatusChanged, S),
385
+ () => t.unregisterTextStreamHandler(et),
386
+ () => t.off(b.TrackSubscribed, e),
387
+ () => t.off(b.TrackUnsubscribed, n),
388
+ () => t.off(b.TrackSubscriptionFailed, i),
389
+ () => t.off(b.ParticipantAttributesChanged, r),
390
+ () => t.off(b.ParticipantConnected, o),
391
+ () => t.off(b.ParticipantDisconnected, r),
392
+ () => t.off(b.ConnectionStateChanged, g),
393
+ () => t.off(b.Disconnected, m)
394
+ ];
395
+ }
396
+ /**
397
+ * Разбор одного сегмента транскрипта (VPT-108 §1.3). Роль определена
398
+ * вызывающим — сравнением identity потока с локальным участником.
399
+ *
400
+ * Одна реплика приходит несколькими потоками с общим `lk.segment_id`
401
+ * (тот же дефект-дубль, что закрывался в №50 §9), поэтому пузырь
402
+ * сшивается по нему, а не по потоку. Чанки копятся ВНУТРИ потока, а текст
403
+ * потока ЗАМЕНЯЕТ текст сегмента: у реплик посетителя каждый поток несёт
404
+ * полный текст на момент отправки, а не хвост (`_ParticipantTranscription
405
+ * Output(is_delta_stream=False)`), и дописывание удвоило бы реплику.
406
+ *
407
+ * `final` — из атрибута `lk.transcription_final`, а не из факта закрытия
408
+ * потока: дельта-поток закрывается штатно, будучи нефинальным. У потока
409
+ * агента `true` приходит в трейлере, и livekit-client домешивает атрибуты
410
+ * трейлера в `reader.info` перед закрытием — поэтому читаем атрибут ПОСЛЕ
411
+ * цикла чтения.
412
+ */
413
+ readTranscriptSegment(t, e, n) {
414
+ var o;
415
+ const i = ((o = t.info.attributes) == null ? void 0 : o[St]) || t.info.id;
416
+ (async () => {
417
+ var g;
418
+ let r = "";
419
+ try {
420
+ for await (const m of t) {
421
+ if (this.destroyed || this.room !== n) return;
422
+ r += m, this.emitter.emit("transcript", { role: e, text: r, final: !1, id: i });
423
+ }
424
+ } catch {
425
+ return;
426
+ }
427
+ !this.destroyed && this.room === n && ((g = t.info.attributes) == null ? void 0 : g[At]) === "true" && this.emitter.emit("transcript", { role: e, text: r, final: !0, id: i });
428
+ })();
429
+ }
430
+ applyLkConnectionState(t) {
431
+ var e;
432
+ t === q.Reconnecting || t === q.SignalReconnecting ? (this.reconnectSeen = !0, this.setConnectionState("reconnecting")) : t === q.Connected && this.connectionState === "reconnecting" && (this.confirmMicrophone(
433
+ this.playAudio && !!((e = this.room) != null && e.localParticipant.isMicrophoneEnabled)
434
+ ), this.setConnectionState("connected"));
435
+ }
436
+ handleDisconnected(t) {
437
+ const e = this.callId, n = this.reconnectSeen, i = this.stoppingLocally;
438
+ if (this.teardownRoom(), i) {
439
+ this.setConnectionState("disconnected"), this.emitter.emit("callEnded", { callId: e, reason: "local" });
440
+ return;
441
+ }
442
+ if (n) {
443
+ this.setConnectionState("disconnected"), this.raiseError("CONNECTION_LOST", "disconnected"), this.emitter.emit("callEnded", { callId: e, reason: "connection_lost" });
444
+ return;
445
+ }
446
+ const r = t !== void 0 && yt.has(t) ? "remote" : "error";
447
+ this.setConnectionState("disconnected"), this.emitter.emit("callEnded", { callId: e, reason: r });
448
+ }
449
+ teardownRoom() {
450
+ this.confirmMicrophone(!1), this.setAgentVideoTrack(null), this.roomCleanup.forEach((t) => t()), this.roomCleanup = [], this.audio.clear(), this.room = null, this.agentState = "unknown";
451
+ }
452
+ syncAgentState(t) {
453
+ var n, i;
454
+ let e = "unknown";
455
+ for (const o of t.remoteParticipants.values()) {
456
+ if ((n = o.attributes) != null && n[tt]) continue;
457
+ const r = (i = o.attributes) == null ? void 0 : i[Et];
458
+ if (r && bt.has(r)) {
459
+ e = r;
460
+ break;
461
+ }
462
+ }
463
+ this.setAgentState(e);
464
+ }
465
+ /** The subscribed video track of the avatar worker, if any (§1.3). */
466
+ findAgentVideoTrack(t) {
467
+ var e;
468
+ for (const n of t.remoteParticipants.values()) {
469
+ const i = (e = n.attributes) == null ? void 0 : e[tt];
470
+ if (!(!i || n.kind !== ot.AGENT) && t.remoteParticipants.has(i))
471
+ for (const o of n.videoTrackPublications.values()) {
472
+ const r = o.track;
473
+ if (r && o.isSubscribed) return r;
474
+ }
475
+ }
476
+ return null;
477
+ }
478
+ syncAgentVideo(t) {
479
+ this.destroyed || this.room !== t || this.setAgentVideoTrack(this.findAgentVideoTrack(t));
480
+ }
481
+ setAgentVideoTrack(t) {
482
+ const e = this.agentVideoTrack;
483
+ e !== t && (e && this.agentVideoElements.forEach((n) => e.detach(n)), this.agentVideoTrack = t, t && this.agentVideoElements.forEach((n) => t.attach(n)), !!e != !!t && this.emitter.emit("agentVideoChanged", { available: !!t }));
484
+ }
485
+ setConnectionState(t) {
486
+ const e = this.connectionState;
487
+ e !== t && (this.connectionState = t, this.emitter.emit("connectionStateChanged", { state: t, previous: e }));
488
+ }
489
+ setAgentState(t) {
490
+ const e = this.agentState;
491
+ e !== t && (this.agentState = t, this.emitter.emit("agentStateChanged", { state: t, previous: e }), t === "speaking" ? this.emitter.emit("agentSpeakingStarted", {}) : e === "speaking" && this.emitter.emit("agentSpeakingStopped", {}));
492
+ }
493
+ /** Переводит ось соединения (если нужно), логирует и эмитит error (§1.4);
494
+ * возвращает VoiceCallError — вызывающий сам решает throw или нет. */
495
+ raiseError(t, e, n) {
496
+ e !== this.connectionState && this.setConnectionState(e);
497
+ const i = new ct(t, this.connectionState, n);
498
+ return this.log.error(`[voice-client] ${t}`, n), this.emitter.emit("error", i), i;
499
+ }
500
+ }
501
+ function vt(a, t, e, n, i) {
502
+ var V, Z;
503
+ const o = document.createElement("button");
504
+ o.type = "button", o.className = "ht-avatar";
505
+ const r = document.createElement("img");
506
+ r.src = t.posterUrl, r.alt = "";
507
+ const g = document.createElement("span");
508
+ g.className = "ht-avatar-video", g.setAttribute("aria-hidden", "true");
509
+ const m = document.createElement("video");
510
+ m.className = "ht-avatar-live", m.muted = !0, m.autoplay = !0, m.playsInline = !0, m.controls = !1, m.setAttribute("tabindex", "-1"), m.setAttribute("aria-hidden", "true"), m.hidden = !0;
511
+ const S = document.createElement("span");
512
+ S.className = "ht-avatar-ai", S.id = `ht-avatar-ai-${Math.random().toString(36).slice(2, 8)}`, S.textContent = "AI avatar", S.hidden = !0;
513
+ const d = document.createElement("span");
514
+ d.className = "ht-mic", d.setAttribute("aria-hidden", "true"), d.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M5 10v2a7 7 0 0014 0v-2M12 19v3M8 22h8"/><path class="ht-mic-slash" d="M2 2l20 20"/></svg>';
515
+ const y = document.createElement("span");
516
+ y.className = "ht-avatar-status", y.setAttribute("role", "status"), o.append(r, g, m, d, y, S), a.append(o);
517
+ const p = new AbortController(), H = window.matchMedia("(prefers-reduced-motion: reduce)");
518
+ let f, $ = !1, u = !1, v, R = !1, N = !1, L = "", K = !1, M = !1, C = !1, _ = !1, T, E;
519
+ function O() {
520
+ const s = window.visualViewport, h = (s == null ? void 0 : s.offsetLeft) ?? 0, k = (s == null ? void 0 : s.offsetTop) ?? 0, x = (s == null ? void 0 : s.width) ?? innerWidth, D = (s == null ? void 0 : s.height) ?? innerHeight, A = o.offsetWidth || t.size || 140, z = a.querySelector(".ht-panel"), P = z.getBoundingClientRect();
521
+ let U = (T == null ? void 0 : T.x) ?? h + x - A - 20, B = (T == null ? void 0 : T.y) ?? k + D - A - 100;
522
+ !z.hidden && U + A > P.left && U < P.right && B + A + 32 > P.top && B < P.bottom && (P.left - A - 16 >= h + 8 ? U = P.left - A - 16 : B = P.top - A - 40), U = Math.max(h + 8, Math.min(U, h + x - A - 8)), B = Math.max(k + 8, Math.min(B, k + D - A - 40)), o.style.left = `${U}px`, o.style.top = `${B}px`, T && (T = { x: U, y: B });
523
+ }
524
+ function w() {
525
+ if (R) return;
526
+ if (u) {
527
+ g.hidden = !0, f == null || f.pause();
528
+ return;
529
+ }
530
+ if (H.matches) {
531
+ g.hidden = !0, f == null || f.pause();
532
+ return;
533
+ }
534
+ if (!f) {
535
+ W();
536
+ return;
537
+ }
538
+ g.hidden = !0;
539
+ const s = N ? t.speakingVideoUrl : t.listeningVideoUrl;
540
+ s !== L && (L = s, f.src({ src: s, type: "video/mp4" }));
541
+ const h = f.play();
542
+ h && h.catch(() => {
543
+ g.hidden = !0;
544
+ });
545
+ }
546
+ g.hidden = !0;
547
+ function W() {
548
+ $ || f || R || ($ = !0, import("video.js").then(({ default: s }) => {
549
+ if (R || u) return;
550
+ const h = document.createElement("video");
551
+ h.className = "video-js", h.muted = !0, h.loop = !0, h.playsInline = !0, h.controls = !1, h.setAttribute("tabindex", "-1"), g.append(h), f = s(h, {
552
+ controls: !1,
553
+ muted: !0,
554
+ loop: !0,
555
+ playsinline: !0,
556
+ preload: "none",
557
+ poster: t.posterUrl,
558
+ children: ["mediaLoader"]
559
+ }), f.on("playing", () => {
560
+ H.matches || (g.hidden = !1);
561
+ }), f.on("waiting", () => {
562
+ g.hidden = !0;
563
+ }), f.on("error", () => {
564
+ g.hidden = !0;
565
+ }), w();
566
+ }).catch(() => {
567
+ g.hidden = !0;
568
+ }));
569
+ }
570
+ function c(s) {
571
+ var h;
572
+ if (!(R || u === s)) {
573
+ if (u = s, v == null || v(), v = void 0, s) {
574
+ v = e.attachAgentVideo(m);
575
+ try {
576
+ (h = m.play()) == null || h.catch(() => {
577
+ });
578
+ } catch {
579
+ }
580
+ }
581
+ m.hidden = !s, S.hidden = !s, s ? o.setAttribute("aria-describedby", S.id) : o.removeAttribute("aria-describedby"), w();
582
+ }
583
+ }
584
+ e.on("agentVideoChanged", ({ available: s }) => c(s)), w(), H.addEventListener("change", w, { signal: p.signal }), o.addEventListener(
585
+ "pointerdown",
586
+ (s) => {
587
+ if (s.button !== 0 || !s.isPrimary) return;
588
+ C = !1, _ = !1;
589
+ const h = o.getBoundingClientRect();
590
+ E = {
591
+ id: s.pointerId,
592
+ x: s.clientX,
593
+ y: s.clientY,
594
+ left: h.left,
595
+ top: h.top
596
+ }, o.setPointerCapture(s.pointerId);
597
+ },
598
+ { signal: p.signal }
599
+ ), o.addEventListener(
600
+ "pointermove",
601
+ (s) => {
602
+ if (!E || s.pointerId !== E.id) return;
603
+ const h = s.clientX - E.x, k = s.clientY - E.y;
604
+ !_ && Math.hypot(h, k) < 6 || (_ = !0, C = !0, T = { x: E.left + h, y: E.top + k }, O());
605
+ },
606
+ { signal: p.signal }
607
+ );
608
+ const l = () => {
609
+ E = void 0;
610
+ };
611
+ o.addEventListener(
612
+ "pointerup",
613
+ (s) => {
614
+ (E == null ? void 0 : E.id) === s.pointerId && s.pointerType === "touch" && !_ && (s.preventDefault(), C = !0, I()), l();
615
+ },
616
+ { signal: p.signal }
617
+ ), o.addEventListener(
618
+ "pointercancel",
619
+ () => {
620
+ C = !0, l();
621
+ },
622
+ { signal: p.signal }
623
+ ), o.addEventListener("lostpointercapture", l, { signal: p.signal }), o.addEventListener(
624
+ "click",
625
+ (s) => {
626
+ if (s.detail !== 0 && C) {
627
+ C = !1;
628
+ return;
629
+ }
630
+ I();
631
+ },
632
+ { signal: p.signal }
633
+ );
634
+ function I() {
635
+ if (!(M || K)) {
636
+ if (e.getState() !== "connected") {
637
+ n();
638
+ return;
639
+ }
640
+ K = !0, e.setMicrophoneEnabled(!e.isMicrophoneEnabled()).catch(i).finally(() => {
641
+ K = !1;
642
+ });
643
+ }
644
+ }
645
+ return window.addEventListener("resize", O, { signal: p.signal }), (V = window.visualViewport) == null || V.addEventListener("resize", O, { signal: p.signal }), (Z = window.visualViewport) == null || Z.addEventListener("scroll", O, { signal: p.signal }), O(), {
646
+ reposition: O,
647
+ update(s, h) {
648
+ const k = e.getState(), x = e.getAgentState(), D = e.isMicrophoneEnabled(), A = k === "connected";
649
+ M = s || k === "connecting" || k === "reconnecting" || A && !h, o.setAttribute("aria-disabled", String(M)), o.setAttribute("aria-pressed", String(D)), o.setAttribute(
650
+ "aria-label",
651
+ A ? D ? "Mute microphone" : "Unmute microphone" : "Start voice call"
652
+ ), d.classList.toggle("ht-muted", !D);
653
+ const z = A && x === "speaking";
654
+ N !== z && (N = z, w());
655
+ const P = s || k === "connecting" ? "Connecting" : k === "reconnecting" ? "Reconnecting" : A ? N ? "Speaking" : h ? D ? x === "listening" ? "Listening" : x === "thinking" ? "Thinking" : "Getting ready" : "Microphone off" : "Text chat" : "Tap to talk";
656
+ y.textContent = P, o.dataset.state = N ? "speaking" : A && D && x === "listening" ? "listening" : "idle";
657
+ },
658
+ destroy() {
659
+ R = !0, p.abort(), v == null || v(), v = void 0, f == null || f.dispose(), o.remove();
660
+ }
661
+ };
662
+ }
663
+ const Ot = 140, It = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6.6 10.8a15.1 15.1 0 0 0 6.6 6.6l2.2-2.2a1 1 0 0 1 1-.25c1.1.37 2.3.57 3.6.57a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1C10.6 21 3 13.4 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1c0 1.3.2 2.5.57 3.6a1 1 0 0 1-.25 1l-2.2 2.2Z" fill="currentColor"/></svg>', kt = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4 5.5A2.5 2.5 0 0 1 6.5 3h11A2.5 2.5 0 0 1 20 5.5v8a2.5 2.5 0 0 1-2.5 2.5H10l-4.4 3.3A1 1 0 0 1 4 18.5v-13Z" fill="currentColor"/></svg>', Rt = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" fill="none"/></svg>', Nt = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 19V5m0 0-6 6m6-6 6 6" stroke="currentColor" stroke-width="2"/></svg>';
664
+ function Lt(a) {
665
+ var W;
666
+ const t = a.mode === "avatar", e = a.avatar;
667
+ if (t && (!e || [e.posterUrl, e.listeningVideoUrl, e.speakingVideoUrl].some(
668
+ (c) => typeof c != "string" || !c.trim()
669
+ )))
670
+ throw new Error("Avatar mode requires posterUrl, listeningVideoUrl and speakingVideoUrl");
671
+ if ((e == null ? void 0 : e.size) !== void 0 && !(Number.isFinite(e.size) && e.size > 0))
672
+ throw new Error("avatar.size must be a positive number of pixels");
673
+ const n = document.createElement("div");
674
+ n.className = "ht-widget", n.dataset.mode = a.mode ?? "standard", e != null && e.size && n.style.setProperty("--ht-avatar-size", `${e.size}px`), n.innerHTML = `<button type="button" class="ht-launcher" aria-label="Open chat" aria-expanded="false">${t ? kt : It}<span class="ht-live-dot" hidden></span></button>
675
+ <section class="ht-panel" aria-label="Voice agent" hidden>
676
+ <header class="ht-head"><span class="ht-title">Voice agent</span><span class="ht-pill" role="status">Ready</span><span class="ht-timer" hidden>0:00</span><button type="button" class="ht-icon ht-collapse" aria-label="Close chat">${Rt}</button></header>
677
+ <details class="ht-details" open><summary>Call details</summary><div class="ht-fields"></div></details>
678
+ <div class="ht-log" role="log" aria-label="Conversation"><p class="ht-empty">Press Call to talk, or Chat to type.</p></div>
679
+ <pre class="ht-debug"></pre>
680
+ <button type="button" class="ht-btn ht-btn-ghost ht-enable-sound" hidden>Enable sound</button>
681
+ <form class="ht-composer"><input aria-label="Message" placeholder="Type a reply and press Enter…" maxlength="1000" disabled><button type="submit" class="ht-icon" aria-label="Send" disabled>${Nt}</button></form>
682
+ <div class="ht-actions"><button type="button" class="ht-btn ht-call-button">Call</button><button type="button" class="ht-btn ht-btn-ghost ht-chat-only" title="No microphone or audio">Chat</button><button type="button" class="ht-btn ht-btn-danger ht-end" disabled>${t ? "End call" : "Hang up"}</button></div>
683
+ </section>`;
684
+ const i = (c) => n.querySelector(c), o = i(".ht-launcher"), r = i(".ht-panel"), g = i(".ht-details"), m = i(".ht-log"), S = i(".ht-composer input"), d = i(".ht-composer button"), y = i(".ht-call-button"), p = i(".ht-chat-only"), H = i(".ht-end"), f = i(".ht-enable-sound"), $ = i(".ht-timer");
685
+ (W = a.renderCallDetails) == null || W.call(a, i(".ht-fields")), g.hidden = !a.renderCallDetails, a.container.append(n);
686
+ const u = new wt(), v = new AbortController(), R = /* @__PURE__ */ new Map();
687
+ let N = !1, L = !1, K = !0, M, C;
688
+ function _(c) {
689
+ r.hidden = !c, o.hidden = !t && c, o.setAttribute("aria-expanded", String(c)), C == null || C.reposition();
690
+ }
691
+ function T(c) {
692
+ const l = document.createElement("p");
693
+ l.className = "ht-msg system", l.textContent = c, m.append(l), _(!0);
694
+ }
695
+ function E() {
696
+ if (N) return;
697
+ const c = u.getState(), l = ["connecting", "connected", "reconnecting"].includes(c), I = L || l;
698
+ y.disabled = p.disabled = I, H.disabled = !l, S.disabled = d.disabled = c !== "connected", i(".ht-live-dot").hidden = !l;
699
+ const V = u.getAgentState();
700
+ i(".ht-pill").className = `ht-pill ${V}`, i(".ht-pill").textContent = L || c === "connecting" ? "Connecting…" : c === "reconnecting" ? "Reconnecting…" : V === "unknown" ? "Ready" : V[0].toUpperCase() + V.slice(1), i(".ht-debug").textContent = `state: ${c} · agent: ${V}${u.getCallId() ? `
701
+ callId: ` + u.getCallId() : ""}`, C == null || C.update(L, K);
702
+ }
703
+ async function O(c) {
704
+ if (!(L || ["connecting", "connected", "reconnecting"].includes(u.getState()))) {
705
+ L = !0, K = c, f.hidden = !0, E();
706
+ try {
707
+ const l = await a.createSession({ avatar: t });
708
+ N || await u.startCall(l, { audio: c });
709
+ } catch {
710
+ !N && u.getState() !== "error" && T("Could not start the call. Check the call details and try again.");
711
+ } finally {
712
+ L = !1, E();
713
+ }
714
+ }
715
+ }
716
+ function w(c, l, I) {
717
+ c.addEventListener(l, I, { signal: v.signal });
718
+ }
719
+ return w(o, "click", () => _(r.hidden)), w(i(".ht-collapse"), "click", () => {
720
+ _(!1), o.focus();
721
+ }), w(y, "click", () => {
722
+ O(!0);
723
+ }), w(p, "click", () => {
724
+ O(!1);
725
+ }), w(H, "click", () => {
726
+ u.stopCall();
727
+ }), w(f, "click", () => {
728
+ u.resumeAudio().then(
729
+ () => {
730
+ f.hidden = !0;
731
+ },
732
+ () => {
733
+ f.hidden = !1;
734
+ }
735
+ );
736
+ }), w(i(".ht-composer"), "submit", (c) => {
737
+ c.preventDefault();
738
+ const l = S.value;
739
+ l.trim() && u.sendText(l).then(
740
+ () => {
741
+ S.value === l && (S.value = "");
742
+ },
743
+ () => {
744
+ }
745
+ );
746
+ }), u.on("connectionStateChanged", E), u.on("agentStateChanged", E), u.on("microphoneStateChanged", E), u.on("audioPlaybackBlocked", () => {
747
+ f.hidden = !1, _(!0);
748
+ }), u.on("callStarted", () => {
749
+ m.replaceChildren(), R.clear(), g.hidden = !0;
750
+ const c = Date.now();
751
+ clearInterval(M), $.hidden = !1;
752
+ const l = () => {
753
+ const I = Math.floor((Date.now() - c) / 1e3);
754
+ $.textContent = `${Math.floor(I / 60)}:${String(I % 60).padStart(2, "0")}`;
755
+ };
756
+ l(), M = setInterval(l, 1e3), E();
757
+ }), u.on("callEnded", (c) => {
758
+ clearInterval(M), $.hidden = !0, f.hidden = !0, g.hidden = !a.renderCallDetails, R.clear(), T(
759
+ c.reason === "remote" ? "The agent ended the call." : c.reason === "connection_lost" ? "Connection lost. Try calling again." : "Call ended."
760
+ ), E();
761
+ }), u.on("error", (c) => {
762
+ T({
763
+ MICROPHONE_PERMISSION_DENIED: "Allow microphone access in your browser, then try again.",
764
+ MICROPHONE_UNAVAILABLE: "Microphone unavailable. Check your device or use Chat.",
765
+ AUDIO_PLAYBACK_BLOCKED: "Tap Enable sound to hear the agent."
766
+ }[c.code] ?? `${c.code}: Please try again.`), E();
767
+ }), u.on("transcript", (c) => {
768
+ var I;
769
+ (I = i(".ht-empty")) == null || I.remove();
770
+ let l = R.get(c.id);
771
+ l || (l = document.createElement("div"), R.set(c.id, l), m.append(l)), l.className = `ht-msg ${c.role}${c.final ? "" : " interim"}`, l.textContent = c.text, m.scrollTop = m.scrollHeight;
772
+ }), t && (C = vt(
773
+ n,
774
+ a.avatar,
775
+ u,
776
+ () => O(!0),
777
+ () => _(!0)
778
+ )), E(), {
779
+ destroy() {
780
+ N || (N = !0, v.abort(), clearInterval(M), C == null || C.destroy(), u.destroy(), n.remove(), R.clear());
781
+ }
782
+ };
783
+ }
784
+ export {
785
+ Ot as DEFAULT_AVATAR_SIZE,
786
+ Lt as mountWidget
787
+ };