npm package: @kolari-ai/widget-sdk
Description: Embeddable voice AI widget SDK for Kolari AI platform
Author: Kolari AI
Latest version: 0.1.10

# @kolari-ai/widget-sdk

> Embeddable voice AI widget SDK for Kolari AI platform

A sleek, unobtrusive pill widget that adds voice AI to any website. Features a live 3D orb animation, one-click voice interaction, and seamless integration. Perfect for customer support, lead qualification, appointment booking, and more.

**🔗 Server URL**: Use `https://www.kolari.ai` as your `serverUrl` in production.

**🐛 Found an issue?** Report it on our [GitHub Issues](https://github.com/Anpu-Labs/kolari-ai-widget-sdk/issues) page.

## Features

- 🎙️ **One-Click Voice Interaction** - Users click to talk, click to end
- ✨ **3D Animated Orb** - Live visualization that responds to voice activity
- 📱 **Fully Responsive** - Works on desktop and mobile browsers
- 🎨 **Customizable Branding** - Match your brand colors and style
- 🔒 **Secure & Private** - HTTPS required, microphone permissions handled automatically
- ⚡ **Fast & Lightweight** - Minimal bundle size, instant loading

## Installation

### Via npm

```bash
npm install @kolari-ai/widget-sdk
```

### Via Local Build (Development)

```html
<!-- Use the locally built browser bundle -->
<script src="./dist/index.browser.js"></script>
```

### Via CDN (Coming Soon)

```html
<script src="https://cdn.kolari.ai/widget/latest/kolari-widget.browser.js"></script>
```

## Quick Start

```html
<!DOCTYPE html>
<html>
<head>
  <title>My Website</title>
</head>
<body>
  <h1>Welcome to my website</h1>

  <!-- Load the widget SDK -->
  <script src="./dist/index.browser.js"></script>

  <!-- Initialize the widget -->
  <script>
    KolariWidget.init({
      // Required
      apiKey: 'pk_live_...',           // Your public API key
      agentId: 'your-agent-id',        // Your agent ID
      serverUrl: 'https://www.kolari.ai', // Kolari server URL

      // Branding (optional)
      branding: {
        agentName: 'Support Assistant',
        primaryColor: '#6366f1',
        secondaryColor: '#8b5cf6'
      },

      // Behavior (optional)
      autoStart: false,  // Set true to auto-connect on page load
      debug: false,      // Enable for development debugging

      // Event handlers (optional)
      onReady: () => console.log('Widget ready!'),
      onConnect: () => console.log('Connected to agent'),
      onDisconnect: () => console.log('Disconnected'),
      onError: (error) => console.error('Widget error:', error)
    });
  </script>
</body>
</html>
```

That's it! The widget will appear as a pill at the bottom-right of your page with a live orb and "VOICE CHAT" button.

### Using with React

```tsx
import { KolariWidget } from '@kolari-ai/widget-sdk';
import { useEffect } from 'react';

function App() {
  useEffect(() => {
    const widget = KolariWidget.init({
      apiKey: 'pk_live_...',
      agentId: 'your-agent-id',
      serverUrl: 'https://www.kolari.ai',

      branding: {
        primaryColor: '#6366f1',
        agentName: 'Assistant'
      },

      onConnect: () => console.log('Connected!'),
      onDisconnect: () => console.log('Disconnected'),
    });

    return () => widget.destroy();
  }, []);

  return <div>Your app content</div>;
}
```

### Using with Vue

```vue
<template>
  <div>Your app content</div>
</template>

<script>
import { KolariWidget } from '@kolari-ai/widget-sdk';

export default {
  mounted() {
    this.widget = KolariWidget.init({
      apiKey: 'pk_live_...',
      agentId: 'your-agent-id',
      serverUrl: 'https://www.kolari.ai',

      branding: {
        primaryColor: '#6366f1',
        agentName: 'Assistant'
      }
    });
  },
  beforeUnmount() {
    this.widget?.destroy();
  }
};
</script>
```

### Using with Next.js

```tsx
'use client'; // For Next.js App Router

import { useEffect } from 'react';

export default function Home() {
  useEffect(() => {
    // Dynamically import to avoid SSR issues
    import('@kolari-ai/widget-sdk').then(({ KolariWidget }) => {
      const widget = KolariWidget.init({
        apiKey: process.env.NEXT_PUBLIC_KOLARI_API_KEY!,
        agentId: process.env.NEXT_PUBLIC_KOLARI_AGENT_ID!,
        serverUrl: process.env.NEXT_PUBLIC_KOLARI_SERVER_URL!,

        branding: {
          primaryColor: '#6366f1',
          agentName: 'Support Assistant'
        }
      });

      return () => widget.destroy();
    });
  }, []);

  return <main>Your app content</main>;
}
```

## Configuration Options

```typescript
interface WidgetConfig {
  // ===== REQUIRED =====
  apiKey: string;              // Your public API key from Kolari dashboard
  agentId: string;             // The agent ID to connect to
  serverUrl: string;           // Kolari server URL
                               // Production: 'https://www.kolari.ai'
                               // Development: 'http://localhost:3000'

  // ===== BRANDING =====
  branding?: {
    primaryColor?: string;     // Hex color code (e.g., '#6366f1')
    secondaryColor?: string;   // Hex color code (e.g., '#8b5cf6')
    agentName?: string;        // Display name for the agent (e.g., 'Support Assistant')
    companyName?: string;      // Your company name
  };

  // ===== DISPLAY =====
  displayMode?: 'compact' | 'expanded';  // Widget display mode (default: 'compact')

  // ===== BEHAVIOR =====
  autoStart?: boolean;         // Auto-connect on page load (default: false)
  enableRecording?: boolean;   // Enable session recording (default: true)

  // ===== EVENT HANDLERS =====
  onReady?: () => void;                          // Widget initialized and ready
  onConnect?: () => void;                        // Connected to agent
  onDisconnect?: () => void;                     // Disconnected from agent
  onError?: (error: WidgetError) => void;        // Error occurred
  onStateChange?: (state: WidgetState) => void;  // Widget state changed
  onAudioLevel?: (level: number) => void;        // Audio level update (0-1)

  // ===== ADVANCED =====
  debug?: boolean;             // Enable debug logging (default: false)
  reconnectAttempts?: number;  // Max reconnection attempts (default: 3)
  reconnectDelay?: number;     // Delay between reconnects in ms (default: 2000)
}
```

### Configuration Examples

**Minimal Setup:**
```typescript
KolariWidget.init({
  apiKey: 'pk_live_...',
  agentId: 'agent-id',
  serverUrl: 'https://www.kolari.ai'
});
```

**With Branding:**
```typescript
KolariWidget.init({
  apiKey: 'pk_live_...',
  agentId: 'agent-id',
  serverUrl: 'https://www.kolari.ai',

  branding: {
    agentName: 'Sales Assistant',
    primaryColor: '#6366f1',
    secondaryColor: '#8b5cf6'
  }
});
```

**With Event Handlers:**
```typescript
KolariWidget.init({
  apiKey: 'pk_live_...',
  agentId: 'agent-id',
  serverUrl: 'https://www.kolari.ai',

  branding: {
    agentName: 'Support Agent',
    primaryColor: '#0066cc'
  },

  onReady: () => {
    console.log('Widget is ready!');
  },

  onConnect: () => {
    console.log('User connected to agent');
    // Track analytics, update UI, etc.
  },

  onDisconnect: () => {
    console.log('User disconnected');
  },

  onError: (error) => {
    console.error('Widget error:', error.code, error.message);
    // Show user-friendly error message
  },

  onStateChange: (state) => {
    console.log('Widget state:', state);
    // Update UI based on widget state
  }
});
```

**Full Production Configuration:**
```typescript
KolariWidget.init({
  // Required
  apiKey: process.env.KOLARI_API_KEY,
  agentId: process.env.KOLARI_AGENT_ID,
  serverUrl: 'https://www.kolari.ai',

  // Branding
  branding: {
    agentName: 'Customer Support',
    primaryColor: '#6366f1',
    secondaryColor: '#8b5cf6',
    companyName: 'Acme Inc'
  },

  // Behavior
  autoStart: false,
  enableRecording: true,

  // Advanced
  reconnectAttempts: 3,
  reconnectDelay: 2000,
  debug: false,

  // Event Handlers
  onReady: () => trackEvent('widget_ready'),
  onConnect: () => trackEvent('call_started'),
  onDisconnect: () => trackEvent('call_ended'),
  onError: (error) => {
    logError(error);
    showUserMessage('Connection failed. Please try again.');
  }
});
```

## API Methods

```typescript
const widget = KolariWidget.init({ ... });

// Connection control
await widget.connect();    // Connect to agent
await widget.disconnect(); // Disconnect from agent

// UI control
widget.show();            // Show the widget
widget.hide();            // Hide the widget
widget.minimize();        // Minimize the widget
widget.maximize();        // Maximize the widget

// Audio control
widget.mute();            // Mute microphone
widget.unmute();          // Unmute microphone
widget.setVolume(0.8);    // Set volume (0-1)

// State queries
widget.isConnected();     // Check connection status
widget.isReady();         // Check if widget is ready
widget.getState();        // Get current widget state
widget.getSession();      // Get session metadata
widget.getStats();        // Get session statistics

// Event listeners
widget.on('connect', () => { ... });
widget.on('disconnect', () => { ... });
widget.on('error', (error) => { ... });
widget.on('stateChange', (state) => { ... });
widget.on('transcript', (message) => { ... });

// Cleanup
widget.destroy();         // Remove widget and cleanup
```

## Events

The widget emits the following events that you can listen to:

| Event | Payload | Description |
|-------|---------|-------------|
| `ready` | `void` | Widget is initialized and ready |
| `connect` | `void` | Connected to voice agent |
| `disconnect` | `void` | Disconnected from agent |
| `error` | `WidgetError` | An error occurred |
| `stateChange` | `WidgetState` | Widget state changed |
| `transcript` | `TranscriptMessage` | New transcript message (expanded mode) |
| `audioLevel` | `number` | Audio level update (0-1) |
| `participantJoined` | `Participant` | Participant joined session |
| `participantLeft` | `Participant` | Participant left session |
| `reconnecting` | `number` | Attempting to reconnect (attempt number) |
| `reconnected` | `void` | Successfully reconnected |

**Example Usage:**
```typescript
const widget = KolariWidget.init({ ... });

widget.on('stateChange', (state) => {
  console.log('Widget state:', state);
  // Update your UI based on state
});

widget.on('error', (error) => {
  console.error('Widget error:', error.code, error.message);
  // Show error message to user
});

widget.on('audioLevel', (level) => {
  // level is 0-1, visualize audio activity
  updateAudioVisualizer(level);
});
```

## Widget States

The widget transitions through the following states:

| State | Description | Button Text (Compact) |
|-------|-------------|----------------------|
| `idle` | Widget initialized, not connected | "VOICE CHAT" |
| `connecting` | Connecting to agent | "CONNECTING..." |
| `connected` | Connected and ready | "END CHAT" |
| `listening` | Listening for user input | "LISTENING..." |
| `speaking` | Agent is speaking | "SPEAKING..." |
| `disconnected` | Disconnected from agent | "VOICE CHAT" |
| `error` | Error occurred | "TRY AGAIN" |

## Microphone Permissions

The widget automatically handles microphone permissions through WebRTC connection. When a user clicks "VOICE CHAT" (or your custom connect button), the browser will:

1. **Request microphone permission** (first time only)
2. **Remember the permission** for future sessions
3. **Auto-connect** if permission was previously granted

**Important Notes:**
- Permission is requested only **once** - the widget delegates all microphone access to Kolari
- Users can revoke permissions through their browser settings
- HTTPS is **required** for microphone access (WebRTC security requirement)
- If permission is denied, the widget will show an error state

**Testing Permissions:**
```typescript
KolariWidget.init({
  apiKey: 'pk_live_...',
  agentId: 'agent-id',
  serverUrl: 'https://www.kolari.ai',
  displayMode: 'compact',
  onError: (error) => {
    if (error.code === 'MICROPHONE_PERMISSION_DENIED') {
      console.error('Microphone access denied by user');
      // Show custom UI to guide user to enable permissions
    }
  }
});
```

## Widget UI States

The widget provides visual feedback through:

### Button Text Changes
- Idle/Disconnected: **"VOICE CHAT"** with 📞 phone icon
- Connecting: **"CONNECTING..."** with 📞 phone icon
- Connected/Listening/Speaking: **"END CHAT"** with 📵 phone-slash icon

### Orb Animation
- Idle: Static colored orb
- Connecting: Pulsing animation
- Listening: Gentle breathing animation
- Speaking: Active wave animation responding to audio levels

### Visual States
- **Hover**: Pill lifts up with enhanced shadow
- **Active**: Border changes to brand color
- **Pulsing**: Subtle opacity animation when connected

## Browser Support

### Desktop Browsers
- ✅ Chrome/Edge 90+
- ✅ Firefox 88+
- ✅ Safari 15+
- ✅ Opera 76+

### Mobile Browsers
- ✅ Mobile Safari (iOS 15+)
- ✅ Chrome Android (90+)
- ✅ Samsung Internet 14+

### Not Supported
- ❌ Internet Explorer (any version)
- ❌ Legacy Edge (pre-Chromium)
- ❌ iOS Safari < 15

## Requirements

### Technical Requirements
- ✅ **HTTPS connection** (required for WebRTC and microphone access)
- ✅ **Modern browser** with WebRTC support
- ✅ **Microphone access** (will be requested on first connection)
- ✅ **JavaScript enabled**

### Development Requirements
- Node.js 18+ (for building from source)
- npm or yarn package manager

### Server Requirements
- Kolari API server
- Valid API keys and agent configuration

## Troubleshooting

### Widget Not Appearing

**Problem**: Widget doesn't render on the page

**Solutions**:
1. Check that the script is loaded:
   ```javascript
   console.log(typeof KolariWidget); // Should output "function"
   ```
2. Verify API key and agent ID are correct
3. Check browser console for errors
4. Ensure you're calling `KolariWidget.init()` after the script loads

### Microphone Permission Issues

**Problem**: Permission requested multiple times or not working

**Solutions**:
1. Ensure you're using **HTTPS** (required for WebRTC)
2. Check browser microphone permissions in settings
3. Clear browser cache and test again
4. On iOS Safari, ensure iOS 15+ is being used

**Checking Permissions:**
```typescript
navigator.permissions.query({ name: 'microphone' as PermissionName })
  .then((result) => {
    console.log('Microphone permission:', result.state);
    // 'granted', 'denied', or 'prompt'
  });
```

### Connection Errors

**Problem**: Widget fails to connect to agent

**Solutions**:
1. Verify `serverUrl` is correct and accessible
2. Check API key has proper permissions
3. Ensure agent ID exists and is active
4. Check network tab for failed requests
5. Enable debug mode: `debug: true`

**Debug Mode:**
```typescript
KolariWidget.init({
  apiKey: '...',
  agentId: '...',
  serverUrl: '...',
  debug: true,  // Enable detailed logging
});
```

### No Audio or Voice

**Problem**: Connected but no audio or agent doesn't respond

**Solutions**:
1. Check system volume and browser audio settings
2. Verify agent has proper voice configuration
3. Test microphone with another application
4. Check for console errors related to audio

### Styling Issues

**Problem**: Widget styling conflicts with site CSS

**Solutions**:
1. Widget uses isolated styles with `kw-` prefix
2. Check for CSS conflicts in browser DevTools
3. Override styles if needed:
   ```css
   /* Override widget styles */
   .kw-compact-pill {
     bottom: 30px !important;
   }
   ```

### React/Next.js SSR Issues

**Problem**: Errors during server-side rendering

**Solutions**:
1. Use dynamic import to load widget client-side only:
   ```typescript
   useEffect(() => {
     import('@kolari-ai/widget-sdk').then(({ KolariWidget }) => {
       // Initialize here
     });
   }, []);
   ```
2. For Next.js, use `'use client'` directive
3. Ensure widget initialization happens in `useEffect`

### Performance Issues

**Problem**: Widget causing page slowdown

**Solutions**:
1. Don't initialize widget multiple times
2. Properly cleanup with `widget.destroy()`
3. Check for memory leaks in browser DevTools
4. Disable debug mode in production (`debug: false`)

### Getting Help

Still having issues? Here's how to get support:

1. **Check the logs**: Enable `debug: true` and check console
2. **Test in isolation**: Create a minimal HTML test page
3. **Browser compatibility**: Verify you're using a supported browser
4. **Network issues**: Check network tab for failed requests
5. **Contact support**: Provide error messages, browser version, and code snippet

## License

MIT

## Support

- **Report Issues**: https://github.com/Anpu-Labs/kolari-ai-widget-sdk/issues
- **Email**: support@kolari.ai
- **Website**: https://www.kolari.ai

## Contributing

Contributions are welcome! Please read our contributing guidelines and submit pull requests to our GitHub repository.

## Changelog

### v0.1.4 (Latest)
- ✨ **DEFAULT COMPACT MODE**: Widget now defaults to `'compact'` display mode
- 📝 Users no longer need to specify `displayMode: 'compact'` - it's automatic
- 🎯 Improved out-of-the-box experience with sleek pill widget by default

### v0.1.3
- 🐛 **CRITICAL FIX**: Resolved duplicate widget pills appearing on page
- 🔒 Added singleton pattern to prevent multiple widget instances
- 🛡️ Implemented render guard in UI renderer to prevent duplicate rendering
- 🧹 Enhanced destroy() method with robust error handling and cleanup verification
- 🔄 Added persistent initialization flag for React Strict Mode compatibility
- 🧰 Improved DOM cleanup to remove orphaned widget elements
- 📊 Enhanced logging for better debugging of widget lifecycle
- ✨ Widget now properly handles React component remounts in development
- 🎯 All widget operations now have proper error boundaries

### v0.1.0
- ✨ Added compact display mode
- ✨ Dynamic phone icon (📞/📵) based on connection state
- ✨ 3D animated orb with audio level visualization
- ✨ Single microphone permission request
- ✨ Improved state management and visual feedback
- 🐛 Fixed multiple permission request issue
- 🐛 Fixed button color transitions in compact mode
- 📝 Comprehensive README and documentation
- 🎨 Enhanced branding customization options