npm package: @llmexpert/chat-widget
Description: Embeddable AI chat widget for knowledge bases - add a chat experience to any website with a few lines of code
Latest version: 1.0.0

# @llmexpert/chat-widget

[![npm version](https://img.shields.io/npm/v/@llmexpert/chat-widget.svg)](https://www.npmjs.com/package/@llmexpert/chat-widget)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Embeddable AI chat widget for knowledge bases.

- Streaming responses via Server-Sent Events
- Dark mode with automatic OS preference detection
- CSS custom properties for full theming control
- Mobile responsive layout
- Keyboard accessible (focus trap, screen reader announcements)
- XSS sanitized output (DOMPurify)

## Quick Start

Install the package:

```bash
npm install @llmexpert/chat-widget
```

Add the widget to your React app:

```tsx
import { ChatWidget } from '@llmexpert/chat-widget';
import '@llmexpert/chat-widget/dist/styles.css';

function App() {
  return (
    <ChatWidget
      apiKey="your-api-key"
      widgetId="your-widget-id"
    />
  );
}
```

## Props API Reference

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `apiKey` | `string` | **required** | Your API key for authenticating widget requests. |
| `widgetId` | `string` | **required** | The widget ID to connect to. |
| `apiBaseUrl` | `string` | `undefined` | API server URL. Defaults to the current page origin if not set. |
| `position` | `'bottom-right' \| 'bottom-left' \| 'top-right' \| 'top-left'` | `'bottom-right'` | Screen corner where the widget launcher appears. |
| `primaryColor` | `string` | `'#007bff'` | Primary brand color used for the header, launcher, and user message bubbles. |
| `greeting` | `string` | `undefined` | Initial greeting message shown in the chat window. Overrides the server-configured greeting. |
| `placeholder` | `string` | `undefined` | Placeholder text for the message input field. Overrides the server-configured placeholder. |
| `showPoweredBy` | `boolean` | `true` | Whether to show the "Powered by" attribution in the widget footer. |
| `theme` | `'auto' \| 'dark' \| 'light'` | `'auto'` | Color theme. `'auto'` detects OS preference via `prefers-color-scheme`. |
| `onError` | `(error: Error) => void` | `undefined` | Callback fired when the widget encounters an error. |
| `onMessageSent` | `(message: string) => void` | `undefined` | Callback fired after the user sends a message. |
| `onMessageReceived` | `(message: Message) => void` | `undefined` | Callback fired when an assistant message is received. |

## CSS Custom Properties

Override these on the embedding page to customize the widget appearance:

```css
.rag-chat-widget {
  --rag-primary-color: #6366f1;
  --rag-bg-color: #fafafa;
  --rag-border-radius: 20px;
  --rag-user-bubble-color: #6366f1;
  --rag-assistant-bubble-color: rgba(99, 102, 241, 0.08);
}
```

### Core Colors

| Property | Default | Description |
|----------|---------|-------------|
| `--rag-primary-color` | `#007bff` | Primary brand color. |
| `--rag-bg-color` | `#ffffff` | Window background color. |
| `--rag-text-color` | `#333333` | Body text color. |
| `--rag-border-color` | `#e0e0e0` | Border and divider color. |

### Shadow

| Property | Default | Description |
|----------|---------|-------------|
| `--rag-shadow` | Multi-layer soft shadow | Window drop shadow. |

### Typography

| Property | Default | Description |
|----------|---------|-------------|
| `--rag-font-family` | System font stack | Font stack for all widget text. |

### Window Geometry

| Property | Default | Description |
|----------|---------|-------------|
| `--rag-border-radius` | `16px` | Window corner radius. |
| `--rag-window-width` | `380px` | Chat window width. |
| `--rag-window-max-height` | `80vh` | Chat window maximum height. |

### Message Bubbles

| Property | Default | Description |
|----------|---------|-------------|
| `--rag-bubble-radius` | `12px` | Message bubble corner radius. |
| `--rag-user-bubble-color` | `var(--rag-primary-color)` | User message background. |
| `--rag-assistant-bubble-color` | `#f0f0f0` | Assistant message background. |

### Header

| Property | Default | Description |
|----------|---------|-------------|
| `--rag-header-bg` | Falls back to primary color | Header background override. Set a color or gradient to replace the default. |

### Launcher Button

| Property | Default | Description |
|----------|---------|-------------|
| `--rag-launcher-size` | `60px` | Floating button diameter. |
| `--rag-launcher-radius` | `50%` | Floating button border-radius. |

## Framework Examples

### Next.js

Use a dynamic import with SSR disabled since the widget requires browser APIs:

```tsx
// components/ChatWidget.tsx
import dynamic from 'next/dynamic';

const ChatWidget = dynamic(
  () => import('@llmexpert/chat-widget').then(mod => ({ default: mod.ChatWidget })),
  { ssr: false }
);

export default function Chat() {
  return (
    <ChatWidget
      apiKey="your-api-key"
      widgetId="your-widget-id"
    />
  );
}
```

Import the styles in your `_app.tsx` or layout file:

```tsx
import '@llmexpert/chat-widget/dist/styles.css';
```

### Gatsby

Import directly in a page component or in `gatsby-browser.js`:

```tsx
// gatsby-browser.js
import '@llmexpert/chat-widget/dist/styles.css';
```

```tsx
// src/pages/index.tsx
import { ChatWidget } from '@llmexpert/chat-widget';

export default function IndexPage() {
  return (
    <ChatWidget
      apiKey="your-api-key"
      widgetId="your-widget-id"
    />
  );
}
```

### Plain HTML (UMD)

Load React and the widget via script tags:

```html
<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="https://unpkg.com/@llmexpert/chat-widget/dist/styles.css" />
</head>
<body>
  <div id="chat-root"></div>

  <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
  <script src="https://unpkg.com/@llmexpert/chat-widget/dist/index.umd.js"></script>
  <script>
    const root = ReactDOM.createRoot(document.getElementById('chat-root'));
    root.render(
      React.createElement(RAGChatWidget.ChatWidget, {
        apiKey: 'your-api-key',
        widgetId: 'your-widget-id'
      })
    );
  </script>
</body>
</html>
```

## Content Security Policy

If your site enforces a strict Content Security Policy, you will need to allow specific directives for the chat widget to function correctly.

### Required Directives

| Directive | Required Value | Reason |
|-----------|---------------|--------|
| `script-src` | URL of the host serving the widget JS bundle | The widget JavaScript must be loaded and executed on the page. |
| `style-src` | `'unsafe-inline'` | The widget sets CSS custom properties on its root element at runtime. All visual styling is in a bundled CSS file; only CSS variable values are injected inline. |
| `connect-src` | Your widget API base URL (the `apiBaseUrl` prop value) | The widget makes HTTP requests for configuration and uses Server-Sent Events for streaming responses. |

### Optional Directives

| Directive | When Required | Reason |
|-----------|--------------|--------|
| `img-src` | Only if `homeView.logoUrl` is configured | The home view can display a logo image. Not needed otherwise. |
| `font-src` | Not required | The widget uses system fonts only. |

### CSP Examples

Replace `https://cdn.example.com` with the URL where your widget bundle is hosted, and `https://api.example.com` with your `apiBaseUrl` prop value.

#### HTML Meta Tag

```html
<meta http-equiv="Content-Security-Policy" content="
  default-src 'self';
  script-src 'self' https://cdn.example.com;
  style-src 'self' 'unsafe-inline';
  connect-src 'self' https://api.example.com;
">
```

#### Nginx Header

```nginx
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; connect-src 'self' https://api.example.com;";
```

#### Next.js (next.config.js)

```js
/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: [
              "default-src 'self'",
              "script-src 'self' https://cdn.example.com",
              "style-src 'self' 'unsafe-inline'",
              "connect-src 'self' https://api.example.com",
            ].join('; '),
          },
        ],
      },
    ];
  },
};

module.exports = nextConfig;
```

#### Gatsby (gatsby-config.js)

Using the `gatsby-plugin-csp` plugin:

```js
module.exports = {
  plugins: [
    {
      resolve: 'gatsby-plugin-csp',
      options: {
        mergeDefaultDirectives: true,
        directives: {
          'script-src': "'self' https://cdn.example.com",
          'style-src': "'self' 'unsafe-inline'",
          'connect-src': "'self' https://api.example.com",
        },
      },
    },
  ],
};
```

### CSP Troubleshooting

**"Refused to apply inline style"** -- Add `'unsafe-inline'` to your `style-src` directive. The widget applies CSS custom properties (color variables) on its root element. The attack surface is minimal -- only CSS variable values are injected, not arbitrary style declarations.

**"Refused to connect to ..."** -- Add your API base URL to the `connect-src` directive. This is the same URL you pass as the `apiBaseUrl` prop.

**"Refused to load the script ..."** -- Add the URL where the widget bundle is hosted to your `script-src` directive.

**"Refused to load the image ..."** -- If using `homeView.logoUrl`, add the image source domain to your `img-src` directive.

## Migrating from @rag-widget/chat-widget

This package was previously published as `@rag-widget/chat-widget`. Update your dependency name and imports to `@llmexpert/chat-widget`. No API changes -- all props, CSS custom properties, and behavior are identical.

## License

[MIT](./LICENSE)