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

# AI hooks

> Five client hooks for streaming, polling, credits, prompts, and history.

AI client code is full of repeating plumbing — `AbortController` for cancellable
generations, decoding streamed responses chunk by chunk, polling long-running tasks,
paginating history. Without these abstractions, every demo or feature picks up the
same 30 lines of boilerplate.

vibestrap ships **five React hooks** under `@/ai/hooks` that wrap all of it. Your
components stay focused on UI. Every hook is purely client-side; the server contract
is a plain JSON or text endpoint you can swap.

## Prerequisites

* Familiarity with the [providers](/ai/providers) doc — these hooks talk to the
  backend the manager exposes.
* API routes mounted at `/api/ai/chat`, `/api/ai/history`, `/api/credits/balance`
  (the defaults — every hook accepts a custom `endpoint`).
* The canonical UI examples live in
  `src/components/demos/{chat-demo,image-demo,document-demo}.tsx`.

## API reference

### `useGeneration(options?)`

Stream text from a server endpoint that emits raw UTF-8 chunks.

**Signature**

```ts theme={null}
function useGeneration(options?: {
  endpoint?: string;      // defaults to '/api/ai/chat'
  model?: string;
  onFinish?: (text: string) => void;
}): {
  text: string;
  status: 'idle' | 'streaming' | 'done' | 'error' | 'cancelled';
  error: string | null;
  isStreaming: boolean;
  generate: (input: { messages: ChatMessage[]; model?: string }) => Promise<string>;
  cancel: () => void;
  reset: () => void;
}
```

**Use it for** chat-like flows where the model emits tokens incrementally. The hook
manages an `AbortController` per call and decodes the response body itself.

```tsx theme={null}
const { text, isStreaming, generate, cancel } = useGeneration({
  onFinish: (final) => setMessages((p) => [...p, { role: 'assistant', content: final }]),
});

await generate({ messages: [{ role: 'user', content: 'hi' }] });
```

Canonical implementation: `src/components/demos/chat-demo.tsx`.

### `useTask(options)`

Long-running task: POST to start, then poll until terminal.

**Signature**

```ts theme={null}
function useTask<T>(options: {
  startEndpoint: string;
  pollEndpoint: string;        // hook appends `?id=<id>`
  intervalMs?: number;         // default 1500
  parse: (json: unknown) => Pick<TaskState<T>, 'status' | 'result' | 'error' | 'progress'>;
}): TaskState<T> & { start: (input: unknown) => Promise<void>; cancel: () => void };

type TaskStatus = 'idle' | 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled';
```

**Use it for** image generation, video, document processing — anything that doesn't fit
in one HTTP request. The `parse` callback decouples the hook from your endpoint shape.

```tsx theme={null}
const task = useTask<{ images: { url: string }[] }>({
  startEndpoint: '/api/ai/image',
  pollEndpoint: '/api/ai/image/status',
  parse: (json) => json as ReturnType<typeof task.parse>,
});

await task.start({ prompt: 'a cat astronaut' });
```

Canonical implementation: `src/components/demos/image-demo.tsx`.

### `useCredits(endpoint?)`

Read the signed-in user's balance.

**Signature**

```ts theme={null}
function useCredits(endpoint?: string): {
  balance: number | null;     // null = anonymous, not an error
  isLoading: boolean;
  error: string | null;
  refetch: () => Promise<void>;
};
```

**Use it for** the header credit badge, balance check before showing CTAs, refresh after
checkout. The default endpoint is `/api/credits/balance`.

```tsx theme={null}
const { balance, refetch } = useCredits();
// after a successful AI call:
useEffect(() => { void refetch(); }, [lastCallId]);
```

The `<CreditsBadge />` primitive wraps this hook so most apps never call it directly.

### `useHistory(endpoint?)`

Cursor-paginated `ai_call` history for the current user.

**Signature**

```ts theme={null}
function useHistory(endpoint?: string): {
  rows: AICallHistoryRow[];
  isLoading: boolean;
  error: string | null;
  hasMore: boolean;
  loadMore: () => Promise<void>;
  refetch: () => Promise<void>;
};
```

`AICallHistoryRow` includes `provider`, `model`, `operation`, `status`,
`inputTokens` (nullable — image / video providers don't return tokens),
`outputTokens` (same), `totalMs`, `createdAt`. Page size is 20. Cursor is the last
row's `createdAt` (ISO).

```tsx theme={null}
const { rows, hasMore, loadMore } = useHistory();
return rows.map((r) => <HistoryRow key={r.id} row={r} />);
```

Canonical implementation: `src/components/demos/document-demo.tsx`.

## Verify they work

1. Boot the dev server, sign in.
2. Open `/playground/chat`. Type. Observe `text` painting tokens — that's
   `useGeneration` streaming.
3. Open `/playground/image`. Submit a prompt. The status pill goes
   `queued → running → succeeded` — that's `useTask`.
4. Open `/dashboard/usage`. The list refreshes and `Load more` extends it —
   that's `useHistory`.

## Common pitfalls

* **Streaming endpoint returns JSON, not text.** `useGeneration` reads the body as
  UTF-8 chunks. If your route does `res.json()` you'll get one big aggregated string
  instead of streaming. Use `new Response(stream)` or `text/plain` content type.
* **Forgetting to `await` `generate()`.** It returns a Promise resolving to the final
  text — fire-and-forget is fine, but you can't show errors without `try/catch`.
* **`useTask.parse` returning the wrong status string.** The hook only stops polling
  on `'succeeded' | 'failed' | 'cancelled'`. Anything else means "keep polling".
* **`useCredits` showing stale balance.** Call `refetch()` after any AI call (the manager
  consumes credits asynchronously, the badge won't update on its own).

## Official docs

* React refs / AbortController: [react.dev](https://react.dev/reference/react)
* Streams API: [developer.mozilla.org/en-US/docs/Web/API/Streams\_API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API)
* Source: `src/ai/hooks/`, `src/components/demos/`
