Skip to main content
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 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
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.
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
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.
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
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.
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
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).
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