Chat

AI SDK
Construa interfaces de chat de IA com streaming, raciocínio e chamada de ferramentas.

O Nitro UI fornece um conjunto de componentes projetados para construir interfaces de chat com IA. Eles se integram perfeitamente ao Vercel AI SDK para respostas em streaming, raciocínio, chamada de ferramentas e mais.

Confira os templates de Chat de IA para Nuxt e Vue no GitHub para implementações prontas para produção.

Componentes

ComponentDescription
ChatMessagesScrollable message list with auto-scroll and loading indicator.
ChatMessageIndividual message bubble with avatar, actions, and slots.
ChatPromptEnhanced textarea for submitting prompts.
ChatPromptSubmitSubmit button with automatic status handling.
ChatReasoningCollapsible block for AI reasoning / thinking process.
ChatToolCollapsible block for AI tool invocation status.
ChatShimmerText shimmer animation for streaming states.
ChatPaletteLayout wrapper for embedding chat in modals or drawers.

Instalação

Os componentes de Chat são projetados para serem usados com o Vercel AI SDK, especificamente a classe Chat para gerenciar o estado do chat e as respostas em streaming.

Install the required dependencies:

pnpm add ai @ai-sdk/gateway @ai-sdk/vue @comark/nuxt

Adicione @comark/nuxt aos seus módulos:

nuxt.config.ts
export default defineNuxtConfig({
  modules: [
    '@nitro/ui',
    '@comark/nuxt'
  ]
})
@comark/nuxt provides the Comark component used to render AI responses as streaming Markdown, it incrementally renders tokens as they arrive, avoiding the flicker and re-parsing that traditional Markdown renderers cause. It also automatically enables Nitro UI's prose components so your content is styled to match your theme.
pnpm add ai @ai-sdk/gateway @ai-sdk/vue @comark/vue
@comark/vue provides the Comark component used to render AI responses as streaming Markdown, it incrementally renders tokens as they arrive, avoiding the flicker and re-parsing that traditional Markdown renderers cause.

Para usar os componentes de prose do Nitro UI com o Comark, habilite a opção prose no seu vite.config.ts:
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nitro/ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      prose: true
    })
  ]
})

Configuração do servidor

Crie um endpoint de API no servidor para tratar as requisições de chat usando streamText. Você pode usar o Vercel AI Gateway para acessar modelos de IA por meio de um endpoint centralizado:

server/api/chat.post.ts
import { streamText, convertToModelMessages, toUIMessageStream, createUIMessageStreamResponse } from 'ai'
import { gateway } from '@ai-sdk/gateway'

export default defineEventHandler(async (event) => {
  const { messages } = await readBody(event)

  const result = streamText({
    model: gateway('anthropic/claude-sonnet-5'),
    maxOutputTokens: 10000,
    instructions: 'You are a helpful assistant.',
    messages: await convertToModelMessages(messages)
  })

  const stream = toUIMessageStream({ stream: result.stream })
  return createUIMessageStreamResponse({ stream })
})

Raciocínio

Para habilitar o raciocínio, configure providerOptions para o seu provedor (Anthropic, Google, OpenAI):

server/api/chat.post.ts
import { streamText, convertToModelMessages, toUIMessageStream, createUIMessageStreamResponse } from 'ai'
import { gateway } from '@ai-sdk/gateway'

export default defineEventHandler(async (event) => {
  const { messages } = await readBody(event)

  const result = streamText({
    model: gateway('anthropic/claude-sonnet-5'),
    maxOutputTokens: 10000,
    instructions: 'You are a helpful assistant.',
    messages: await convertToModelMessages(messages),
    providerOptions: {
      anthropic: {
        thinking: {
          type: 'adaptive'
        },
        effort: 'low'
      },
      google: {
        thinkingConfig: {
          includeThoughts: true,
          thinkingLevel: 'low'
        }
      },
      openai: {
        reasoningEffort: 'low',
        reasoningSummary: 'detailed'
      }
    }
  })

  const stream = toUIMessageStream({ stream: result.stream })
  return createUIMessageStreamResponse({ stream })
})

Busca na web

Some providers offer built-in web search tools: Anthropic, Google, OpenAI.

import { streamText, convertToModelMessages, toUIMessageStream, createUIMessageStreamResponse } from 'ai'
import { anthropic } from '@ai-sdk/anthropic'
import { gateway } from '@ai-sdk/gateway'

export default defineEventHandler(async (event) => {
  const { messages } = await readBody(event)

  const result = streamText({
    model: gateway('anthropic/claude-sonnet-5'),
    instructions: 'You are a helpful assistant.',
    messages: await convertToModelMessages(messages),
    tools: {
      web_search: anthropic.tools.webSearch_20250305({})
    }
  })

  const stream = toUIMessageStream({ stream: result.stream })
  return createUIMessageStreamResponse({ stream })
})

Cliente MCP

Potencialize seu chatbot com recursos avançados de chamada de ferramentas usando o Model Context Protocol (MCP) de @ai-sdk/mcp. O MCP permite que sua IA execute ações dinâmicas, como buscar na sua documentação ou executar tarefas personalizadas, para fornecer respostas mais relevantes e precisas.

Para começar, instale o pacote MCP:

npm install @ai-sdk/mcp

Depois, configure o endpoint do seu servidor para usar ferramentas MCP:

server/api/chat.post.ts
import { streamText, convertToModelMessages, isStepCount, toUIMessageStream, createUIMessageStreamResponse } from 'ai'
import { createMCPClient } from '@ai-sdk/mcp'
import { gateway } from '@ai-sdk/gateway'

export default defineEventHandler(async (event) => {
  const { messages } = await readBody(event)

  const httpClient = await createMCPClient({
    transport: { type: 'http', url: 'https://your-app.com/mcp' }
  })
  try {
    const tools = await httpClient.tools()

    const result = streamText({
      model: gateway('anthropic/claude-sonnet-5'),
      maxOutputTokens: 10000,
      instructions: 'You are a helpful assistant. Use your tools to search for relevant information before answering questions.',
      messages: await convertToModelMessages(messages),
      stopWhen: isStepCount(6),
      tools,
      onEnd: async () => {
        await httpClient.close()
      },
      onError: async (error) => {
        console.error(error)
        await httpClient.close()
      }
    })

    const stream = toUIMessageStream({ stream: result.stream })
    return createUIMessageStreamResponse({ stream })
  } catch (error) {
    // Close the MCP client if setup fails before streaming starts
    await httpClient.close()
    throw error
  }
})

Aprovação de ferramenta

Exija uma confirmação do usuário antes de uma ferramenta executar com a opção toolApproval. O tool part pausa no estado approval-requested até o usuário responder:

server/api/chat.post.ts
import { streamText, convertToModelMessages, toUIMessageStream, createUIMessageStreamResponse, tool } from 'ai'
import { gateway } from '@ai-sdk/gateway'
import { z } from 'zod'

export default defineEventHandler(async (event) => {
  const { messages } = await readBody(event)

  const result = streamText({
    model: gateway('anthropic/claude-sonnet-5'),
    maxOutputTokens: 10000,
    instructions: 'You are a helpful assistant.',
    messages: await convertToModelMessages(messages),
    tools: {
      deleteFile: tool({
        description: 'Delete a file from the project',
        inputSchema: z.object({ path: z.string() }),
        execute: async ({ path }) => ({ deleted: path })
      })
    },
    toolApproval: {
      deleteFile: 'user-approval'
    }
  })

  const stream = toUIMessageStream({ stream: result.stream })
  return createUIMessageStreamResponse({ stream })
})

Configuração do cliente

Use o composable useChat de @ai-sdk/vue para gerenciar o estado do chat e conectar-se ao endpoint do seu servidor:

<script setup lang="ts">
import { isReasoningUIPart, isTextUIPart, isToolUIPart, getToolName, lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai'
import { useChat } from '@ai-sdk/vue'
import { isPartStreaming, isToolStreaming } from '@nitro/ui/utils/ai'
import highlight from '@comark/nuxt/plugins/highlight'

const input = ref('')

const { messages, status, error, sendMessage, regenerate, stop, addToolApprovalResponse } = useChat({
  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
  onError(error) {
    console.error(error)
  }
})

function onSubmit() {
  sendMessage({ text: input.value })

  input.value = ''
}
</script>

<template>
  <NChatMessages
    :messages="messages"
    :status="status"
  >
    <template #content="{ message }">
      <template
        v-for="(part, index) in message.parts"
        :key="`${message.id}-${part.type}-${index}`"
      >
        <NChatReasoning
          v-if="isReasoningUIPart(part)"
          :text="part.text"
          :streaming="isPartStreaming(part)"
        >
          <Comark
            :markdown="part.text"
            :streaming="isPartStreaming(part)"
            :plugins="[highlight()]"
            class="*:first:mt-0 *:last:mb-0"
          />
        </NChatReasoning>

        <NChatTool
          v-else-if="isToolUIPart(part)"
          :text="getToolName(part)"
          :streaming="isToolStreaming(part)"
          :actions="part.state === 'approval-requested' ? [
            { label: 'Approve', onClick: () => addToolApprovalResponse({ id: part.approval.id, approved: true }) },
            { label: 'Deny', color: 'neutral', variant: 'ghost', onClick: () => addToolApprovalResponse({ id: part.approval.id, approved: false }) }
          ] : undefined"
        />

        <template v-else-if="isTextUIPart(part)">
          <Comark
            v-if="message.role === 'assistant'"
            :markdown="part.text"
            :streaming="isPartStreaming(part)"
            :plugins="[highlight()]"
            class="*:first:mt-0 *:last:mb-0"
          />
          <p v-else-if="message.role === 'user'" class="whitespace-pre-wrap">
            {{ part.text }}
          </p>
        </template>
      </template>
    </template>
  </NChatMessages>

  <NChatPrompt
    v-model="input"
    :error="error"
    @submit="onSubmit"
  >
    <NChatPromptSubmit
      :status="status"
      @stop="stop()"
      @reload="regenerate()"
    />
  </NChatPrompt>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { isReasoningUIPart, isTextUIPart, isToolUIPart, getToolName, lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai'
import { useChat } from '@ai-sdk/vue'
import { isPartStreaming, isToolStreaming } from '@nitro/ui/utils/ai'
import { Comark } from '@comark/vue'
import highlight from '@comark/vue/plugins/highlight'

const input = ref('')

const { messages, status, error, sendMessage, regenerate, stop, addToolApprovalResponse } = useChat({
  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
  onError(error) {
    console.error(error)
  }
})

function onSubmit() {
  sendMessage({ text: input.value })

  input.value = ''
}
</script>

<template>
  <NChatMessages
    :messages="messages"
    :status="status"
  >
    <template #content="{ message }">
      <template
        v-for="(part, index) in message.parts"
        :key="`${message.id}-${part.type}-${index}`"
      >
        <NChatReasoning
          v-if="isReasoningUIPart(part)"
          :text="part.text"
          :streaming="isPartStreaming(part)"
        >
          <Comark
            :markdown="part.text"
            :streaming="isPartStreaming(part)"
            :plugins="[highlight()]"
            class="*:first:mt-0 *:last:mb-0"
          />
        </NChatReasoning>

        <NChatTool
          v-else-if="isToolUIPart(part)"
          :text="getToolName(part)"
          :streaming="isToolStreaming(part)"
          :actions="part.state === 'approval-requested' ? [
            { label: 'Approve', onClick: () => addToolApprovalResponse({ id: part.approval.id, approved: true }) },
            { label: 'Deny', color: 'neutral', variant: 'ghost', onClick: () => addToolApprovalResponse({ id: part.approval.id, approved: false }) }
          ] : undefined"
        />

        <template v-else-if="isTextUIPart(part)">
          <Comark
            v-if="message.role === 'assistant'"
            :markdown="part.text"
            :streaming="isPartStreaming(part)"
            :plugins="[highlight()]"
            class="*:first:mt-0 *:last:mb-0"
          />
          <p v-else-if="message.role === 'user'" class="whitespace-pre-wrap">
            {{ part.text }}
          </p>
        </template>
      </template>
    </template>
  </NChatMessages>

  <NChatPrompt
    v-model="input"
    :error="error"
    @submit="onSubmit"
  >
    <NChatPromptSubmit
      :status="status"
      @stop="stop()"
      @reload="regenerate()"
    />
  </NChatPrompt>
</template>
Para uma configuração reutilizável do Comark (plugins, class, etc.), use defineComarkComponent para criar um componente personalizado em vez de passar as props inline a cada vez.
components/chat/Comark.ts
import highlight from '@comark/nuxt/plugins/highlight'

export default defineComarkComponent({
  name: 'ChatComark',
  plugins: [highlight()],
  class: '*:first:mt-0 *:last:mb-0'
})
components/chat/Comark.ts
import { defineComarkComponent } from '@comark/vue'
import highlight from '@comark/vue/plugins/highlight'

export default defineComarkComponent({
  name: 'ChatComark',
  plugins: [highlight()],
  class: '*:first:mt-0 *:last:mb-0'
})
Ao usar o plugin highlight, adicione o seguinte CSS à sua folha de estilo para dar suporte ao modo escuro:
main.css
html.dark .shiki span {
  color: var(--shiki-dark) !important;
  background-color: var(--shiki-dark-bg) !important;
  font-style: var(--shiki-dark-font-style) !important;
  font-weight: var(--shiki-dark-font-weight) !important;
  text-decoration: var(--shiki-dark-text-decoration) !important;
}