npm package: @maxischmaxi/bootai-react
Description: Drop-in AI chat widget for React apps and static sites. Ships as both a React component (`ChatBot`) and a standalone IIFE script (`widget.global.js`) for use without a build step.
Latest version: 0.2.0

# @maxischmaxi/bootai-react

Drop-in AI chat widget for React apps and static sites. Ships as both a React component (`ChatBot`) and a standalone IIFE script (`widget.global.js`) for use without a build step.

## Installation

```bash
# npm
npm install @maxischmaxi/bootai-react

# pnpm
pnpm add @maxischmaxi/bootai-react

# yarn
yarn add @maxischmaxi/bootai-react
```

Peer dependencies: `react >=18` and `react-dom >=18`.

## Quick Start

### React

```tsx
import { ChatBot } from "@maxischmaxi/bootai-react";

function App() {
  return (
    <ChatBot
      siteId="your-site-id"
      apiUrl="https://api.example.com"
      consentGiven={true}
    />
  );
}
```

### Script Tag (no build step)

```html
<script src="https://cdn.example.com/widget.global.js"></script>
<script>
  BootAI.render({
    siteId: "your-site-id",
    apiUrl: "https://api.example.com",
    consentGiven: true,
  });
</script>
```

---

## Props

### `ChatBotProps`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `siteId` | `string` | **required** | Unique identifier for your site. |
| `apiUrl` | `string` | **required** | Base URL of the BootAI chat API. |
| `theme` | `"light" \| "dark" \| "auto"` | `"light"` | Color theme. `"auto"` follows `prefers-color-scheme`. |
| `display` | `"floating" \| "inline"` | `"floating"` | Floating shows a FAB + popup panel. Inline renders the chat directly in the DOM flow. |
| `consentGiven` | `boolean \| ConsentConfig` | `false` | Controls whether the chat is active. See [Consent](#consent). |
| `classNames` | `ChatBotClassNames` | — | CSS class names per UI slot. See [Custom Styling](#custom-styling). |
| `styles` | `ChatBotStyles` | — | Inline style overrides per UI slot. See [Custom Styling](#custom-styling). |
| `renderCallbacks` | `ChatBotRenderCallbacks` | — | Replace any UI section with a custom render function. See [Render Callbacks](#render-callbacks). |

---

## Consent

The widget renders nothing until consent is granted. There are two ways to provide consent:

### Simple boolean

Pass `consentGiven={true}` when your app already manages consent externally.

```tsx
<ChatBot siteId="..." apiUrl="..." consentGiven={true} />
```

### Consent config (built-in banner)

Pass a `ConsentConfig` object to show a built-in consent banner before the chat loads.

```tsx
<ChatBot
  siteId="..."
  apiUrl="..."
  consentGiven={{
    text: "This chat is operated by a third party. By using it you agree to data processing.",
    privacyUrl: "https://example.com/privacy",
    acceptLabel: "Start Chat",        // optional, default: "Akzeptieren"
    storage: "localStorage",          // optional, default: "sessionStorage"
  }}
/>
```

#### `ConsentConfig`

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `text` | `string` | **required** | Consent banner message. |
| `privacyUrl` | `string` | **required** | Link to your privacy policy. |
| `acceptLabel` | `string` | `"Akzeptieren"` | Label for the accept button. |
| `storage` | `"localStorage" \| "sessionStorage" \| "none"` | `"sessionStorage"` | Where to persist the consent decision. `"none"` means the banner appears on every visit. |

---

## Display Modes

### Floating (default)

A floating action button (FAB) in the bottom-right corner. Clicking it opens/closes the chat panel as a popup.

```tsx
<ChatBot siteId="..." apiUrl="..." display="floating" consentGiven={true} />
```

### Inline

The chat panel renders directly inside the parent element, like a regular component. No FAB, no popup.

```tsx
<ChatBot siteId="..." apiUrl="..." display="inline" consentGiven={true} />
```

---

## Theming

Three theme modes are available:

| Value | Behavior |
|-------|----------|
| `"light"` | Light color scheme (white background, dark text). |
| `"dark"` | Dark color scheme (dark background, light text). |
| `"auto"` | Follows the user's system preference via `prefers-color-scheme`. |

The active theme is exposed on the root element as `data-bootai-theme`.

### Colors Object

Both themes resolve to a `Colors` object that is passed to all render callbacks:

```ts
interface Colors {
  bg: string;          // main background
  surface: string;     // secondary surface (e.g. input area)
  border: string;      // border color
  text: string;        // primary text color
  muted: string;       // secondary text color
  userBg: string;      // user message bubble background
  assistantBg: string; // assistant message bubble background
  inputBg: string;     // input field background
  inputBorder: string; // input field border
}
```

---

## Custom Styling

Every UI element in the widget is identified by a **slot key**. You can target any slot with CSS class names, inline style overrides, or both.

### Slot Keys

| Slot | Element |
|------|---------|
| `consentBanner` | Consent banner container |
| `consentText` | Consent text paragraph |
| `consentPrivacyLink` | Privacy policy link |
| `consentAcceptButton` | Accept/start button |
| `floatingRoot` | Floating mode root (fixed position container) |
| `floatingPanel` | Floating chat panel (popup) |
| `fab` | Floating action button |
| `fabIcon` | Icon inside the FAB |
| `inlineRoot` | Inline mode root container |
| `header` | Chat header bar |
| `headerStatusDot` | Green status indicator dot |
| `headerTitle` | Header title text |
| `headerSubtitle` | Header subtitle text |
| `messagesContainer` | Scrollable messages area |
| `emptyState` | "How can I help?" placeholder |
| `userMessage` | User message bubble |
| `assistantMessage` | Assistant message bubble |
| `loadingIndicator` | Typing indicator (dots) |
| `inputContainer` | Input area wrapper |
| `input` | Text input field |
| `sendButton` | Send button |

### `classNames` — CSS Classes

Add CSS class names to any slot. Useful for Tailwind CSS, CSS Modules, or plain stylesheets.

```tsx
<ChatBot
  siteId="..."
  apiUrl="..."
  consentGiven={true}
  classNames={{
    header: "my-chat-header",
    userMessage: "my-user-bubble",
    sendButton: "my-send-btn",
  }}
/>
```

### `styles` — Inline Style Overrides

Override default inline styles per slot. Your styles are merged on top of the defaults (spread after), so they take precedence.

```tsx
<ChatBot
  siteId="..."
  apiUrl="..."
  consentGiven={true}
  styles={{
    header: { background: "#92400e" },
    headerStatusDot: { backgroundColor: "#fbbf24" },
    sendButton: { background: "#92400e" },
    inlineRoot: { borderColor: "#d97706", maxHeight: 600 },
  }}
/>
```

### Combining `classNames` and `styles`

Both can be used simultaneously on the same slot. The `className` is applied to the element, and `styles` are merged into the `style` attribute.

```tsx
<ChatBot
  siteId="..."
  apiUrl="..."
  consentGiven={true}
  classNames={{ header: "shadow-lg rounded-t-xl" }}
  styles={{ header: { background: "linear-gradient(135deg, #667eea, #764ba2)" } }}
/>
```

---

## Render Callbacks

For full control over a UI section, provide a render callback. When a render callback is set, it **completely replaces** the default rendering for that section. Each callback receives contextual props and must return `ReactNode`.

### `ChatBotRenderCallbacks`

```ts
interface ChatBotRenderCallbacks {
  renderConsentBanner?: (props: ConsentBannerRenderProps) => ReactNode;
  renderHeader?:           (props: HeaderRenderProps) => ReactNode;
  renderMessage?:          (props: MessageRenderProps) => ReactNode;
  renderEmptyState?:       (props: EmptyStateRenderProps) => ReactNode;
  renderLoadingIndicator?: (props: LoadingIndicatorRenderProps) => ReactNode;
  renderInput?:            (props: InputRenderProps) => ReactNode;
  renderFab?:              (props: FabRenderProps) => ReactNode;
}
```

### Callback Props Reference

#### `ConsentBannerRenderProps`

| Prop | Type | Description |
|------|------|-------------|
| `isDark` | `boolean` | Whether dark mode is active. |
| `display` | `"floating" \| "inline"` | Current display mode. |
| `text` | `string` | Consent text from config. |
| `privacyUrl` | `string` | Privacy policy URL. |
| `acceptLabel` | `string` | Accept button label. |
| `onAccept` | `() => void` | **Must be called** to grant consent and persist the decision. |

#### `HeaderRenderProps`

| Prop | Type | Description |
|------|------|-------------|
| `colors` | `Colors` | Active theme colors. |

#### `MessageRenderProps`

| Prop | Type | Description |
|------|------|-------------|
| `message` | `Message` | The message object (`{ role, content }`). |
| `colors` | `Colors` | Active theme colors. |
| `renderedContent` | `ReactNode` | Pre-rendered content with parsed links. Use this to keep default link rendering. |

#### `EmptyStateRenderProps`

| Prop | Type | Description |
|------|------|-------------|
| `colors` | `Colors` | Active theme colors. |

#### `LoadingIndicatorRenderProps`

| Prop | Type | Description |
|------|------|-------------|
| `colors` | `Colors` | Active theme colors. |

#### `InputRenderProps`

| Prop | Type | Description |
|------|------|-------------|
| `value` | `string` | Current input value. |
| `isLoading` | `boolean` | Whether a response is streaming. |
| `colors` | `Colors` | Active theme colors. |
| `onChange` | `(value: string) => void` | Update the input value. |
| `onSend` | `() => void` | Send the current message. |
| `inputRef` | `RefObject<HTMLInputElement \| null>` | Ref for auto-focus behavior. Attach to your `<input>` element. |

#### `FabRenderProps`

| Prop | Type | Description |
|------|------|-------------|
| `isOpen` | `boolean` | Whether the chat panel is open. |
| `onToggle` | `() => void` | Toggle the chat panel. |

### Examples

#### Custom header with branding

```tsx
<ChatBot
  siteId="..."
  apiUrl="..."
  consentGiven={true}
  renderCallbacks={{
    renderHeader: ({ colors }) => (
      <div style={{ padding: "16px 20px", background: "#92400e", color: "#fff", display: "flex", alignItems: "center", gap: 10 }}>
        <img src="/logo.svg" alt="" width={24} height={24} />
        <div>
          <div style={{ fontWeight: 600, fontSize: 14 }}>My Store Support</div>
          <div style={{ fontSize: 12, opacity: 0.85 }}>We typically reply instantly</div>
        </div>
      </div>
    ),
  }}
/>
```

#### Custom empty state

```tsx
renderCallbacks={{
  renderEmptyState: ({ colors }) => (
    <div style={{ padding: 40, textAlign: "center", color: colors.muted }}>
      <p style={{ fontSize: 24 }}>👋</p>
      <p>Ask me anything about our products!</p>
    </div>
  ),
}}
```

#### Custom message rendering

```tsx
renderCallbacks={{
  renderMessage: ({ message, colors, renderedContent }) => (
    <div style={{
      display: "flex",
      justifyContent: message.role === "user" ? "flex-end" : "flex-start",
    }}>
      {message.role === "assistant" && <img src="/bot-avatar.png" width={28} height={28} />}
      <div style={{
        padding: "10px 14px",
        borderRadius: 12,
        background: message.role === "user" ? colors.userBg : colors.assistantBg,
        color: message.role === "user" ? "#fff" : colors.text,
        maxWidth: "75%",
      }}>
        {renderedContent}
      </div>
    </div>
  ),
}}
```

#### Custom FAB

```tsx
renderCallbacks={{
  renderFab: ({ isOpen, onToggle }) => (
    <button
      onClick={onToggle}
      style={{
        width: 60, height: 60, borderRadius: "50%",
        background: isOpen ? "#ef4444" : "#10b981",
        color: "#fff", border: "none", cursor: "pointer",
        fontSize: 24,
      }}
    >
      {isOpen ? "✕" : "💬"}
    </button>
  ),
}}
```

---

## Widget Script (IIFE)

The package also ships a standalone script at `dist/widget.global.js` that bundles React internally. Use it on any HTML page without a build step.

### Loading

```html
<script src="https://cdn.example.com/widget.global.js"></script>
```

This exposes a global `BootAI` object with two methods:

### `BootAI.render(options)`

Renders the chat widget. Accepts all the same options as the React component props.

```js
BootAI.render({
  siteId: "your-site-id",
  apiUrl: "https://api.example.com",
  theme: "auto",
  display: "floating",
  consentGiven: {
    text: "We use a chat assistant powered by AI.",
    privacyUrl: "/privacy",
    acceptLabel: "Got it",
    storage: "localStorage",
  },
  classNames: {
    header: "my-header-class",
  },
  styles: {
    header: { background: "#1e3a5f" },
    sendButton: { background: "#1e3a5f" },
  },
});
```

### `BootAI.destroy()`

Unmounts the widget and removes its DOM container.

```js
BootAI.destroy();
```

### Render Callbacks in the Widget Script

Since widget users don't have React, render callbacks return **`HTMLElement`** or **`string`** (HTML) instead of `ReactNode`. The widget internally bridges these to React using an `HtmlBridge` component.

```js
BootAI.render({
  siteId: "...",
  apiUrl: "...",
  consentGiven: true,
  renderCallbacks: {
    // Return an HTML string
    renderHeader: function (props) {
      return '<div style="padding:16px 20px;background:#1e3a5f;color:#fff">' +
        '<b>My Custom Header</b></div>';
    },

    // Or return a DOM element
    renderEmptyState: function (props) {
      var div = document.createElement("div");
      div.style.padding = "40px";
      div.style.textAlign = "center";
      div.style.color = props.colors.muted;
      div.textContent = "How can we help you today?";
      return div;
    },
  },
});
```

The callback props are identical to the React version. The only difference is the return type: `HTMLElement | string` instead of `ReactNode`.

---

## Web Component

The package ships a custom element at `dist/web-component.global.js` that bundles React internally. Use it on any HTML page with a single `<script>` tag — no build step, no JS required.

### Loading

```html
<script src="https://cdn.example.com/web-component.global.js"></script>
```

This registers the `<bootai-chat>` custom element.

### HTML Attributes

Simple string/boolean values are set via HTML attributes:

| Attribute | Type | Description |
|-----------|------|-------------|
| `site-id` | `string` | **Required.** Your site identifier. |
| `api-url` | `string` | Base URL of the BootAI chat API. |
| `theme` | `"light" \| "dark" \| "auto"` | Color theme. Default: `"light"`. |
| `display` | `"floating" \| "inline"` | Display mode. Default: `"floating"`. |
| `consent-given` | `"true"` | Set to `"true"` to skip the consent banner. |

### JS Properties

For complex data (objects, callbacks), set properties via JavaScript:

| Property | Type | Description |
|----------|------|-------------|
| `consentGiven` | `boolean \| ConsentConfig` | Consent config object or boolean. |
| `classNames` | `ChatBotClassNames` | CSS class overrides per slot. |
| `styles` | `ChatBotStyles` | Inline style overrides per slot. |
| `renderCallbacks` | `WidgetRenderCallbacks` | Custom render functions (return `HTMLElement \| string`). |

### Example

```html
<script src="https://cdn.example.com/web-component.global.js"></script>

<!-- Simple usage with HTML attributes -->
<bootai-chat
  site-id="your-site-id"
  api-url="https://api.example.com"
  theme="auto"
  consent-given="true"
></bootai-chat>

<!-- Advanced usage with JS properties -->
<bootai-chat
  site-id="your-site-id"
  api-url="https://api.example.com"
  display="inline"
></bootai-chat>

<script>
  const chat = document.querySelector("bootai-chat");
  chat.consentGiven = {
    text: "This chat uses AI. Do you agree?",
    privacyUrl: "/privacy",
    acceptLabel: "Accept",
    storage: "localStorage",
  };
  chat.styles = {
    header: { background: "#1e3a5f" },
    sendButton: { background: "#1e3a5f" },
  };
</script>
```

---

## TypeScript

All types are exported from the package entry point:

```ts
import type {
  ChatBotProps,
  ConsentConfig,
  ChatBotSlot,
  ChatBotClassNames,
  ChatBotStyles,
  ChatBotRenderCallbacks,
  Message,
  Colors,
  ConsentBannerRenderProps,
  HeaderRenderProps,
  MessageRenderProps,
  EmptyStateRenderProps,
  LoadingIndicatorRenderProps,
  InputRenderProps,
  FabRenderProps,
} from "@maxischmaxi/bootai-react";
```

### Key Types

```ts
// All targetable UI slots
type ChatBotSlot =
  | "consentBanner" | "consentText" | "consentPrivacyLink" | "consentAcceptButton"
  | "floatingRoot" | "floatingPanel" | "fab" | "fabIcon"
  | "inlineRoot"
  | "header" | "headerStatusDot" | "headerTitle" | "headerSubtitle"
  | "messagesContainer" | "emptyState" | "userMessage" | "assistantMessage" | "loadingIndicator"
  | "inputContainer" | "input" | "sendButton";

// CSS class name overrides
type ChatBotClassNames = Partial<Record<ChatBotSlot, string>>;

// Inline style overrides
type ChatBotStyles = Partial<Record<ChatBotSlot, React.CSSProperties>>;

// Chat message
interface Message {
  role: "user" | "assistant";
  content: string;
}
```

---

## How It Works

1. **Consent gate** — The widget renders nothing until `consentGiven` is truthy. If a `ConsentConfig` is passed, a banner is shown first.
2. **Readiness check** — After consent, the widget calls `GET {apiUrl}/chat/ready?siteId={siteId}`. It only renders the chat UI when the API responds `{ ready: true }`.
3. **Streaming responses** — Messages are sent via `POST {apiUrl}/chat` and responses are streamed using `ReadableStream`. The assistant's reply appears token by token.
4. **Link detection** — Assistant messages automatically render Markdown-style links (`[text](url)`) and bare URLs as clickable `<a>` tags.
5. **Hydration-safe** — The widget defers rendering until after client-side mount to prevent hydration mismatches in SSR frameworks (Next.js, Remix, etc.).

### API Contract

#### `GET /chat/ready?siteId={siteId}`

Returns `{ ready: boolean }`. The widget polls this once after consent.

#### `POST /chat`

Request body:

```json
{
  "siteId": "your-site-id",
  "message": "user's message",
  "history": [
    { "role": "user", "content": "previous message" },
    { "role": "assistant", "content": "previous response" }
  ]
}
```

Response: streaming text body. An internal `---SOURCES_END---\n` marker separates metadata from the visible response.

---

## Build Outputs

| File | Format | Description |
|------|--------|-------------|
| `dist/index.js` | ESM | React component (tree-shakeable). `react` and `react-dom` are external. |
| `dist/index.cjs` | CJS | React component for CommonJS environments. |
| `dist/index.d.ts` | TypeScript | Type declarations. |
| `dist/widget.global.js` | IIFE | Standalone script with React bundled. Exposes `BootAI` global. |
| `dist/web-component.global.js` | IIFE | Custom element `<bootai-chat>` with React bundled. |

---

## License

See repository root for license information.