跳转到主要内容
vibestrap 在 @/ai/hooks 下提供 五个 React hook,封掉了那些写起来烦人的部分: AbortController、流解码、轮询循环、游标分页 —— 让你的客户端代码专心写 UI。每个 hook 都纯客户端,服务端契约是普通的 JSON / text 端点,可以替换。

前置条件

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

API 参考

useGeneration(options?)

从一个吐 UTF-8 chunk 的服务端端点流式拉文本。 签名
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 和响应体解码。
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 启动 → 轮询到结束态。 签名
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 和 具体响应结构解耦。
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?)

读当前登录用户的积分余额。 签名
function useCredits(endpoint?: string): {
  balance: number | null;     // null = 未登录,不是错误
  isLoading: boolean;
  error: string | null;
  refetch: () => Promise<void>;
};
适用场景:顶栏积分徽章、CTA 前的余额校验、付费回调后刷新。默认端点 /api/credits/balance
const { balance, refetch } = useCredits();
// 一次 AI 调用成功后:
useEffect(() => { void refetch(); }, [lastCallId]);
<CreditsBadge /> 已经包了这个 hook,大多数业务不直接用。

usePrompt(slug)

按 slug 拉一个已注册的 prompt,并支持客户端渲染。 签名
function usePrompt(slug: string): {
  prompt: PromptView | null;     // null = 404(slug 不存在)
  isLoading: boolean;
  error: string | null;
  render: (variables: Record<string, string>) => string | null;
  refetch: () => Promise<void>;
};
适用场景:prompt 编辑器、用户填变量时的实时预览、驱动 <PromptVariableForm />
const { prompt, render } = usePrompt('summarize-article');
const preview = prompt ? render({ article: input }) : null;
render 替换 {{variable}} 占位符。服务端渲染请直接走 registry —— 见 Prompts

useHistory(endpoint?)

当前用户的 ai_call 历史,游标分页。 签名
function useHistory(endpoint?: string): {
  rows: AICallHistoryRow[];
  isLoading: boolean;
  error: string | null;
  hasMore: boolean;
  loadMore: () => Promise<void>;
  refetch: () => Promise<void>;
};
AICallHistoryRow 包含 providermodeloperationstatusinputTokensoutputTokenscostMicroCentstotalMscreatedAt。每页 20 条, 游标是末行的 createdAt(ISO 字符串)。
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 是异步扣 积分的,徽章不会自己更新。
  • usePrompt 在 slug 切换时 race。 同一组件里多个 slug 并发会乱序。如果会重渲 染换 slug,建议在外层用 key={slug}

官方文档