@burdenoff/website-sdk 2026.1104.4 → 2026.1105.2
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 +67 -3
- package/dist/avatar-live/index.d.mts +2 -2
- package/dist/avatar-live/index.d.ts +2 -2
- package/dist/avatar-live/index.js +11 -1
- package/dist/avatar-live/index.js.map +1 -1
- package/dist/avatar-live/index.mjs +11 -1
- package/dist/avatar-live/index.mjs.map +1 -1
- package/dist/avatar-live-view/index.d.mts +2 -2
- package/dist/avatar-live-view/index.d.ts +2 -2
- package/dist/avatar-live-view/index.js +1059 -45
- package/dist/avatar-live-view/index.js.map +1 -1
- package/dist/avatar-live-view/index.mjs +1054 -40
- package/dist/avatar-live-view/index.mjs.map +1 -1
- package/dist/avatar-mic/index.d.mts +193 -0
- package/dist/avatar-mic/index.d.ts +193 -0
- package/dist/avatar-mic/index.js +738 -0
- package/dist/avatar-mic/index.js.map +1 -0
- package/dist/avatar-mic/index.mjs +736 -0
- package/dist/avatar-mic/index.mjs.map +1 -0
- package/dist/{chunk-7MSB3EUI.mjs → chunk-MVNN6LLO.mjs} +4 -3
- package/dist/chunk-MVNN6LLO.mjs.map +1 -0
- package/dist/{chunk-WSEQ2I6I.js → chunk-XIZE37IB.js} +4 -3
- package/dist/chunk-XIZE37IB.js.map +1 -0
- package/dist/{explore-avatar-live-config-B6_p0q1c.d.mts → explore-avatar-live-config-BL8FTm0I.d.mts} +9 -1
- package/dist/{explore-avatar-live-config-DRpS-mqQ.d.ts → explore-avatar-live-config-auLKxSzx.d.ts} +9 -1
- package/dist/index.d.mts +9 -4
- package/dist/index.d.ts +9 -4
- package/dist/index.js +9 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5 -3
- package/dist/index.mjs.map +1 -1
- package/dist/{types-CXqaLlNB.d.mts → types-CTUJNvQu.d.mts} +10 -0
- package/dist/{types-CXqaLlNB.d.ts → types-CTUJNvQu.d.ts} +10 -0
- package/package.json +1 -1
- package/dist/chunk-7MSB3EUI.mjs.map +0 -1
- package/dist/chunk-WSEQ2I6I.js.map +0 -1
package/README.md
CHANGED
|
@@ -63,8 +63,9 @@ in `tsup.config.ts`) and the package declares `"sideEffects": false`:
|
|
|
63
63
|
|
|
64
64
|
The live-avatar guarantees are unchanged: `livekit-client` stays external to
|
|
65
65
|
the `./avatar-live` entry alone, and the avatar's view remains a chunk of its
|
|
66
|
-
own that only the root loads on demand
|
|
67
|
-
fails the build
|
|
66
|
+
own that only the root loads on demand, as does the Avatar tab's mic, which
|
|
67
|
+
only the view loads (`scripts/validate-avatar-live.mjs` fails the build
|
|
68
|
+
otherwise).
|
|
68
69
|
|
|
69
70
|
## Quick Start
|
|
70
71
|
|
|
@@ -395,6 +396,68 @@ publishes audio, video or data, and it sends the avatar nothing to say.
|
|
|
395
396
|
`ExplorePanel` (as `SiteAssistantDock` does) so a page change calls off a
|
|
396
397
|
"Start when ready" at once; without it, the window's path is checked before
|
|
397
398
|
the create.
|
|
399
|
+
- **A mic in the Avatar tab's box (BOFF-9797 Stage C, opt-in, dark).** With
|
|
400
|
+
`mic: true` on `avatar.live` (or `websiteAvatarLive({ …, mic: true })`; no
|
|
401
|
+
site sets it yet), the box gets a mic button beside Send, but only while the
|
|
402
|
+
last availability answer carries the gateway's mic offer (`stt: {token,
|
|
403
|
+
expiresAt}`, sent only while the gateway's website speech-to-text is on) and
|
|
404
|
+
the box takes the first question of a new chat. Tap to start, tap to stop,
|
|
405
|
+
thirty seconds at most (it stops by itself), with a recording dot, the time
|
|
406
|
+
against `0:30` and the words as they are recognised. The final text goes into
|
|
407
|
+
the box (after anything typed, within its 300 characters): the visitor
|
|
408
|
+
corrects it and presses Send. **Nothing is ever sent by itself.** The first
|
|
409
|
+
tap shows a mic notice of its own (versioned: the SDK shows and names
|
|
410
|
+
`web-mic-2026-11-05`, and the speech service takes a press only when that is
|
|
411
|
+
its `AVATAR_WEB_STT_NOTICE_VERSION`), then the browser's permission prompt; a
|
|
412
|
+
refused permission says so and the box stays for typing. The stream is the
|
|
413
|
+
gateway's `wss://…/api/web-avatar/v1/stt/stream` (subprotocols
|
|
414
|
+
`boff.web-stt.v1` and `offer.<token>`, `en-IN`, 16 kHz PCM); a refused
|
|
415
|
+
upgrade (4401) asks availability again and retries once. The microphone is
|
|
416
|
+
released on tap-stop, the limit, Stop, closing the panel, the Chat tab, a
|
|
417
|
+
hidden page, `pagehide`, the view going and any error. Its capture (ported
|
|
418
|
+
from fe-libs), stream and state machine are a chunk of their own,
|
|
419
|
+
`dist/avatar-mic/`, loaded from the view the first time the mic is used: a
|
|
420
|
+
site's main bundle and the view's chunk carry none of it. On alpha, a browser
|
|
421
|
+
WebSocket cannot carry Cloudflare Access service-token headers, so the
|
|
422
|
+
stream needs the Access cookie for the gateway host.
|
|
423
|
+
- **The answers after the first (BOFF-9797 Stage D, dark).** Only where the
|
|
424
|
+
create's 201 carries `followUps: {maxAnswers, idleSeconds,
|
|
425
|
+
maxAnswerWaitSeconds}` (the avatar service adds it only under its
|
|
426
|
+
`AVATAR_WEB_MULTI_ANSWER`, read strictly into `AvatarSession.followUps`;
|
|
427
|
+
absent or unreadable, the session reads its one answer exactly as before),
|
|
428
|
+
the visitor agreed to consent copy whose words describe them (`followUps:
|
|
429
|
+
true`: `web-consent-2026-11-19-d` with the bot check, `web-consent-2026-11-20-d`
|
|
430
|
+
without, DRAFTS no server names yet), and the tab has its own box. The box
|
|
431
|
+
then stays under the live avatar, and a question asked there (typed, or by
|
|
432
|
+
the mic where it is offered) is the next one of the same conversation, sent
|
|
433
|
+
with `surface: "AVATAR"`; the same session reads its answer. The view tells
|
|
434
|
+
the session through the gateway's `POST /api/web-avatar/v1/sessions/{id}/turn`
|
|
435
|
+
(`{secret, kind, nonce, messageId?, idempotencyKey?}`, a simple request like
|
|
436
|
+
the end, `requestTurn`): `asking` as Send is pressed, `listening` while the
|
|
437
|
+
mic records (once the microphone is open and its stream ready, and as it
|
|
438
|
+
stops), `answer` with the answer's id once it is written, and `interrupt`
|
|
439
|
+
when the mic is tapped while the avatar speaks. The session reads one answer
|
|
440
|
+
at a time and refuses a later one `busy` while the one before plays, so an
|
|
441
|
+
answer written while the avatar speaks is sent once that answer has played
|
|
442
|
+
out here, and a `busy` is asked again with the same key after the refusal's
|
|
443
|
+
`retryAfterSeconds` (doubling up to 8 s, at most 24 times, never past the
|
|
444
|
+
session's end; this also covers a room that tells no voice). An answer that
|
|
445
|
+
got no reply is asked again once with the same key. An answer the session
|
|
446
|
+
does not read stays in Chat, said in one line (`no_time`: "There isn't
|
|
447
|
+
enough time left in this session to read that answer. It's in Chat.";
|
|
448
|
+
`not_eligible`: "This answer stays in Chat as text. …"; also `max_answers`,
|
|
449
|
+
`answer_claimed`, `limits`, `paused`, `busy` past its retries), and the box
|
|
450
|
+
keeps asking while answers are left. An answer this browser can tell will
|
|
451
|
+
not be read (a failed answer, or one its checks refuse) is sent all the same
|
|
452
|
+
with this browser's line shown: only the session's refusal ends the wait an
|
|
453
|
+
`asking` mark holds open. When an answer has played out is the worker's voice going quiet for
|
|
454
|
+
2.5 s (the room's new `speaking` event, from the media server's active
|
|
455
|
+
speakers); the status line then says "The avatar ends after about thirty
|
|
456
|
+
seconds without a question.", and the server ends the session after its
|
|
457
|
+
idle period (the room closing ends it here, as any server end). Under
|
|
458
|
+
`endUnattended`, a session out of sight ends once its answer has played out
|
|
459
|
+
and nothing is on its way. Stop, an interrupt and a correction never create a
|
|
460
|
+
session or ask for an answer twice.
|
|
398
461
|
- **One answer: the first, to a suggested question.** The avatar service
|
|
399
462
|
accepts a conversation only when it holds exactly two messages: the first user
|
|
400
463
|
message (a suggested question) and the first assistant answer, completed. The
|
|
@@ -603,7 +666,8 @@ Testing hooks (`data-boff-explore`): `avatar-live` (with `data-phase`),
|
|
|
603
666
|
`avatar-live-start`, `avatar-live-not-now`, `avatar-live-stop`,
|
|
604
667
|
`avatar-live-video`, `avatar-live-audio`, `avatar-live-badge`,
|
|
605
668
|
`avatar-live-captions`, `avatar-live-countdown`, `avatar-live-sound`,
|
|
606
|
-
`avatar-live-continue`, `avatar-live-back`, `avatar-live-bot-check
|
|
669
|
+
`avatar-live-continue`, `avatar-live-back`, `avatar-live-bot-check`,
|
|
670
|
+
`avatar-live-idle` (Stage D: the idle line under a refusal's).
|
|
607
671
|
|
|
608
672
|
#### Site assistant dock (`SiteAssistantDock`, every product site)
|
|
609
673
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as ReceiveOnlyJoinOptions, a as ReceiveOnlySession } from '../types-
|
|
2
|
-
export { A as AvatarLiveModule, b as ReceiveOnlyEvent } from '../types-
|
|
1
|
+
import { R as ReceiveOnlyJoinOptions, a as ReceiveOnlySession } from '../types-CTUJNvQu.mjs';
|
|
2
|
+
export { A as AvatarLiveModule, b as ReceiveOnlyEvent } from '../types-CTUJNvQu.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Join the room in `options.url` with the receive-only `options.token`, and play the avatar worker's video and voice
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { R as ReceiveOnlyJoinOptions, a as ReceiveOnlySession } from '../types-
|
|
2
|
-
export { A as AvatarLiveModule, b as ReceiveOnlyEvent } from '../types-
|
|
1
|
+
import { R as ReceiveOnlyJoinOptions, a as ReceiveOnlySession } from '../types-CTUJNvQu.js';
|
|
2
|
+
export { A as AvatarLiveModule, b as ReceiveOnlyEvent } from '../types-CTUJNvQu.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Join the room in `options.url` with the receive-only `options.token`, and play the avatar worker's video and voice
|
|
@@ -61,6 +61,16 @@ async function joinReceiveOnly(options) {
|
|
|
61
61
|
sound: !room.canPlaybackAudio
|
|
62
62
|
});
|
|
63
63
|
};
|
|
64
|
+
let workerSpeaking = false;
|
|
65
|
+
const reportSpeakers = (speakers) => {
|
|
66
|
+
if (leaving) return;
|
|
67
|
+
const speaking = speakers.some(
|
|
68
|
+
(speaker) => speaker.identity === workerIdentity
|
|
69
|
+
);
|
|
70
|
+
if (speaking === workerSpeaking) return;
|
|
71
|
+
workerSpeaking = speaking;
|
|
72
|
+
onEvent({ type: "speaking", speaking });
|
|
73
|
+
};
|
|
64
74
|
room.on(livekitClient.RoomEvent.TrackPublished, consider).on(
|
|
65
75
|
livekitClient.RoomEvent.TrackSubscribed,
|
|
66
76
|
(track, publication, participant) => {
|
|
@@ -93,7 +103,7 @@ async function joinReceiveOnly(options) {
|
|
|
93
103
|
} else {
|
|
94
104
|
voiceGone(participant);
|
|
95
105
|
}
|
|
96
|
-
}).on(livekitClient.RoomEvent.AudioPlaybackStatusChanged, reportPlayback).on(livekitClient.RoomEvent.VideoPlaybackStatusChanged, reportPlayback).on(livekitClient.RoomEvent.Disconnected, (reason) => {
|
|
106
|
+
}).on(livekitClient.RoomEvent.AudioPlaybackStatusChanged, reportPlayback).on(livekitClient.RoomEvent.VideoPlaybackStatusChanged, reportPlayback).on(livekitClient.RoomEvent.ActiveSpeakersChanged, reportSpeakers).on(livekitClient.RoomEvent.Disconnected, (reason) => {
|
|
97
107
|
if (leaving) return;
|
|
98
108
|
onEvent({
|
|
99
109
|
type: "disconnected",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/avatar-live/receive-only.ts"],"names":["DisconnectReason","Track","Room","RoomEvent"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * BOFF-9797. The live avatar's room, joined to watch and listen only.\n *\n * A visitor's browser on a product website holds a subscribe-only room token: it can receive the avatar worker's\n * video and voice and do nothing else. This adapter keeps to that from its side too:\n *\n * - **Nothing is subscribed automatically** (`autoSubscribe: false`). Only the tracks of `workerIdentity`, the avatar\n * worker the create answer named, are subscribed, and only their video and audio. Any other participant's track\n * (the server's own speaker included) is never asked for, and is dropped again should the server send it anyway.\n * - **Nothing is published.** No microphone, no camera, no data, no RPC method: this file never calls a publishing\n * API, and the token would refuse one.\n * - **Safari and iOS** hold sound back until a gesture. The room reports it (`playback-blocked`, with `sound` when the\n * sound is held back and not only the video) and the panel offers \"Tap to turn on sound\", whose tap calls\n * `enablePlayback`.\n * - **The end of the speech** is told (`speech-ended`) from what any viewer of the room sees: the other voice in it, the\n * server's speaker that the worker follows, leaving the room or unpublishing its voice. Its track is never subscribed.\n *\n * Written for the website from the media library's public API: nothing here is copied from the product apps.\n */\nimport {\n DisconnectReason,\n Room,\n RoomEvent,\n Track,\n type RemoteParticipant,\n type RemoteTrack,\n type RemoteTrackPublication,\n} from \"livekit-client\";\n\nimport type { ReceiveOnlyJoinOptions, ReceiveOnlySession } from \"./types\";\n\n/** The reasons a room closes because the server closed it, rather than because the connection was lost. */\nconst CLOSED_BY_SERVER: ReadonlySet<DisconnectReason> = new Set([\n DisconnectReason.ROOM_DELETED,\n DisconnectReason.ROOM_CLOSED,\n DisconnectReason.PARTICIPANT_REMOVED,\n]);\n\nfunction isMedia(publication: RemoteTrackPublication): boolean {\n return (\n publication.kind === Track.Kind.Video ||\n publication.kind === Track.Kind.Audio\n );\n}\n\n/**\n * Join the room in `options.url` with the receive-only `options.token`, and play the avatar worker's video and voice\n * in `options.video` and `options.audio`. Resolves once connected; rejects when the room cannot be joined (the panel\n * then ends the session).\n */\nexport async function joinReceiveOnly(\n options: ReceiveOnlyJoinOptions,\n): Promise<ReceiveOnlySession> {\n const { url, token, workerIdentity, video, audio, onEvent } = options;\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error(\"live avatar: the room address is not a URL\");\n }\n if (parsed.protocol !== \"wss:\") {\n throw new Error(\"live avatar: the room address must be wss:\");\n }\n if (!token || !workerIdentity) {\n throw new Error(\"live avatar: the room token or speaker is missing\");\n }\n\n const room = new Room({\n // One small video, always shown while the session lasts: no quality switching, no layer pausing.\n adaptiveStream: false,\n dynacast: false,\n // Closing the tab or leaving the page leaves the room at once. Belt and braces with the panel's own page-hide\n // handler, which also asks the server to end the session: either may run first, and both are safe to repeat.\n disconnectOnPageLeave: true,\n });\n let leaving = false;\n\n const isSpeaker = (participant: RemoteParticipant) =>\n participant.identity === workerIdentity;\n\n /**\n * Everyone else in the room seen publishing a voice: the server's speaker, whose speech the worker voices. Noted\n * only, never subscribed to, so that the end of that speech can be told (`speech-ended`).\n */\n const voiced = new Set<string>();\n let speechEnded = false;\n\n /** Subscribe to `publication` when it is the speaker's video or audio; leave everything else alone. */\n const consider = (\n publication: RemoteTrackPublication,\n participant: RemoteParticipant,\n ) => {\n if (leaving) return;\n if (!isSpeaker(participant)) {\n if (publication.kind === Track.Kind.Audio) {\n voiced.add(participant.identity);\n }\n return;\n }\n if (!isMedia(publication)) return;\n if (!publication.isSubscribed) publication.setSubscribed(true);\n };\n\n /** A voice seen published has gone (its track, or its participant): the speech has ended at its source. Once. */\n const voiceGone = (participant: RemoteParticipant) => {\n if (leaving || speechEnded || !voiced.has(participant.identity)) return;\n speechEnded = true;\n onEvent({ type: \"speech-ended\" });\n };\n\n const reportPlayback = () => {\n if (leaving) return;\n onEvent({\n type: \"playback-blocked\",\n blocked: !room.canPlaybackAudio || !room.canPlaybackVideo,\n sound: !room.canPlaybackAudio,\n });\n };\n\n room\n .on(RoomEvent.TrackPublished, consider)\n .on(\n RoomEvent.TrackSubscribed,\n (\n track: RemoteTrack,\n publication: RemoteTrackPublication,\n participant: RemoteParticipant,\n ) => {\n if (leaving) return;\n if (!isSpeaker(participant) || !isMedia(publication)) {\n // Never asked for: drop it rather than play it.\n publication.setSubscribed(false);\n return;\n }\n if (track.kind === Track.Kind.Video) {\n track.attach(video);\n onEvent({ type: \"speaker-track\", kind: \"video\" });\n } else {\n track.attach(audio);\n onEvent({ type: \"speaker-track\", kind: \"audio\" });\n }\n },\n )\n .on(RoomEvent.TrackUnsubscribed, (track: RemoteTrack) => {\n track.detach();\n })\n .on(\n RoomEvent.TrackUnpublished,\n (publication: RemoteTrackPublication, participant: RemoteParticipant) => {\n if (!isSpeaker(participant) && publication.kind === Track.Kind.Audio) {\n voiceGone(participant);\n }\n },\n )\n .on(RoomEvent.ParticipantDisconnected, (participant: RemoteParticipant) => {\n if (leaving) return;\n if (isSpeaker(participant)) {\n onEvent({ type: \"speaker-left\" });\n } else {\n voiceGone(participant);\n }\n })\n .on(RoomEvent.AudioPlaybackStatusChanged, reportPlayback)\n .on(RoomEvent.VideoPlaybackStatusChanged, reportPlayback)\n .on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {\n if (leaving) return;\n onEvent({\n type: \"disconnected\",\n byServer: reason !== undefined && CLOSED_BY_SERVER.has(reason),\n });\n });\n\n const detachAll = () => {\n for (const participant of room.remoteParticipants.values()) {\n for (const publication of participant.trackPublications.values()) {\n publication.track?.detach();\n }\n }\n video.srcObject = null;\n audio.srcObject = null;\n };\n\n try {\n await room.connect(url, token, { autoSubscribe: false });\n } catch (error) {\n leaving = true;\n room.removeAllListeners();\n detachAll();\n throw error;\n }\n\n // Whoever was in the room before this viewer joined: their tracks were published before the listener above.\n for (const participant of room.remoteParticipants.values()) {\n for (const publication of participant.trackPublications.values()) {\n consider(publication, participant);\n }\n }\n reportPlayback();\n\n return {\n enablePlayback: async () => {\n // Both started inside the visitor's gesture, before anything is awaited: Safari allows playback only there.\n // A refusal leaves the room as it was, and the report below keeps the button offered.\n await Promise.allSettled([room.startAudio(), room.startVideo()]);\n reportPlayback();\n },\n leave: async () => {\n if (leaving) return;\n leaving = true;\n detachAll();\n try {\n await room.disconnect();\n } finally {\n room.removeAllListeners();\n }\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/avatar-live/receive-only.ts"],"names":["DisconnectReason","Track","Room","RoomEvent"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * BOFF-9797. The live avatar's room, joined to watch and listen only.\n *\n * A visitor's browser on a product website holds a subscribe-only room token: it can receive the avatar worker's\n * video and voice and do nothing else. This adapter keeps to that from its side too:\n *\n * - **Nothing is subscribed automatically** (`autoSubscribe: false`). Only the tracks of `workerIdentity`, the avatar\n * worker the create answer named, are subscribed, and only their video and audio. Any other participant's track\n * (the server's own speaker included) is never asked for, and is dropped again should the server send it anyway.\n * - **Nothing is published.** No microphone, no camera, no data, no RPC method: this file never calls a publishing\n * API, and the token would refuse one.\n * - **Safari and iOS** hold sound back until a gesture. The room reports it (`playback-blocked`, with `sound` when the\n * sound is held back and not only the video) and the panel offers \"Tap to turn on sound\", whose tap calls\n * `enablePlayback`.\n * - **The end of the speech** is told (`speech-ended`) from what any viewer of the room sees: the other voice in it, the\n * server's speaker that the worker follows, leaving the room or unpublishing its voice. Its track is never subscribed.\n * - **Whether the worker's voice is heard** is told (`speaking`) each time it changes, from the room server's active\n * speakers, which it reports to a speaker's subscribers.\n *\n * Written for the website from the media library's public API: nothing here is copied from the product apps.\n */\nimport {\n DisconnectReason,\n Room,\n RoomEvent,\n Track,\n type Participant,\n type RemoteParticipant,\n type RemoteTrack,\n type RemoteTrackPublication,\n} from \"livekit-client\";\n\nimport type { ReceiveOnlyJoinOptions, ReceiveOnlySession } from \"./types\";\n\n/** The reasons a room closes because the server closed it, rather than because the connection was lost. */\nconst CLOSED_BY_SERVER: ReadonlySet<DisconnectReason> = new Set([\n DisconnectReason.ROOM_DELETED,\n DisconnectReason.ROOM_CLOSED,\n DisconnectReason.PARTICIPANT_REMOVED,\n]);\n\nfunction isMedia(publication: RemoteTrackPublication): boolean {\n return (\n publication.kind === Track.Kind.Video ||\n publication.kind === Track.Kind.Audio\n );\n}\n\n/**\n * Join the room in `options.url` with the receive-only `options.token`, and play the avatar worker's video and voice\n * in `options.video` and `options.audio`. Resolves once connected; rejects when the room cannot be joined (the panel\n * then ends the session).\n */\nexport async function joinReceiveOnly(\n options: ReceiveOnlyJoinOptions,\n): Promise<ReceiveOnlySession> {\n const { url, token, workerIdentity, video, audio, onEvent } = options;\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error(\"live avatar: the room address is not a URL\");\n }\n if (parsed.protocol !== \"wss:\") {\n throw new Error(\"live avatar: the room address must be wss:\");\n }\n if (!token || !workerIdentity) {\n throw new Error(\"live avatar: the room token or speaker is missing\");\n }\n\n const room = new Room({\n // One small video, always shown while the session lasts: no quality switching, no layer pausing.\n adaptiveStream: false,\n dynacast: false,\n // Closing the tab or leaving the page leaves the room at once. Belt and braces with the panel's own page-hide\n // handler, which also asks the server to end the session: either may run first, and both are safe to repeat.\n disconnectOnPageLeave: true,\n });\n let leaving = false;\n\n const isSpeaker = (participant: RemoteParticipant) =>\n participant.identity === workerIdentity;\n\n /**\n * Everyone else in the room seen publishing a voice: the server's speaker, whose speech the worker voices. Noted\n * only, never subscribed to, so that the end of that speech can be told (`speech-ended`).\n */\n const voiced = new Set<string>();\n let speechEnded = false;\n\n /** Subscribe to `publication` when it is the speaker's video or audio; leave everything else alone. */\n const consider = (\n publication: RemoteTrackPublication,\n participant: RemoteParticipant,\n ) => {\n if (leaving) return;\n if (!isSpeaker(participant)) {\n if (publication.kind === Track.Kind.Audio) {\n voiced.add(participant.identity);\n }\n return;\n }\n if (!isMedia(publication)) return;\n if (!publication.isSubscribed) publication.setSubscribed(true);\n };\n\n /** A voice seen published has gone (its track, or its participant): the speech has ended at its source. Once. */\n const voiceGone = (participant: RemoteParticipant) => {\n if (leaving || speechEnded || !voiced.has(participant.identity)) return;\n speechEnded = true;\n onEvent({ type: \"speech-ended\" });\n };\n\n const reportPlayback = () => {\n if (leaving) return;\n onEvent({\n type: \"playback-blocked\",\n blocked: !room.canPlaybackAudio || !room.canPlaybackVideo,\n sound: !room.canPlaybackAudio,\n });\n };\n\n /** Whether the worker's voice is heard, as last told: `speaking` goes out only when it changes. */\n let workerSpeaking = false;\n const reportSpeakers = (speakers: Participant[]) => {\n if (leaving) return;\n const speaking = speakers.some(\n (speaker) => speaker.identity === workerIdentity,\n );\n if (speaking === workerSpeaking) return;\n workerSpeaking = speaking;\n onEvent({ type: \"speaking\", speaking });\n };\n\n room\n .on(RoomEvent.TrackPublished, consider)\n .on(\n RoomEvent.TrackSubscribed,\n (\n track: RemoteTrack,\n publication: RemoteTrackPublication,\n participant: RemoteParticipant,\n ) => {\n if (leaving) return;\n if (!isSpeaker(participant) || !isMedia(publication)) {\n // Never asked for: drop it rather than play it.\n publication.setSubscribed(false);\n return;\n }\n if (track.kind === Track.Kind.Video) {\n track.attach(video);\n onEvent({ type: \"speaker-track\", kind: \"video\" });\n } else {\n track.attach(audio);\n onEvent({ type: \"speaker-track\", kind: \"audio\" });\n }\n },\n )\n .on(RoomEvent.TrackUnsubscribed, (track: RemoteTrack) => {\n track.detach();\n })\n .on(\n RoomEvent.TrackUnpublished,\n (publication: RemoteTrackPublication, participant: RemoteParticipant) => {\n if (!isSpeaker(participant) && publication.kind === Track.Kind.Audio) {\n voiceGone(participant);\n }\n },\n )\n .on(RoomEvent.ParticipantDisconnected, (participant: RemoteParticipant) => {\n if (leaving) return;\n if (isSpeaker(participant)) {\n onEvent({ type: \"speaker-left\" });\n } else {\n voiceGone(participant);\n }\n })\n .on(RoomEvent.AudioPlaybackStatusChanged, reportPlayback)\n .on(RoomEvent.VideoPlaybackStatusChanged, reportPlayback)\n .on(RoomEvent.ActiveSpeakersChanged, reportSpeakers)\n .on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {\n if (leaving) return;\n onEvent({\n type: \"disconnected\",\n byServer: reason !== undefined && CLOSED_BY_SERVER.has(reason),\n });\n });\n\n const detachAll = () => {\n for (const participant of room.remoteParticipants.values()) {\n for (const publication of participant.trackPublications.values()) {\n publication.track?.detach();\n }\n }\n video.srcObject = null;\n audio.srcObject = null;\n };\n\n try {\n await room.connect(url, token, { autoSubscribe: false });\n } catch (error) {\n leaving = true;\n room.removeAllListeners();\n detachAll();\n throw error;\n }\n\n // Whoever was in the room before this viewer joined: their tracks were published before the listener above.\n for (const participant of room.remoteParticipants.values()) {\n for (const publication of participant.trackPublications.values()) {\n consider(publication, participant);\n }\n }\n reportPlayback();\n\n return {\n enablePlayback: async () => {\n // Both started inside the visitor's gesture, before anything is awaited: Safari allows playback only there.\n // A refusal leaves the room as it was, and the report below keeps the button offered.\n await Promise.allSettled([room.startAudio(), room.startVideo()]);\n reportPlayback();\n },\n leave: async () => {\n if (leaving) return;\n leaving = true;\n detachAll();\n try {\n await room.disconnect();\n } finally {\n room.removeAllListeners();\n }\n },\n };\n}\n"]}
|
|
@@ -59,6 +59,16 @@ async function joinReceiveOnly(options) {
|
|
|
59
59
|
sound: !room.canPlaybackAudio
|
|
60
60
|
});
|
|
61
61
|
};
|
|
62
|
+
let workerSpeaking = false;
|
|
63
|
+
const reportSpeakers = (speakers) => {
|
|
64
|
+
if (leaving) return;
|
|
65
|
+
const speaking = speakers.some(
|
|
66
|
+
(speaker) => speaker.identity === workerIdentity
|
|
67
|
+
);
|
|
68
|
+
if (speaking === workerSpeaking) return;
|
|
69
|
+
workerSpeaking = speaking;
|
|
70
|
+
onEvent({ type: "speaking", speaking });
|
|
71
|
+
};
|
|
62
72
|
room.on(RoomEvent.TrackPublished, consider).on(
|
|
63
73
|
RoomEvent.TrackSubscribed,
|
|
64
74
|
(track, publication, participant) => {
|
|
@@ -91,7 +101,7 @@ async function joinReceiveOnly(options) {
|
|
|
91
101
|
} else {
|
|
92
102
|
voiceGone(participant);
|
|
93
103
|
}
|
|
94
|
-
}).on(RoomEvent.AudioPlaybackStatusChanged, reportPlayback).on(RoomEvent.VideoPlaybackStatusChanged, reportPlayback).on(RoomEvent.Disconnected, (reason) => {
|
|
104
|
+
}).on(RoomEvent.AudioPlaybackStatusChanged, reportPlayback).on(RoomEvent.VideoPlaybackStatusChanged, reportPlayback).on(RoomEvent.ActiveSpeakersChanged, reportSpeakers).on(RoomEvent.Disconnected, (reason) => {
|
|
95
105
|
if (leaving) return;
|
|
96
106
|
onEvent({
|
|
97
107
|
type: "disconnected",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/avatar-live/receive-only.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * BOFF-9797. The live avatar's room, joined to watch and listen only.\n *\n * A visitor's browser on a product website holds a subscribe-only room token: it can receive the avatar worker's\n * video and voice and do nothing else. This adapter keeps to that from its side too:\n *\n * - **Nothing is subscribed automatically** (`autoSubscribe: false`). Only the tracks of `workerIdentity`, the avatar\n * worker the create answer named, are subscribed, and only their video and audio. Any other participant's track\n * (the server's own speaker included) is never asked for, and is dropped again should the server send it anyway.\n * - **Nothing is published.** No microphone, no camera, no data, no RPC method: this file never calls a publishing\n * API, and the token would refuse one.\n * - **Safari and iOS** hold sound back until a gesture. The room reports it (`playback-blocked`, with `sound` when the\n * sound is held back and not only the video) and the panel offers \"Tap to turn on sound\", whose tap calls\n * `enablePlayback`.\n * - **The end of the speech** is told (`speech-ended`) from what any viewer of the room sees: the other voice in it, the\n * server's speaker that the worker follows, leaving the room or unpublishing its voice. Its track is never subscribed.\n *\n * Written for the website from the media library's public API: nothing here is copied from the product apps.\n */\nimport {\n DisconnectReason,\n Room,\n RoomEvent,\n Track,\n type RemoteParticipant,\n type RemoteTrack,\n type RemoteTrackPublication,\n} from \"livekit-client\";\n\nimport type { ReceiveOnlyJoinOptions, ReceiveOnlySession } from \"./types\";\n\n/** The reasons a room closes because the server closed it, rather than because the connection was lost. */\nconst CLOSED_BY_SERVER: ReadonlySet<DisconnectReason> = new Set([\n DisconnectReason.ROOM_DELETED,\n DisconnectReason.ROOM_CLOSED,\n DisconnectReason.PARTICIPANT_REMOVED,\n]);\n\nfunction isMedia(publication: RemoteTrackPublication): boolean {\n return (\n publication.kind === Track.Kind.Video ||\n publication.kind === Track.Kind.Audio\n );\n}\n\n/**\n * Join the room in `options.url` with the receive-only `options.token`, and play the avatar worker's video and voice\n * in `options.video` and `options.audio`. Resolves once connected; rejects when the room cannot be joined (the panel\n * then ends the session).\n */\nexport async function joinReceiveOnly(\n options: ReceiveOnlyJoinOptions,\n): Promise<ReceiveOnlySession> {\n const { url, token, workerIdentity, video, audio, onEvent } = options;\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error(\"live avatar: the room address is not a URL\");\n }\n if (parsed.protocol !== \"wss:\") {\n throw new Error(\"live avatar: the room address must be wss:\");\n }\n if (!token || !workerIdentity) {\n throw new Error(\"live avatar: the room token or speaker is missing\");\n }\n\n const room = new Room({\n // One small video, always shown while the session lasts: no quality switching, no layer pausing.\n adaptiveStream: false,\n dynacast: false,\n // Closing the tab or leaving the page leaves the room at once. Belt and braces with the panel's own page-hide\n // handler, which also asks the server to end the session: either may run first, and both are safe to repeat.\n disconnectOnPageLeave: true,\n });\n let leaving = false;\n\n const isSpeaker = (participant: RemoteParticipant) =>\n participant.identity === workerIdentity;\n\n /**\n * Everyone else in the room seen publishing a voice: the server's speaker, whose speech the worker voices. Noted\n * only, never subscribed to, so that the end of that speech can be told (`speech-ended`).\n */\n const voiced = new Set<string>();\n let speechEnded = false;\n\n /** Subscribe to `publication` when it is the speaker's video or audio; leave everything else alone. */\n const consider = (\n publication: RemoteTrackPublication,\n participant: RemoteParticipant,\n ) => {\n if (leaving) return;\n if (!isSpeaker(participant)) {\n if (publication.kind === Track.Kind.Audio) {\n voiced.add(participant.identity);\n }\n return;\n }\n if (!isMedia(publication)) return;\n if (!publication.isSubscribed) publication.setSubscribed(true);\n };\n\n /** A voice seen published has gone (its track, or its participant): the speech has ended at its source. Once. */\n const voiceGone = (participant: RemoteParticipant) => {\n if (leaving || speechEnded || !voiced.has(participant.identity)) return;\n speechEnded = true;\n onEvent({ type: \"speech-ended\" });\n };\n\n const reportPlayback = () => {\n if (leaving) return;\n onEvent({\n type: \"playback-blocked\",\n blocked: !room.canPlaybackAudio || !room.canPlaybackVideo,\n sound: !room.canPlaybackAudio,\n });\n };\n\n room\n .on(RoomEvent.TrackPublished, consider)\n .on(\n RoomEvent.TrackSubscribed,\n (\n track: RemoteTrack,\n publication: RemoteTrackPublication,\n participant: RemoteParticipant,\n ) => {\n if (leaving) return;\n if (!isSpeaker(participant) || !isMedia(publication)) {\n // Never asked for: drop it rather than play it.\n publication.setSubscribed(false);\n return;\n }\n if (track.kind === Track.Kind.Video) {\n track.attach(video);\n onEvent({ type: \"speaker-track\", kind: \"video\" });\n } else {\n track.attach(audio);\n onEvent({ type: \"speaker-track\", kind: \"audio\" });\n }\n },\n )\n .on(RoomEvent.TrackUnsubscribed, (track: RemoteTrack) => {\n track.detach();\n })\n .on(\n RoomEvent.TrackUnpublished,\n (publication: RemoteTrackPublication, participant: RemoteParticipant) => {\n if (!isSpeaker(participant) && publication.kind === Track.Kind.Audio) {\n voiceGone(participant);\n }\n },\n )\n .on(RoomEvent.ParticipantDisconnected, (participant: RemoteParticipant) => {\n if (leaving) return;\n if (isSpeaker(participant)) {\n onEvent({ type: \"speaker-left\" });\n } else {\n voiceGone(participant);\n }\n })\n .on(RoomEvent.AudioPlaybackStatusChanged, reportPlayback)\n .on(RoomEvent.VideoPlaybackStatusChanged, reportPlayback)\n .on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {\n if (leaving) return;\n onEvent({\n type: \"disconnected\",\n byServer: reason !== undefined && CLOSED_BY_SERVER.has(reason),\n });\n });\n\n const detachAll = () => {\n for (const participant of room.remoteParticipants.values()) {\n for (const publication of participant.trackPublications.values()) {\n publication.track?.detach();\n }\n }\n video.srcObject = null;\n audio.srcObject = null;\n };\n\n try {\n await room.connect(url, token, { autoSubscribe: false });\n } catch (error) {\n leaving = true;\n room.removeAllListeners();\n detachAll();\n throw error;\n }\n\n // Whoever was in the room before this viewer joined: their tracks were published before the listener above.\n for (const participant of room.remoteParticipants.values()) {\n for (const publication of participant.trackPublications.values()) {\n consider(publication, participant);\n }\n }\n reportPlayback();\n\n return {\n enablePlayback: async () => {\n // Both started inside the visitor's gesture, before anything is awaited: Safari allows playback only there.\n // A refusal leaves the room as it was, and the report below keeps the button offered.\n await Promise.allSettled([room.startAudio(), room.startVideo()]);\n reportPlayback();\n },\n leave: async () => {\n if (leaving) return;\n leaving = true;\n detachAll();\n try {\n await room.disconnect();\n } finally {\n room.removeAllListeners();\n }\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/avatar-live/receive-only.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * BOFF-9797. The live avatar's room, joined to watch and listen only.\n *\n * A visitor's browser on a product website holds a subscribe-only room token: it can receive the avatar worker's\n * video and voice and do nothing else. This adapter keeps to that from its side too:\n *\n * - **Nothing is subscribed automatically** (`autoSubscribe: false`). Only the tracks of `workerIdentity`, the avatar\n * worker the create answer named, are subscribed, and only their video and audio. Any other participant's track\n * (the server's own speaker included) is never asked for, and is dropped again should the server send it anyway.\n * - **Nothing is published.** No microphone, no camera, no data, no RPC method: this file never calls a publishing\n * API, and the token would refuse one.\n * - **Safari and iOS** hold sound back until a gesture. The room reports it (`playback-blocked`, with `sound` when the\n * sound is held back and not only the video) and the panel offers \"Tap to turn on sound\", whose tap calls\n * `enablePlayback`.\n * - **The end of the speech** is told (`speech-ended`) from what any viewer of the room sees: the other voice in it, the\n * server's speaker that the worker follows, leaving the room or unpublishing its voice. Its track is never subscribed.\n * - **Whether the worker's voice is heard** is told (`speaking`) each time it changes, from the room server's active\n * speakers, which it reports to a speaker's subscribers.\n *\n * Written for the website from the media library's public API: nothing here is copied from the product apps.\n */\nimport {\n DisconnectReason,\n Room,\n RoomEvent,\n Track,\n type Participant,\n type RemoteParticipant,\n type RemoteTrack,\n type RemoteTrackPublication,\n} from \"livekit-client\";\n\nimport type { ReceiveOnlyJoinOptions, ReceiveOnlySession } from \"./types\";\n\n/** The reasons a room closes because the server closed it, rather than because the connection was lost. */\nconst CLOSED_BY_SERVER: ReadonlySet<DisconnectReason> = new Set([\n DisconnectReason.ROOM_DELETED,\n DisconnectReason.ROOM_CLOSED,\n DisconnectReason.PARTICIPANT_REMOVED,\n]);\n\nfunction isMedia(publication: RemoteTrackPublication): boolean {\n return (\n publication.kind === Track.Kind.Video ||\n publication.kind === Track.Kind.Audio\n );\n}\n\n/**\n * Join the room in `options.url` with the receive-only `options.token`, and play the avatar worker's video and voice\n * in `options.video` and `options.audio`. Resolves once connected; rejects when the room cannot be joined (the panel\n * then ends the session).\n */\nexport async function joinReceiveOnly(\n options: ReceiveOnlyJoinOptions,\n): Promise<ReceiveOnlySession> {\n const { url, token, workerIdentity, video, audio, onEvent } = options;\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error(\"live avatar: the room address is not a URL\");\n }\n if (parsed.protocol !== \"wss:\") {\n throw new Error(\"live avatar: the room address must be wss:\");\n }\n if (!token || !workerIdentity) {\n throw new Error(\"live avatar: the room token or speaker is missing\");\n }\n\n const room = new Room({\n // One small video, always shown while the session lasts: no quality switching, no layer pausing.\n adaptiveStream: false,\n dynacast: false,\n // Closing the tab or leaving the page leaves the room at once. Belt and braces with the panel's own page-hide\n // handler, which also asks the server to end the session: either may run first, and both are safe to repeat.\n disconnectOnPageLeave: true,\n });\n let leaving = false;\n\n const isSpeaker = (participant: RemoteParticipant) =>\n participant.identity === workerIdentity;\n\n /**\n * Everyone else in the room seen publishing a voice: the server's speaker, whose speech the worker voices. Noted\n * only, never subscribed to, so that the end of that speech can be told (`speech-ended`).\n */\n const voiced = new Set<string>();\n let speechEnded = false;\n\n /** Subscribe to `publication` when it is the speaker's video or audio; leave everything else alone. */\n const consider = (\n publication: RemoteTrackPublication,\n participant: RemoteParticipant,\n ) => {\n if (leaving) return;\n if (!isSpeaker(participant)) {\n if (publication.kind === Track.Kind.Audio) {\n voiced.add(participant.identity);\n }\n return;\n }\n if (!isMedia(publication)) return;\n if (!publication.isSubscribed) publication.setSubscribed(true);\n };\n\n /** A voice seen published has gone (its track, or its participant): the speech has ended at its source. Once. */\n const voiceGone = (participant: RemoteParticipant) => {\n if (leaving || speechEnded || !voiced.has(participant.identity)) return;\n speechEnded = true;\n onEvent({ type: \"speech-ended\" });\n };\n\n const reportPlayback = () => {\n if (leaving) return;\n onEvent({\n type: \"playback-blocked\",\n blocked: !room.canPlaybackAudio || !room.canPlaybackVideo,\n sound: !room.canPlaybackAudio,\n });\n };\n\n /** Whether the worker's voice is heard, as last told: `speaking` goes out only when it changes. */\n let workerSpeaking = false;\n const reportSpeakers = (speakers: Participant[]) => {\n if (leaving) return;\n const speaking = speakers.some(\n (speaker) => speaker.identity === workerIdentity,\n );\n if (speaking === workerSpeaking) return;\n workerSpeaking = speaking;\n onEvent({ type: \"speaking\", speaking });\n };\n\n room\n .on(RoomEvent.TrackPublished, consider)\n .on(\n RoomEvent.TrackSubscribed,\n (\n track: RemoteTrack,\n publication: RemoteTrackPublication,\n participant: RemoteParticipant,\n ) => {\n if (leaving) return;\n if (!isSpeaker(participant) || !isMedia(publication)) {\n // Never asked for: drop it rather than play it.\n publication.setSubscribed(false);\n return;\n }\n if (track.kind === Track.Kind.Video) {\n track.attach(video);\n onEvent({ type: \"speaker-track\", kind: \"video\" });\n } else {\n track.attach(audio);\n onEvent({ type: \"speaker-track\", kind: \"audio\" });\n }\n },\n )\n .on(RoomEvent.TrackUnsubscribed, (track: RemoteTrack) => {\n track.detach();\n })\n .on(\n RoomEvent.TrackUnpublished,\n (publication: RemoteTrackPublication, participant: RemoteParticipant) => {\n if (!isSpeaker(participant) && publication.kind === Track.Kind.Audio) {\n voiceGone(participant);\n }\n },\n )\n .on(RoomEvent.ParticipantDisconnected, (participant: RemoteParticipant) => {\n if (leaving) return;\n if (isSpeaker(participant)) {\n onEvent({ type: \"speaker-left\" });\n } else {\n voiceGone(participant);\n }\n })\n .on(RoomEvent.AudioPlaybackStatusChanged, reportPlayback)\n .on(RoomEvent.VideoPlaybackStatusChanged, reportPlayback)\n .on(RoomEvent.ActiveSpeakersChanged, reportSpeakers)\n .on(RoomEvent.Disconnected, (reason?: DisconnectReason) => {\n if (leaving) return;\n onEvent({\n type: \"disconnected\",\n byServer: reason !== undefined && CLOSED_BY_SERVER.has(reason),\n });\n });\n\n const detachAll = () => {\n for (const participant of room.remoteParticipants.values()) {\n for (const publication of participant.trackPublications.values()) {\n publication.track?.detach();\n }\n }\n video.srcObject = null;\n audio.srcObject = null;\n };\n\n try {\n await room.connect(url, token, { autoSubscribe: false });\n } catch (error) {\n leaving = true;\n room.removeAllListeners();\n detachAll();\n throw error;\n }\n\n // Whoever was in the room before this viewer joined: their tracks were published before the listener above.\n for (const participant of room.remoteParticipants.values()) {\n for (const publication of participant.trackPublications.values()) {\n consider(publication, participant);\n }\n }\n reportPlayback();\n\n return {\n enablePlayback: async () => {\n // Both started inside the visitor's gesture, before anything is awaited: Safari allows playback only there.\n // A refusal leaves the room as it was, and the report below keeps the button offered.\n await Promise.allSettled([room.startAudio(), room.startVideo()]);\n reportPlayback();\n },\n leave: async () => {\n if (leaving) return;\n leaving = true;\n detachAll();\n try {\n await room.disconnect();\n } finally {\n room.removeAllListeners();\n }\n },\n };\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import { E as ExplorePanelAvatarLive, a as ExploreLocale, A as AvatarConversation } from '../explore-avatar-live-config-
|
|
4
|
-
import '../types-
|
|
3
|
+
import { E as ExplorePanelAvatarLive, a as ExploreLocale, A as AvatarConversation } from '../explore-avatar-live-config-BL8FTm0I.mjs';
|
|
4
|
+
import '../types-CTUJNvQu.mjs';
|
|
5
5
|
import '../use-explore-chat-CbfohA9O.mjs';
|
|
6
6
|
import '../explore-types-Ol1Mm3dW.mjs';
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import { E as ExplorePanelAvatarLive, a as ExploreLocale, A as AvatarConversation } from '../explore-avatar-live-config-
|
|
4
|
-
import '../types-
|
|
3
|
+
import { E as ExplorePanelAvatarLive, a as ExploreLocale, A as AvatarConversation } from '../explore-avatar-live-config-auLKxSzx.js';
|
|
4
|
+
import '../types-CTUJNvQu.js';
|
|
5
5
|
import '../use-explore-chat-B3dOOvJN.js';
|
|
6
6
|
import '../explore-types-Ol1Mm3dW.js';
|
|
7
7
|
|