> ## Documentation Index
> Fetch the complete documentation index at: https://help.statisfy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Digital Worker Chat

> Embed the DigitalWorkerChat widget, pin onboarding threads, render inline forms, and drive conversations with the headless client.

The chat components let your customers converse with one of your [Digital Workers](/agent-studio/agents/overview) from inside your app — with streaming replies, inline forms, conversation history, unread badges, and live replies from a human CSM.

All components on this page must be rendered inside a [`<StatisfyProvider>`](/sdk/getting_started).

## `DigitalWorkerChat`

The full chat widget. The only required prop is the worker to talk to.

```tsx theme={null}
import { DigitalWorkerChat } from '@statisfy/digital-workers-react'

<DigitalWorkerChat
  workerId="your-digital-worker-uuid"
  workerName="Onboarding Assistant"
  greeting="Hi! Ask me anything about getting set up."
  className="dw-h-[600px]"
/>
```

### Props

| Prop                   | Type                      | Default            | Description                                                                                                                                           |
| ---------------------- | ------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `workerId`             | `string`                  | —                  | **Required.** UUID of the Digital Worker to converse with.                                                                                            |
| `workerName`           | `string`                  | `"Digital Worker"` | Display name in the header.                                                                                                                           |
| `greeting`             | `string`                  | —                  | First message shown before the user types.                                                                                                            |
| `className`            | `string`                  | —                  | Extra classes on the root container (e.g. height).                                                                                                    |
| `initialThreadId`      | `string`                  | —                  | Pin a deterministic thread on the first turn. Consulted only before the first message; later turns continue the server's thread.                      |
| `formRenderer`         | `FormRenderer`            | —                  | Render callback for inline `present_form` tool calls. See [Inline forms](#inline-forms).                                                              |
| `autoLoadConversation` | `boolean`                 | `false`            | On mount, load prior history **and** the orchestrator's current next action (e.g. the form to show) without faking a user turn.                       |
| `presented`            | `boolean`                 | `true`             | Whether the chat is currently on-screen. Pass your launcher's open state; while `false`, inbound replies accrue as unread instead of being auto-read. |
| `onUnreadChange`       | `(count: number) => void` | —                  | Fires when the total unread count across the customer's threads changes. Badge your own launcher with it.                                             |
| `onClose`              | `() => void`              | —                  | When set, renders a close (✕) button in the header that calls this. Omit for always-visible inline embeds.                                            |
| `history`              | `boolean`                 | `true`             | Enable the history panel, unread badge + "new reply" toast, and the background poll that powers them.                                                 |
| `newChat`              | `boolean`                 | `true`             | Show a "New chat" affordance.                                                                                                                         |

### Embedding in a launcher

For a collapsible chat bubble, pass your open state to `presented` and badge the launcher with `onUnreadChange`:

```tsx theme={null}
function ChatLauncher() {
  const [open, setOpen] = useState(false)
  const [unread, setUnread] = useState(0)

  return (
    <>
      <button onClick={() => setOpen(true)}>
        Chat {unread > 0 && <span className="badge">{unread}</span>}
      </button>
      {open && (
        <DigitalWorkerChat
          workerId="your-digital-worker-uuid"
          presented={open}
          onUnreadChange={setUnread}
          onClose={() => setOpen(false)}
        />
      )}
    </>
  )
}
```

<Note>
  **Unread is tracked client-side** (per-thread last-seen counts in `localStorage`, refreshed by a background poll roughly every 30s). The active thread auto-marks read while `presented` is `true`.
</Note>

### Live CSM replies

When a human CSM replies to a conversation from Statisfy, the chat detects the new message on its next poll and surfaces it in the open thread — and as unread elsewhere. No extra wiring is needed; just keep `history` enabled (the default).

## `OnboardingModule`

A thin wrapper around `DigitalWorkerChat` for onboarding surfaces. It:

* Reads the `customer_id` from the session token and pins `initialThreadId` to `onboarding:<customer_id>` — so the same customer always resumes the same onboarding thread.
* Enables `autoLoadConversation`, so the chat opens with the right form (or closing message) already on screen and prior history restored across refreshes.
* Defaults `formRenderer` to `DefaultFormRenderer`.

```tsx theme={null}
import { OnboardingModule } from '@statisfy/digital-workers-react'

<OnboardingModule workerId="your-onboarding-worker-uuid" />
```

`OnboardingModuleProps` accepts the same props as `DigitalWorkerChat` **except** `initialThreadId` and `autoLoadConversation` (both are derived for you).

## Inline forms

When the agent needs structured input it emits a `present_form` tool call. Provide a `formRenderer` to render it as a real form; on submit, the chat posts a new turn with the collected values as `structured_input`.

The SDK ships a ready-made renderer, `DefaultFormRenderer`:

```tsx theme={null}
import { DigitalWorkerChat, DefaultFormRenderer } from '@statisfy/digital-workers-react'

<DigitalWorkerChat workerId="…" formRenderer={DefaultFormRenderer} />
```

To render forms in your own design system, supply a custom `FormRenderer`:

```tsx theme={null}
import type { FormRenderer } from '@statisfy/digital-workers-react'

const myFormRenderer: FormRenderer = (formSpec, ctx) => (
  <form
    onSubmit={(e) => {
      e.preventDefault()
      const data = Object.fromEntries(new FormData(e.currentTarget))
      ctx.onSubmit(data) // posts a new turn with structured_input
    }}
  >
    <p>{formSpec.prompt}</p>
    {formSpec.fields.map((f) => (
      <label key={f.key}>
        {f.label}
        <input name={f.key} placeholder={f.placeholder} required={f.mandatory} />
      </label>
    ))}
    <button type="submit" disabled={ctx.disabled}>Submit</button>
  </form>
)
```

`FormSpec` has `{ prompt, fields }`; each `FormField` has `{ key, label, type, options?, placeholder?, mandatory? }`. The `ctx.onSubmit(values)` call replays the turn with those values; `ctx.disabled` is `true` while a submission is in flight.

<Note>
  If you omit `formRenderer`, `present_form` calls render as a generic "tool running" chip and the agent falls back to free-text input.
</Note>

## Headless client

Need full control of the conversation UI? Skip the components and drive the transport directly with `createDigitalWorkerClient`. (The provider isn't required for the headless client — you pass config explicitly.)

```ts theme={null}
import { createDigitalWorkerClient } from '@statisfy/digital-workers-react'

const client = createDigitalWorkerClient({
  baseUrl: 'https://api.statisfy.app',
  workerId: 'your-digital-worker-uuid',
  getToken: async () => sessionToken,
  publishableKey: 'pk_live_your_key'
})
```

The client exposes four methods:

### `sendMessage(content, threadId?, structuredInput?, formPrompt?)`

One-shot send; resolves with the full reply.

```ts theme={null}
const res = await client.sendMessage('What are my open tasks?')
// res: { reply, thread_id, conversation_id, agent_message_id, pending_approval?, next_action? }
console.log(res.reply, res.thread_id)
```

### `streamMessage(options)`

Streams the reply token-by-token over Server-Sent Events. Drive your UI from the handler callbacks.

```ts theme={null}
await client.streamMessage({
  content: 'Walk me through onboarding',
  threadId: existingThreadId, // optional
  handlers: {
    onTextDelta: (chunk) => appendToBubble(chunk),
    onToolCallStarted: (tool) => showToolChip(tool),
    onToolCallCompleted: (tool) => clearToolChip(tool),
    onToolCallError: (tool, err) => showError(tool, err),
    onDone: ({ threadId, conversationId, agentMessageId, nextAction }) => finish(threadId),
    onError: (err) => showError(err.message)
  },
  signal: abortController.signal // optional; abort on unmount / thread switch
})
```

### `loadConversation({ threadId, signal? })`

Load a thread's prior messages plus the orchestrator's current next action.

```ts theme={null}
const snapshot = await client.loadConversation({ threadId })
// snapshot: { thread_id, conversation_id, messages[], next_action? }
```

Each message includes `role` (`user` | `assistant` | `system`), `content`, and a `sender_kind` (`customer` | `agent` | `teammate`) so you can distinguish AI replies from a human CSM's.

### `listConversations({ source?, limit?, signal? })`

List the customer's conversations for a history panel.

```ts theme={null}
const { conversations, total } = await client.listConversations({ limit: 20 })
// each: { thread_id, conversation_id, title, last_message_at, message_count }
```

Statisfy doesn't store read-state — compute unread client-side by diffing each thread's `message_count` against the last count you saw.

See the [API Reference](/sdk/api_reference) for the underlying endpoints, request/response shapes, and error codes.
