npm package: @mypatientspace/chatbot-widget
Description: Embeddable healthcare chatbot widget for websites and mobile apps
Author: myPatientSpace
Latest version: 1.0.78

# MPS Chatbot Widget

A standalone chatbot widget that third-party websites can embed via a single `<script>` tag.

[![npm version](https://img.shields.io/npm/v/@mypatientspace/chatbot-widget.svg)](https://www.npmjs.com/package/@mypatientspace/chatbot-widget)

## Features

- Self-contained React application bundled into a single JS file
- Easy integration with any website
- Embedded mode (default) or floating button mode
- Customizable theming with light, dark, and system color modes
- Style isolation (won't conflict with host site CSS)
- Native mobile support via WebView
- Voice input support
- Quick action buttons
- Survey/questionnaire support
- Content citations with overlay view

## Tech Stack

- React 19 with TypeScript
- Vite (library mode build)
- Emotion (CSS-in-JS for style isolation)
- lucide-react (icons)

## Compatibility

### Via CDN/Script Tag (Any Project)

When using the UMD bundle via `<script>` tag, the widget bundles its own React and renders in an isolated container. **Your project's React version doesn't matter** — this works with any framework (or no framework at all), including:

- Vanilla JavaScript/HTML
- jQuery
- Angular, Vue, Svelte
- Older React versions (14, 15, 16, etc.)

### Via npm Import (React 18+ Required)

When importing as an ES module in a React project:

```javascript
import '@mypatientspace/chatbot-widget';
```

You need **React 18 or higher**. The widget uses modern React features (hooks, Context API) that aren't available in older versions.

| Integration Method | React Version Required |
|--------------------|------------------------|
| CDN / Script tag   | Any (bundles own React) |
| npm import         | React 18+ |

## Installation

### Via CDN (Recommended)

**unpkg:**
```html
<script src="https://unpkg.com/@mypatientspace/chatbot-widget@latest/dist/mypatientspace-widget.umd.js"></script>
```

**jsDelivr:**
```html
<script src="https://cdn.jsdelivr.net/npm/@mypatientspace/chatbot-widget@latest/dist/mypatientspace-widget.umd.js"></script>
```

### Via npm
```bash
npm install @mypatientspace/chatbot-widget
```

## Development

```bash
# Install dependencies
yarn install

# Start dev server (includes demo UI)
yarn dev

# Build for production
yarn build              # Builds both widget and demo
yarn build:widget       # Widget only
yarn build:demo         # Demo only

# Preview production build
yarn preview
```

## Web Integration

### Minimal Setup
```html
<script src="https://unpkg.com/@mypatientspace/chatbot-widget@latest/dist/mypatientspace-widget.umd.js"></script>
<script>
  ChatbotWidget.init({
    apiEndpoint: 'https://your-api.com',
    token: 'your-auth-token'
  });
</script>
```

### Full Customization
```html
<script src="https://unpkg.com/@mypatientspace/chatbot-widget@latest/dist/mypatientspace-widget.umd.js"></script>
<script>
  ChatbotWidget.init({
    // API & Auth
    apiEndpoint: 'https://your-api.com',
    token: 'your-auth-token',
    userType: 'patient',             // 'patient' (default) or 'staff'
    assistantId: 'your-assistant-id',

    // Display mode
    floatingMode: true,          // false = embedded (default), true = FAB button
    position: 'bottom-left',     // FAB position (default: 'bottom-right')
    hideExpandButton: true,      // Hide expand/minimize button from header
    showCloseSlot: true,         // Empty placeholder for custom close button

    // Content
    headerTitle: 'Health Support',
    headerSubtitle: 'Online now',
    greeting: 'Hi! How can I help?',
    placeholder: 'Ask me anything...',
    brandingText: 'Powered by MyCompany',
    disclaimerText: 'AI can make mistakes. Verify important information.',
    headerIcon: '/logo.png',
    fabIcon: '/avatar.png',       // Only used in floating mode
    quickActions: ['I need help', 'Book appointment'],

    // Appearance
    colorMode: 'system',  // 'light', 'dark', or 'system' (auto-detect)
    theme: {
      colors: {
        primary: '#ff6600',        // FAB button, links, main accent
        primaryDark: '#cc5200',    // Hover states
        secondary: '#2d3748',      // Header background
        headerText: '#ffffff',     // Header text color
        background: '#ffffff',     // Chat window background
        surface: '#f7fafc',        // Input/card backgrounds
        text: '#2d3748',           // Primary text
        textLight: '#718096',      // Secondary text, timestamps
        userBubble: '#ff6600',     // User message background
        userBubbleText: '#ffffff', // User message text
        botBubble: '#edf2f7',      // Bot message background
        botBubbleText: '#2d3748',  // Bot message text
      },
      fontFamily: 'Inter, system-ui, sans-serif',
      borderRadius: '8px',
      shadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
    },

    // Callbacks
    onOpen: () => console.log('Chat opened'),
    onClose: () => console.log('Chat closed'),
    onMessageSent: (message) => console.log('Sent:', message),
    onMessageReceived: (message) => console.log('Received:', message),
    onQuickAction: (action) => console.log('Quick action:', action),
    onStreamChunk: (chunk) => console.log('Chunk:', chunk),
    onCitationClick: (citation) => console.log('Citation:', citation),
    onNewChat: () => console.log('New chat started'),
    onWidgetReady: (api) => console.log('Widget ready:', api),
  });
</script>
```

### Via npm import
```javascript
import '@mypatientspace/chatbot-widget';

ChatbotWidget.init({
  apiEndpoint: 'https://your-api.com'
});
```

### Patient & Staff Integration

The widget supports two user types with separate API routing. Each type uses different backend endpoints for chat, message history, and assistant lookup.

#### Patient (Default)

Patient mode is the default — no `userType` needed:

```html
<script>
  ChatbotWidget.init({
    apiEndpoint: 'https://your-api.com',
    token: 'your-auth-token'
  });
</script>
```

API endpoints used:
- `POST /api/patient/chat/stream` — Send messages
- `GET /api/patient/chat/messages` — Load history
- `GET /api/patient/assistant` — Get assistant config

#### Staff

Staff mode requires `userType: 'staff'` along with `contextPatientId` and `patientTypeId`:

```html
<script>
  ChatbotWidget.init({
    apiEndpoint: 'https://your-api.com',
    token: 'your-auth-token',
    userType: 'staff',
    contextPatientId: 'patient-123',
    patientTypeId: 'type-456'
  });
</script>
```

API endpoints used:
- `POST /api/staff/chat/stream` — Send messages (includes `contextPatientId` in body)
- `GET /api/staff/chat/messages` — Load history (requires `contextPatientId` query param)
- `GET /api/staff/assistant/{patientTypeId}` — Get assistant config

#### Required Fields by User Type

| Field | Patient | Staff |
|-------|---------|-------|
| `apiEndpoint` | Required | Required |
| `token` | Required | Required |
| `userType` | Optional (default) | `'staff'` |
| `contextPatientId` | Optional | Required |
| `patientTypeId` | - | Required |

### API Methods
```javascript
ChatbotWidget.open();         // Open chat window
ChatbotWidget.close();        // Close chat window
ChatbotWidget.toggle();       // Toggle open/close
ChatbotWidget.startNewChat(); // Clear session and start new chat
ChatbotWidget.update(config); // Update widget configuration
ChatbotWidget.destroy();      // Remove widget completely
```

## Embedded Mode

By default, the widget is embedded inside a container element. You can enable floating mode for a FAB button popup.

### Using containerSelector

Embed the chat inside an existing element:

```html
<div id="my-chat" style="width: 400px; height: 600px;"></div>

<script src="https://unpkg.com/@mypatientspace/chatbot-widget@latest/dist/mypatientspace-widget.umd.js"></script>
<script>
  ChatbotWidget.init({
    apiEndpoint: 'https://your-api.com',
    containerSelector: '#my-chat'
  });
</script>
```

### Using floatingMode

Enable floating mode with FAB button (classic chatbot style):

```html
<script>
  ChatbotWidget.init({
    apiEndpoint: 'https://your-api.com',
    floatingMode: true
  });
</script>
```

### Comparison

| Feature | Embedded (default) | `floatingMode: true` |
|---------|-------------------|----------------------|
| FAB button | Hidden | Shown |
| Chat visibility | Always open | Opens on click |
| Position | Fills container | Fixed bottom-right/left |
| Sizing | 100% of parent | 380x520px |

## Mobile Integration

### Android (Kotlin)
```kotlin
class ChatActivity : AppCompatActivity() {
    private lateinit var webView: WebView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        webView = WebView(this).apply {
            settings.javaScriptEnabled = true
            settings.domStorageEnabled = true
            loadDataWithBaseURL(
                "https://your-domain.com",
                """
                <!DOCTYPE html>
                <html>
                <head>
                    <meta name="viewport" content="width=device-width, initial-scale=1">
                </head>
                <body style="margin:0;padding:0;">
                    <script src="https://unpkg.com/@mypatientspace/chatbot-widget@latest/dist/mypatientspace-widget.umd.js"></script>
                    <script>
                        ChatbotWidget.init({
                            apiEndpoint: 'https://your-api.com'
                        });
                        ChatbotWidget.open();
                    </script>
                </body>
                </html>
                """.trimIndent(),
                "text/html", "UTF-8", null
            )
        }
        setContentView(webView)
    }
}
```

### iOS (Swift)
```swift
import UIKit
import WebKit

class ChatViewController: UIViewController {
    var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let config = WKWebViewConfiguration()
        config.preferences.javaScriptEnabled = true

        webView = WKWebView(frame: view.bounds, configuration: config)
        webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(webView)

        let html = """
        <!DOCTYPE html>
        <html>
        <head>
            <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
            <style>body { margin: 0; padding: 0; }</style>
        </head>
        <body>
            <script src="https://unpkg.com/@mypatientspace/chatbot-widget@latest/dist/mypatientspace-widget.umd.js"></script>
            <script>
                ChatbotWidget.init({
                    apiEndpoint: 'https://your-api.com'
                });
                ChatbotWidget.open();
            </script>
        </body>
        </html>
        """

        webView.loadHTMLString(html, baseURL: URL(string: "https://your-domain.com"))
    }
}
```

### iOS (Objective-C)
```objc
#import <UIKit/UIKit.h>
#import <WebKit/WebKit.h>

@interface ChatViewController : UIViewController
@property (nonatomic, strong) WKWebView *webView;
@end

@implementation ChatViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    config.preferences.javaScriptEnabled = YES;

    self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
    self.webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [self.view addSubview:self.webView];

    NSString *html = @"<!DOCTYPE html>"
        "<html>"
        "<head>"
        "<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'>"
        "<style>body { margin: 0; padding: 0; }</style>"
        "</head>"
        "<body>"
        "<script src='https://unpkg.com/@mypatientspace/chatbot-widget@latest/dist/mypatientspace-widget.umd.js'></script>"
        "<script>"
        "ChatbotWidget.init({ apiEndpoint: 'https://your-api.com' });"
        "ChatbotWidget.open();"
        "</script>"
        "</body>"
        "</html>";

    [self.webView loadHTMLString:html baseURL:[NSURL URLWithString:@"https://your-domain.com"]];
}

@end
```

### React Native
```jsx
import { WebView } from 'react-native-webview';

const ChatScreen = () => {
  const html = `
    <!DOCTYPE html>
    <html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1">
    </head>
    <body style="margin:0;padding:0;">
        <script src="https://unpkg.com/@mypatientspace/chatbot-widget@latest/dist/mypatientspace-widget.umd.js"></script>
        <script>
            ChatbotWidget.init({
                apiEndpoint: 'https://your-api.com'
            });
            ChatbotWidget.open();
        </script>
    </body>
    </html>
  `;

  return (
    <WebView
      source={{ html }}
      javaScriptEnabled={true}
      domStorageEnabled={true}
    />
  );
};
```

## Configuration Reference

### All Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `apiEndpoint` | `string` | - | Base API URL, e.g. `https://your-api.com` (widget offline if not set) |
| `token` | `string` | - | Auth token for API requests |
| `userType` | `'patient' \| 'staff'` | `'patient'` | User type for API endpoint routing |
| `patientTypeId` | `string` | - | Patient type ID for staff assistant lookup (required when `userType` is `'staff'`) |
| `sessionId` | `string` | - | Existing session ID to resume |
| `assistantId` | `string` | - | Specific assistant ID to use |
| `contextPatientId` | `string` | - | Patient context for the chat |
| `loadHistory` | `boolean` | `true` | Load previous chat history |
| `colorMode` | `'light' \| 'dark' \| 'system'` | `'light'` | Color mode for the widget |
| `theme` | `ThemeConfig` | - | Custom theme colors, fonts, border radius, shadow |
| `position` | `'bottom-right' \| 'bottom-left'` | `'bottom-right'` | FAB button position |
| `containerSelector` | `string` | - | CSS selector for embedded container |
| `floatingMode` | `boolean` | `false` | Show FAB button and floating chat |
| `hideExpandButton` | `boolean` | `false` | Hide the expand/minimize button from header |
| `showCloseSlot` | `boolean` | `false` | Show empty circle placeholder for custom close button |
| `greeting` | `string` | From API | Initial bot greeting message |
| `placeholder` | `string` | From API | Input field placeholder text |
| `headerTitle` | `string` | From API | Chat window header title |
| `headerSubtitle` | `string` | From API | Chat window header subtitle |
| `headerIcon` | `string` | From API | Header icon image URL |
| `fabIcon` | `string` | From API | FAB button image URL |
| `brandingText` | `string` | From API | Footer branding text |
| `disclaimerText` | `string` | From API | Disclaimer text below input area |
| `quickActions` | `string[]` | From API | Predefined quick action buttons |

> **Note:** Content fields (`greeting`, `headerTitle`, `placeholder`, `brandingText`, `headerIcon`, `fabIcon`, `quickActions`, `disclaimerText`) are loaded from the assistant's API configuration by default. Setting them in the widget config overrides the API values.

### Callbacks

| Callback | Parameters | Description |
|----------|------------|-------------|
| `onOpen` | `()` | Called when chat window opens |
| `onClose` | `()` | Called when chat window closes |
| `onMessageSent` | `(message: Message)` | Called after user sends a message |
| `onMessageReceived` | `(message: Message)` | Called after bot response completes |
| `onQuickAction` | `(action: string)` | Called when quick action is clicked |
| `onStreamChunk` | `(chunk: string)` | Called for each streamed response chunk |
| `onCitationClick` | `(citation: Citation)` | Called when a content card is clicked |
| `onNewChat` | `()` | Called when user starts a new chat |
| `onWidgetReady` | `(api: WidgetApi)` | Called when widget is ready with API methods |

### Available Theme Colors

| Color | Light Default | Description |
|-------|---------------|-------------|
| `primary` | `#00c2d1` | FAB button, links, main accent |
| `primaryDark` | `#017992` | Hover states |
| `secondary` | `#3c4d73` | Header background |
| `accent` | `#33b1e6` | Secondary accent color |
| `accentLight` | `#b8ebff` | Light accent |
| `success` | `#00dec4` | Success states |
| `info` | `#30d7ed` | Info states |
| `background` | `#ffffff` | Chat window background |
| `surface` | `#fdfefe` | Input/card backgrounds |
| `text` | `#3c4d73` | Primary text |
| `textLight` | `#6b7280` | Secondary text, timestamps |
| `userBubble` | `#00c2d1` | User message background |
| `userBubbleText` | `#ffffff` | User message text |
| `botBubble` | `#e0f7fa` | Bot message background |
| `botBubbleText` | `#3c4d73` | Bot message text |
| `border` | `#d1d5db` | Border color |
| `borderLight` | `#e5e7eb` | Light border |
| `error` | `#ef4444` | Error states |
| `errorBackground` | `#fef2f2` | Error background |
| `white` | `#ffffff` | White color |
| `recording` | `#ef4444` | Voice recording indicator |
| `headerText` | `#ffffff` | Header text and icon color |
| `scrollbarThumb` | `rgba(60, 77, 115, 0.2)` | Scrollbar thumb color |
| `scrollbarThumbHover` | `rgba(60, 77, 115, 0.3)` | Scrollbar thumb hover |

### Default Fallback Values

Content fields are resolved with this priority: **Config > Assistant API > Component default**

| Field | Component Fallback |
|-------|-------------------|
| `headerTitle` | "AI Doctor" |
| `greeting` | "Hello! Welcome to our healthcare support. How can I assist you today?" |
| `placeholder` | "Type a message..." |
| `brandingText` | "Developed by myPatientSpace" |
| `disclaimerText` | "AI can make mistakes. Verify important information." |
| `headerIcon` | MPS logo |
| `fabIcon` | MPS logo |
| `quickActions` | Empty (from API only) |

## License

MIT