Building a Chat
End-to-end patterns for streaming chat with the AI SDK and headless UI.
Server handler
createChatHandler accepts a LanguageModel, streams UI messages compatible with useAiChat, and enables reasoning/sources by default.
import { createChatHandler } from 'ai-elements-nuxt/server'
import { openai } from '@ai-sdk/openai'
export default createChatHandler({
model: openai('gpt-4o'),
system: 'You are a helpful assistant.',
})useAiChat
api: '/api/chat'— connects to AI SDKChat+ transportbody/headers— dynamic request fields (see Custom Transport & RAG)aiMessages— mapped props forAiMessageaddToolApprovalResponse/addToolOutput— tool lifecycle- Omit
apifor local-only demos viauseAiChatLocal
Persistence
const chat = useAiChatPersisted({
key: 'my-app-chat',
storage: 'localStorage',
})
// chat.clearHistory(), chat.restoredMarkdown messages
Use useAiMarkdown or AiMarkdown for assistant HTML (GFM via marked).
Error states
useAiChat returns error and clearError. When isStreaming is false and error is set, the last generation failed — show a retry UI.
<script setup lang="ts">
const { aiMessages, input, handleSubmit, isStreaming, error, clearError } = useAiChat({
api: '/api/chat',
})
</script>
<template>
<AiMessage v-for="(msg, i) in aiMessages" :key="i" v-bind="msg" />
<!-- Stream or tool failure -->
<AiErrorBoundary
v-if="error && !isStreaming"
:error="error"
@retry="clearError"
/>
<form @submit="handleSubmit">
<AiPromptInput v-model="input" :loading="isStreaming" :disabled="!!error" />
</form>
</template>clearError() resets the error and re-enables the input. For agents, tool failures surface through the step's status: 'failed' — handle them in the AiAgent step timeline.