# @ringg/web

Embeddable AI voice and chat widget for any website. Add a single `<ringg-widget>` custom element and your users can talk to your Ringg AI agent — voice or text — without leaving the page.

Built with [Lit 3](https://lit.dev/) web components. Works everywhere: plain HTML, React, Vue, Svelte, Angular, Next.js, Nuxt, Astro, WordPress, Webflow — any framework or none at all.

## Features

- **Voice calls** — Real-time voice conversations with your AI agent via WebRTC
- **Text chat** — Send and receive text messages with markdown support
- **Slash commands** — Backend-driven quick actions (e.g., `/book`, `/transfer`) surfaced via a command menu
- **Interactive flows** — Agent-triggered multi-step forms, calendars, button groups, and confirmations
- **Post-call feedback** — Star ratings and optional text feedback after conversations
- **Theming** — Full visual customization via CSS custom properties or a JavaScript theme object, including gradient support
- **Notification sounds** — Configurable audio alerts for new agent messages
- **Auto-scroll and scroll lock** — Messages auto-scroll to latest; scroll lock prevents page scroll bleed
- **Framework-agnostic** — Web Components v1, zero framework lock-in
- **Zero peer dependencies** — Lit and the real-time transport are bundled, nothing extra to install

---

## Quick Start

### CDN (no build step)

The fastest way to add the widget to any site:

```html
<script src="https://cdn.ringg.ai/web/ringg-web.umd.js"></script>

<ringg-widget agent-id="YOUR_AGENT_ID" x-api-key="YOUR_API_KEY"></ringg-widget>
```

A floating button appears in the bottom-right corner. Click it to open the widget and start a voice call or text chat.

### npm

```bash
npm install @ringg/web
# or
pnpm add @ringg/web
# or
yarn add @ringg/web
```

```js
// Import once — registers <ringg-widget> globally
import "@ringg/web";
```

```html
<ringg-widget agent-id="YOUR_AGENT_ID" x-api-key="YOUR_API_KEY"></ringg-widget>
```

---

## Configuration

### HTML Attributes

For simple setups, use attributes directly:

```html
<ringg-widget agent-id="YOUR_AGENT_ID" x-api-key="YOUR_API_KEY" mode="prod"></ringg-widget>
```

| Attribute   | Type     | Default    | Description                                  |
| ----------- | -------- | ---------- | -------------------------------------------- |
| `agent-id`  | `string` | _required_ | Your Ringg agent ID                          |
| `x-api-key` | `string` | _required_ | Your Ringg API key                           |
| `mode`      | `string` | `"prod"`   | Environment: `"prod"`, `"stage"`, or `"dev"` |

### JavaScript Config Object

For full control, pass a config object:

```html
<ringg-widget id="my-widget"></ringg-widget>

<script>
  const widget = document.getElementById("my-widget");
  widget.config = {
    agentId: "YOUR_AGENT_ID",
    xApiKey: "YOUR_API_KEY",
    mode: "prod",

    // Header
    title: "Support",
    description: "Talk to our AI assistant",
    logoUrl: "https://yoursite.com/logo.png",

    // Tabs
    defaultTab: "audio", // "audio" | "text"
    hideTabSelector: false,

    // Context variables passed to the agent
    variables: {
      userName: "Jane",
      accountId: "12345",
      plan: "premium",
    },

    // Theme
    theme: {
      primaryColor: "#4f46e5", // Accepts hex, rgb, hsl, or CSS gradients
      primaryTextColor: "#ffffff",
      backgroundColor: "#ffffff",
      surfaceColor: "#f9fafb",
      textColor: "#111827",
      mutedTextColor: "#6b7280",
      borderColor: "#e5e7eb",
      errorColor: "#ef4444",
      successColor: "#22c55e",
      fontFamily: '"Inter", system-ui, sans-serif',
      borderRadius: "20px",
      buttonStyle: "rounded", // "rounded" (12px) | "pill" (9999px) | "square" (6px)
    },

    // Legal disclaimer (shown below the Start Call button)
    legalDisclaimer: {
      text: "By starting a call you agree to our Privacy Policy and Terms.",
      links: {
        "Privacy Policy": "https://yoursite.com/privacy",
        Terms: "https://yoursite.com/terms",
      },
    },

    // Post-call feedback screen
    feedbackScreen: {
      title: "How was your call?",
      description: "Your feedback helps us improve!",
      placeholder: "We'd love to hear more...",
      submitBtnCTA: "Submit Feedback",
      starsCount: 5,
      starsStyles: {
        filledColor: "#facc15",
        emptyColor: "#d1d5db",
      },
    },

    // Button customization
    buttons: {
      modalTrigger: {
        icon: {
          url: "https://yoursite.com/chat-icon.svg",
          size: 24,
        },
      },
      call: {
        textBeforeCall: "Call Now",
      },
      text: {
        textBeforeCall: "Chat Now",
      },
    },

    // Notification sound
    notificationTuneUrl: "https://assets.ringg.ai/audio/notification.mp3",
  };
</script>
```

### Full Config Reference

| Property              | Type                    | Default                                   | Description                                              |
| --------------------- | ----------------------- | ----------------------------------------- | -------------------------------------------------------- |
| `agentId`             | `string`                | _required_                                | Your Ringg agent ID                                      |
| `xApiKey`             | `string`                | _required_                                | Your Ringg API key                                       |
| `variables`           | `Record<string,string>` | `{}`                                      | Context variables sent to the agent                      |
| `mode`                | `string`                | `"prod"`                                  | Environment mode                                         |
| `title`               | `string`                | `"Ringg AI Support"`                      | Widget header title                                      |
| `description`         | `string`                | `"Ringg AI offers 24/7 voice support..."` | Widget header description                                |
| `logoUrl`             | `string`                | Ringg logo                                | Custom logo URL                                          |
| `defaultTab`          | `"audio" \| "text"`     | `"audio"`                                 | Which tab is active on open                              |
| `hideTabSelector`     | `boolean`               | `false`                                   | Hide the Chat/Voice Call tab bar                         |
| `theme`               | `WidgetTheme`           | See [Theming](#theming)                   | Visual customization                                     |
| `legalDisclaimer`     | `object`                | Default Ringg privacy/terms               | Custom legal text with link substitutions                |
| `feedbackScreen`      | `FeedbackScreenConfig`  | See defaults above                        | Post-call feedback screen customization                  |
| `buttons`             | `ButtonsConfig`         | —                                         | Customize trigger icon, call/chat button text and styles |
| `notificationTuneUrl` | `string`                | Ringg default sound                       | Audio URL for new message notifications                  |
| `widgetPosition`      | `WidgetPositionConfig`  | `{ hideTriggerOnExpand: true }`           | Trigger placement and widget alignment                   |
| `innerWindowProps`    | `InnerWindowConfig`     | —                                         | Override widget panel width, height, borderRadius        |

---

## Framework Guides

### React

Create a wrapper component that handles the web component lifecycle properly:

```tsx
// components/RinggWidget.tsx
import { useRef, useEffect } from "react";
import type { RinggWidgetConfig } from "@ringg/web";

interface RinggWidgetProps {
  agentId: string;
  apiKey: string;
  mode?: "prod" | "stage" | "dev";
  variables?: Record<string, string>;
  theme?: RinggWidgetConfig["theme"];
  title?: string;
  description?: string;
  logoUrl?: string;
  defaultTab?: "audio" | "text";
  hideTabSelector?: boolean;
  feedbackScreen?: RinggWidgetConfig["feedbackScreen"];
  legalDisclaimer?: RinggWidgetConfig["legalDisclaimer"];
  buttons?: RinggWidgetConfig["buttons"];
}

export function RinggWidget({ agentId, apiKey, mode = "prod", variables = {}, ...rest }: RinggWidgetProps) {
  const ref = useRef<HTMLElement>(null);

  useEffect(() => {
    import("@ringg/web").then(() => {
      if (ref.current) {
        (ref.current as any).config = {
          agentId,
          xApiKey: apiKey,
          mode,
          variables,
          ...rest,
        };
      }
    });
  }, [agentId, apiKey, mode]);

  return <ringg-widget ref={ref} />;
}
```

Then use it anywhere:

```tsx
import { RinggWidget } from "./components/RinggWidget";

function App() {
  return <RinggWidget agentId="YOUR_AGENT_ID" apiKey="YOUR_API_KEY" variables={{ userName: "Jane" }} theme={{ primaryColor: "#4f46e5" }} />;
}
```

**TypeScript** — add a declaration file so JSX doesn't complain about the custom element tag:

```ts
// src/ringg.d.ts
declare namespace JSX {
  interface IntrinsicElements {
    "ringg-widget": React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
  }
}
```

### Next.js / SSR

The widget requires browser APIs (DOM, WebRTC). In Next.js, mark it as client-only:

```tsx
// components/RinggWidget.tsx
"use client";

import { useRef, useEffect } from "react";
import type { RinggWidgetConfig } from "@ringg/web";

export function RinggWidget({
  agentId,
  apiKey,
  ...config
}: {
  agentId: string;
  apiKey: string;
} & Partial<RinggWidgetConfig>) {
  const ref = useRef<HTMLElement>(null);

  useEffect(() => {
    import("@ringg/web").then(() => {
      if (ref.current) {
        (ref.current as any).config = {
          agentId,
          xApiKey: apiKey,
          variables: {},
          ...config,
        };
      }
    });
  }, [agentId, apiKey]);

  return <ringg-widget ref={ref} />;
}
```

```tsx
// app/page.tsx
import { RinggWidget } from "@/components/RinggWidget";

export default function Page() {
  return (
    <main>
      <h1>My App</h1>
      <RinggWidget agentId="YOUR_AGENT_ID" apiKey="YOUR_API_KEY" variables={{ plan: "premium" }} />
    </main>
  );
}
```

**TypeScript support** — add a declaration file so JSX recognizes the custom element:

```ts
// src/ringg.d.ts
declare namespace JSX {
  interface IntrinsicElements {
    "ringg-widget": React.DetailedHTMLProps<
      React.HTMLAttributes<HTMLElement> & {
        "agent-id"?: string;
        "x-api-key"?: string;
        mode?: string;
      },
      HTMLElement
    >;
  }
}
```

**Passing the config object in React** — use a `ref`:

```tsx
import "@ringg/web";
import { useRef, useEffect } from "react";

function App() {
  const ref = useRef<HTMLElement>(null);

  useEffect(() => {
    if (ref.current) {
      (ref.current as any).config = {
        agentId: "YOUR_AGENT_ID",
        xApiKey: "YOUR_API_KEY",
        theme: { primaryColor: "#4f46e5" },
        variables: { userName: "Jane" },
      };
    }
  }, []);

  return <ringg-widget ref={ref} />;
}
```

### Next.js / SSR

The widget requires browser APIs (DOM, WebRTC). In SSR frameworks, load it on the client only:

```tsx
"use client";

import { useEffect, useRef } from "react";

export function RinggWidget({ agentId, apiKey }: { agentId: string; apiKey: string }) {
  const ref = useRef<HTMLElement>(null);

  useEffect(() => {
    import("@ringg/web").then(() => {
      if (ref.current) {
        (ref.current as any).config = {
          agentId,
          xApiKey: apiKey,
          variables: {},
        };
      }
    });
  }, [agentId, apiKey]);

  return <ringg-widget ref={ref} />;
}
```

### Vue

```vue
<template>
  <ringg-widget agent-id="YOUR_AGENT_ID" x-api-key="YOUR_API_KEY" />
</template>

<script setup>
import "@ringg/web";
</script>
```

Tell Vue to treat `ringg-*` as custom elements in `vite.config.ts`:

```ts
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: (tag) => tag.startsWith("ringg-"),
        },
      },
    }),
  ],
});
```

**Passing the config object in Vue** — use a template ref:

```vue
<template>
  <ringg-widget ref="widgetRef" />
</template>

<script setup>
import "@ringg/web";
import { ref, onMounted } from "vue";

const widgetRef = ref(null);

onMounted(() => {
  widgetRef.value.config = {
    agentId: "YOUR_AGENT_ID",
    xApiKey: "YOUR_API_KEY",
    theme: { primaryColor: "#4f46e5" },
    variables: { userName: "Jane" },
  };
});
</script>
```

### Nuxt

```vue
<!-- components/RinggWidget.client.vue -->
<template>
  <ringg-widget agent-id="YOUR_AGENT_ID" x-api-key="YOUR_API_KEY" />
</template>

<script setup>
import "@ringg/web";
</script>
```

The `.client.vue` suffix ensures Nuxt only renders it client-side. Add custom element config in `nuxt.config.ts`:

```ts
export default defineNuxtConfig({
  vue: {
    compilerOptions: {
      isCustomElement: (tag) => tag.startsWith("ringg-"),
    },
  },
});
```

### Svelte

```svelte
<script>
  import "@ringg/web";
</script>

<ringg-widget
  agent-id="YOUR_AGENT_ID"
  x-api-key="YOUR_API_KEY"
/>
```

### Angular

```ts
// app.module.ts
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core";
import "@ringg/web";

@NgModule({
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  // ...
})
export class AppModule {}
```

```html
<!-- app.component.html -->
<ringg-widget agent-id="YOUR_AGENT_ID" x-api-key="YOUR_API_KEY"></ringg-widget>
```

### Astro

```astro
---
// src/components/RinggWidget.astro
---

<ringg-widget
  agent-id="YOUR_AGENT_ID"
  x-api-key="YOUR_API_KEY"
></ringg-widget>

<script>
  import "@ringg/web";
</script>
```

### WordPress / Webflow / Static HTML

Add to your site's `<body>` — no build step required:

```html
<script src="https://cdn.ringg.ai/web/ringg-web.umd.js"></script>
<ringg-widget agent-id="YOUR_AGENT_ID" x-api-key="YOUR_API_KEY"></ringg-widget>
```

---

## Events

The widget dispatches `CustomEvent`s on `window` so you can integrate with your application logic:

```js
// Widget opened or minimized
window.addEventListener("ringg:widget_status", (e) => {
  console.log(e.detail);
  // { status: "maximised" | "minimised", mode: "audio" | "text" }
});

// Call or chat started / ended
window.addEventListener("ringg:conversation_status", (e) => {
  console.log(e.detail);
  // { status: "started" | "ended", mode: "audio" | "text", callId: "..." }
});

// Calendar booking shown or confirmed
window.addEventListener("ringg:calendar_booking", (e) => {
  console.log(e.detail);
  // { status: "shown" | "confirmed", componentId: "..." }
});

// Feedback submitted or skipped
window.addEventListener("ringg:feedback_status", (e) => {
  console.log(e.detail);
  // { status: "submitted" | "skipped", callId: "...", rating?: number }
});
```

---

## Theming

### CSS Custom Properties

Override design tokens from outside the Shadow DOM — no JavaScript needed:

```css
ringg-widget {
  /* Colors */
  --ringg-primary: #4f46e5;
  --ringg-primary-text: #ffffff;
  --ringg-bg: #ffffff;
  --ringg-surface: #f9fafb;
  --ringg-text: #111827;
  --ringg-muted: #6b7280;
  --ringg-border: #e5e7eb;
  --ringg-error: #ef4444;
  --ringg-success: #22c55e;

  /* Typography */
  --ringg-font-family: "Inter", system-ui, sans-serif;
  --ringg-font-size-xs: 0.75rem;
  --ringg-font-size-sm: 0.875rem;
  --ringg-font-size-base: 1rem;
  --ringg-font-size-lg: 1.125rem;
  --ringg-font-size-xl: 1.25rem;

  /* Sizing & Layout */
  --ringg-radius: 20px;
  --ringg-button-radius: 12px;
  --ringg-widget-width: 360px;
  --ringg-widget-max-height: 96dvh;
  --ringg-trigger-size: 56px;

  /* Shadows */
  --ringg-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
  --ringg-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);

  /* Transitions */
  --ringg-transition: 200ms ease;
}
```

### JavaScript Theme Object

Or pass theme values in the config — these take precedence over CSS custom properties:

```js
widget.config = {
  // ...
  theme: {
    primaryColor: "#4f46e5", // hex, rgb, hsl, or CSS gradient
    primaryTextColor: "#ffffff",
    backgroundColor: "#ffffff",
    surfaceColor: "#f9fafb",
    textColor: "#111827",
    mutedTextColor: "#6b7280",
    borderColor: "#e5e7eb",
    errorColor: "#ef4444",
    successColor: "#22c55e",
    fontFamily: '"Inter", system-ui, sans-serif',
    borderRadius: "20px",
    buttonStyle: "rounded", // "rounded" | "pill" | "square"
  },
};
```

**Gradient support** — `primaryColor` accepts CSS gradients:

```js
theme: {
  primaryColor: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
}
```

The widget automatically uses `background` for gradients and `background-color` for solid colors.

### Default Theme Values

| Token              | Default Value |
| ------------------ | ------------- |
| `primaryColor`     | `#313335`     |
| `primaryTextColor` | `#ffffff`     |
| `backgroundColor`  | `#fafafa`     |
| `surfaceColor`     | `#F3F3F5`     |
| `textColor`        | `#212121`     |
| `mutedTextColor`   | `#11111180`   |
| `borderColor`      | `#e5e5e5`     |
| `errorColor`       | `#ef4444`     |
| `successColor`     | `#10b981`     |
| `buttonStyle`      | `"rounded"`   |
| `borderRadius`     | `20px`        |
| `fontFamily`       | `inherit`     |

---

## Interactive Components

The widget supports agent-triggered interactive UI components during a conversation. These are driven entirely by your agent's backend — no frontend configuration needed.

| Component         | Description                                                          |
| ----------------- | -------------------------------------------------------------------- |
| **Form**          | Dynamic forms with text, email, phone, date, select, checkbox fields |
| **Calendar**      | Date and time slot picker for appointment booking                    |
| **Buttons**       | Button groups for quick selections or navigation                     |
| **Confirmation**  | Success, info, warning, or error screens with structured data        |
| **Policy Finder** | Disease/condition search with sub-questions (insurance use case)     |

These render inline within the chat as the agent sends them.

---

## Slash Commands

Slash commands are quick actions available to users via a `/` menu in text mode. They are **backend-driven** — your agent configuration defines which commands are available. The widget automatically surfaces them.

```
/book    → Trigger a calendar booking flow
/transfer → Transfer to a human agent
/summary  → Get a conversation summary
```

Commands come from two sources (merged automatically):

1. The `enabledSlashCommands` config property (if provided)
2. The agent's backend configuration (returned at call start)

---

## Browser Support

| Browser | Minimum Version |
| ------- | --------------- |
| Chrome  | 90+             |
| Firefox | 90+             |
| Safari  | 15+             |
| Edge    | 90+             |

Requires: ES2021, Web Components v1, WebRTC.

## Bundle Size

| Format | Size      | Gzipped |
| ------ | --------- | ------- |
| ESM    | ~1,023 KB | ~205 KB |
| UMD    | ~554 KB   | ~143 KB |

Includes Lit (~7 KB gzipped) and the real-time transport (~120 KB gzipped). No peer dependencies.

## License

MIT
