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

# Building a Chatbot

> A production chat experience end to end: a conversational prompt, a thin backend, and a streaming React frontend.

This guide builds a real-time chatbot on PromptJuggler: your user types a message, the
answer streams into the page token by token, and the conversation remembers itself. It
takes one prompt, two small backend endpoints, and one React component.

## 1. The prompt

Create a prompt (say `assistant`) with **read-write memory** — it sees the conversation
so far and appends its replies to it (see
[Memory & Threads](/concepts/memory-and-threads)). Give it your system instructions and
an input variable for the user's message, then publish it to `production`.

## 2. The backend

Two endpoints, both thin. One mints the [stream credential](/concepts/streaming), one
triggers runs. Your API key stays here; the browser never sees it.

```ts theme={null}
import { PromptJuggler } from '@promptjuggler/sdk';
import { randomUUID } from 'node:crypto';

const pj = new PromptJuggler(process.env.PROMPTJUGGLER_API_KEY);

// The stream credential: scoped to one thread, valid 24h. Authenticate your
// own user first — whoever holds this can read that thread's stream.
app.get('/api/chat/:thread/stream-token', async (req, res) => {
  res.json(await pj.createStreamToken(req.params.thread));
});

// One message: trigger a run on the thread. Returns immediately; the answer
// arrives on the stream.
app.post('/api/chat/:thread/messages', async (req, res) => {
  const run = await pj.runPrompt('assistant', 'production', {
    message: req.body.message,
  }, { thread: req.params.thread });
  res.json({ runId: run.id });
});
```

The thread ID is yours to choose — generate a UUID per conversation
(`randomUUID()`), keep it in your session or database, and pass the same one to both
endpoints. The thread doesn't need to exist before the first run; minting a stream token
for it first is exactly the right order.

## 3. The frontend

Connect **before** sending the first message — a fresh subscription starts at the live
tip, so open the stream first and you see every token from the beginning.

```tsx theme={null}
import { useState } from 'react';
import { usePromptJugglerStream } from '@promptjuggler/browser/react';

export function Chat({ threadId }: { threadId: string }) {
  const [draft, setDraft] = useState('');
  const { connected, runs } = usePromptJugglerStream(
    {
      getToken: () =>
        fetch(`/api/chat/${threadId}/stream-token`).then((r) => r.json()),
    },
    [threadId],
  );

  const send = async () => {
    setDraft('');
    await fetch(`/api/chat/${threadId}/messages`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ message: draft }),
    });
  };

  return (
    <div>
      {Object.entries(runs).map(([runId, run]) => (
        <div key={runId} className={run.status === 'failed' ? 'error' : ''}>
          {run.text}
          {run.status === 'streaming' && <Cursor />}
        </div>
      ))}
      <input value={draft} onChange={(e) => setDraft(e.target.value)} />
      <button onClick={send} disabled={!connected}>
        Send
      </button>
    </div>
  );
}
```

That's the whole loop: `send` triggers a run, the run streams into `runs[runId].text`,
and `status` flips to `done` when it finishes. Reconnects, retries, and workflow
segments are handled inside the hook.

<Note>
  When a terminal state carries `gapped: true`, the streamed text may be incomplete —
  fetch the run through your backend (`pj.getPromptRun(runId)`) and swap in the
  authoritative output. With connect-before-send this is rare, but handling it is what
  makes the UI trustworthy.
</Note>

## Showing the user's side

The stream carries the model's output; your user's messages you already have locally.
Render them optimistically from your own state, keyed however you like — the runs map
and your local messages interleave naturally by time. On a full page reload, rebuild the
transcript from your backend (the thread's runs via the API, or your own message store)
and let the stream take over for new messages.

## Workflows instead of a single prompt

Swap `runPrompt` for `runWorkflow` and nothing else changes — every prompt node streams
into the same connection. If the workflow has internal lanes (research, tool use,
synthesis), put the user-facing node on a dedicated
[channel](/concepts/memory-and-threads#channels) and pass `channels: ['support']` to the
hook so only that lane renders.
