npm package: @revrag-ai/embed-angular
Description: Angular SDK for the Revrag AI assistant widget
Latest version: 1.0.6

# @revrag-ai/embed-angular

Angular SDK for the [Revrag AI](https://revrag.ai) assistant widget. Add a fully-featured AI voice + chat assistant to any Angular application in minutes.

> **Self-contained.** The core SDK logic is bundled inside this package — no separate `@revrag-ai/embed-core` install required.

---

## Table of Contents

- [Requirements](#requirements)
- [Installation](#installation)
- [Setup](#setup)
  - [1. Provide animations](#1-provide-animations)
  - [2. Add the stylesheet](#2-add-the-stylesheet)
  - [3. Initialize the SDK](#3-initialize-the-sdk)
- [Integration Patterns](#integration-patterns)
  - [Pattern 1 — Manual conditional rendering](#pattern-1--manual-conditional-rendering)
  - [Pattern 2 — Provider with `includeScreens`](#pattern-2--provider-with-includescreens)
  - [Pattern 3 — Object-based config](#pattern-3--object-based-config)
  - [Pattern 4 — Tab-based navigation (no router)](#pattern-4--tab-based-navigation-no-router)
- [Module-based Apps (NgModule)](#module-based-apps-ngmodule)
- [Component API](#component-api)
  - [`EmbedProviderComponent`](#embedprovidercomponent)
  - [`EmbedButtonComponent`](#embedbuttoncomponent)
- [Service API](#service-api)
  - [`EmbedInitService`](#embedinitservice)
  - [`EmbedLiveKitService`](#embdlivekitservice)
- [Advanced Configuration](#advanced-configuration)
  - [Positioning](#positioning)
  - [Group-based visibility](#group-based-visibility)
- [Type Reference](#type-reference)
- [Troubleshooting](#troubleshooting)

---

## Requirements

| Dependency | Version |
|---|---|
| `@angular/core` | `>=15.0.0` |
| `@angular/common` | `>=15.0.0` |
| `@angular/animations` | `>=15.0.0` |
| `@angular/router` | `>=15.0.0` |
| `livekit-client` | `^2.0.0` |
| `lottie-web` | `^5.10.0` |

---

## Installation

```bash
npm install @revrag-ai/embed-angular livekit-client lottie-web
```

---

## Setup

Three one-time steps are required before placing any widget component.

### 1. Provide animations

Angular animations must be enabled at the application level. Without this the widget transitions will not work.

**Standalone bootstrap (`app.config.ts`):**

```typescript
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/animations';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideAnimations(), // required
  ],
};
```

**NgModule bootstrap (`app.module.ts`):**

```typescript
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
  imports: [BrowserModule, BrowserAnimationsModule],
})
export class AppModule {}
```

---

### 2. Add the stylesheet

Include the widget stylesheet in your Angular build. Open `angular.json` and add the path to the `styles` array under your project's `build` target:

```json
{
  "projects": {
    "your-app": {
      "architect": {
        "build": {
          "options": {
            "styles": [
              "src/styles.css",
              "node_modules/@revrag-ai/embed-angular/styles/widget.css"
            ]
          }
        }
      }
    }
  }
}
```

> **Note:** `styles/widget.css` is bundled inside `@revrag-ai/embed-angular` at publish time — no separate `@revrag-ai/embed-core` install is needed.

---

### 3. Initialize the SDK

Call `EmbedInitService.initialize()` **once**, as early as possible — typically in your root `AppComponent`. It validates your API key, fetches the widget configuration from Revrag's backend, and caches the result for the session.

```typescript
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { RouterModule } from '@angular/router';
import { EmbedInitService } from '@revrag-ai/embed-angular';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterModule],
  template: `<router-outlet></router-outlet>`,
})
export class AppComponent implements OnInit {
  constructor(private embedInit: EmbedInitService) {}

  ngOnInit(): void {
    this.embedInit.initialize('YOUR_API_KEY');
  }
}
```

Get your API key from the [Revrag dashboard](https://revrag.ai).

---

## Integration Patterns

Choose the pattern that best fits your application's routing strategy.

---

### Pattern 1 — Manual conditional rendering

**Best for:** apps where you want explicit, imperative control over when the widget appears.

Import `EmbedButtonComponent` directly and use `*ngIf` to conditionally render it based on the current route.

```typescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs';
import { EmbedButtonComponent } from '@revrag-ai/embed-angular';

@Component({
  selector: 'app-shell',
  standalone: true,
  imports: [CommonModule, RouterModule, EmbedButtonComponent],
  template: `
    <nav>
      <a routerLink="/">Home</a>
      <a routerLink="/offers">Offers</a>
      <a routerLink="/about">About</a>
    </nav>

    <router-outlet></router-outlet>

    <!-- Widget only on the /offers route -->
    <revrag-embed-button
      *ngIf="showEmbed"
      positioning="fixed"
      side="center"
    ></revrag-embed-button>
  `,
})
export class AppShellComponent {
  showEmbed = false;

  constructor(private router: Router) {
    this.router.events
      .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd))
      .subscribe((e) => {
        this.showEmbed = e.urlAfterRedirects.includes('/offers');
      });
  }
}
```

---

### Pattern 2 — Provider with `includeScreens`

**Best for:** router-based apps where the widget should appear on a known set of routes.

Wrap your layout with `<revrag-embed-provider>` and declare which paths should show the widget. The provider listens to the Angular router automatically if you keep `currentPath` in sync.

```typescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs';
import { EmbedProviderComponent } from '@revrag-ai/embed-angular';

@Component({
  selector: 'app-shell',
  standalone: true,
  imports: [CommonModule, RouterModule, EmbedProviderComponent],
  template: `
    <revrag-embed-provider
      [currentPath]="currentPath"
      [includeScreens]="['/offers', '/help']"
      matchMode="exact"
      widgetPositioning="fixed"
    >
      <nav>
        <a routerLink="/">Home</a>
        <a routerLink="/offers">Offers</a>
        <a routerLink="/help">Help</a>
      </nav>

      <router-outlet></router-outlet>
    </revrag-embed-provider>
  `,
})
export class AppShellComponent {
  currentPath = '/';

  constructor(private router: Router) {
    this.router.events
      .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd))
      .subscribe((e) => {
        this.currentPath = e.urlAfterRedirects;
      });
  }
}
```

Use `matchMode="startsWith"` to show the widget on `/offers` **and** any sub-routes like `/offers/details`.

---

### Pattern 3 — Object-based config

**Best for:** teams that prefer keeping widget configuration in a separate object (easier to share across components or load from a config service).

```typescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs';
import { EmbedProviderComponent } from '@revrag-ai/embed-angular';

const EMBED_CONFIG = {
  includeScreens: ['/offers', '/help', '/support'],
  matchMode: 'exact' as const,
};

@Component({
  selector: 'app-shell',
  standalone: true,
  imports: [CommonModule, RouterModule, EmbedProviderComponent],
  template: `
    <revrag-embed-provider
      [currentPath]="currentPath"
      [includeScreens]="embedConfig.includeScreens"
      [matchMode]="embedConfig.matchMode"
      widgetPositioning="fixed"
    >
      <nav>
        <a routerLink="/offers">Offers</a>
        <a routerLink="/help">Help</a>
        <a routerLink="/support">Support</a>
      </nav>

      <router-outlet></router-outlet>
    </revrag-embed-provider>
  `,
})
export class AppShellComponent {
  currentPath = '/';
  embedConfig = EMBED_CONFIG;

  constructor(private router: Router) {
    this.router.events
      .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd))
      .subscribe((e) => {
        this.currentPath = e.urlAfterRedirects;
      });
  }
}
```

---

### Pattern 4 — Tab-based navigation (no router)

**Best for:** single-page apps or dashboards that use tab components instead of the Angular router.

Map your active tab to a virtual path and pass it to the provider. The widget appears whenever `currentPath` matches a path in `includeScreens`.

```typescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { EmbedProviderComponent } from '@revrag-ai/embed-angular';

interface Tab { id: string; label: string; path: string; }

const TABS: Tab[] = [
  { id: 'dashboard', label: 'Dashboard', path: '/dashboard' },
  { id: 'offers',    label: 'Offers',    path: '/offers'    },
  { id: 'settings',  label: 'Settings',  path: '/settings'  },
];

@Component({
  selector: 'app-shell',
  standalone: true,
  imports: [CommonModule, EmbedProviderComponent],
  template: `
    <revrag-embed-provider
      [currentPath]="currentPath"
      [includeScreens]="['/offers']"
      widgetPositioning="fixed"
    >
      <div class="tab-bar">
        <button
          *ngFor="let tab of tabs"
          (click)="setActiveTab(tab.id)"
          [class.active]="activeTab === tab.id"
        >
          {{ tab.label }}
        </button>
      </div>

      <div class="tab-content">
        <ng-container [ngSwitch]="activeTab">
          <div *ngSwitchCase="'dashboard'">Dashboard content</div>
          <div *ngSwitchCase="'offers'">Offers content — widget is visible here</div>
          <div *ngSwitchCase="'settings'">Settings content</div>
        </ng-container>
      </div>
    </revrag-embed-provider>
  `,
})
export class AppShellComponent {
  tabs = TABS;
  activeTab = 'dashboard';

  get currentPath(): string {
    return this.tabs.find((t) => t.id === this.activeTab)?.path ?? '/';
  }

  setActiveTab(tabId: string): void {
    this.activeTab = tabId;
  }
}
```

---

## Module-based Apps (NgModule)

For applications that have not migrated to standalone components, import `EmbedModule` into your feature or root module:

```typescript
// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { EmbedModule } from '@revrag-ai/embed-angular';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    BrowserAnimationsModule, // required
    EmbedModule,
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}
```

All component selectors and inputs remain the same. You do **not** need to import individual components when `EmbedModule` is imported.

---

## Component API

### `EmbedProviderComponent`

**Selector:** `revrag-embed-provider`

The top-level declarative wrapper. Wraps your page layout via `<ng-content>` and controls widget visibility based on the current path. This is the recommended entry point for most apps.

| Input | Type | Default | Description |
|---|---|---|---|
| `currentPath` | `string \| undefined` | `undefined` | The active URL path. Keep this in sync with the router (or the active tab) for automatic show/hide behaviour. |
| `includeScreens` | `string[]` | `[]` | List of paths where the widget should be visible. |
| `matchMode` | `'exact' \| 'startsWith'` | `'exact'` | `'exact'` — path must match exactly. `'startsWith'` — path and all sub-paths match. |
| `embedButtonDelayMs` | `number` | `0` | Global delay in milliseconds before the button appears after the route becomes active. |
| `embedButtonVisibilityConfig` | `EmbedButtonVisibilityConfig \| undefined` | `undefined` | Advanced group-based delay and visibility rules. See [Group-based visibility](#group-based-visibility). |
| `embedButtonPosition` | `EmbedButtonPosition \| undefined` | `undefined` | Override the button's pixel position (`bottom`, `right`). |
| `widgetPositioning` | `'fixed' \| 'embedded'` | `'fixed'` | `'fixed'` attaches the widget to the viewport. `'embedded'` places it in normal document flow. |
| `widgetSide` | `'left' \| 'right' \| undefined` | `undefined` | Horizontal alignment of the widget. |
| `widgetBottomOffset` | `number` | `0` | Additional pixels to raise the widget from the bottom of the screen. |
| `widgetClassName` | `string` | `''` | Extra CSS class applied to the widget container. |

**Usage:**

```html
<revrag-embed-provider
  [currentPath]="currentPath"
  [includeScreens]="['/offers', '/help']"
  matchMode="startsWith"
  widgetPositioning="fixed"
  widgetSide="right"
  [widgetBottomOffset]="24"
>
  <!-- your page layout -->
  <router-outlet></router-outlet>
</revrag-embed-provider>
```

---

### `EmbedButtonComponent`

**Selector:** `revrag-embed-button`

The interactive floating button. Manages the expand/collapse state, LiveKit voice connection, and tooltip display internally. Use this when you want imperative control (via `*ngIf`) instead of the declarative provider.

| Input | Type | Default | Description |
|---|---|---|---|
| `positioning` | `'fixed' \| 'embedded'` | `'fixed'` | Layout mode. |
| `side` | `'left' \| 'right' \| 'center' \| undefined` | `undefined` | Horizontal alignment. |
| `bottomOffset` | `number` | `0` | Additional pixels from the bottom of the screen. |
| `className` | `string` | `''` | Extra CSS class on the button container. |
| `position` | `PositionConfig \| undefined` | `undefined` | Fine-grained CSS position override (`bottom`, `right`, `left`, `top`). |

**Usage:**

```html
<!-- Right-aligned, fixed to viewport -->
<revrag-embed-button
  positioning="fixed"
  side="right"
  [bottomOffset]="32"
></revrag-embed-button>

<!-- Centred at the bottom -->
<revrag-embed-button
  positioning="fixed"
  side="center"
></revrag-embed-button>

<!-- Custom pixel position -->
<revrag-embed-button
  [position]="{ bottom: '40px', right: '24px' }"
></revrag-embed-button>
```

---

## Service API

All services are `providedIn: 'root'` — inject them directly without any additional provider configuration.

### `EmbedInitService`

Handles SDK initialization and session management. Must be called once before any widget is rendered.

**Injection:**

```typescript
import { EmbedInitService } from '@revrag-ai/embed-angular';

constructor(private embedInit: EmbedInitService) {}
```

**`initialize(apiKey, options?)`**

Validates the API key, fetches widget configuration, and stores it in session storage. Call this once in `AppComponent.ngOnInit()`.

```typescript
// Minimal
this.embedInit.initialize('YOUR_API_KEY');

// With options
this.embedInit.initialize('YOUR_API_KEY', {
  baseUrl: 'https://custom.api.example.com', // custom backend endpoint
  enableTracker: true,                        // enable analytics event tracking
  trackerCallback: (event) => {               // receive tracked events
    console.log('Revrag event:', event);
  },
});
```

| Parameter | Type | Description |
|---|---|---|
| `apiKey` | `string` | Your Revrag API key (**required**). |
| `options.baseUrl` | `string` | Override the default Revrag API base URL. |
| `options.enableTracker` | `boolean` | Enable automatic user interaction tracking. |
| `options.trackerCallback` | `(event: EventPayload) => void` | Callback that fires for every tracked event. |

**Observables:**

```typescript
// Subscribe to initialization state
this.embedInit.isInitialized$.subscribe((ready) => {
  console.log('SDK ready:', ready);
});

this.embedInit.isLoading$.subscribe((loading) => {
  // show a spinner while the SDK initializes
});

this.embedInit.error$.subscribe((err) => {
  if (err) console.error('SDK init failed:', err);
});
```

| Observable | Type | Description |
|---|---|---|
| `isInitialized$` | `Observable<boolean>` | Emits `true` after successful initialization. |
| `isLoading$` | `Observable<boolean>` | Emits `true` while initialization is in progress. |
| `error$` | `Observable<string \| null>` | Emits an error message if initialization fails, otherwise `null`. |
| `sessionData$` | `Observable<SessionStorageData>` | Emits the full cached session data object. |

**Synchronous getter:**

```typescript
if (this.embedInit.isInitialized) {
  // safe to interact with the widget
}
```

---

### `EmbedLiveKitService`

Wraps the LiveKit voice connection. Use this if you need to control the voice call programmatically — for example, triggering a call from your own UI.

**Injection:**

```typescript
import { EmbedLiveKitService } from '@revrag-ai/embed-angular';

constructor(private liveKit: EmbedLiveKitService) {}
```

**Observables:**

```typescript
this.liveKit.state$.subscribe((state) => {
  console.log('Connected:', state.isConnected);
  console.log('Connecting:', state.isConnecting);
  console.log('Agent connected:', state.agentConnected);
});

this.liveKit.isMuted$.subscribe((muted) => {
  // update a mute indicator
});

this.liveKit.dataTranscription$.subscribe((data) => {
  // receive structured data pushed from the agent
});
```

| Observable | Type | Description |
|---|---|---|
| `state$` | `Observable<LiveKitState>` | Full LiveKit connection state. |
| `isMuted$` | `Observable<boolean>` | Whether the local microphone is muted. |
| `dataTranscription$` | `Observable<Record<string, string>>` | Structured data (calculations, text) sent by the agent. |
| `room$` | `Observable<Room>` | The underlying LiveKit `Room` instance. |

**Methods:**

```typescript
// Start a voice call
await this.liveKit.connect();

// End the call
this.liveKit.disconnect();

// Toggle microphone mute
await this.liveKit.toggleMute();

// Clear any received transcription data
this.liveKit.clearDataTranscription();
```

---

## Advanced Configuration

### Positioning

The widget can be positioned using the `side` shorthand or a custom `position` object.

**Shorthand side values:**

```html
<!-- Bottom-right (default) -->
<revrag-embed-button side="right"></revrag-embed-button>

<!-- Bottom-left -->
<revrag-embed-button side="left"></revrag-embed-button>

<!-- Bottom-centre -->
<revrag-embed-button side="center"></revrag-embed-button>
```

**Custom pixel position:**

```html
<revrag-embed-button
  [position]="{ bottom: '80px', right: '32px' }"
></revrag-embed-button>
```

**Raise the widget above a bottom navigation bar:**

```html
<revrag-embed-provider
  [currentPath]="currentPath"
  [includeScreens]="allowedRoutes"
  [widgetBottomOffset]="64"
>
  <!-- content -->
</revrag-embed-provider>
```

**Embedded (in-flow) mode** — place the widget inside a scrollable container rather than the viewport:

```html
<revrag-embed-button
  positioning="embedded"
  side="right"
></revrag-embed-button>
```

---

### Group-based visibility

Use `embedButtonVisibilityConfig` on `EmbedProviderComponent` to define per-group delay and continuity rules. This is useful in multi-step flows where you want the widget to appear after a delay on first entry, but immediately on return visits.

```typescript
import { EmbedButtonVisibilityConfig } from '@revrag-ai/embed-angular';

// In your component class:
visibilityConfig: EmbedButtonVisibilityConfig = {
  defaultDelayMs: 0,
  groups: [
    {
      id: 'checkout-flow',
      screens: ['/offers', '/checkout', '/payment'],
      continuity: 'continuous',  // remember state across these screens
      delayMs: 3000,             // wait 3s before showing on first entry
      delayPolicy: 'oncePerGroupEntry', // only delay on first entry
    },
    {
      id: 'support',
      screens: ['/help', '/support'],
      continuity: 'perScreen',   // reset state on each screen
      delayMs: 1000,
    },
  ],
};
```

```html
<revrag-embed-provider
  [currentPath]="currentPath"
  [includeScreens]="allScreens"
  [embedButtonVisibilityConfig]="visibilityConfig"
>
  <router-outlet></router-outlet>
</revrag-embed-provider>
```

| `delayPolicy` | Behaviour |
|---|---|
| `'oncePerGroupEntry'` | Delay fires only the first time the user enters this group of screens. |
| `'oncePerAppSession'` | Delay fires only once per browser session across all groups. |
| `'perScreen'` | Delay fires every time the user navigates to any screen in the group. |

| `continuity` | Behaviour |
|---|---|
| `'continuous'` | Expand/collapse state is preserved while navigating within the group. |
| `'perScreen'` | State resets every time the user moves to a different screen. |

---

## Type Reference

```typescript
import type {
  // Config types
  SDKConfig,
  EmbedButtonVisibilityConfig,
  EmbedButtonGroupConfig,
  EmbedButtonDelayPolicy,
  EmbedButtonContinuity,
  EmbedButtonPosition,
  PositionConfig,

  // State types
  LiveKitState,
  InitResponse,
  TokenResponse,

  // Event types
  EventPayload,
  EventKeys,
} from '@revrag-ai/embed-angular';
```

**`SDKConfig`**

```typescript
interface SDKConfig {
  apiKey: string;
  baseUrl?: string;
  enableTracker?: boolean;
  trackerCallback?: (event: EventPayload) => void;
}
```

**`PositionConfig`**

```typescript
interface PositionConfig {
  bottom?: string;
  right?: string;
  left?: string;
  top?: string;
  transform?: string;
  zIndex?: number;
}
```

**`LiveKitState`**

```typescript
interface LiveKitState {
  isConnected: boolean;
  isConnecting: boolean;
  error: string | null;
  participants: number;
  agentConnected: boolean;
}
```

**`EmbedButtonVisibilityConfig`**

```typescript
interface EmbedButtonVisibilityConfig {
  defaultDelayMs?: number;
  groups?: EmbedButtonGroupConfig[];
}

interface EmbedButtonGroupConfig {
  id: string;
  screens: string[];
  continuity: 'continuous' | 'perScreen';
  delayMs?: number;
  delayPolicy?: 'oncePerGroupEntry' | 'oncePerAppSession' | 'perScreen';
}
```

---

## Troubleshooting

**Widget does not appear**

- Confirm `EmbedInitService.initialize()` is called in `AppComponent.ngOnInit()` (not in a lazy-loaded module).
- Check the browser console for init errors — your API key may be invalid.
- Verify that `currentPath` is being updated on navigation and matches one of the values in `includeScreens`.

**Animations are broken or missing**

- Ensure `provideAnimations()` (standalone) or `BrowserAnimationsModule` (NgModule) is present in your app config. The SDK components use Angular animations and will silently degrade without this.

**Stylesheet is not applied**

- Confirm the widget CSS path is included in your `angular.json` `styles` array and that you have rebuilt the application after the change.

**Voice call button does nothing**

- `livekit-client` must be installed as a peer dependency.
- Check the browser console for LiveKit connection errors. The agent must be provisioned and running in your Revrag dashboard.

**Widget appears behind other elements**

- The widget uses a high `z-index` by default. If your app has elements with a higher stacking context, use the `position` input to set a custom `zIndex`:

```html
<revrag-embed-button [position]="{ bottom: '24px', right: '24px', zIndex: 99999 }">
</revrag-embed-button>
```

**Server-side rendering (SSR / Angular Universal)**

The SDK uses `PlatformService` internally to detect the browser platform and skips all DOM and LiveKit operations during SSR. No additional configuration is needed — the widget renders nothing on the server and hydrates cleanly on the client.

---

## License

MIT © [Revrag AI](https://revrag.ai)