@alxxsck/ai-assistant 1.5.0 → 1.8.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 realtime voice mode backed by Lola WebRTC
6
+ - optional realtime voice mode backed by the Lola Socket.IO proxy and xAI
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
- - Lola + OpenAI Realtime WebRTC (voice mode)
28
+ - Socket.IO + AudioWorklet (realtime voice mode through Lola Backend)
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` - Lola voice session, WebRTC media, and realtime events
40
+ - `src/domain/voice` - Lola voice session, PCM media, and xAI 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
@@ -78,17 +78,27 @@ High-level structure:
78
78
  - agentic steps merge/update and rendering support
79
79
 
80
80
  Live updates arrive via AnyCable channel events and are pushed into the store.
81
+ Versioned `messageDelta` updates are applied by `messageId` and increasing
82
+ `sequence`; each frame contains the current accumulated text, so duplicate or
83
+ stale frames cannot append content twice. The UI renders every accepted frame
84
+ immediately and animates only the newly arrived suffix. See
85
+ [`docs/chat-streaming.ru.md`](docs/chat-streaming.ru.md) for the wire contract,
86
+ lifecycle, rendering, and scroll behavior.
81
87
 
82
88
  ### 4) Voice Domain
83
89
 
84
90
  `useVoiceStore` manages the Lola voice turn lifecycle:
85
91
 
86
92
  - 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
+ - captures mono microphone input and converts it to PCM16 24 kHz in an AudioWorklet
94
+ - connects to the authenticated `/assistant` Socket.IO namespace, activates the
95
+ provider session with `voice.connect`, and sends sequenced Lola protocol v4
96
+ `voice.audio.append` events with acknowledgements
97
+ - plays `response.output_audio.delta` through a continuous AudioWorklet queue
98
+ - unwraps canonical `voice.event` envelopes, renders Grok realtime transcripts,
99
+ and keeps avatar talking state in sync
100
+ - creates a fresh VoiceSession after an unexpected voice socket disconnect
101
+ - closes the session through `voice.disconnect` with the REST endpoint as a fallback
92
102
 
93
103
  ### 5) Avatar Domain
94
104
 
@@ -130,7 +140,7 @@ Actual widget config
130
140
  - `apiUrl: string`
131
141
  - `AIAvatar?: boolean` - is 3D avatar be used
132
142
  - `voiceEnabled?: boolean` - is voice mode enebled
133
- - `voiceConfig?: { voice: string }` - optional Lola Realtime voice
143
+ - `voiceConfig?: { voice: "ara" | "eve" | "leo" | "rex" | "sal" }` - optional xAI voice
134
144
  - `background?: boolean` - clear or visible background
135
145
  - `footer?: boolean` - is footer rendered
136
146
  - `debug?: boolean` (avatar debug GUI)
@@ -226,6 +236,10 @@ Outputs include:
226
236
 
227
237
  ### Other project scripts
228
238
 
239
+ The demo app reads its service credential from
240
+ `VITE_SERVICE_ACCOUNT_API_KEY`; credentials must not be committed to source or
241
+ static config files.
242
+
229
243
  - `npm run lint`
230
244
  - `npm run lint-fix`
231
245
  - `npm run format`
@@ -346,6 +360,6 @@ This allows cache-friendly hashed asset delivery without changing code reference
346
360
  - No live updates:
347
361
  - verify `wsUrl`, websocket reachability, and token validity
348
362
  - Voice fails to start:
349
- - verify mic permission, Lola `apiUrl`, backend `Project.settings.voiceEnabled`, and OpenAI Realtime configuration
363
+ - verify mic permission, Lola `apiUrl`, Socket.IO reachability, and backend `Project.settings.voiceEnabled`
350
364
  - Avatar never becomes ready:
351
365
  - verify manifest URL + asset availability under static host
@@ -29337,6 +29337,8 @@ class iS extends EventTarget {
29337
29337
  };
29338
29338
  controls;
29339
29339
  locale;
29340
+ animationFrame = null;
29341
+ renderingEnabled = !0;
29340
29342
  lightAmbient;
29341
29343
  lightDirectional;
29342
29344
  loadingManager;
@@ -29526,10 +29528,19 @@ class iS extends EventTarget {
29526
29528
  !e || !t || e == this.width && t == this.height || (this.camera.aspect = e / t, this.camera.updateProjectionMatrix(), this.renderer.setSize(e, t, !1), this.render(), this.width = e, this.height = t);
29527
29529
  };
29528
29530
  animate = () => {
29529
- requestAnimationFrame(this.animate);
29531
+ if (this.animationFrame = null, !this.renderingEnabled) return;
29530
29532
  const e = this.clock.getDelta(), t = this.sound?.seek();
29531
- this.animationManager.update(e, t), this.controls?.update(), this.render();
29533
+ this.animationManager.update(e, t), this.controls?.update(), this.render(), this.animationFrame = requestAnimationFrame(this.animate);
29532
29534
  };
29535
+ setRenderingEnabled(e) {
29536
+ if (this.renderingEnabled !== e) {
29537
+ if (this.renderingEnabled = e, !e) {
29538
+ this.animationFrame !== null && (cancelAnimationFrame(this.animationFrame), this.animationFrame = null), this.clock.stop();
29539
+ return;
29540
+ }
29541
+ this.clock.start(), this.animationFrame === null && this.animationManager && (this.animationFrame = requestAnimationFrame(this.animate));
29542
+ }
29543
+ }
29533
29544
  SPEAKING_ANIMATION_NAME_1 = "generic_speaking_1";
29534
29545
  SPEAKING_ANIMATION_NAME_2 = "generic_speaking_2";
29535
29546
  IDLE_ANIMATION_NAME_1 = "idle_1";
@@ -29593,7 +29604,7 @@ class iS extends EventTarget {
29593
29604
  this.api.onAvatarReady(this.externalLaunchAnimationAction);
29594
29605
  };
29595
29606
  async initDebugControls(e) {
29596
- const { OrbitControls: t } = await import("./OrbitControls-BCIW0o5s.js");
29607
+ const { OrbitControls: t } = await import("./OrbitControls-BhEr8WlG.js");
29597
29608
  this.controls = new t(e, this.renderer.domElement);
29598
29609
  }
29599
29610
  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-_GvrPzkn.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-BAlmduYw.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,
@@ -16,5 +16,5 @@ export declare const listConversations: (config: WidgetConfig, api: AppApi) => P
16
16
  export declare const listConversationMessages: (config: WidgetConfig, api: AppApi, conversationId: string) => Promise<MessageData[]>;
17
17
  export declare const createConversation: (config: WidgetConfig, api: AppApi) => Promise<ConversationData>;
18
18
  export declare const selectConversation: (config: WidgetConfig, api: AppApi, conversationId: string) => Promise<ConversationData>;
19
- export declare function sendMessage(config: WidgetConfig, api: AppApi, messageText: string, conversationId?: string): Promise<MessageData>;
19
+ export declare function sendMessage(config: WidgetConfig, api: AppApi, messageText: string, conversationId?: string, clientMessageId?: `${string}-${string}-${string}-${string}-${string}`): Promise<MessageData>;
20
20
  export {};
@@ -85,4 +85,10 @@ export type AgenticStepsAppendedData = {
85
85
  threadId: string;
86
86
  agenticSteps: AgenticStep[];
87
87
  };
88
+ export type ChatMessageDeltaData = {
89
+ messageId: string;
90
+ sequence: number;
91
+ text: string;
92
+ done: boolean;
93
+ };
88
94
  export {};
@@ -1,10 +1,11 @@
1
1
  import type { AppApi } from "@/App";
2
2
  import type { WidgetConfig } from "@/index.types";
3
- import type { ConnectVoiceSessionData, VoiceSessionData } from "./voice.api.types";
3
+ import type { VoiceSessionData } from "./voice.api.types";
4
4
  export declare class VoiceApiError extends Error {
5
5
  readonly status: number;
6
6
  constructor(status: number, message: string);
7
7
  }
8
+ export declare const parseVoiceSession: (value: unknown) => VoiceSessionData;
8
9
  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 getVoiceSession: (config: WidgetConfig, api: AppApi, voiceSessionId: string) => Promise<VoiceSessionData>;
10
11
  export declare const endVoiceSession: (config: WidgetConfig, api: AppApi, voiceSessionId: string, reason?: string) => Promise<VoiceSessionData>;
@@ -1,21 +1,19 @@
1
+ export type VoiceSessionStatus = "CREATED" | "CONNECTING" | "ACTIVE" | "ENDED" | "FAILED" | "EXPIRED";
2
+ export type VoiceWebsocketContract = {
3
+ namespace: "/assistant";
4
+ connectEvent: "voice.connect";
5
+ audioAppendEvent: "voice.audio.append";
6
+ disconnectEvent: "voice.disconnect";
7
+ serverEvent: "voice.event";
8
+ eventProtocol: "xai-realtime-v1";
9
+ };
1
10
  export type VoiceSessionData = {
2
11
  voiceSessionId: string;
3
12
  conversationId: string;
4
- status: string;
13
+ status: VoiceSessionStatus;
5
14
  voiceConfig: {
6
15
  voice: string;
7
16
  };
8
17
  voiceProtocolVersion: number;
9
- webrtc: {
10
- offerPath: string;
11
- dataChannel: string;
12
- eventProtocol: string;
13
- };
14
- };
15
- export type ConnectVoiceSessionData = {
16
- voiceSessionId: string;
17
- answerSdp: string;
18
- model: string;
19
- voice: string;
20
- dataChannel: string;
18
+ websocket: VoiceWebsocketContract;
21
19
  };
@@ -34,6 +34,8 @@ export declare class Avatar extends EventTarget {
34
34
  private cameraParams;
35
35
  private controls?;
36
36
  private locale?;
37
+ private animationFrame;
38
+ private renderingEnabled;
37
39
  constructor(canvas: HTMLCanvasElement, videoContainer: HTMLDivElement, config: WidgetConfig, api: AppApi);
38
40
  private lightAmbient;
39
41
  private lightDirectional;
@@ -47,6 +49,7 @@ export declare class Avatar extends EventTarget {
47
49
  private height;
48
50
  onResize: (width?: number, height?: number) => void;
49
51
  animate: () => void;
52
+ setRenderingEnabled(enabled: boolean): void;
50
53
  private SPEAKING_ANIMATION_NAME_1;
51
54
  private SPEAKING_ANIMATION_NAME_2;
52
55
  private IDLE_ANIMATION_NAME_1;