Chat
Nuxt UIは、AIを活用したチャットインターフェースを構築するために設計された一連のコンポーネントを提供します。これらは Vercel AI SDK とシームレスに統合し、応答のストリーミング、推論、ツールコールなどを行います。
コンポーネント
| コンポーネント | 説明 |
|---|---|
| チャットメッセージ | 自動スクロールとロードインジケータ付きのスクロール可能なメッセージリスト。 |
| チャットメッセージ | アバター、アクション、スロットを含む個々のメッセージバブル。 |
| チャットプロンプト | プロンプト送信用のテキストエリアを強化。 |
| チャットプロンプト投稿 | 自動ステータス処理付き送信ボタン。 |
| ChatReasoning | AIの推論/思考プロセスのための折りたたみブロック。 |
| ChatTool | AIツールの呼び出しステータスの折りたたみブロック。 |
| ChatShimmer | ストリーミング状態のためのテキストシマーアニメーション。 |
| ChatPalette | モーダルや引き出しにチャットを埋め込むためのレイアウトラッパー。 |
インストール
Chatコンポーネントは Vercel AI SDK Chatで使用するように設計されています。このページの例はAI SDK v7を対象としています。
必要な依存関係をインストールする:
pnpm add ai @ai-sdk/gateway @ai-sdk/vue @comark/nuxt
yarn add ai @ai-sdk/gateway @ai-sdk/vue @comark/nuxt
npm install ai @ai-sdk/gateway @ai-sdk/vue @comark/nuxt
bun add ai @ai-sdk/gateway @ai-sdk/vue @comark/nuxt
モジュールに@comark/nuxtを追加します。
export default defineNuxtConfig({
modules: [
'@nuxt/ui',
'@comark/nuxt'
]
})
@comark/nuxtMarkdownコンポーネントを提供します。従来のMarkdownレンダラーが引き起こすちらつきや再解析を回避します。また、Nuxt UIの proseコンポーネントなので、コンテンツはテーマに合わせてスタイリングされます。pnpm add ai @ai-sdk/gateway @ai-sdk/vue @comark/vue
yarn add ai @ai-sdk/gateway @ai-sdk/vue @comark/vue
npm install ai @ai-sdk/gateway @ai-sdk/vue @comark/vue
bun add ai @ai-sdk/gateway @ai-sdk/vue @comark/vue
@comark/vue)Nuxt UIの proseコンポーネントをComarkで使用するには、
vite.config.tsでproseオプションを有効にします。import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
prose: true
})
]
})
サーバー設定
streamTextを使用して、チャット要求を処理するサーバー APIエンドポイントを作成します。 Vercel AIゲートウェイを使用して、集中型エンドポイントを介してAIモデルにアクセスできます。
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 })
})
推論
reasoning を有効にするには、プロバイダのproviderOptionsを設定します。 Anthropic )
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 })
})
ウェブ検索
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_20260209({})
}
})
const stream = toUIMessageStream({ stream: result.stream })
return createUIMessageStreamResponse({ stream })
})
import { streamText, convertToModelMessages, toUIMessageStream, createUIMessageStreamResponse } from 'ai'
import { google } from '@ai-sdk/google'
import { gateway } from '@ai-sdk/gateway'
export default defineEventHandler(async (event) => {
const { messages } = await readBody(event)
const result = streamText({
model: gateway('google/gemini-3-flash'),
instructions: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages),
tools: {
google_search: google.tools.googleSearch({})
}
})
const stream = toUIMessageStream({ stream: result.stream })
return createUIMessageStreamResponse({ stream })
})
import { streamText, convertToModelMessages, toUIMessageStream, createUIMessageStreamResponse } from 'ai'
import { openai } from '@ai-sdk/openai'
import { gateway } from '@ai-sdk/gateway'
export default defineEventHandler(async (event) => {
const { messages } = await readBody(event)
const result = streamText({
model: gateway('openai/gpt-5-nano'),
instructions: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages),
tools: {
web_search: openai.tools.webSearch({})
}
})
const stream = toUIMessageStream({ stream: result.stream })
return createUIMessageStreamResponse({ stream })
})
MCPクライアント
Model Context Protocol MCP @ai-sdk/mcpから使用して、チャットボットに高度なツール呼び出し機能を提供します。MCPを使用すると、ドキュメントの検索やカスタムタスクの実行など、AIが動的なアクションを実行して、より関連性の高い正確な応答を提供できます。
開始するには、MCPパッケージをインストールします。
npm install @ai-sdk/mcp
pnpm add @ai-sdk/mcp
yarn add @ai-sdk/mcp
次に、MCPツールを使用するようにサーバーエンドポイントを設定します。
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
}
})
ツール承認
toolApprovalオプションを指定してツールを実行する前にユーザー確認を必要とします。ユーザーが応答するまで、ツール部分はapproval-requested状態で一時停止します。
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 })
})
クライアント設定
@ai-sdk/vueから構成可能なuseChatを使用して、チャット状態を管理し、サーバーエンドポイントに接続します。
<script setup lang="ts">
import { isReasoningUIPart, isTextUIPart, isToolUIPart, getToolName, lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai'
import { useChat } from '@ai-sdk/vue'
import { isPartStreaming, isToolStreaming } from '@nuxt/ui/utils/ai'
import shiki from '@comark/nuxt/plugins/shiki'
const plugins = [shiki()]
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>
<UChatMessages
:messages="messages"
:status="status"
>
<template #content="{ message }">
<template
v-for="(part, index) in message.parts"
:key="`${message.id}-${part.type}-${index}`"
>
<UChatReasoning
v-if="isReasoningUIPart(part)"
:text="part.text"
:streaming="isPartStreaming(part)"
>
<Markdown
:value="part.text"
:streaming="isPartStreaming(part)"
:plugins="plugins"
class="*:first:mt-0 *:last:mb-0"
/>
</UChatReasoning>
<UChatTool
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)">
<Markdown
v-if="message.role === 'assistant'"
:value="part.text"
:streaming="isPartStreaming(part)"
:plugins="plugins"
class="*:first:mt-0 *:last:mb-0"
/>
<p v-else-if="message.role === 'user'" class="whitespace-pre-wrap">
{{ part.text }}
</p>
</template>
</template>
</template>
</UChatMessages>
<UChatPrompt
v-model="input"
:error="error"
@submit="onSubmit"
>
<UChatPromptSubmit
:status="status"
@stop="stop()"
@reload="regenerate()"
/>
</UChatPrompt>
</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 '@nuxt/ui/utils/ai'
import { Markdown } from '@comark/vue'
import shiki from '@comark/vue/plugins/shiki'
const plugins = [shiki()]
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>
<UChatMessages
:messages="messages"
:status="status"
>
<template #content="{ message }">
<template
v-for="(part, index) in message.parts"
:key="`${message.id}-${part.type}-${index}`"
>
<UChatReasoning
v-if="isReasoningUIPart(part)"
:text="part.text"
:streaming="isPartStreaming(part)"
>
<Markdown
:value="part.text"
:streaming="isPartStreaming(part)"
:plugins="plugins"
class="*:first:mt-0 *:last:mb-0"
/>
</UChatReasoning>
<UChatTool
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)">
<Markdown
v-if="message.role === 'assistant'"
:value="part.text"
:streaming="isPartStreaming(part)"
:plugins="plugins"
class="*:first:mt-0 *:last:mb-0"
/>
<p v-else-if="message.role === 'user'" class="whitespace-pre-wrap">
{{ part.text }}
</p>
</template>
</template>
</template>
</UChatMessages>
<UChatPrompt
v-model="input"
:error="error"
@submit="onSubmit"
>
<UChatPromptSubmit
:status="status"
@stop="stop()"
@reload="regenerate()"
/>
</UChatPrompt>
</template>
defineMarkdownComponentを使用して、毎回propsをインラインで渡す代わりにカスタムコンポーネントを作成します。import shiki from '@comark/nuxt/plugins/shiki'
export default defineMarkdownComponent({
name: 'ChatMarkdown',
plugins: [shiki()],
class: '*:first:mt-0 *:last:mb-0'
})
import { defineMarkdownComponent } from '@comark/vue'
import shiki from '@comark/vue/plugins/shiki'
export default defineMarkdownComponent({
name: 'ChatMarkdown',
plugins: [shiki()],
class: '*:first:mt-0 *:last:mb-0'
})
shikiプラグインを使用する場合は、ダークモードをサポートするためにスタイルシートに次の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;
}