> ## 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

> 五个客户端 hook —— 流式、轮询、积分、prompt、历史，全包。

AI 客户端代码里全是重复的水管 —— 用 `AbortController` 取消生成、按 chunk 解码流式
响应、轮询长任务、分页历史记录。如果不抽象出来,每个 demo 或 feature 都会带上同样的
30 行模板代码。

vibestrap 在 `@/ai/hooks` 下提供 **五个 React hook**,把这些全封掉。你的组件专心写
UI。每个 hook 都纯客户端,服务端契约是普通的 JSON / text 端点,可以替换。

## 前置条件

* 看过 [providers](/zh/ai/providers) 文档 —— 这些 hook 调的就是 manager 暴露出来的
  后端。
* API 路由挂在 `/api/ai/chat`、`/api/ai/history`、`/api/credits/balance`、
  （默认值；每个 hook 都接受自定义 `endpoint`）。
* 标准 UI 示例在
  `src/components/demos/{chat-demo,image-demo,document-demo}.tsx`。

## API 参考

### `useGeneration(options?)`

从一个吐 UTF-8 chunk 的服务端端点流式拉文本。

**签名**

```ts theme={null}
function useGeneration(options?: {
  endpoint?: string;      // 默认 '/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;
}
```

**适用场景**：模型逐 token 输出的对话型流程。Hook 自己管 `AbortController` 和响应体解码。

```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' }] });
```

参考实现：`src/components/demos/chat-demo.tsx`。

### `useTask(options)`

长任务：POST 启动 → 轮询到结束态。

**签名**

```ts theme={null}
function useTask<T>(options: {
  startEndpoint: string;
  pollEndpoint: string;        // hook 自动追加 `?id=<id>`
  intervalMs?: number;         // 默认 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';
```

**适用场景**：图像生成、视频、文档处理 —— 一次 HTTP 装不下的任务。`parse` 把 hook 和
具体响应结构解耦。

```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' });
```

参考实现：`src/components/demos/image-demo.tsx`。

### `useCredits(endpoint?)`

读当前登录用户的积分余额。

**签名**

```ts theme={null}
function useCredits(endpoint?: string): {
  balance: number | null;     // null = 未登录，不是错误
  isLoading: boolean;
  error: string | null;
  refetch: () => Promise<void>;
};
```

**适用场景**：顶栏积分徽章、CTA 前的余额校验、付费回调后刷新。默认端点
`/api/credits/balance`。

```tsx theme={null}
const { balance, refetch } = useCredits();
// 一次 AI 调用成功后：
useEffect(() => { void refetch(); }, [lastCallId]);
```

`<CreditsBadge />` 已经包了这个 hook，大多数业务不直接用。

### `useHistory(endpoint?)`

当前用户的 `ai_call` 历史，游标分页。

**签名**

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

`AICallHistoryRow` 包含 `provider`、`model`、`operation`、`status`、
`inputTokens`（nullable —— 图像 / 视频 provider 不返回 token）、`outputTokens`（同）、
`totalMs`、`createdAt`。每页 20 条，游标是末行的 `createdAt`（ISO 字符串）。

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

参考实现：`src/components/demos/document-demo.tsx`。

## 验证生效

1. 起 dev server，登录。
2. 打开 `/playground/chat`，输入。看 `text` 一字一字出来 —— 那就是 `useGeneration`
   在流。
3. 打开 `/playground/image`，提交 prompt。状态从 `queued → running → succeeded` —
   那就是 `useTask`。
4. 打开 `/dashboard/usage`。列表刷新，`Load more` 能加更多行 —— 那就是 `useHistory`。

## 常见坑

* **流式端点返回 JSON 而不是 text。** `useGeneration` 是按 UTF-8 chunk 读 body 的。
  如果路由 `res.json()`，你会一次拿到全文而不是流。请用 `new Response(stream)` 或
  `text/plain` content type。
* **忘记 `await generate()`。** 它返回的是最终文本的 Promise，发完不管也行，但不
  `try/catch` 就看不到错误。
* **`useTask.parse` 返回了错的状态字符串。** Hook 只在 `'succeeded' | 'failed' |
  'cancelled'` 时停止轮询，其他都当「继续轮」。
* **`useCredits` 显示旧余额。** 任何 AI 调用之后都要 `refetch()`，manager 是异步扣
  积分的，徽章不会自己更新。

## 官方文档

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