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 SDK Chat + transport
  • body / headers — dynamic request fields (see Custom Transport & RAG)
  • aiMessages — mapped props for AiMessage
  • addToolApprovalResponse / addToolOutput — tool lifecycle
  • Omit api for local-only demos via useAiChatLocal

Persistence

const chat = useAiChatPersisted({
  key: 'my-app-chat',
  storage: 'localStorage',
})
// chat.clearHistory(), chat.restored

Markdown 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.