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
Property | Type | Required | Description |
---|---|---|---|
api | string | Yes | API endpoint for chat requests |
systemPrompt | string | No | Application-specific system prompt |
initialMessages | UIMessage[] | No | Initial chat messages |
onFinish | (message: UIMessage) => void | No | Callback when a message is completed |
onError | (error: Error) => void | No | Error 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
Property | Type | Description |
---|---|---|
messages | UIMessage[] | Array of chat messages |
input | string | Current input value |
isLoading | boolean | Whether a request is in progress |
error | Error | null | Current error state |
Action Methods
Method | Parameters | Description |
---|---|---|
handleInputChange | e: ChangeEvent | Handle input field changes |
handleSubmit | e?: FormEvent | Submit the current message |
setInput | input: string | Set input value programmatically |
setMessages | messages: UIMessage[] | Replace all messages |
append | message: UIMessage | Add a new message |
reload | None | Regenerate the last AI response |
stop | None | Stop 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:
Field | Always Present? | Source | Description |
---|---|---|---|
systemPrompt | Optional | Your hook option / per-call override | Original app-specific instructions |
enrichedSystemPrompt | Yes (generated unless overridden) | Built by useAIChat | Structured 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
- Leverage Enrichment: Always rely on
enrichedSystemPrompt
downstream; it is more informative than a raw prompt. - Handle Errors Gracefully: Implement
onError
for production apps. - Validate Input: Check user input before sending.
- Manage Memory: Trim long histories to reduce token usage.
- Performance: Use stable selectors and avoid unnecessary re-renders.
- Security: Avoid sending sensitive data in context/focus.
- Observability: Log resolved system prompts (but redact sensitive parts if any).
- Per-Call Overrides: Use
body.enrichedSystemPrompt
sparingly—prefer built-in builder unless you have a specialized orchestrator.