# SideNetAI SDK

Embeddable AI chat sidebar for any web application. Uses Shadow DOM for complete CSS isolation — your styles won't affect the sidebar, and the sidebar won't affect your styles.

## Contents

- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Configuration](#configuration)
- [API Reference](#api-reference)
  - [Lifecycle](#lifecycle)
  - [Visibility](#visibility)
  - [Messaging](#messaging)
  - [Configuration Updates](#configuration-updates)
  - [Runtime Authentication](#runtime-authentication)
  - [Agents](#agents)
  - [Defaults & Constants](#defaults--constants)
- [Events](#events)
- [Theming](#theming)
- [Greeting & Suggestions](#greeting--suggestions)
- [Content Push](#content-push)
- [TypeScript](#typescript)
- [React Hook](#react-hook)
- [Advanced: Direct Component Usage](#advanced-direct-component-usage)
- [License](#license)

## Features

- **Shadow DOM Isolation** — Complete CSS encapsulation
- **Framework Agnostic** — Works with React, Vue, Angular, or vanilla JS
- **Two Layout Modes** — Sidebar (slides from edge) or Modal (floating panel)
- **Full TypeScript Support** — Type definitions included
- **Runtime Authentication** — Pass user credentials for tools (GitHub, Slack, etc.)
- **Theming** — Light/dark mode with customizable design tokens
- **Keyboard Shortcuts** — Configurable Cmd/Ctrl+key shortcuts
- **Content Push** — Optionally push your page content when sidebar opens
- **Events** — Callbacks, CSS custom properties, and window events

## Installation

### NPM (React/Next.js/Vite)

```bash
npm install sidenetai-sdk
```

```typescript
import { initSidenet } from 'sidenetai-sdk';

initSidenet({
  orgId: 'your-org-id',
  copilotId: 'your-copilot-id',
});
```

### CDN (Static sites, WordPress, vanilla JS)

```html
<script src="https://unpkg.com/sidenetai-sdk@latest/dist/sidenetai-sdk.standalone.umd.js"></script>
<script>
  Sidenet.initSidenet({
    orgId: 'your-org-id',
    copilotId: 'your-copilot-id',
  });
</script>
```

The standalone bundle (~13MB uncompressed, ~2.8MB gzipped) includes React and all dependencies. Use the NPM package if you already have React.

## Quick Start

```typescript
import { initSidenet, toggleSidenet, sendMessage } from 'sidenetai-sdk';

// Initialize
await initSidenet({
  orgId: 'org-123',
  copilotId: 'copilot-456',
  position: 'right',
  defaultOpen: false,
  keyboardShortcut: 'k', // Cmd+k / Ctrl+k
});

// Toggle with a button
document.getElementById('chat-btn').onclick = toggleSidenet;

// Send a message programmatically
await sendMessage('Hello!');
```

## Configuration

All options for `initSidenet()`:

```typescript
await initSidenet({
  // Required
  orgId: string,              // Organization ID (sent as X-Organization-Id header)

  // Layout
  position: 'left' | 'right', // Sidebar position (default: 'right')
  width: string,              // Panel width, e.g. '400px', '45vw' (default: '45vw')
  height: string,             // Panel height (modal default: '600px', sidebar: auto)
  layout: 'sidebar' | 'modal',// Layout mode (default: 'sidebar')
  offset: {                   // Positioning offsets
    top?: string,
    bottom?: string,
    left?: string,
    right?: string,
  },

  // Behavior
  defaultOpen: boolean,       // Start open or closed (default: false)
  keyboardShortcut: string,   // Key for Cmd/Ctrl shortcut, e.g. 'b'
  contentElement: string,     // CSS selector for content to push aside, e.g. '#app'
  debug: boolean,             // Enable verbose console logs (default: false)

  // Identity
  userId: string,             // User identifier
  copilotId: string,          // Copilot identifier
  apiKey: string,             // API key for backend requests

  // Agent & Threading
  agentVersionId: string,     // Use specific agent (skips agent selection)
  showAgentDropdown: boolean, // Force-show/hide agent dropdown
                              //   undefined (default) → auto: show if multiple
                              //   subagents AND no agentVersionId provided
  groupName: string,          // Group name for chat requests
  lastThread: boolean,        // Auto-select most recent thread on init
  threadId: string,           // Pin to specific thread
  read_only: boolean,         // Hide composer (requires threadId)
  use_routing: boolean,       // Enable backend message routing (default: true)

  // Context & Messages
  context: string[],          // Context strings sent with requests
  firstMessage: string,       // Auto-send on first open

  // Runtime Auth
  runtimeAuth: [{
    providerName: string,
    credentials: Record<string, any>,
  }],

  // Theming
  theme: 'light' | 'dark',    // Color scheme (default: 'light')
  customCSS: string,          // Raw CSS injected into Shadow DOM
  customStyles: Record<string, Record<string, string>>, // Structured CSS rules

  // UI chrome
  showClose: boolean,         // Show a close/collapse button in the header (default: false)

  // Greeting / placeholder / suggestions / theme tokens (overrides backend sdk_ui)
  // Layers on top of the DB config — only the fields you pass are overridden.
  sdkUi: Partial<SidebarSDKConfig>,

  // Callbacks
  onOpen: (detail) => void,
  onClose: (detail) => void,
  onResize: (detail) => void,
  onReady: (detail) => void,
});
```

## API Reference

### Lifecycle

| Function | Description |
|----------|-------------|
| `initSidenet(options)` | Initialize the sidebar (async, call once) |
| `destroySidenet()` | Remove sidebar completely (async, returns Promise) |

### Visibility

| Function | Description |
|----------|-------------|
| `toggleSidenet()` | Toggle sidebar visibility |
| `openSidenet()` | Open the sidebar |
| `closeSidenet()` | Close the sidebar |
| `getSidenetState()` | Returns `{ isOpen: boolean, isInitialized: boolean }` |
| `triggerKeyboardShortcut()` | Programmatically trigger the shortcut action |

### Messaging

| Function | Description |
|----------|-------------|
| `sendMessage(text, options?)` | Send a message programmatically (async) |

```typescript
await sendMessage('Analyze this code', {
  agentVersionId: 'agent-123',          // Optional: use specific agent
  openSidebar: true,                    // Optional: auto-open sidebar (default: true)
  prompt: 'Respond as a senior staff engineer.', // Optional: one-shot system prompt for this send only
});
```

**Options**

| Field | Type | Description |
|-------|------|-------------|
| `agentVersionId` | `string` | Route this message to a specific agent version. |
| `openSidebar` | `boolean` | Auto-open the sidebar before sending. Defaults to `true`. |
| `prompt` | `string` | One-shot system prompt prepended to this single request only — same semantics as `text.suggestions[].prompt`. Cleared automatically after the message is sent, so follow-up messages revert to the agent's normal behaviour. |

> `sendMessage()` always starts a **new thread** for the programmatic message — the active thread is reset before sending. Thread titles are auto-generated by the backend after streaming completes.

### Configuration Updates

| Function | Description |
|----------|-------------|
| `updateSidenetConfig(options)` | Update SDK behaviour / layout / identity |
| `updateSidenetStyledConfig(styleConfig)` | Update greeting / suggestions / theme tokens |
| `refreshSidenetConfig()` | Re-fetch config from backend (async) |
| `refreshSidenetStyles()` | Re-apply container styles |
| `getRuntimeConfig()` | Get current merged config |

Two update functions, separated by what they change:

- **`updateSidenetConfig(options)`** — SDK behaviour and chrome. Accepts every init option **except** `orgId`, the callbacks (`onOpen` / `onClose` / `onResize` / `onReady`), the one-shot init-only fields (`context`, `customCSS`, `customStyles`, `sdkUi`, `defaultOpen`), and the read-once auth fields (`userId`, `copilotId`, `apiKey` — listed in the type but normally set at init). Concretely: `width`, `height`, `position`, `layout`, `offset`, `theme` (`'light' | 'dark'`), `agentVersionId`, `showAgentDropdown`, `keyboardShortcut`, `threadId`, `read_only`, `use_routing`, `runtimeAuth`, `groupName`, `firstMessage`, `lastThread`, `debug`, `showClose`. Two `null` sentinels: `keyboardShortcut: null` disables the shortcut, `threadId: null` clears the pinned thread.
- **`updateSidenetStyledConfig(styleConfig)`** — visual content. Accepts `Partial<SidebarSDKConfig>` — the same shape the backend returns as `sdk_ui`. Use it to override `text` (greeting / placeholder / suggestions) and `theme` design tokens. Partial calls merge into the current config, so passing only `text.suggestions` keeps `text.greeting` and the loaded `theme` intact.
- **`getRuntimeConfig()`** — synchronous read of the current merged `{ text, theme }`. Useful for snapshotting the resolved config (defaults + backend `sdk_ui` + your overrides) without subscribing via `useSidebarConfig`.

Rule of thumb: "where the sidebar is and how it behaves" → `updateSidenetConfig`. "what the sidebar looks like and what copy it shows" → `updateSidenetStyledConfig`.

### Runtime Authentication

| Function | Description |
|----------|-------------|
| `updateSidenetRuntimeAuth(credentials)` | Update auth credentials dynamically |
| `getSidenetRuntimeAuthProviders()` | Get available auth providers (async) |

```typescript
// Update credentials after init
updateSidenetRuntimeAuth([
  { providerName: 'github', credentials: { token: 'ghp_...' } },
  { providerName: 'slack', credentials: { token: 'xoxb-...' } },
]);

// Get available providers
const providers = await getSidenetRuntimeAuthProviders();
// [{ providerName: 'github', authType: 'oauth', authFields: ['token'] }, ...]
```

### Agents

| Function | Description |
|----------|-------------|
| `getAgents(copilotId, apiKey, orgId)` | Fetch available subagents (async) |

```typescript
import { getAgents } from 'sidenetai-sdk';

// Same three values you pass to initSidenet — handy for building a
// custom picker before the sidebar is mounted.
const agents = await getAgents('copilot-456', 'sk_...', 'org-123');
```

### Defaults & Constants

| Export | Description |
|--------|-------------|
| `DEFAULT_THEME` | `ThemeInput` — the full default token set the SDK ships with. Read it to inspect defaults, or spread it into `updateSidenetStyledConfig({ theme })` as a starting point. |
| `SIDENET_EVENTS` | `{ OPEN, CLOSE, RESIZE, READY }` — typed event-name constants. Prefer these over raw `'sidenet:open'` strings for type safety. |

```typescript
import { DEFAULT_THEME, SIDENET_EVENTS, updateSidenetStyledConfig } from 'sidenetai-sdk';

// Start from defaults and tweak one token
updateSidenetStyledConfig({
  theme: { ...DEFAULT_THEME, accentColor: { light: '#ff5722', dark: '#ff8a65' } },
});

window.addEventListener(SIDENET_EVENTS.OPEN, (e) => {
  console.log('Opened at', e.detail.width);
});
```

## Events

Three ways to respond to sidebar state changes:

### 1. Callbacks (Simplest)

```typescript
initSidenet({
  orgId: 'org-123',
  onOpen: (detail) => console.log('Opened', detail.width),
  onClose: (detail) => console.log('Closed'),
  onResize: (detail) => console.log('Resized to', detail.width),
  onReady: (detail) => console.log('Ready'),
});
```

### 2. CSS Custom Properties (Zero-code layout)

```css
.my-content {
  margin-right: var(--sidenet-width, 0);
  transition: margin 0.3s ease;
}
```

Available properties (set on the host page's `<html>` element):
- `--sidenet-width` — Current width (e.g., `400px`) or `0` when closed
- `--sidenet-open` — `1` when open, `0` when closed
- `--sidenet-position` — `left` or `right`

> Theme token variables (`--background`, `--aui-accent-color`, etc.) live **inside the Shadow DOM** and are not exposed to the host page. To customize them, use `updateSidenetStyledConfig({ theme })` rather than CSS overrides.

### 3. Window Events (Decoupled)

```typescript
import { SIDENET_EVENTS } from 'sidenetai-sdk';

window.addEventListener(SIDENET_EVENTS.OPEN, (e) => {
  console.log('Opened', e.detail.width, e.detail.position);
});

window.addEventListener(SIDENET_EVENTS.CLOSE, (e) => {
  console.log('Closed');
});

window.addEventListener(SIDENET_EVENTS.RESIZE, (e) => {
  console.log('Resized', e.detail.width);
});

window.addEventListener(SIDENET_EVENTS.READY, (e) => {
  console.log('Ready');
});
```

Raw event-name strings (`'sidenet:open'`, `'sidenet:close'`, `'sidenet:resize'`, `'sidenet:ready'`) also work — the constants exist for type safety and refactor resilience.

Event detail shape:
```typescript
interface SidenetEventDetail {
  width: string;           // e.g., '400px'
  position: 'left' | 'right';
  layout: 'sidebar' | 'modal';
}
```

## Theming

### Light/Dark Mode

```typescript
initSidenet({
  orgId: 'org-123',
  theme: 'dark', // or 'light'
});

// Update at runtime
updateSidenetConfig({ theme: 'light' });
```

### Design Tokens

Each token accepts a `ThemeTokenInput` — either a plain `string` (applies to both light and dark) or a `{ light?: string; dark?: string }` object for per-mode values. If only one side is set, that value falls back to both modes.

Use `updateSidenetStyledConfig()` to customize colors, radii, and fonts:

```typescript
updateSidenetStyledConfig({
  theme: {
    // Single value applies to both modes
    radius: '0.5rem',
    fontFamily: '"Inter", sans-serif',

    // Per-mode values
    background: { light: '#ffffff', dark: '#09090b' },
    foreground: { light: '#09090b', dark: '#fafafa' },
    accentColor: { light: '#0ea5e9', dark: '#38bdf8' },
    userMessageBackground: { light: '#eff6ff', dark: '#1e3a5f' },
  },
});
```

Available tokens:

| Token | Description |
|-------|-------------|
| `radius` | Base border radius |
| `composerRadius` | Input area radius |
| `messageRadius` | Message bubble radius |
| `suggestionRadius` | Suggestion card radius |
| `sendButtonRadius` | Send button radius (default: pill) |
| `headerButtonRadius` | Header icon button radius |
| `selectRadius` | Dropdown trigger radius |
| `modalRadius` | Modal container radius |
| `threadMaxWidth` | Max width of conversation area |
| `fontFamily` | Font family |
| `accentColor` | Interactive element color |
| `background` | Main background |
| `foreground` | Main text color |
| `muted` | Secondary surface background |
| `mutedForeground` | Secondary text color |
| `border` | Border color |
| `userMessageBackground` | User bubble background |
| `userMessageForeground` | User bubble text |
| `userMessageBorder` | User bubble border |
| `assistantMessageBackground` | Assistant bubble background |
| `assistantMessageForeground` | Assistant bubble text |
| `assistantMessageBorder` | Assistant bubble border |
| `composerBackground` | Input area background |
| `suggestionBackground` | Suggestion card background |
| `suggestionBorder` | Suggestion card border |

### Custom CSS

```typescript
initSidenet({
  orgId: 'org-123',
  customCSS: `
    .aui-message-content { font-size: 14px; }
  `,
  // Or structured:
  customStyles: {
    '.aui-message-content': { 'font-size': '14px' },
  },
});
```

## Greeting & Suggestions

The empty-state greeting, composer placeholder, and suggestion cards are loaded from the backend's `sdk_ui.text`. You can override any of these via `initSidenet({ sdkUi })` or at runtime via `updateSidenetStyledConfig({ text })`. Suggestion cards can also bind a specific agent for one-shot routing on click.

```typescript
await initSidenet({
  orgId: 'org-123',
  copilotId: 'copilot-456',
  sdkUi: {
    text: {
      greeting: { title: 'Hello!', subtitle: 'Pick a quick action below.' },
      placeholder: 'Ask anything…',
      suggestions: [
        // agentSlug matches on the part before the first underscore — both
        // 'le-closer' and 'le-closer_v1_1' resolve to the same agent. The SDK
        // always picks the highest-versioned matching subagent at send time.
        { title: 'Refunds this week', subtitle: '', agentSlug: 'le-comptable' },
        // Looser editTolerance — user can rewrite up to 40% before the binding drops.
        { title: 'Top selling hotel', subtitle: 'today', agentSlug: 'le-closer', editTolerance: 0.4 },
        // System prompt only — biases the assistant for this one send (no agent binding).
        { title: 'Translate to French', subtitle: 'next message', prompt: 'Respond in concise French regardless of input language.' },
        // No agentSlug → routes through the default (dropdown selection or auto).
        { title: 'Free-form question', subtitle: 'no agent bound' },
      ],
    },
  },
});

// Or at runtime — partial updates merge into the existing config:
updateSidenetStyledConfig({
  text: {
    suggestions: [
      { title: 'New idea', subtitle: '', agentSlug: 'le-closer' },
    ],
  },
});
```

**Per-suggestion bindings.** A suggestion can attach two pieces of intent to the next user send:

- **`agentSlug`** — adds an `X-Agent-Id` header pointing at the resolved agent version. Matching is **version-tolerant**: both `'le-closer'` and `'le-closer_v1_1'` are accepted. The SDK splits on the first underscore on both sides and picks the highest-versioned matching subagent at send time, so you don't need to update your suggestions config when an agent ships a new version. If `agentSlug` doesn't match any loaded subagent at click time, the binding is dropped silently (a `console.warn` appears when `debug: true`).
- **`prompt`** — prepends a `system` message to the `messages` array sent for that single send (standard AI SDK pattern). Use it to bias the assistant for the suggestion's intent — persona, output format, language, etc. The backend sees a turn that starts with the system message and ends with the user's message.

Both are gated by **edit tolerance**: if the user edits the prefilled prompt before sending, the SDK computes the Levenshtein distance against the original suggestion text. If `distance > editTolerance × originalLength` (default `editTolerance: 0.25`, with an absolute floor of 6 characters), both bindings are dropped and the request goes through with the dropdown's defaults.

`agentSlug` and `prompt` are independent — a suggestion can have either, both, or neither.

## Content Push

Automatically push your page content when the sidebar opens:

```typescript
initSidenet({
  orgId: 'org-123',
  position: 'right',
  contentElement: '#app', // CSS selector for your main content
});
```

The element will receive `margin-right` (or `margin-left` for left position) matching the sidebar width.

## TypeScript

All types are exported:

```typescript
import type {
  SidenetOptions,
  SidenetEventDetail,
  SidenetCallbacks,
  RuntimeAuthCredentials,
  RuntimeAuthProvider,
  Subagent,
  SidebarSDKConfig,
  SidebarRuntimeConfig,
  SidebarTextConfig,
  SidebarSuggestionItem,
  ThemeTokens,
  ThemeInput,
  ThemeTokenInput,
} from 'sidenetai-sdk';
```

## React Hook

For React apps, use the `useSidebarConfig` hook to access runtime config:

```typescript
import { useSidebarConfig } from 'sidenetai-sdk';

function MyComponent() {
  const config = useSidebarConfig();
  return <div>Greeting: {config.text.greeting.title}</div>;
}
```

## Advanced: Direct Component Usage

For full control, use the `Sidebar` component directly:

```typescript
import { Sidebar } from 'sidenetai-sdk';

<Sidebar
  config={{
    position: 'right',
    width: '400px',
    userId: 'user-123',
    copilotId: 'copilot-456',
  }}
  onWidthChange={(width) => console.log(width)}
  onAgentChange={(agentId) => console.log(agentId)}
/>
```

## License

MIT
