Skip to main content
vibestrap ships five React hooks under @/ai/hooks that wrap the boring parts — AbortController, decoding streams, polling loops, cursor pagination — so your client code stays 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, /api/prompts/<slug> (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.

usePrompt(slug)

Look up a registered prompt and render it client-side. Signature
function usePrompt(slug: string): {
  prompt: PromptView | null;     // null = 404 (slug doesn't exist)
  isLoading: boolean;
  error: string | null;
  render: (variables: Record<string, string>) => string | null;
  refetch: () => Promise<void>;
};
Use it for showing the prompt editor, generating previews as the user fills in variables, or driving the <PromptVariableForm /> primitive.
const { prompt, render } = usePrompt('summarize-article');
const preview = prompt ? render({ article: input }) : null;
render substitutes {{variable}} placeholders. Server-side prompt rendering should hit the registry directly — see Prompts.

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, outputTokens, costMicroCents, 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).
  • usePrompt race on slug change. Multiple slugs in flight from the same component can interleave. Use key={slug} on the parent if you re-render with a new slug.

Official docs