Skip to Content
APIHooksuseAIChat

useAIChat

The useAIChat hook is the main hook for managing chat state and AI interactions. It automatically integrates with context items from useAIContext, focus items from useAIFocus, frontend tools from useAIFrontendTool, and now automatically builds and sends an enrichedSystemPrompt that gives models structured awareness of those capabilities.

Syntax

interface UseAIChatOptions { api: string; systemPrompt?: string; initialMessages?: UIMessage[]; onFinish?: (message: UIMessage) => void; onError?: (error: Error) => void; } function useAIChat(options: UseAIChatOptions): UseAIChatReturn

System Prompt Enrichment (Automatic)

The ALWAYS generates an enrichedSystemPrompt for each request. This enriched prompt:

  • Adds a standardized preamble describing that the assistant runs in an enhanced environment (context, focus, tools).
  • Includes conditional sections only when data exists (tools, context, focus).
  • Lists tool names with descriptions (no parameter schemas; those are already sent separately).
  • Appends the original systemPrompt you provided (if any) at the end with a clear separator.

Per-call override: you can still override the final string by passing body.enrichedSystemPrompt in sendMessage (advanced usage). If you only provide systemPrompt, enrichment still runs and your original content is appended.

Parameters

PropertyTypeRequiredDescription
apistringYesAPI endpoint for chat requests
systemPromptstringNoApplication-specific system prompt
initialMessagesUIMessage[]NoInitial chat messages
onFinish(message: UIMessage) => voidNoCallback when a message is completed
onError(error: Error) => voidNoError callback

Return Value

interface UseAIChatReturn { messages: UIMessage[]; input: string; isLoading: boolean; error: Error | null; handleInputChange: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void; handleSubmit: (e?: FormEvent) => void; setInput: (input: string) => void; setMessages: (messages: UIMessage[]) => void; append: (message: UIMessage) => void; reload: () => void; stop: () => void; // Additional exposed convenience + state: sendMessageWithContext: (text: string) => void; sendAICommandMessage: (text: string, toolName: string, systemPrompt?: string) => void; retryLastMessage: () => void; clearError: () => void; context: ContextItem[]; tools: { name: string; description: string; inputSchema: unknown }[]; focusItems: FocusItem[]; }

State Properties

PropertyTypeDescription
messagesUIMessage[]Array of chat messages
inputstringCurrent input value
isLoadingbooleanWhether a request is in progress
errorError | nullCurrent error state

Action Methods

MethodParametersDescription
handleInputChangee: ChangeEventHandle input field changes
handleSubmite?: FormEventSubmit the current message
setInputinput: stringSet input value programmatically
setMessagesmessages: UIMessage[]Replace all messages
appendmessage: UIMessageAdd a new message
reloadNoneRegenerate the last AI response
stopNoneStop the current streaming response

Examples

Basic Usage

import { useAIChat } from "ai-chat-bootstrap"; function ChatInterface() { const chat = useAIChat({ api: "/api/chat", systemPrompt: "You are a helpful AI assistant." }); return ( <ChatContainer chat={chat} /> ); }

With Initial Messages

const initialMessages: UIMessage[] = [ { id: "1", role: "assistant", parts: [{ type: "text", text: "Hello! How can I help you today?" }] } ]; const chat = useAIChat({ api: "/api/chat", systemPrompt: "You are a helpful assistant.", initialMessages });

With Event Handlers

const chat = useAIChat({ api: "/api/chat", onFinish: (message) => { console.log("AI response completed:", message); saveMessageToHistory(message); }, onError: (error) => { console.error("Chat error:", error); toast.error("Sorry, there was an error processing your request."); } });

Programmatic Message Control

function ChatWithControls() { const chat = useAIChat({ api: "/api/chat" }); const sendPredefinedMessage = () => { chat.append({ id: Date.now().toString(), role: "user", parts: [{ type: "text", text: "What's the weather like?" }] }); chat.handleSubmit(); }; const clearChat = () => { chat.setMessages([]); }; const stopGeneration = () => { chat.stop(); }; return ( <div> <div className="mb-4 space-x-2"> <button onClick={sendPredefinedMessage}>Ask about weather</button> <button onClick={clearChat}>Clear chat</button> <button onClick={stopGeneration}>Stop generation</button> </div> <ChatContainer chat={chat} /> </div> ); }

Automatic Integration

The useAIChat hook automatically integrates with other hooks:

Context Integration

function ChatWithContext() { const [user, setUser] = useState({ name: "Alice", role: "admin" }); useAIContext({ description: "User Profile", value: user, priority: 100 }); const chat = useAIChat({ api: "/api/chat", systemPrompt: "You have access to the user's profile information." }); return <ChatContainer chat={chat} />; }

Focus Integration

function ChatWithFocus() { const { focusedIds, allFocusItems } = useAIFocus(); const chat = useAIChat({ api: "/api/chat", systemPrompt: "You have access to the user's focused documents." }); return ( <div> <div className="mb-2"> {allFocusItems.length > 0 && ( <div className="text-sm text-muted-foreground"> {allFocusItems.length} items in focus </div> )} </div> <ChatContainer chat={chat} /> </div> ); }

Tools Integration

function ChatWithTools() { const [counter, setCounter] = useState(0); useAIFrontendTool({ name: "increment_counter", description: "Increment the counter", parameters: z.object({ amount: z.number().default(1) }), execute: async ({ amount }) => { setCounter(prev => prev + amount); return { newValue: counter + amount }; } }); const chat = useAIChat({ api: "/api/chat", systemPrompt: "You can control the counter using the available tools." }); return ( <div> <div className="mb-4">Counter: {counter}</div> <ChatContainer chat={chat} /> </div> ); }

System Prompt Enrichment Details

The client sends two related fields in the request body:

FieldAlways Present?SourceDescription
systemPromptOptionalYour hook option / per-call overrideOriginal app-specific instructions
enrichedSystemPromptYes (generated unless overridden)Built by useAIChatStructured preamble + conditional sections + appended original systemPrompt

Downstream code should prefer enrichedSystemPrompt if provided.

Override Mechanism

You can override enrichment per-call:

chat.sendMessage( { text: "Analyze document" }, { body: { enrichedSystemPrompt: "Custom fully composed system message..." } } );

Request Payload

The hook automatically sends a structured payload:

interface ChatRequestPayload { messages: UIMessage[]; systemPrompt?: string; // Original (legacy) prompt (optional) enrichedSystemPrompt: string; // Always provided by the hook tools?: Record<string, ToolDefinition>; context?: ContextItem[]; focus?: FocusItem[]; }

Example API Route

import { openai } from "@ai-sdk/openai"; import { convertToModelMessages, streamText } from "ai"; export async function POST(req: Request) { const { messages, enrichedSystemPrompt, tools } = await req.json(); const result = await streamText({ model, messages: [ { role: "system", content: enrichedSystemPrompt }, ...convertToModelMessages(messages), ], tools, }); return result.toUIMessageStreamResponse(); }

Advanced Usage

Custom Message Processing

function ChatWithCustomProcessing() { const chat = useAIChat({ api: "/api/chat", onFinish: (message) => { const textParts = message.parts .filter(part => part.type === 'text') .map(part => part.text); const codeBlocks = textParts .join(' ') .match(/```[\s\S]*?```/g) || []; if (codeBlocks.length > 0) { saveCodeBlocks(codeBlocks); } } }); return <ChatContainer chat={chat} />; }

Error Recovery

function ChatWithErrorRecovery() { const [retryCount, setRetryCount] = useState(0); const chat = useAIChat({ api: "/api/chat", onError: (error) => { console.error("Chat error:", error); if (error.name === 'NetworkError' && retryCount < 3) { setTimeout(() => { setRetryCount(prev => prev + 1); chat.reload(); }, 1000 * (retryCount + 1)); } else { toast.error(`Failed to get response: ${error.message}`); } } }); return <ChatContainer chat={chat} />; }

Message Validation

function ChatWithValidation() { const chat = useAIChat({ api: "/api/chat", onFinish: (message) => { const textContent = message.parts .filter(part => part.type === 'text') .map(part => part.text) .join(' '); if (containsInappropriateContent(textContent)) { const filteredMessage = { ...message, parts: message.parts.map(part => part.type === 'text' ? { ...part, text: filterContent(part.text) } : part ) }; chat.setMessages(prev => prev.map(msg => msg.id === message.id ? filteredMessage : msg ) ); } } }); return <ChatContainer chat={chat} />; }

Message Format

Works with Vercel AI SDK UIMessage:

interface UIMessage { id: string; role: 'system' | 'user' | 'assistant'; metadata?: unknown; parts: Array<UIMessagePart>; } type UIMessagePart = | { type: 'text'; text: string; state?: 'streaming' | 'done' } | { type: 'reasoning'; text: string; state?: 'streaming' | 'done' } | { type: 'file'; mediaType: string; filename?: string; url: string } | { type: 'source-url'; sourceId: string; url: string; title?: string };

Performance Considerations

Message History Management

function ChatWithHistoryLimit() { const MAX_MESSAGES = 50; const chat = useAIChat({ api: "/api/chat", onFinish: () => { if (chat.messages.length > MAX_MESSAGES) { const trimmed = chat.messages.slice(-MAX_MESSAGES); chat.setMessages(trimmed); } } }); return <ChatContainer chat={chat} />; }

Debounced Input

import { useMemo } from 'react'; import { debounce } from 'lodash'; function ChatWithDebouncedInput() { const chat = useAIChat({ api: "/api/chat" }); const debouncedSubmit = useMemo( () => debounce(chat.handleSubmit, 500), [chat.handleSubmit] ); const handleInputChange = (e) => { chat.handleInputChange(e); if (e.target.value.trim()) { debouncedSubmit(); } }; return ( <ChatContainer chat={chat} inputProps={{ onChange: (v) => handleInputChange({ target: { value: v } } as any) }} // Note: debounced submit must be bound to form submit // Consumers can wire a custom form if needed /> ); }

Error Types

interface ChatError extends Error { name: 'ChatError'; cause?: unknown; } interface NetworkError extends Error { name: 'NetworkError'; status?: number; statusText?: string; } interface ValidationError extends Error { name: 'ValidationError'; field?: string; value?: unknown; }

Best Practices

  1. Leverage Enrichment: Always rely on enrichedSystemPrompt downstream; it is more informative than a raw prompt.
  2. Handle Errors Gracefully: Implement onError for production apps.
  3. Validate Input: Check user input before sending.
  4. Manage Memory: Trim long histories to reduce token usage.
  5. Performance: Use stable selectors and avoid unnecessary re-renders.
  6. Security: Avoid sending sensitive data in context/focus.
  7. Observability: Log resolved system prompts (but redact sensitive parts if any).
  8. Per-Call Overrides: Use body.enrichedSystemPrompt sparingly—prefer built-in builder unless you have a specialized orchestrator.

See Also

Last updated on