@alxxsck/ai-assistant 1.0.0 → 1.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 CHANGED
@@ -3,7 +3,7 @@
3
3
  Embeddable React + TypeScript chat widget for Bridge AI agents, with:
4
4
 
5
5
  - text chat
6
- - optional real-time voice mode (WebRTC)
6
+ - optional realtime voice mode backed by Lola WebRTC
7
7
  - optional animated 3D avatar (three.js)
8
8
  - WebSocket live message updates (AnyCable)
9
9
  - Shadow DOM isolation for host-page style safety
@@ -25,7 +25,7 @@ The instance mounts a React app into a host element's Shadow DOM, fetches static
25
25
  - Zustand (state stores)
26
26
  - three.js + FBX assets (avatar rendering/animation)
27
27
  - AnyCable Web client (live chat transport)
28
- - WebRTC + data channels (voice mode)
28
+ - Lola + OpenAI Realtime WebRTC (voice mode)
29
29
 
30
30
  ## Repository Layout
31
31
 
@@ -37,7 +37,7 @@ High-level structure:
37
37
  - `src/react/mount.tsx` - React root mount helper
38
38
  - `src/ui/components` - UI components (chat widget, routes, controls)
39
39
  - `src/domain/message` - message store + websocket channel integration
40
- - `src/domain/voice` - voice lifecycle + WebRTC state
40
+ - `src/domain/voice` - Lola voice session, WebRTC media, and realtime events
41
41
  - `src/avatar` - three.js avatar scene, animation manager, blend shapes
42
42
  - `src/api` - HTTP clients for messages/voice/session-related calls
43
43
  - `src/context/ChatWidgetContext.tsx` - widget-level UI state/context
@@ -81,15 +81,14 @@ Live updates arrive via AnyCable channel events and are pushed into the store.
81
81
 
82
82
  ### 4) Voice Domain
83
83
 
84
- `useVoiceStore` manages full voice call lifecycle:
84
+ `useVoiceStore` manages the Lola voice turn lifecycle:
85
85
 
86
- - starts voice session via API (`StartVoiceSession`)
87
- - builds RTCPeerConnection using TURN credentials from session payload
88
- - sends SDP offer / receives answer
89
- - buffers + sends ICE candidates
90
- - consumes voice data channels (`voice-events`, `voice-input`)
91
- - streams remote audio to an `Audio` element
92
- - handles mic enable/disable, device switching, and teardown (`EndSession` + `EndVoiceSession`)
86
+ - creates a canonical `POST /voice/sessions` session
87
+ - captures the microphone with `getUserMedia`
88
+ - exchanges SDP through the backend-provided `/voice/sessions/:id/webrtc` path
89
+ - plays the OpenAI Realtime remote audio track
90
+ - renders native realtime transcripts and keeps avatar talking state in sync
91
+ - closes media resources and calls `/voice/sessions/:id/end`
93
92
 
94
93
  ### 5) Avatar Domain
95
94
 
@@ -131,6 +130,7 @@ Actual widget config
131
130
  - `apiUrl: string`
132
131
  - `AIAvatar?: boolean` - is 3D avatar be used
133
132
  - `voiceEnabled?: boolean` - is voice mode enebled
133
+ - `voiceConfig?: { voice: string }` - optional Lola Realtime voice
134
134
  - `background?: boolean` - clear or visible background
135
135
  - `footer?: boolean` - is footer rendered
136
136
  - `debug?: boolean` (avatar debug GUI)
@@ -340,6 +340,6 @@ This allows cache-friendly hashed asset delivery without changing code reference
340
340
  - No live updates:
341
341
  - verify `wsUrl`, websocket reachability, and token validity
342
342
  - Voice fails to start:
343
- - verify mic permission, TURN reachability, and voice API endpoints
343
+ - verify mic permission, Lola `apiUrl`, backend `Project.settings.voiceEnabled`, and OpenAI Realtime configuration
344
344
  - Avatar never becomes ready:
345
345
  - verify manifest URL + asset availability under static host
@@ -29156,6 +29156,7 @@ class nS {
29156
29156
  animations = [];
29157
29157
  zeroMorphingInfluences = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
29158
29158
  visemeDataIndex = 0;
29159
+ previousSpeechTime = 0;
29159
29160
  visemeData;
29160
29161
  mixer;
29161
29162
  actions = /* @__PURE__ */ new Map();
@@ -29205,9 +29206,12 @@ class nS {
29205
29206
  this.actions.set(t.name, {
29206
29207
  animation: i,
29207
29208
  blendShapeScenario: r
29208
- }), this.loadedAnimations++, this.totalAnimations === this.loadedAnimations && this.avatar.onAllAssetsLoaded();
29209
+ }), this.avatar.onAnimationLoaded(t.name), this.loadedAnimations++, this.totalAnimations === this.loadedAnimations && this.avatar.onAllAssetsLoaded();
29209
29210
  }
29210
29211
  fadeDuration = 0.2;
29212
+ hasAction(e) {
29213
+ return this.actions.has(e);
29214
+ }
29211
29215
  fadeToAction(e) {
29212
29216
  const t = this.actions.get(this.activeActionName);
29213
29217
  this.activeActionName = e;
@@ -29218,7 +29222,7 @@ class nS {
29218
29222
  this.actions.get(this.activeActionName).blendShapeScenario?.play();
29219
29223
  }
29220
29224
  resetVisemeData() {
29221
- this.visemeDataIndex = 0;
29225
+ this.visemeDataIndex = 0, this.previousSpeechTime = 0;
29222
29226
  }
29223
29227
  setVisemeData() {
29224
29228
  this.visemeData = Jy.trim().split(`
@@ -29233,7 +29237,7 @@ class nS {
29233
29237
  convertVisemeDataToAnimationScenario() {
29234
29238
  }
29235
29239
  update(e, t = 0) {
29236
- this.mixer && this.mixer.update(e), !(this.visemeDataIndex >= this.visemeData.length) && t && t >= this.visemeData[this.visemeDataIndex].time - this.blendShapeResetAnimationLength * 0.7 && (this.playVisemeAnimation(
29240
+ this.mixer && this.mixer.update(e), t < this.previousSpeechTime && (this.visemeDataIndex = 0), this.previousSpeechTime = t, !(this.visemeDataIndex >= this.visemeData.length) && t && t >= this.visemeData[this.visemeDataIndex].time - this.blendShapeResetAnimationLength * 0.7 && (this.playVisemeAnimation(
29237
29241
  t,
29238
29242
  this.visemeData[this.visemeDataIndex],
29239
29243
  this.visemeData[this.visemeDataIndex + 1]
@@ -29531,8 +29535,22 @@ class iS extends EventTarget {
29531
29535
  IDLE_ANIMATION_NAME_2 = "waiting_idle_2";
29532
29536
  speakingAnimationName = this.SPEAKING_ANIMATION_NAME_1;
29533
29537
  idleAnimationName = this.IDLE_ANIMATION_NAME_1;
29538
+ talkingRequested = !1;
29539
+ talkingApplied = !1;
29534
29540
  setTalking(e) {
29535
- e ? (this.sound.play(), this.animationManager.fadeToAction(this.speakingAnimationName)) : (this.sound.stop(), this.sound.seek(0), this.animationManager.resetMorphing(), this.animationManager.resetVisemeData(), this.speakingAnimationName = this.speakingAnimationName === this.SPEAKING_ANIMATION_NAME_1 ? this.SPEAKING_ANIMATION_NAME_2 : this.SPEAKING_ANIMATION_NAME_1, this.idleAnimationName = this.idleAnimationName === this.IDLE_ANIMATION_NAME_1 ? this.IDLE_ANIMATION_NAME_2 : this.IDLE_ANIMATION_NAME_1, this.animationManager.activeActionName === this.speakingAnimationName && this.animationManager.fadeToAction(this.idleAnimationName));
29541
+ if (this.talkingRequested = e, e) {
29542
+ this.applyTalkingWhenReady();
29543
+ return;
29544
+ }
29545
+ if (!this.talkingApplied || !this.sound || !this.animationManager) return;
29546
+ const t = this.animationManager.activeActionName === this.speakingAnimationName;
29547
+ this.sound.loop(!1), this.sound.stop(), this.sound.seek(0), this.animationManager.resetMorphing(), this.animationManager.resetVisemeData(), this.talkingApplied = !1, this.speakingAnimationName = this.speakingAnimationName === this.SPEAKING_ANIMATION_NAME_1 ? this.SPEAKING_ANIMATION_NAME_2 : this.SPEAKING_ANIMATION_NAME_1, this.idleAnimationName = this.idleAnimationName === this.IDLE_ANIMATION_NAME_1 ? this.IDLE_ANIMATION_NAME_2 : this.IDLE_ANIMATION_NAME_1, t && this.animationManager.hasAction(this.idleAnimationName) && this.animationManager.fadeToAction(this.idleAnimationName);
29548
+ }
29549
+ applyTalkingWhenReady() {
29550
+ !this.talkingRequested || this.talkingApplied || !this.sound || !this.animationManager?.hasAction(this.speakingAnimationName) || (this.sound.loop(!0), this.sound.play(), this.animationManager.fadeToAction(this.speakingAnimationName), this.talkingApplied = !0);
29551
+ }
29552
+ onAnimationLoaded(e) {
29553
+ e === this.speakingAnimationName && this.applyTalkingWhenReady();
29536
29554
  }
29537
29555
  playReaction(e, t) {
29538
29556
  t === "win_big" && (t = "win_small"), t === "welcome_game" && (t = "spin"), this.animationManager.fadeToAction(t), this.animationManager.resetVisemeData(), this.speakPhrase(e);
@@ -29574,7 +29592,7 @@ class iS extends EventTarget {
29574
29592
  this.api.onAvatarReady(this.externalLaunchAnimationAction);
29575
29593
  };
29576
29594
  async initDebugControls(e) {
29577
- const { OrbitControls: t } = await import("./OrbitControls-XelJ40xM.js");
29595
+ const { OrbitControls: t } = await import("./OrbitControls-BE3BANZi.js");
29578
29596
  this.controls = new t(e, this.renderer.domElement);
29579
29597
  }
29580
29598
  async addDebugGui() {
@@ -1,4 +1,4 @@
1
- import { C as S, V as p, M as u, T as _, Q as E, S as P, a as c, R as w, P as O, b as R } from "./Avatar-XISRbl7X.js";
1
+ import { C as S, V as p, M as u, T as _, Q as E, S as P, a as c, R as w, P as O, b as R } from "./Avatar-B6ktAPlH.js";
2
2
  const D = { type: "change" }, b = { type: "start" }, M = { type: "end" }, f = new w(), T = new O(), j = Math.cos(70 * R.DEG2RAD), n = new p(), r = 2 * Math.PI, o = {
3
3
  NONE: -1,
4
4
  ROTATE: 0,
@@ -0,0 +1,20 @@
1
+ import type { AppApi } from "@/App";
2
+ import type { ProtectedAudioMimeType, StreamingSpeechMimeType } from "@/domain/speech/speech.constants";
3
+ import type { WidgetConfig } from "@/index.types";
4
+ export type ProtectedAudioRequest = {
5
+ audioPath: string;
6
+ method: "POST";
7
+ mimeType: ProtectedAudioMimeType;
8
+ };
9
+ export type StreamingAudioRequest = ProtectedAudioRequest & {
10
+ mimeType: StreamingSpeechMimeType;
11
+ format: "pcm16";
12
+ sampleRate: 24_000;
13
+ channels: 1;
14
+ };
15
+ export declare class AudioApiError extends Error {
16
+ readonly status: number;
17
+ constructor(status: number, message: string);
18
+ }
19
+ export declare const fetchProtectedAudio: (config: WidgetConfig, api: AppApi, request: ProtectedAudioRequest) => Promise<Blob>;
20
+ export declare const fetchProtectedAudioStream: (config: WidgetConfig, api: AppApi, request: StreamingAudioRequest, signal: AbortSignal) => Promise<ReadableStream<Uint8Array>>;
@@ -1,65 +1,10 @@
1
- import type { ChatParticipant } from "./messages.api.types";
2
1
  import type { AppApi } from "@/App";
3
- import type { GetTurnBootstrapPayload, StartVoiceSessionResponse, VoiceSessionData, VoiceTurn } from "./voice.api.types";
4
2
  import type { WidgetConfig } from "@/index.types";
5
- export declare const startVoiceSession: (config: WidgetConfig, api: AppApi) => Promise<StartVoiceSessionResponse>;
6
- export declare function getVoiceSession(config: WidgetConfig, api: AppApi, id: string): Promise<VoiceSessionData>;
7
- type VoiceSessionStatus = "VOICE_SESSION_STATUS_ENDED" | "VOICE_SESSION_STATUS_ACTIVE";
8
- type VoiceConfig = {
9
- voice: string;
10
- };
11
- type EndVoiceSessionResponse = {
12
- agentId: string;
13
- chatId: string;
14
- companyId: string;
15
- createdAt: string;
16
- endedAt: string;
17
- id: string;
18
- modelId: string;
19
- participant: ChatParticipant;
20
- signalingUrl: string;
21
- status: VoiceSessionStatus;
22
- tokenExpiresAt: string;
23
- updatedAt: string;
24
- voiceConfig: VoiceConfig;
25
- };
26
- export declare function endVoiceSession(config: WidgetConfig, api: AppApi, id: string, reason?: string): Promise<EndVoiceSessionResponse>;
27
- type SendOfferPayload = {
28
- voiceBaseUrl: string;
29
- voiceSessionId: string;
30
- sessionToken: string;
31
- clientInstanceId: string;
32
- protocolVersion: number;
33
- expectedConnectionGeneration: number;
34
- offerSdp: string;
35
- };
36
- type SendOfferResponse = {
37
- answerSdp: string;
38
- protocolVersion: number;
39
- voiceSessionId: string;
40
- connectionId: string;
41
- connectionGeneration: number;
42
- };
43
- export declare function sendOffer({ voiceBaseUrl, voiceSessionId, sessionToken, clientInstanceId, protocolVersion, expectedConnectionGeneration, offerSdp, }: SendOfferPayload): Promise<SendOfferResponse>;
44
- type SendCandidatesPayload = {
45
- voiceBaseUrl: string;
46
- voiceSessionId: string;
47
- sessionToken: string;
48
- clientInstanceId: string;
49
- connectionId: string;
50
- connectionGeneration: number;
51
- candidates: RTCIceCandidateInit[];
52
- };
53
- export declare function sendCandidates({ voiceBaseUrl, voiceSessionId, sessionToken, clientInstanceId, connectionId, connectionGeneration, candidates, }: SendCandidatesPayload): Promise<void>;
54
- type EndWebRtcSessionPayload = {
55
- voiceBaseUrl: string;
56
- voiceSessionId: string;
57
- sessionToken: string;
58
- clientInstanceId: string;
59
- connectionId: string;
60
- connectionGeneration: number;
61
- reason?: string;
62
- };
63
- export declare function endWebRtcSession({ voiceBaseUrl, voiceSessionId, sessionToken, clientInstanceId, connectionId, connectionGeneration, reason, }: EndWebRtcSessionPayload): Promise<void>;
64
- export declare function getTurnBootstrap({ voiceBaseUrl, voiceSessionId, sessionToken, clientInstanceId, protocolVersion, }: GetTurnBootstrapPayload): Promise<VoiceTurn>;
65
- export {};
3
+ import type { ConnectVoiceSessionData, VoiceSessionData } from "./voice.api.types";
4
+ export declare class VoiceApiError extends Error {
5
+ readonly status: number;
6
+ constructor(status: number, message: string);
7
+ }
8
+ export declare const startVoiceSession: (config: WidgetConfig, api: AppApi, conversationId?: string) => Promise<VoiceSessionData>;
9
+ export declare const connectVoiceSession: (config: WidgetConfig, api: AppApi, offerPath: string, offerSdp: string) => Promise<ConnectVoiceSessionData>;
10
+ export declare const endVoiceSession: (config: WidgetConfig, api: AppApi, voiceSessionId: string, reason?: string) => Promise<VoiceSessionData>;
@@ -1,37 +1,21 @@
1
- import type { ChatParticipant } from "./messages.api.types";
2
- export type StartVoiceSessionResponse = {
3
- status: number;
4
- json: VoiceSessionData | ApiErrorResponse;
5
- };
6
- export type ApiErrorResponse = {
7
- code: string;
8
- message: string;
9
- };
10
1
  export type VoiceSessionData = {
11
- session: {
12
- id: string;
13
- companyId: string;
14
- agentId: string;
15
- chatId: string;
16
- participant: ChatParticipant;
2
+ voiceSessionId: string;
3
+ conversationId: string;
4
+ status: string;
5
+ voiceConfig: {
6
+ voice: string;
17
7
  };
18
- sessionToken: string;
19
8
  voiceProtocolVersion: number;
20
- voiceBaseUrl: string;
21
- voiceTurn?: VoiceTurn;
9
+ webrtc: {
10
+ offerPath: string;
11
+ dataChannel: string;
12
+ eventProtocol: string;
13
+ };
22
14
  };
23
- export type GetTurnBootstrapPayload = {
24
- voiceBaseUrl: string;
15
+ export type ConnectVoiceSessionData = {
25
16
  voiceSessionId: string;
26
- sessionToken: string;
27
- clientInstanceId: string;
28
- protocolVersion: number;
29
- };
30
- export type VoiceTurn = {
31
- urls: string[];
32
- username: string;
33
- credential: string;
34
- credentialExpiresAt: string;
35
- realm: string;
36
- useSessionTokenCredential: boolean;
17
+ answerSdp: string;
18
+ model: string;
19
+ voice: string;
20
+ dataChannel: string;
37
21
  };
@@ -1,6 +1,6 @@
1
1
  import { Object3D } from "three";
2
2
  import type { VisemeData } from "./Avatar";
3
- import { Avatar } from "./Avatar";
3
+ import type { Avatar } from "./Avatar";
4
4
  export declare const BLEND_SHAPE_KEYS: readonly ["blendShape1.AE", "blendShape1.ChJ", "blendShape1.FV", "blendShape1.Oh", "blendShape1.Ah", "blendShape1.Ih", "blendShape1.L", "blendShape1.SZ", "blendShape1.Woo", "blendShape1.MBP", "blendShape1.Smile_Closed", "blendShape1.Smile_Open", "blendShape1.Kiss"];
5
5
  export type BlendShapeKey = (typeof BLEND_SHAPE_KEYS)[number];
6
6
  export default class AnimationManager {
@@ -10,6 +10,7 @@ export default class AnimationManager {
10
10
  private animations;
11
11
  private zeroMorphingInfluences;
12
12
  private visemeDataIndex;
13
+ private previousSpeechTime;
13
14
  private visemeData;
14
15
  private mixer;
15
16
  private actions;
@@ -23,6 +24,7 @@ export default class AnimationManager {
23
24
  private loadAnimationLazy;
24
25
  private onLoadAnimation;
25
26
  private fadeDuration;
27
+ hasAction(name: string): boolean;
26
28
  fadeToAction(name: string): void;
27
29
  applyActiveActionBlendShapeScenario(): void;
28
30
  resetVisemeData(): void;
@@ -53,7 +53,11 @@ export declare class Avatar extends EventTarget {
53
53
  private IDLE_ANIMATION_NAME_2;
54
54
  private speakingAnimationName;
55
55
  private idleAnimationName;
56
+ private talkingRequested;
57
+ private talkingApplied;
56
58
  setTalking(talking: boolean): void;
59
+ private applyTalkingWhenReady;
60
+ onAnimationLoaded(name: string): void;
57
61
  playReaction(speech: string, animation: string): void;
58
62
  speakPhrase(phrase: string): void;
59
63
  externalLaunchAnimationAction: (name: ExternallyAvailableActions) => void;