# Use your own UI

> Drives a Mindset agent from your own chat interface with the UI-less client, and draws its quick replies, cards and widgets.

**Skill for your coding agent:** [`mindset-uiless/SKILL.md`](https://docs.mindset.ai/skills/mindset-uiless/SKILL.md)

In the browser, the SDK runs the conversation with Mindset, with your page tools and widgets. It gets its session from your server, which creates it with your org API key. Mindset holds the agent and runs its model and tools. Highlighted: Browser.

```
Build our own chat panel for the billing-help agent with the UI-less client, in our app's style. Draw quick replies and capability cards as buttons, and show the summary for any other widget.
```

Skill: [mindset-uiless](https://docs.mindset.ai/skills/mindset-uiless/SKILL.md)

The UI-less client runs the same agent as the element, in the page, with no Mindset UI: you draw every message. It uses the same session endpoint as [Put an agent on your page](https://docs.mindset.ai/docs/put-an-agent-on-your-page). [The UI-less client](https://docs.mindset.ai/docs/the-ui-less-client) is the full API.

```html
<script type="module">
  import { createAgentConversation } from "https://eu.mindset.ai/sdk/mindset-agent-uiless.js";

  const chat = createAgentConversation({
    agent: "billing-help",
    getSession: async () => {
      const r = await fetch("/api/session");
      if (!r.ok) throw new Error("Could not start a session");
      return r.json();
    },
  });

  let reply = "";
  chat.on((event) => {
    if (event.type === "text_delta") reply += event.content;
    if (event.type === "complete") console.log("Agent:", reply);
    if (event.type === "run_error") console.error("Turn failed:", event.message);
  });

  try {
    await chat.send("What does my last invoice cover?");
  } catch {
    // The run_error event above already reported what happened.
  }
</script>
```

In a bundled app, import it at runtime and tell your bundler not to resolve the address. Create the conversation once (in React, guard it in a ref), because each `createAgentConversation` asks your backend for a session.

```javascript
const UILESS_URL = "https://eu.mindset.ai/sdk/mindset-agent-uiless.js";

// Vite: /* @vite-ignore */   webpack: /* webpackIgnore: true */
const { createAgentConversation } = await import(/* @vite-ignore */ UILESS_URL);
```

## Widgets

A widget arrives as a `widget-payload` event during a turn, or as `widget` on a message in a restored transcript. `kind`, `summary` and `toolCallId` are public contract, and a message that showed a widget has its `summary` as its `content`.

| `kind`                                                         | What `doc` holds                              | What to draw                                                                   |
| -------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------ |
| `quick_replies`                                                | `{ replies: [{ label, value? }] }`            | One button per reply. A tap sends `value`, or `label` when there is no `value` |
| `capability_cards`                                             | `{ cards: [{ title, description, prompt }] }` | One card per entry. A tap sends `prompt`                                       |
| `chart`, `callout`, `table`, `card`, `badge`, `stat`, `widget` | A widget tree. Not a public contract          | `summary`, as plain text                                                       |

> **Warning:** Branch on `kind`. The widget tree in `doc` is typed `unknown` and can change in any release, so never parse it for the widgets in the last row.

This module draws text, quick replies, capability cards, and a summary for everything else:

```html
<div id="log"></div>
<form id="composer">
  <input id="message" autocomplete="off" />
  <button type="submit">Send</button>
</form>

<script type="module">
  import { createAgentConversation } from "https://eu.mindset.ai/sdk/mindset-agent-uiless.js";

  const log = document.querySelector("#log");
  const form = document.querySelector("#composer");
  const input = document.querySelector("#message");

  const chat = createAgentConversation({
    agent: "billing-help",
    getSession: async () => {
      const r = await fetch("/api/session");
      if (!r.ok) throw new Error("Could not start a session");
      return r.json();
    },
  });

  function addLine(text, className) {
    const p = document.createElement("p");
    p.className = className;
    p.textContent = text;
    log.appendChild(p);
    return p;
  }

  function send(text) {
    addLine(text, "user");
    chat.send(text).catch(() => {});
  }

  function addButtons(items) {
    const row = document.createElement("div");
    for (const { label, text, detail } of items) {
      const button = document.createElement("button");
      button.type = "button";
      button.textContent = detail ? `${label}: ${detail}` : label;
      button.addEventListener("click", () => {
        row.querySelectorAll("button").forEach((b) => (b.disabled = true));
        send(text);
      });
      row.appendChild(button);
    }
    log.appendChild(row);
  }

  function drawWidget(kind, doc, summary) {
    if (kind === "quick_replies" && Array.isArray(doc?.replies)) {
      addButtons(doc.replies.map((r) => ({ label: r.label, text: (r.value ?? r.label).trim() })));
    } else if (kind === "capability_cards" && Array.isArray(doc?.cards)) {
      addButtons(doc.cards.map((c) => ({ label: c.title, detail: c.description, text: c.prompt })));
    } else if (summary) {
      addLine(summary, "widget-summary");
    }
  }

  let reply = null;

  chat.on((event) => {
    switch (event.type) {
      case "text_delta":
        reply ??= addLine("", "agent");
        reply.textContent += event.content;
        break;
      case "widget-payload":
        drawWidget(event.kind, event.doc, event.summary);
        break;
      case "complete":
      case "run_error":
        reply = null;
        break;
      default:
        break;
    }
  });

  form.addEventListener("submit", (e) => {
    e.preventDefault();
    const text = input.value.trim();
    if (!text) return;
    input.value = "";
    send(text);
  });
</script>
```

For a returning user's history, call `drawWidget(m.widget.kind, m.widget.doc, m.widget.summary)` for each message in `history_settled` that has a `widget`. [Events reference](https://docs.mindset.ai/docs/events-reference#widget-payload) has every field.

## What you can't do yet

- **Draw charts, tables, cards and authored widgets reliably.** Nothing promises the shape of `doc`.
- **Run a scripted agent.** A script's greeting, cards and phases appear only in the element.

If you need these, use [the element](https://docs.mindset.ai/docs/put-an-agent-on-your-page), or have the agent answer in text for your integration.

## See it working

**Live demo:** [Halcyon](https://docs.mindset.ai/demos/halcyon). A sleep app draws the agent’s breathing widget as its own orb, with no Mindset UI on screen.

**Live demo:** [Ask your data](https://docs.mindset.ai/demos/ask-your-data). Ask sales questions and a dashboard draws the answers from what the agent’s functions return.
