npm package: @maxaiagent/widget-sdk
Description: MaxAI Widget SDK — embed AI chat with page context, client-side tools, and event system
Latest version: 1.6.0

# @maxaiagent/widget-sdk

SDK do osadzania widgetu czatu MaxAI na dowolnej stronie internetowej.

## Instalacja

### NPM (React, Vue, Next.js, itp.)

```bash
npm install @maxaiagent/widget-sdk
```

```typescript
import { MaxAI } from '@maxaiagent/widget-sdk'

const widget = MaxAI.init({ key: 'TWOJ_KLUCZ' })
```

### CDN (zwykły HTML)

```html
<script src="https://maxaiagent.app/sdk.js" data-key="TWOJ_KLUCZ"></script>
```

Widget automatycznie się zainicjalizuje. Aby korzystać z API SDK:

```html
<script src="https://maxaiagent.app/sdk.js" data-key="TWOJ_KLUCZ"></script>
<script>
  // MaxAI jest dostępny globalnie
  MaxAI.setPageContext({ pageType: 'product', visibleData: { name: 'Buty Nike', price: 549 } })
</script>
```

---

## API

### `MaxAI.init(options)`

Inicjalizuje widget. Zwraca `WidgetInstance`.

```typescript
const widget = MaxAI.init({
  key: 'TWOJ_KLUCZ',            // Wymagane — klucz z panelu MaxAI
  position: 'bottom-right',      // 'bottom-right' | 'bottom-left'
  baseUrl: 'https://maxaiagent.app', // Opcjonalne — override URL serwera
  nonce: 'abc123',               // Opcjonalne — CSP nonce dla stylów
  lazy: false,                   // Opcjonalne — lazy load iframe
  styles: {                      // Opcjonalne — style widgetu (patrz sekcja Styles)
    theme: 'dark',
    density: 'compact',
  },
})
```

---

### `widget.setPageContext(context)`

Wysyła kontekst strony do agenta AI. Agent automatycznie zbiera tytuł, URL, meta tagi i JSON-LD. Użyj tej metody, aby dodać **dodatkowe** dane.

```typescript
widget.setPageContext({
  pageType: 'product',
  visibleData: {
    name: 'Nike Air Max 90',
    price: 549,
    currency: 'PLN',
    sizes: [40, 41, 42, 43, 44, 45, 46],
  },
  customContext: {
    user_type: 'premium',
    cart_count: '3',
  },
})
```

---

### `widget.registerTool(definition)`

Rejestruje narzędzie po stronie klienta. Agent AI może je wywoływać w trakcie rozmowy. Zwraca funkcję do wyrejestrowania.

```typescript
const unregister = widget.registerTool({
  slug: 'add_to_cart',
  name: 'Dodaj do koszyka',
  description: 'Dodaje produkt do koszyka użytkownika',
  schema: {
    type: 'object',
    properties: {
      product_id: { type: 'string', description: 'ID produktu' },
      quantity: { type: 'number', description: 'Ilość', default: 1 },
      size: { type: 'string', description: 'Rozmiar (np. "42")' },
    },
    required: ['product_id'],
  },
  requiresConfirmation: true, // Pokaż dialog potwierdzenia
  handler: async (args) => {
    const result = await myAPI.addToCart(args.product_id, args.quantity, args.size)
    return { success: true, cart_count: result.totalItems }
  },
})

// Później:
unregister()
```

---

### `widget.on(event, callback)`

Nasłuchuje na zdarzenia widgetu. Zwraca funkcję do odsubskrybowania.

```typescript
widget.on('message', (msg) => console.log(`${msg.role}: ${msg.content}`))
widget.on('tool:call', (call) => console.log(`Tool: ${call.name}`, call.args))
widget.on('open', () => console.log('Widget otwarty'))
widget.on('close', () => console.log('Widget zamknięty'))
widget.on('ready', () => console.log('Widget gotowy'))
widget.on('error', (err) => console.error('Błąd:', err.message))
```

---

### `widget.open()` / `widget.close()` / `widget.destroy()`

```typescript
widget.open()      // Otwórz panel czatu
widget.close()     // Zamknij panel czatu
widget.destroy()   // Usuń widget ze strony i wyczyść listenery
```

---

## Style — `widget.updateStyles(styles)`

Steruj wyglądem widgetu z poziomu kodu. Style można podać przy inicjalizacji (`init({ styles })`) lub zmienić w runtime (np. przy zmianie theme).

### Podanie przy inicjalizacji

```typescript
const widget = MaxAI.init({
  key: 'KLUCZ',
  styles: {
    theme: 'dark',
    density: 'compact',
    radius: 14,
    fontSize: 13,
    hideHeader: true,
    hideLauncher: true,
    maxHeight: 500,
    container: {
      background: '#07111a',
      border: '1px solid rgba(255,255,255,0.08)',
      shadow: '0 8px 32px rgba(0,0,0,0.3)',
    },
    bubbles: {
      assistantBg: '#0c1824',
      userBg: '#13283a',
      assistantText: '#e2e8f0',
      userText: '#f1f5f9',
      borderRadius: 12,
    },
    input: {
      background: '#091521',
      border: '1px solid rgba(255,255,255,0.06)',
      textColor: '#f1f5f9',
      placeholderColor: '#7f92a6',
    },
    header: {
      background: '#0a1929',
      textColor: '#e2e8f0',
      border: '1px solid rgba(255,255,255,0.06)',
    },
  },
})
```

### Zmiana w runtime (np. theme toggle)

```typescript
// Aktualizuje WSZYSTKIE widgety: bubble + wszystkie inline
widget.updateStyles({ theme: 'light', density: 'default' })
```

### Pełna lista `WidgetStyles`

| Pole | Typ | Opis |
|---|---|---|
| `theme` | `"dark" \| "light" \| "auto"` | Motyw kolorystyczny |
| `density` | `"default" \| "compact" \| "dense"` | Gęstość treści (padding, gap, font) |
| `radius` | `number` | Border radius kontenera w px |
| `fontSize` | `number` | Bazowy rozmiar fontu w px |
| `maxHeight` | `string \| number` | Max wysokość (inline mode) |
| `minHeight` | `string \| number` | Min wysokość |
| `width` | `string \| number` | Szerokość |
| `height` | `string \| number` | Wysokość |
| `hideHeader` | `boolean` | Ukryj nagłówek |
| `hideInput` | `boolean` | Ukryj pole wpisywania (read-only) |
| `hideLauncher` | `boolean` | Ukryj przycisk launchera |
| `container` | `{ background, border, shadow, borderRadius }` | Style kontenera |
| `bubbles` | `{ assistantBg, userBg, assistantText, userText, borderRadius }` | Style dymków |
| `input` | `{ background, border, textColor, placeholderColor }` | Style pola wpisywania |
| `header` | `{ background, textColor, border }` | Style nagłówka |

### Eksportowane stałe

```typescript
import { WIDGET_THEMES, WIDGET_DENSITIES } from '@maxaiagent/widget-sdk'
// WIDGET_THEMES = ["dark", "light", "auto"] as const
// WIDGET_DENSITIES = ["default", "compact", "dense"] as const

import type { WidgetTheme, WidgetDensity } from '@maxaiagent/widget-sdk'
```

---

## Inline Widget — `widget.mountInline(el, options?)`

Osadź widget bezpośrednio w kontenerze na stronie (zamiast pływającego bubble).
Zwraca `InlineInstance` — ref do sterowania tym konkretnym widgetem.

### Sposób 1: Z kodu (SDK)

```typescript
const widget = MaxAI.init({ key: 'KLUCZ' })

const chatRef = widget.mountInline('#news-chat-panel', {
  sessionKey: 'article-123',      // Opcjonalne — klucz sesji (patrz niżej)
  sessionPolicy: 'per-key',       // 'per-key' | 'sticky' | 'always-new'
  styles: { maxHeight: 500 },     // Opcjonalne — style per-instancja
})
```

### Sposób 2: Z HTML (auto-mount)

Dodaj div z klasą `maxai-inline` — SDK automatycznie zamontuje widget (także dla elementów dodanych dynamicznie przez SPA):

```html
<div
  class="maxai-inline"
  data-key="TWOJ_KLUCZ"
  data-session-key="article-123"
  style="height: 500px"
></div>
```

| Atrybut | Opis |
|---|---|
| `data-key` | Klucz widgetu (opcjonalny jeśli podany w `init()`) |
| `data-session-key` | Klucz sesji — różne klucze = osobne rozmowy |
| `data-context-*` | Dodatkowy kontekst, np. `data-context-article-id="123"` |

### Dostęp do instancji przez ref (Vue / React)

Jeśli widget jest zamontowany przez auto-mount (klasa `maxai-inline`), możesz uzyskać dostęp do jego API przez `getInlineInstance()`:

**Vue 3:**
```vue
<template>
  <div class="maxai-inline" data-key="KLUCZ" data-session-key="news-123" ref="chatDiv" />
</template>

<script setup>
import { ref, onMounted } from 'vue'

const chatDiv = ref()

onMounted(() => {
  const inst = MaxAI.getInlineInstance(chatDiv.value)
  inst?.addMessage('Witaj! Oto podsumowanie tego artykułu...')
})
</script>
```

**React:**
```tsx
import { useRef, useEffect } from 'react'

function NewsChat({ newsId }: { newsId: string }) {
  const chatRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    const inst = MaxAI.getInlineInstance(chatRef.current!)
    inst?.setSessionKey(newsId)
    inst?.addMessage('Oto podsumowanie...')
  }, [newsId])

  return <div className="maxai-inline" data-key="KLUCZ" ref={chatRef} style={{ height: 500 }} />
}
```

---

## Sesje — `InlineInstance` API

Każdy inline widget może mieć **osobną sesję** powiązaną z kluczem (`sessionKey`).
To umożliwia np. osobną rozmowę per artykuł, per produkt, per ticket.

### `inline.setSessionKey(key)`

Zmiana klucza sesji. Gdy klucz się zmienia — widget czyści rozmowę i startuje nową sesję.
Gdy klucz jest taki sam — **no-op** (deduplikacja, bez resetu przy rerender).

```typescript
const chat = widget.mountInline('#panel', { sessionKey: 'news-A' })

// Użytkownik przełącza się na inny news:
chat.setSessionKey('news-B')  // → nowa sesja

// Powrót do poprzedniego newsa — sesja jest zachowana:
chat.setSessionKey('news-A')  // → przywraca wcześniejszą rozmowę
```

### Zachowanie sesji

| Akcja | Efekt |
|---|---|
| `setSessionKey('X')` gdy current = `'X'` | **no-op** (deduplikacja) |
| `setSessionKey('Y')` gdy current = `'X'` | clear + nowa sesja dla `'Y'` |
| Rerender komponentu Vue/React | **nic** — nie resetuje sesji |
| `resetSession()` | Force clear nawet dla tego samego klucza |

Sesje per-key są trzymane w `localStorage` jako `maxai_st_{widgetKey}_scope_{sessionKey}`.
Powrót do wcześniejszego klucza przywraca zapisaną rozmowę.

### `sessionPolicy`

| Wartość | Zachowanie |
|---|---|
| `"per-key"` (domyślne) | Nowa sesja gdy `sessionKey` się zmienia |
| `"sticky"` | Nigdy nie resetuj sesji (nawet przy zmianie klucza) |
| `"always-new"` | Nowa sesja przy każdym wywołaniu |

---

## Wiadomości — `inline.addMessage()` / `widget.addMessage()`

Wstrzyknij wiadomość do czatu programowo (np. gotowy dymek z podsumowaniem).

```typescript
// Na konkretnej instancji inline:
chat.addMessage('Oto podsumowanie tego artykułu...')
chat.addMessage('Mam pytanie o cenę', 'user')

// Na głównym bubble widgecie:
widget.addMessage('Witaj! Jak mogę Ci pomóc?')
widget.addMessage('Chcę zwrócić produkt', 'user')
```

Parametry:
- `content: string` — treść wiadomości
- `role?: "assistant" | "user"` — domyślnie `"assistant"`

---

## Pełne API `InlineInstance`

Zwracane przez `widget.mountInline()` lub `MaxAI.getInlineInstance(el)`.

| Metoda | Opis |
|---|---|
| `setSessionKey(key)` | Zmień klucz sesji (nowy klucz = nowa rozmowa) |
| `addMessage(content, role?)` | Wstrzyknij wiadomość do czatu |
| `updateStyles(styles)` | Zmień style tej instancji |
| `resetSession()` | Wymuś nową sesję (nawet dla tego samego klucza) |
| `destroy()` | Usuń widget z DOM |
| `sessionKey` (readonly) | Aktualny klucz sesji |
| `container` | HTMLElement kontenera |
| `iframe` | HTMLIFrameElement |

---

## Pełne API `WidgetInstance`

Zwracane przez `MaxAI.init()`.

| Metoda | Opis |
|---|---|
| `mountInline(el, options?)` | Zamontuj inline widget, zwraca `InlineInstance` |
| `getInlineInstance(el)` | Pobierz instancję zamontowaną na danym elemencie |
| `setPageContext(ctx)` | Wyślij kontekst strony do agenta |
| `setIdentity(payload)` | Ustaw tożsamość użytkownika (HMAC podpis) |
| `registerTool(def)` | Zarejestruj narzędzie klienta |
| `unregisterTool(slug)` | Wyrejestruj narzędzie |
| `on(event, callback)` | Nasłuchuj na zdarzenia |
| `off(event, callback)` | Odsubskrybuj zdarzenie |
| `updateStyles(styles)` | Zmień style na bubble + wszystkich inline |
| `addMessage(content, role?)` | Wstrzyknij wiadomość do bubble widgetu |
| `open()` | Otwórz panel czatu |
| `close()` | Zamknij panel czatu |
| `destroy()` | Usuń widget i wyczyść listenery |

---

## Atrybuty `data-context-*`

Dodatkowy kontekst można przekazać bezpośrednio w tagu `<script>` lub na kontenerze inline:

```html
<script
  src="https://maxaiagent.app/sdk.js"
  data-key="TWOJ_KLUCZ"
  data-context-page="product"
  data-context-user-type="premium"
></script>
```

Agent otrzyma te dane jako `customContext: { page: 'product', user_type: 'premium' }`.

---

## Przykłady

### Terminal / News — inline widget z sesjami per artykuł

```typescript
import { MaxAI, type InlineInstance } from '@maxaiagent/widget-sdk'

const widget = MaxAI.init({
  key: 'KLUCZ',
  styles: {
    theme: 'dark',
    density: 'compact',
    container: { background: '#07111a', border: '1px solid rgba(255,255,255,0.08)' },
    bubbles: { assistantBg: '#0c1824', userBg: '#13283a' },
    input: { background: '#091521', placeholderColor: '#7f92a6' },
  },
})

let chatRef: InlineInstance | null = null

function showNews(newsId: string, title: string, summary: string) {
  if (!chatRef) {
    chatRef = widget.mountInline('#news-chat', {
      sessionKey: newsId,
      styles: { maxHeight: 500, hideHeader: true, hideLauncher: true },
    })
  } else {
    chatRef.setSessionKey(newsId)
  }
  chatRef.addMessage(`**${title}**\n\n${summary}`)
}

// Użytkownik klika na news:
showNews('bybit-risk-2026', 'Bybit zmienia limity ryzyka', 'Bybit dostosowuje limity...')

// Przełącza na inny news:
showNews('shib-81b', 'SHIB nadpodaż', '81 bilionów tokenów...')

// Powrót do pierwszego — sesja zachowana:
showNews('bybit-risk-2026', 'Bybit zmienia limity ryzyka', 'Bybit dostosowuje limity...')
```

### E-commerce — strona produktu (CDN)

```html
<script src="https://maxaiagent.app/sdk.js" data-key="KLUCZ"></script>
<script>
  MaxAI.setPageContext({
    pageType: 'product',
    visibleData: {
      name: document.querySelector('h1').textContent,
      price: document.querySelector('.price').textContent,
    },
  })

  MaxAI.registerTool({
    slug: 'add_to_cart',
    name: 'Dodaj do koszyka',
    description: 'Dodaje produkt do koszyka',
    schema: {
      type: 'object',
      properties: {
        size: { type: 'string', description: 'Rozmiar' },
        quantity: { type: 'number', default: 1 },
      },
    },
    requiresConfirmation: true,
    handler: async (args) => {
      document.querySelector('.add-to-cart-btn').click()
      return { success: true }
    },
  })
</script>
```

### SPA (Vue 3) — dynamiczny theme toggle

```vue
<template>
  <button @click="toggleTheme">Toggle theme</button>
</template>

<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
import { MaxAI } from '@maxaiagent/widget-sdk'

const isDark = ref(true)
let widget = null

onMounted(() => {
  widget = MaxAI.init({
    key: 'KLUCZ',
    styles: { theme: 'dark' },
  })
})

onUnmounted(() => widget?.destroy())

function toggleTheme() {
  isDark.value = !isDark.value
  widget?.updateStyles({ theme: isDark.value ? 'dark' : 'light' })
}
</script>
```

---

## Typy TypeScript

```typescript
import type {
  MaxAIInitOptions,
  PageContext,
  ToolDefinition,
  WidgetInstance,
  WidgetEvent,
  ChatMessage,
  ToolCallEvent,
  WidgetStyles,
  WidgetTheme,
  WidgetDensity,
  SessionPolicy,
  InlineMountOptions,
  InlineInstance,
} from '@maxaiagent/widget-sdk'

import { WIDGET_THEMES, WIDGET_DENSITIES } from '@maxaiagent/widget-sdk'
```

---

## Funkcje automatyczne

Po włączeniu w panelu MaxAI (zakładka "Inteligencja"):

| Funkcja | Opis | Wymaga kodu? |
|---|---|---|
| **Kontekst strony** | Agent widzi tytuł, URL, meta, JSON-LD, treść `<main>` | Nie |
| **Point-and-Ask** | Użytkownik wskazuje element na stronie | Nie |
| **Zgłaszanie problemów** | Przycisk bug report z logami konsoli | Nie |
| **GDPR** | Baner zgody przed zbieraniem danych | Nie |
| **Auto-adaptive theme** | Widget dopasowuje kolory do strony | Nie |
| **Glassmorphism** | Półprzezroczysty efekt szkła | Nie |
| **Narzędzia klienta** | `registerTool()` — agent wywołuje funkcje | **Tak** |
| **Explicit context** | `setPageContext()` — dodatkowe dane | **Tak** |
| **Inline widget** | `mountInline()` — osadzony w kontenerze | **Tak** |
| **Style SDK** | `updateStyles()` — zmiana wyglądu z kodu | **Tak** |
| **Sesje per-key** | `setSessionKey()` — osobna rozmowa per kontekst | **Tak** |
| **Inject messages** | `addMessage()` — wstrzyknij dymek programowo | **Tak** |