# @yak-io/javascript

> 📚 **Full documentation:** https://docs.yak.io/docs/sdks/javascript
>
> 🤖 **For LLMs / AI agents:** https://docs.yak.io/llms.txt

Framework-agnostic core SDK for [Yak](https://docs.yak.io) — an embeddable AI assistant (text chat **and** push-to-talk voice) for web apps. This package is the low-level runtime that every framework SDK (`@yak-io/react`, `@yak-io/vue`, …) is built on. It owns iframe messaging, the WebRTC voice session, DOM rendering of the trigger pill + chat panel, and an optional server handler.

**Use this package directly when** you're on vanilla JS/TS, building a new framework adapter, or need the server handler (`@yak-io/javascript/server`) outside Next.js. On a supported framework, prefer that framework's package instead.

```bash
pnpm add @yak-io/javascript
```

## Exports

| Export | Kind | Purpose |
| --- | --- | --- |
| `YakEmbed` | class | Drop-in widget: trigger pill + chat panel + voice, all wired. Start here. |
| `YakClient` | class | Headless chat-only iframe client (no DOM). Advanced. |
| `YakVoiceSession` | class | Headless WebRTC voice session. Advanced. |
| `createYakToolset` | fn | Compose tool adapters (GraphQL, REST, tRPC, custom) into one merged manifest + one routed `onToolCall`. |
| `createYakServerAdapter` | fn | Wrap a server handler endpoint (`createYakHandler`) as a `ToolAdapter` for `createYakToolset`. |
| `enableYakLogging` / `disableYakLogging` / `isYakLoggingEnabled` | fn | Toggle verbose SDK logging. |
| `EMBED_PROTOCOL_VERSION` | const | Host ↔ iframe protocol version. |
| Types | — | `YakEmbedConfig`, `YakClientConfig`, `Theme`, `WidgetMode`, `VoiceState`, `VoiceMachine`, `ToolCallEvent`, `ChatConfig`, and more (see [Types](#types)). |
| `@yak-io/javascript/server` | subpath | `createYakHandler` + route/tool source types (see [Server](#server-side-handler)). |

## Quickstart

```ts
import { YakEmbed } from "@yak-io/javascript";

const embed = new YakEmbed({
  appId: "your-app-id",
  mode: "both", // "chat" | "voice" | "both" — default "chat"
  trigger: true, // render the floating launcher pill
  theme: { position: "bottom-right", colorMode: "system" },
  // Routes + tools the assistant may use. Usually fetched from your server.
  getConfig: async () => {
    const res = await fetch("/api/yak");
    return res.json(); // ChatConfig: { routes, tools? }
  },
  // Execute a tool the assistant decides to call.
  onToolCall: async (name, args) => {
    const res = await fetch("/api/yak", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name, args }),
    });
    const data = await res.json();
    if (!data.ok) throw new Error(data.error);
    return data.result;
  },
});

embed.mount(); // inject into the DOM
```

## Programmatic control

Every method works whether or not the trigger pill is shown — pass `trigger: false` to drive a fully custom UI.

```ts
// Chat
embed.open();
embed.close();
embed.toggle();
embed.openWithPrompt("How do I export my data?");

// Voice (requires mode "voice" or "both"; must be called from a user gesture)
await embed.voiceStart();
await embed.voiceStop();
await embed.voiceToggle();

// State
embed.getState(); // { isOpen, isReady, isLoading, isExpanded }
const stop = embed.onStateChange((s) => console.log(s.isLoading));
const stopVoice = embed.onVoiceStateChange((m) => console.log(m.state));
```

`isLoading` is `isOpen && !isReady` — true from the moment the panel opens until the iframe handshakes ready. Drive a custom loading spinner off it instead of re-deriving the condition. For voice, the equivalent "still spinning up" check is `getVoiceState().state === "connecting"`.

## API reference

### `new YakEmbed(config)`

**Config** (`YakEmbedConfig`):

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `appId` | `string` | — | Your Yak app ID (required). |
| `mode` | `"chat" \| "voice" \| "both"` | `"chat"` | Which surfaces the widget exposes. |
| `trigger` | `boolean \| TriggerButtonConfig` | `true` | Show the floating pill. `false` = headless. `TriggerButtonConfig` recolors it. |
| `theme` | `Theme` | — | Position, color mode, and colors. |
| `getConfig` | `ChatConfigProvider` | — | Async provider of `{ routes, tools? }`. Called on open and on each voice start. |
| `onToolCall` | `ToolCallHandler` | — | Executes a tool the assistant calls. Compose adapters with `createYakToolset`. |
| `onRedirect` | `(path: string) => void` | `window.location.assign` | Handle navigation requested by the assistant. |
| `onToolCallComplete` | `(event: ToolCallEvent) => void` | — | Fires after every tool call (use for cache invalidation). |
| `user` | `UserIdentity` | — | Signed end-user identity for server-side conversation persistence. See [end-user identity](https://docs.yak.io/docs/customization/end-user-identity). |
| `target` | `HTMLElement` | `document.body` | Where to mount the widget DOM. |
| `options.disableRestartButton` | `boolean` | `false` | Hide the restart-session button in the header. |

**Methods:** `mount()`, `destroy()`, `open()`, `close()`, `toggle()`, `openWithPrompt(prompt)`, `getState()`, `onStateChange(fn)`, `voiceStart()`, `voiceStop()`, `voiceToggle()`, `getVoiceState()`, `onVoiceStateChange(fn)`, `getClient()`, `getVoiceSession()`, `getMode()`.

### `Theme`

```ts
type Theme = {
  position?: WidgetPosition;            // default "bottom-left"
  colorMode?: "light" | "dark" | "system";
  displayMode?: "chatbox" | "drawer";   // floating panel vs full-height side drawer
  fullscreen?: boolean;
  light?: ThemeColors;                  // { background?, border?, messageBackground?, ... }
  dark?: ThemeColors;
};
// WidgetPosition: top-left | top-center | top-right | left-center | right-center
//               | bottom-left | bottom-center | bottom-right
```

### `VoiceState` / `VoiceMachine`

```ts
type VoiceState = "idle" | "connecting" | "listening" | "thinking" | "speaking" | "error";
interface VoiceMachine { state: VoiceState; errorMessage?: string }
```

### `YakClient` / `YakVoiceSession`

Headless building blocks used internally by `YakEmbed`. Reach for them only when composing a bespoke integration — most apps should use `YakEmbed`.

## Server-side handler

`@yak-io/javascript/server` builds a framework-agnostic `Request`/`Response` handler (Remix, Fastify, Hono, plain Node, …):

```ts
import { createYakHandler } from "@yak-io/javascript/server";

export const { GET, POST } = createYakHandler({
  // GET returns the route + tool manifest the assistant sees.
  routes: [
    { path: "/", title: "Home" },
    { path: "/tasks", title: "Tasks" },
  ],
  // POST executes a tool call.
  tools: {
    getTools: async () => [
      {
        name: "tasks.list",
        description: "Return all tasks",
        inputSchema: { type: "object", properties: {} },
      },
    ],
    executeTool: async (name, args) => {
      if (name === "tasks.list") return { tasks: [] };
      throw new Error(`Unknown tool: ${name}`);
    },
  },
});
```

`routes` and `tools` each accept a single source or an array of sources, so you can compose filesystem routes with adapters like [`@yak-io/trpc`](https://docs.yak.io/docs/tool-adapters/trpc) or [`@yak-io/prismic`](https://docs.yak.io/docs/sdks/prismic).

## Logging

```ts
import { enableYakLogging, disableYakLogging, isYakLoggingEnabled } from "@yak-io/javascript";

enableYakLogging(); // verbose SDK logs
```

To point the widget at a non-production chat UI (for example one running on `localhost`), pass the `origin` option: `new YakEmbed({ appId, origin: "http://localhost:3001" })`.

## Types

All types are exported from the package root:

```ts
import type {
  YakEmbedConfig,
  YakEmbedState,
  YakClientConfig,
  WidgetMode,
  Theme,
  ThemeColors,
  WidgetPosition,
  TriggerButtonConfig,
  VoiceState,
  VoiceMachine,
  ChatConfig,
  ChatConfigProvider,
  RouteManifest,
  RouteInfo,
  ToolManifest,
  ToolDefinition,
  ToolCallHandler,
  ToolCallEvent,
  ToolAdapter,
  YakToolset,
} from "@yak-io/javascript";
```

## License

Proprietary — see [LICENSE](./LICENSE).
