@revrag-ai/embed-angular
v1.0.6
Published
Angular SDK for the Revrag AI assistant widget
Downloads
309
Maintainers
Readme
@revrag-ai/embed-angular
Angular SDK for the 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-coreinstall required.
Table of Contents
- Requirements
- Installation
- Setup
- Integration Patterns
- Module-based Apps (NgModule)
- Component API
- Service API
- Advanced Configuration
- Type Reference
- 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
npm install @revrag-ai/embed-angular livekit-client lottie-webSetup
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):
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):
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:
{
"projects": {
"your-app": {
"architect": {
"build": {
"options": {
"styles": [
"src/styles.css",
"node_modules/@revrag-ai/embed-angular/styles/widget.css"
]
}
}
}
}
}
}Note:
styles/widget.cssis bundled inside@revrag-ai/embed-angularat publish time — no separate@revrag-ai/embed-coreinstall 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.
// 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.
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.
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.
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).
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.
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:
// 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. |
| 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:
<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:
<!-- 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:
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().
// 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:
// 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:
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:
import { EmbedLiveKitService } from '@revrag-ai/embed-angular';
constructor(private liveKit: EmbedLiveKitService) {}Observables:
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:
// 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:
<!-- 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:
<revrag-embed-button
[position]="{ bottom: '80px', right: '32px' }"
></revrag-embed-button>Raise the widget above a bottom navigation bar:
<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:
<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.
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,
},
],
};<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
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
interface SDKConfig {
apiKey: string;
baseUrl?: string;
enableTracker?: boolean;
trackerCallback?: (event: EventPayload) => void;
}PositionConfig
interface PositionConfig {
bottom?: string;
right?: string;
left?: string;
top?: string;
transform?: string;
zIndex?: number;
}LiveKitState
interface LiveKitState {
isConnected: boolean;
isConnecting: boolean;
error: string | null;
participants: number;
agentConnected: boolean;
}EmbedButtonVisibilityConfig
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 inAppComponent.ngOnInit()(not in a lazy-loaded module). - Check the browser console for init errors — your API key may be invalid.
- Verify that
currentPathis being updated on navigation and matches one of the values inincludeScreens.
Animations are broken or missing
- Ensure
provideAnimations()(standalone) orBrowserAnimationsModule(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.jsonstylesarray and that you have rebuilt the application after the change.
Voice call button does nothing
livekit-clientmust 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-indexby default. If your app has elements with a higher stacking context, use thepositioninput to set a customzIndex:
<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
