# Bridge AI Chat Widget

Embeddable React + TypeScript chat widget for Bridge AI agents, with:

- text chat
- optional real-time voice mode (WebRTC)
- optional animated 3D avatar (three.js)
- WebSocket live message updates (AnyCable)
- Shadow DOM isolation for host-page style safety

## What This Project Is

`bridgeapp-ai-chat-widget` is a library bundle (ESM + UMD) that host applications instantiate via:

- `new ChatWidgetInstance(config)`

The instance mounts a React app into a host element's Shadow DOM, fetches static asset mappings from a remote manifest, initializes chat/voice/avatar modules, and exposes a small integration API (`setOpen`, `destroy`, `setCustomerInteractionSession`, events).

## Tech Stack

- React 19
- TypeScript
- Vite (dev app + library build)
- Tailwind CSS 4
- Zustand (state stores)
- three.js + FBX assets (avatar rendering/animation)
- AnyCable Web client (live chat transport)
- WebRTC + data channels (voice mode)

## Repository Layout

High-level structure:

- `src/index.ts` - public library entry (`ChatWidgetInstance`)
- `src/index.types.ts` - public config/types surface
- `src/App.tsx` - provider composition root
- `src/react/mount.tsx` - React root mount helper
- `src/ui/components` - UI components (chat widget, routes, controls)
- `src/domain/message` - message store + websocket channel integration
- `src/domain/voice` - voice lifecycle + WebRTC state
- `src/avatar` - three.js avatar scene, animation manager, blend shapes
- `src/api` - HTTP clients for messages/voice/session-related calls
- `src/context/ChatWidgetContext.tsx` - widget-level UI state/context
- `src/dev` - local/dev/demo stand bootstrap (non-library integration path)
- `src/assets` - local assets used by the widget and avatar (not used directly, rather this whole folder is getting pushed to assets repo)
- `scripts` - utility scripts (for example asset sync to remote static repo)

## Runtime Architecture

### 1) Host Integration Layer

`ChatWidgetInstance` (`src/index.ts`) is the host-facing API:

- validates `mountElementId`
- creates `shadowRoot` on mount element
- injects bundled CSS into shadow root
- fetches `${ASSETS_URL}/manifest.json`
- resolves hashed asset paths via `resolveAssetPath(path)`
- optionally merges remote config (`remoteConfig`)
- mounts React app and wires `AppApi` callbacks/events

### 2) React App + Providers

`App` composes:

- `ChatWidgetContextProvider` (widget open state, sizing, config, loading progress)
- `CableProvider` (AnyCable websocket service + network status)
- `HashRouter` (voice/text/list routes)

### 3) Messaging Domain

`useMessageStore` handles:

- initial thread bootstrap (`ListMessages`, `ListMessagesForThreads`)
- optimistic and server-driven message updates
- thread map and per-thread message derivation
- message send (`CreateMessage`)
- agentic steps merge/update and rendering support

Live updates arrive via AnyCable channel events and are pushed into the store.

### 4) Voice Domain

`useVoiceStore` manages full voice call lifecycle:

- starts voice session via API (`StartVoiceSession`)
- builds RTCPeerConnection using TURN credentials from session payload
- sends SDP offer / receives answer
- buffers + sends ICE candidates
- consumes voice data channels (`voice-events`, `voice-input`)
- streams remote audio to an `Audio` element
- handles mic enable/disable, device switching, and teardown (`EndSession` + `EndVoiceSession`)

### 5) Avatar Domain

`Avatar` + `AnimationManager`:

- initializes three.js renderer/camera/lights/materials
- loads model + texture + animation assets from resolved remote paths
- emits `loading_progress`
- starts animation mixer loops
- maps viseme timeline to morph target animation during speech
- exposes external animation actions (`excited`, `fix_hair`, `spin`, `kiss`)

When all assets load, widget emits `avatar_ready` through `ChatWidgetInstance`.

## Public API

Import:

```ts
import { ChatWidgetInstance, type WidgetConfig } from "bridgeapp-ai-chat-widget";
```

### `ExternalWidgetConfig`

- `mountElementId: string`
- - `configName: string` - remote config, lives at the same place as other static assets
- `customerInteractionSession?: { accessToken; chatId; expiresAt }` - if there's no session (separate case for dev/demo mode) - we look for customerId in remote config and create session ourselves.

### `RemoteWidgetConfig`

Actual widget config

- `agentId: string`
- `customerId?: string`
- `wsUrl: string`
- `apiUrl: string`
- `AIAvatar?: boolean` - is 3D avatar be used
- `voiceEnabled?: boolean` - is voice mode enebled
- `background?: boolean` - clear or visible background
- `footer?: boolean` - is footer rendered
- `debug?: boolean` (avatar debug GUI)
- `manualOpen?: boolean` - is widget trigger rendered or should it be open only programatically

### Instance Methods

- `setOpen(open: boolean): void` - open/close widget
- `setCustomerInteractionSession(session): void` - update session after initialization (on session expire)
- `playAnimation(name): void`
- `destroy(): void`

### Events

- `avatar_ready` - emitted once avatar assets are fully loaded
- `renew_session` - emitted when session is expired or near expiry

## Host Usage Example

```ts
import { ChatWidgetInstance } from "bridgeapp-ai-chat-widget";

const widget = new ChatWidgetInstance({
  mountElementId: "chat-widget-root",
  customerInteractionSession: CustomerInteractionSession,
  AIAvatar: true,
  background: false,
  voiceEnabled: true,
  footer: false,
  manualOpen: true,
});

widget.addEventListener("avatar_ready", () => {
  widget.setOpen(true);
});

widget.addEventListener("renew_session", async () => {
  const newSession = await fetchNewSessionSomehow();
  widget.setCustomerInteractionSession(newSession);
});
```

Teardown:

```ts
widget.destroy();
```

## Local Development

Install:

```bash
npm install
```

Start dev server:

```bash
npm run dev
```

Notes:

- Vite dev server runs on `5179` by default.
- `src/dev/main.tsx` is the local bootstrap path (creates session and mounts widget directly).
- `vite.config.ts` is for app/dev runtime.
- `vite.config.lib.ts` is for distributable library build.

## Build, Publish, and Deployment

### Build local dev app

```bash
npm run build-dev
```

### Build library artifacts

```bash
npm run build-lib
```

Outputs include:

- `dist/bridge-ai-chat-widget.es.js`
- `dist/bridge-ai-chat-widget.umd.js`
- `dist/index.types.d.ts`

### Publish flow

- `prepublishOnly` runs `build-lib`
- package exports are defined in `package.json` under `"exports"`

### Other project scripts

- `npm run lint`
- `npm run lint-fix`
- `npm run format`
- `npm run format:check`
- deployment scripts for S3 buckets (project-specific, to be reworked)
- `npm run push-assets-metamediastatic` (sync `src/assets` into remote static assets repo)

## Session Handling

Widget relies on `customerInteractionSession`:

- used as Bearer token for HTTP APIs
- used in websocket query for AnyCable auth
- periodically checked by `useSessionExpiryChecks`

When session is near expiry or invalid, `renew_session` event is emitted; host app must provide a fresh session via `setCustomerInteractionSession`.

## Asset Pipeline

Runtime asset resolution:

1. fetch remote manifest from `https://metamediastatic.com/manifest.json`
2. map logical path (for example `animations/idle_1.fbx`) to hashed URL
3. load via `resolveAssetPath`

This allows cache-friendly hashed asset delivery without changing code references.

## Troubleshooting

- Widget does not mount:
  - verify mount element exists and `mountElementId` is correct
- No messages or send failures:
  - verify `apiUrl`, `accessToken`, and `chatId`
- No live updates:
  - verify `wsUrl`, websocket reachability, and token validity
- Voice fails to start:
  - verify mic permission, TURN reachability, and voice API endpoints
- Avatar never becomes ready:
  - verify manifest URL + asset availability under static host
