# HawkAI Assistant

A self-contained AI chat widget that drops into any web page with a single script tag. Powered by an agentic API backend, it supports streaming responses, multi-turn tool use, custom tool rendering, file uploads, and full UI customization — all isolated in a Shadow DOM so it won't conflict with your page's styles.

---

## Installation

### CDN (recommended)

Once published to npm, load the widget directly from a CDN:

```html
<script src="https://cdn.jsdelivr.net/npm/@bridgeline-digital/hawkai-assistant/dist/hawkai-assistant.js"></script>
```

Or via unpkg:

```html
<script src="https://unpkg.com/@bridgeline-digital/hawkai-assistant/dist/hawkai-assistant.js"></script>
```

### Self-hosted

Install the package and copy the built file to your project:

```bash
npm install @bridgeline-digital/hawkai-assistant
# or
pnpm add @bridgeline-digital/hawkai-assistant
```

Then copy `node_modules/@bridgeline-digital/hawkai-assistant/dist/hawkai-assistant.js` to your static assets directory and load it with a `<script>` tag.

---

## Quick Start

```html
<script src="hawkai-assistant.js"></script>
<script>
  HawkAI.Assistant.initialize({
    apiUrl: 'https://assistant.hawksearch.net',
    accountId: 'your-account-id',
    agentId: 'your-agent-uuid',
  });
</script>
```

That's it. The widget mounts itself as a fixed floating button in the corner of the page.

---

## Configuration

Pass a `Config` object to `initialize()`:

```ts
HawkAI.Assistant.initialize(config: Config): Promise<void>
```

### `Config`

| Property | Type | Required | Description |
|----------|------|----------|-------------|
| `apiUrl` | `string` | Yes | Base URL of the assistant API. |
| `accountId` | `string` | Yes | Your account identifier. |
| `agentId` | `string` | Yes | UUID of the agent to invoke. |
| `actorId` | `string` | No | User/actor identifier. Falls back to a stored value in `localStorage`, then a generated UUID. |
| `context` | `Record<string, unknown>` | No | Arbitrary contextual data sent with every API request (e.g. location, user preferences). |
| `tools` | `Tool[]` | No | Custom tools the model can invoke. See [Custom Tools](#custom-tools). |
| `disabledTools` | `DefaultToolName[] \| '*'` | No | Disable built-in tools. Pass an array of built-in tool names (`['display_products']`) to disable specific ones, or `'*'` to disable all of them. See [Built-in Tools](#built-in-tools). |
| `addToCart` | `(product: Product) => void` | No | Callback invoked when the user clicks "Add to Cart" in a product carousel. When omitted, no button is shown. |
| `ui` | `UiConfig` | No | UI customization options. See [UI Config](#uiconfig). |
| `loadConfigFromApi` | `boolean` | No | When `true` (default), the widget fetches UI configuration from the API on init and merges it under your local `ui` (your `config.ui` values win on conflict). The widget also will not mount if the account's token limit has been reached. Set to `false` to skip the API config fetch and mount immediately using only the locally supplied config. |

### `UiConfig`

| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `open` | `boolean` | `false` | Whether the widget starts open. |
| `display` | `'window' \| 'drawer'` | `'window'` | Layout mode — floating window or a drawer panel. |
| `position` | `'left' \| 'right'` | `'right'` | Side of the screen for the button and panel. |
| `theme` | `'light' \| 'dark' \| 'auto'` | `'auto'` | Color scheme. `'auto'` follows the user's OS preference. |
| `allowFullScreen` | `boolean` | `true` | Whether the user can expand the widget to full screen. |
| `allowFileUpload` | `boolean` | `true` | Whether the user can attach files to messages. |
| `logoUrl` | `string` | — | URL of a logo image shown in the widget header. |
| `heading` | `string` | — | Title text shown in the widget header. |
| `initialMessage` | `string` | — | First message shown to the user when the widget opens. |
| `promptOptions` | `string[]` | — | Quick-start prompt buttons shown when the chat is empty. Clicking one submits it as a message. |
| `placeholder` | `string` | — | Placeholder text for the message input. |
| `showDisclaimer` | `boolean` | `true` | Whether to show the disclaimer text below the message input. |
| `disclaimer` | `string` | `'AI-generated responses may be inaccurate, incomplete, or outdated.'` | Custom disclaimer text shown below the message input when `showDisclaimer` is `true`. Whitespace-only values fall back to the default. |
| `colors.primary.default` | `string` | — | CSS color value for the primary accent (button, send icon, etc.). |
| `colors.primary.hover` | `string` | — | CSS color value for the primary accent on hover. |
| `styles` | `string` | — | Raw CSS string injected into the shadow root after built-in styles. Use BEM class names (e.g. `.hawkai-assistant__window`). |
| `stylesheetUrl` | `string` | — | URL of an external CSS file loaded via `<link>` inside the shadow root. |

#### Example with full UI config

```js
HawkAI.Assistant.initialize({
  apiUrl: 'https://assistant.hawksearch.net',
  accountId: '123',
  agentId: 'your-agent-uuid',
  ui: {
    open: true,
    display: 'window',
    position: 'right',
    allowFullScreen: true,
    allowFileUpload: true,
    heading: 'Support Assistant',
    logoUrl: 'https://example.com/logo.png',
    initialMessage: 'Hi! How can I help you today?',
    promptOptions: ['What are your best deals?', 'Track my order', 'Return an item'],
    placeholder: 'Type your question…',
    showDisclaimer: true,
    disclaimer: 'Responses are AI-generated and may not be accurate.',
    theme: 'auto',
    colors: {
      primary: {
        default: '#2563eb',
        hover: '#1d4ed8',
      },
    },
    styles: '.hawkai-assistant__window { border-radius: 16px; }',
  },
});
```

### Custom Styling

The widget's Shadow DOM prevents host-page styles from bleeding in, but you can target widget internals using the stable BEM class names exposed on every element:

| Class | Element |
|-------|---------|
| `.hawkai-assistant__trigger` | The floating action button |
| `.hawkai-assistant__trigger--open` | Modifier applied when the widget is open |
| `.hawkai-assistant__window` | The chat panel container |
| `.hawkai-assistant__header` | The panel header bar |
| `.hawkai-assistant__message` | Individual message bubbles |
| `.hawkai-assistant__form` | The message input area |
| `.hawkai-assistant__disclaimer` | The disclaimer text below the input |

Inject styles via `config.ui.styles` (inline string, good for small overrides) or `config.ui.stylesheetUrl` (external file, good for larger stylesheets). Both are applied inside the shadow root after built-in styles.

```js
HawkAI.Assistant.initialize({
  // ...
  ui: {
    styles: `
      .hawkai-assistant__window { border-radius: 16px; }
      .hawkai-assistant__trigger { background: purple; }
    `,
    // or:
    stylesheetUrl: 'https://example.com/my-assistant-theme.css',
  },
});
```

---

## Public API

After the script loads, all methods are available on `window.HawkAI.Assistant`:

### `initialize(config)`

```ts
HawkAI.Assistant.initialize(config: Config): Promise<void>
```

Mounts the widget. Can only be called once — subsequent calls are silently ignored. Config is frozen after initialization; do not mutate it.

### `toggle(open)`

```ts
HawkAI.Assistant.toggle(open: boolean): void
```

Programmatically open or close the widget.

```js
HawkAI.Assistant.toggle(true);   // open
HawkAI.Assistant.toggle(false);  // close
```

### `sendMessage(message?, files?)`

```ts
HawkAI.Assistant.sendMessage(message?: string, files?: File[]): Promise<void>
```

Opens the widget and submits a message into the conversation. Useful for triggering the assistant from your own UI elements.

```js
HawkAI.Assistant.sendMessage('Show me the latest deals');
```

### `clearChat()`

```ts
HawkAI.Assistant.clearChat(): void
```

Clears the conversation history and regenerates the session ID.

### `setActorId(value)`

```ts
HawkAI.Assistant.setActorId(value: string | undefined): void
```

Changes the actor/user identity. Automatically calls `clearChat()` so the new actor starts a fresh session. Pass `undefined` to generate a new anonymous ID.

```js
// After a user logs in:
HawkAI.Assistant.setActorId(user.id);

// After logout:
HawkAI.Assistant.setActorId(undefined);
```

### `setContext(context)`

```ts
HawkAI.Assistant.setContext(context: Record<string, unknown> | undefined): void
```

Updates the contextual data sent with every subsequent API request.

```js
HawkAI.Assistant.setContext({ cart: { items: 3, total: '$89.97' } });
```

---

## Custom Tools

Tools let the model take actions on your page. Each tool has a name, a description (for the model), a JSON schema for its inputs, and an `execute` function the widget calls when the model invokes it.

```ts
interface Tool<TInput, TOutput> {
  name: string;
  description: string;
  inputSchema: {
    type: 'object';
    properties: Record<string, unknown>;
    required?: string[];
  };
  execute: (input: TInput, options?: { signal?: AbortSignal }) => Promise<TOutput>;
  render?: (data: RenderToolData<TInput>, helpers: RenderToolHelpers) => AsyncRenderToolResult;
}
```

`execute` receives an optional `options.signal` (an `AbortSignal`) as its second argument. The signal aborts when the user stops the response; long-running tools may pass it to `fetch` (or otherwise observe it) to cancel in-flight work. Ignoring it is fine for synchronous or fast tools.

Pass tools in the `tools` array of `Config`:

```js
HawkAI.Assistant.initialize({
  apiUrl: '...',
  accountId: '...',
  agentId: '...',
  tools: [
    {
      name: 'add_to_cart',
      description: 'Adds a product to the shopping cart.',
      inputSchema: {
        type: 'object',
        properties: {
          product_name: { type: 'string', description: 'Product name' },
          price: { type: 'string', description: 'Product price (optional)' },
        },
        required: ['product_name'],
      },
      execute: async ({ product_name, price }) => {
        // Your logic here — runs in the browser when the model calls this tool
        addItemToCart(product_name, price);
        return `${product_name} added to cart.`;
      },
    },
  ],
});
```

### Optional `render` function

Tools can optionally render UI inline in the chat message. Return a Preact `ComponentChild` or a plain `HTMLElement`:

```ts
interface RenderToolData<TInput> {
  input: TInput;
  status: 'pending' | 'success' | 'error' | undefined;
}

interface RenderToolHelpers {
  sendMessage: (text: string) => Promise<void>;
}

type AsyncRenderToolResult = ComponentChild | HTMLElement | null | Promise<ComponentChild | HTMLElement | null>;
```

- `status` is `'pending'` while `execute()` is running, then `'success'` or `'error'`. It is `undefined` when a tool use is loaded from conversation history — treat this the same as `'success'`.
- `helpers.sendMessage(text)` injects a follow-up user message into the agentic loop, useful for interactive buttons.
- Return `null` to render nothing.
- Return a `Promise` when setup is async (e.g. lazy-loading a web component).

#### Example — plain HTMLElement render

```js
{
  name: 'show_rating',
  description: 'Displays a visual rating bar for a product.',
  inputSchema: {
    type: 'object',
    properties: {
      label: { type: 'string' },
      score: { type: 'number', description: 'Rating from 0 to 10' },
    },
    required: ['label', 'score'],
  },
  execute: async ({ label, score }) => ({ label, score }),
  render: ({ input, status }) => {
    const el = document.createElement('div');

    if (status === 'pending') {
      el.textContent = 'Loading rating…';
      return el;
    }

    const pct = Math.round(Math.min(Math.max(input.score, 0), 10) * 10);
    el.innerHTML = `<strong>${input.label}</strong>: ${input.score}/10`;

    const bar = document.createElement('div');
    bar.style.cssText = 'margin-top:6px;height:8px;border-radius:4px;background:#e2e8f0;overflow:hidden';

    const fill = document.createElement('div');
    fill.style.cssText = `height:100%;width:${pct}%;background:#2563eb;border-radius:4px`;
    bar.appendChild(fill);
    el.appendChild(bar);

    return el;
  },
}
```

---

## Built-in Tools

The widget registers two tools automatically — you don't need to declare them:

### `display_products`

Renders a product carousel inline in the chat. The model calls this tool when it wants to show product recommendations. Each product in the input array may include: `id`, `title`, `url`, `imageUrl`, `price`, `salePrice`, `rating`, `brand`, `sku`, `description`. When `config.addToCart` is provided, each product card displays an "Add to Cart" button that invokes the callback.

### `retrieve_page_context`

Reads the current page's URL, title, meta description, first `<h1>`, and main content text. The model calls this to understand what page the user is on without any setup on your part.

### Disabling built-in tools

Use `config.disabledTools` to opt out of built-in tools:

```js
// Disable a specific tool
HawkAI.Assistant.initialize({
  // ...
  disabledTools: ['display_products']
});

// Disable all built-in tools
HawkAI.Assistant.initialize({
  // ...
  disabledTools: '*'
});
```

You can also **replace** a built-in tool by registering a custom tool in `config.tools` with the same `name` — the custom tool wins.

---

## Session & Actor IDs

**Session ID** — identifies the current conversation. Persisted in `localStorage` so the user's conversation survives page reloads. Regenerated when `clearChat()` is called.

**Actor ID** — identifies the user across sessions. Resolved in this order:
1. `config.actorId` (if provided)
2. Previously stored value in `localStorage`
3. Auto-generated UUID

Use `setActorId()` to update the actor when a user logs in or out. This clears the conversation so the new actor starts fresh.

---

## Custom Build

If you need to bundle your own tools with strong TypeScript typings — or encapsulate the `initialize` call for a specific site — you can create a custom build script using the npm package as a library.

### 1. Install

```bash
npm install @bridgeline-digital/hawkai-assistant
# or
pnpm add @bridgeline-digital/hawkai-assistant
```

### 2. Create your entry file

```ts
// src/my-assistant.ts
import { initialize } from '@bridgeline-digital/hawkai-assistant';
import type { Tool } from '@bridgeline-digital/hawkai-assistant';

interface AddToCartInput {
  product_name: string;
  price?: string;
}

const addToCartTool: Tool<AddToCartInput, string> = {
  name: 'add_to_cart',
  description: 'Adds a product to the shopping cart.',
  inputSchema: {
    type: 'object',
    properties: {
      product_name: { type: 'string', description: 'Product name' },
      price: { type: 'string', description: 'Product price (optional)' },
    },
    required: ['product_name'],
  },
  execute: async ({ product_name, price }) => {
    // your cart logic here
    return `${product_name} added to cart.`;
  },
};

initialize({
  apiUrl: 'https://assistant.hawksearch.net',
  accountId: '123',
  agentId: 'your-agent-uuid',
  tools: [addToCartTool],
});
```

### 3. Add a Vite config

```ts
// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    lib: {
      entry: 'src/my-assistant.ts',
      name: 'MyAssistant',
      formats: ['iife'],
      fileName: () => 'my-assistant.js',
    },
  },
});
```

### 4. Build and deploy

```bash
pnpm build
```

This produces `dist/my-assistant.js` — a single self-contained IIFE you can drop on any page:

```html
<script src="my-assistant.js"></script>
```

No separate `initialize()` call is needed; your entry file handles it.

---

## Browser Support

The widget requires a browser that supports Shadow DOM, `CSSStyleSheet.adoptedStyleSheets`, and `EventSource`. All modern browsers (Chrome, Firefox, Safari, Edge) meet these requirements.
