ai-ui-patterns について
ai-ui-patternsは、ViteとNext.jsの実装例を含む、AIインターフェース構築のためのReact開発者向けガイドです。会話状態やストリーミング応答、エラー処理のパターンに加え、Vercel AI SDKと再利用可能なコンポーネントを扱います。
主な機能
- 01プロンプトと会話状態の管理
- 02AI応答のストリーミング表示
- 03入力のデバウンス処理
- 04エラー処理と再試行
- 05再利用可能なUIコンポーネントの作成
- 06ViteとNext.jsの構成比較
活用シーン
- 01Reactアプリのチャット画面構築
- 02リアルタイムAIオートコンプリートの実装
- 03サーバーからのAI APIの安全な呼び出し
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ patternsdev/ skills --skill ai-ui-patternsインストール後、Claude Code で/ai-ui-patternsを入力して読み込まれたか確認します。
詳細
AI UI パターン
目次
AI を活用したインターフェースを構築するには、チャットボットからインテリジェントなアシスタントまで、バックエンドの AI サービスとリアクティブな UI コンポーネントを慎重に統合する必要があります。この章では、このようなインターフェースの React におけるデザインパターンを、2 つの実装に焦点を当てて解説します。Vite を使う通常の React アプリと、Next.js アプリです。AI エンジンには Vercel AI SDK 経由で OpenAI の API を使用し、スタイリングには TailwindCSS を使用します。主なトピックとして、プロンプト管理、ストリーミングレスポンス、入力のデバウンス、エラー処理、そしてこれらのパターンが Vite と Next.js でどのように異なるかを取り上げます。また、洗練されたチャット UI を構築するための再利用可能なコンポーネントパターンと、**Vercel の AI UI コンポーネント(AI Elements)**についても紹介します。
使用する場面
- LLM からのレスポンスをストリーミングする会話型 AI インターフェースを構築する場合に使用します
- OpenAI、Anthropic、その他の AI プロバイダーを React アプリケーションに統合する際に役立ちます
- プロンプト管理、ストリーミング、エラー処理、AI 固有の UI のパターンが必要な場合に使用します
手順
- Vercel AI SDK の
useChatフックを使用して、会話の状態とストリーミングレスポンスを管理します - API キーはサーバー上に保持します。AI 呼び出しには、Next.js API ルートまたは別のバックエンドを使用します
- チャットインターフェースでリアルタイムのレスポンスを実現するため、ストリーミング(
stream: true)を有効にします - オートコンプリート機能では入力をデバウンスし、チャットではレスポンスのストリーミング中に入力を無効にします
- データ取得ロジックから切り離した、再利用可能なコンポーネント(ChatMessage、InputBox)を構築します
詳細
**注:**この記事では例として OpenAI を使用していますが、Vercel AI SDK は Gemini、OpenAI、Anthropic など、複数のモデルプロバイダーをサポートしています。SDK の統一されたインターフェースを通じて、プロバイダーを簡単に切り替えられます。ここではデモのために、選択肢の 1 つとして OpenAI を使用しています。
はじめに:React の AI インターフェース
ChatGPT のような LLM の普及に伴い、AI 主導のユーザーインターフェース(UI)が広く使われるようになりました。従来の UI とは異なり、AI インターフェースでは会話型のインタラクション、動的なコンテンツのストリーミング、非同期のバックエンド呼び出しがよく発生します。これにより、React 開発者には固有の課題やパターンが生まれます。一般的な AI チャットインターフェースは、ユーザー入力とレスポンスの表示を担うフロントエンドと、AI モデルを呼び出すバックエンドで構成されます。セキュリティとパフォーマンスの観点から、API キーや負荷の高い処理をクライアントから切り離すには、バックエンドが不可欠です。Vercel の AI SDK などのツールを使うと、プロバイダー(OpenAI、HuggingFace など)への接続や、レスポンスのリアルタイムストリーミングが容易になります。これらの課題に対処するための Next.js アプリと Vite(React)アプリのセットアップ方法を解説し、どちらにも適用できるベストプラクティスについて説明します。
取り上げる主なパターン:
- AI プロンプトデータの構成と会話状態の管理
- リアルタイムのフィードバックを実現する、AI レスポンスの UI へのストリーミング
- API への過剰なリクエストを避けるためのユーザー入力のデバウンス
- UX におけるエラー処理とフォールバック
- メッセージや入力欄などの再利用可能な UI コンポーネント(TailwindCSS を使用)
- アーキテクチャの違い:Next.js のルートハンドラーと、Node バックエンドを使う Vite
この章を読み終える頃には、Next.js と Vite のどちらを選んでも、レスポンスが速く堅牢な AI 搭載型 React UI を構築できるようになります。
プロジェクトのセットアップとツール
コードに取りかかる前に、必要なパッケージと設定が揃っていることを確認してください。
-
React & Vite: Vite + React プロジェクトを初期化します(例:
npm create vite@latest my-ai-app -- --template react)。Next.js の場合は、npx create-next-appまたは Next 13 App Router テンプレートを使用できます。どちらも利用できます。違いについては、順を追って説明します。 -
TailwindCSS: 手早くスタイルを設定できるよう、プロジェクトに Tailwind をセットアップします。
-
OpenAI API & Vercel AI SDK: OpenAI のライブラリまたは Vercel AI SDK をインストールします。ここでは、便利な React フック(
useChat、useCompletion)とサーバーユーティリティを提供する Vercel の AI SDK(npm i ai)を使用します。この SDK はフレームワークに依存せず、Next.js、vanilla React、Svelte などで動作します。ストリーミングと状態管理を簡素化でき、無料で利用できるオープンソースです。 -
API キー: OpenAI ダッシュボードから OpenAI API キーを取得し、安全に保管します。Next.js では、
.env.localに記述し(例:OPENAI_API_KEY=sk-...)、絶対にコミットしないでください。Vite アプリでは、クライアントコードにキーを公開しないでください。代わりに、バックエンドのプロキシまたはサーバー上の環境変数を使用します。
AI エンドポイントのセットアップ(Next.js と Vite)
Next.js の実装: Next.js では、サーバーレス関数としてルートハンドラーを作成できます。React フロントエンドが AI の応答を取得するために呼び出す API ルートを定義できます。
// app/api/chat/route.ts (Next.js)
import { Configuration, OpenAIApi } from 'openai-edge';
import { OpenAIStream, StreamingTextResponse } from 'ai';
export const runtime = 'edge';
const config = new Configuration({ apiKey: process.env.OPENAI_API_KEY });
const openai = new OpenAIApi(config);
export async function POST(req: Request) {
const { messages } = await req.json();
const response = await openai.createChatCompletion({
model: 'gpt-3.5-turbo',
stream: true,
messages: messages.map((m: any) => ({ role: m.role, content: m.content }))
});
const stream = OpenAIStream(response);
return new StreamingTextResponse(stream);
}
このハンドラーでは、メッセージの配列(チャット履歴)を含む JSON ボディを受け取ります。stream: true を指定して OpenAI のチャット補完を呼び出し、ストリーミング応答を取得します。次に、AI SDK が提供する StreamingTextResponse で応答をラップし、チャンク単位でクライアントに返します。Next.js の API ルートを使うことで、API キーをサーバー上に保持し、効率よくデータをストリーミングできます。
Vite(React)の実装: Vite アプリにはサーバーが組み込まれていないため、OpenAI の呼び出し用に独自のバックエンドを作成する必要があります。シンプルな Node/Express サーバーを利用できます。
// backend/server.js (Node/Express for Vite app)
import express from 'express';
import { Configuration, OpenAIApi } from 'openai';
const app = express();
app.use(express.json());
const config = new Configuration({ apiKey: process.env.OPENAI_API_KEY });
const openai = new OpenAIApi(config);
app.post('/api/chat', async (req, res) => {
try {
const { messages = [] } = req.body;
const systemMsg = { role: 'system', content: 'You are a helpful assistant.' };
const inputMessages = [systemMsg, ...messages];
const response = await openai.createChatCompletion({
model: 'gpt-3.5-turbo',
stream: false,
messages: inputMessages
});
const content = response.data.choices[0].message?.content;
res.json({ content });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Internal Server Error' });
}
});
app.listen(6000, () => console.log('API server listening on http://localhost:6000'));
開発中は、Vite の開発サーバーで /api の呼び出しをこのバックエンドにプロキシするよう設定できます(例:vite.config.js で server.proxy['/api'] = 'http://localhost:6000' を設定します)。重要なのは、React アプリが相対パスの /api/chat エンドポイントを呼び出し、プロキシまたはホスティング環境がそのリクエストをサーバーコードに振り分けることです。これにより、OpenAI のキーを非公開に保てます。
Node でのストリーミングの有効化: 上記の Express の例では、簡潔さのため stream: false とし、完了後に応答全体を返しています。Node でストリーミングするには、OpenAI の HTTP ストリームを使用します。stream: true を設定し、応答をデータストリームとして処理します。そのためには、response.data ストリームを読み取り、res.write() でチャンクをクライアントに送信します。ストリーミングなしで応答全体を返す方法を選んでも、UI のパターンはおおむね同じように適用できますが、ストリーミングによって UX は大幅に向上します。
プロンプトの処理と会話状態
あらゆる AI インターフェースの中核となるのは、プロンプト管理です。ユーザー入力(およびコンテキスト)を組み立てて、AI モデルに渡すプロンプトまたはメッセージのシーケンスを作成します。チャットのシナリオでは、ロールとコンテンツを持つメッセージのリストを保持します。OpenAI の Chat API では、メッセージを { role: 'user' | 'assistant' | 'system', content: string } という形式で指定します。通常、最初にシステムメッセージ(アシスタントの動作やコンテキストを設定するもの)を置き、会話が進むにつれてユーザーメッセージとアシスタントメッセージを交互に追加します。
React での状態管理: コンポーネントの状態に会話を保存できます。Vercel SDK の React フックを使うと、次のようになります。
import { useChat } from 'ai/react';
function ChatInterface() {
const { messages, input, handleInputChange, handleSubmit } = useChat();
// ...
}
useChat フックは多くの処理を担います。メッセージオブジェクトの配列である messages の状態と、現在のテキスト入力を保持する input の状態を管理し、handleInputChange と handleSubmit というヘルパーを提供します。デフォルトでは、useChat() は送信時に /api/chat へ POST リクエストを送ります。
手動での状態管理: useChat を使用しない場合は、useState やコンテキストで状態を管理できます。フォームの送信時に API を呼び出し、ユーザーのクエリとアシスタントの応答を追加してメッセージ配列を更新します。
システムプロンプトとコンテキスト: よく使われるパターンは、アシスタントの役割やナレッジベースを説明する初期システムメッセージを含めることです。たとえば、ドキュメントのヘルパーを構築する場合、システムメッセージの内容は「あなたはドキュメントアシスタントです。ドキュメントの例を使って回答してください。」のようになります。
単一ターンと複数ターン: 会話の記憶を必要としない単一の質問応答インターフェースであれば、代わりに Vercel SDK の useCompletion フックを使用できます。チャットボットや複数ターンの対話には、各リクエストでメッセージ履歴を保持して送信する useChat が一般的なパターンです。
AI の応答を UI にストリーミングする
最新の AI UI を特徴づけるものの一つが、ストリーミング出力です。AI がトークンを生成すると、ユーザーには返信がリアルタイムで表示されます。モデルが生成する回答は長かったり、時間がかかったりするため、UX の向上に欠かせません。何秒も無言で待つ代わりに、ストリーミングを使えば部分的な結果をすぐに表示できます。
ストリーミングの仕組み: OpenAI API で stream: true を有効にすると、応答は単一の JSON オブジェクトではなく、一連のチャンク(データイベント)として送信されます。Vercel AI SDK を使うと、これらのチャンクを簡単に処理できます。サーバーでは、応答をテキストストリーム(StreamingTextResponse)に変換しました。クライアント側では、useChat フックがこのストリームの読み取りを処理し、新しいテキストが届くたびにメッセージの状態を逐次更新します。
SDK を使わずに React でストリーミングを手動で実装する場合は、次のようになります。
const res = await fetch('/api/chat', { method: 'POST', body: JSON.stringify({ messages }) });
const reader = res.body.getReader();
const decoder = new TextDecoder();
let partial = "";
while(true) {
const { value, done } = await reader.read();
if (done) break;
partial += decoder.decode(value);
setAssistantMessage(partial);
}
自動スクロール: ストリーミング時の UX で気をつけたい点の一つは、最新のメッセージが表示されるようにすることです。useEffect でメッセージ配列の長さを監視し、更新時にメッセージコンテナーを自動スクロールする方法があります。
部分的なレンダリングと完了: ストリーミング中は、点滅するカーソルや「AI が入力中…」のメッセージなど、視覚的なインジケーターを表示します。ストリームが終了したら、メッセージの表示を確定します。
入力処理とデバウンス
チャットでのやり取りでは、通常、ユーザーがフォームを送信したときにクエリを送信します。ただし、一部の AI アプリケーションでは、入力に継続的に反応させたい場合があります。たとえば、オートコンプリートの候補やAI によるリアルタイム検証などです。このような場合は、デバウンスが重要です。
デバウンスが必要な理由: キー入力のたびに OpenAI API を呼び出すのは、非常に非効率でコストもかかります。デバウンスを使うと、ユーザーがしばらく入力を止めてから API を呼び出すようにできます。
const [draft, setDraft] = useState("");
useEffect(() => {
if (!draft) return;
const timeout = setTimeout(() => {
getSuggestion(draft);
}, 500);
return () => clearTimeout(timeout);
}, [draft]);
明示的な「送信」操作があるシンプルなチャットボットでは、通常、デバウンスは必要ありません。ユーザーが Enter キーを押したときに送信します。ただし、AI の応答中に入力を無効にする、または複数回の送信を防ぐと便利です。
エラー処理と耐障害性
AI アプリケーションでは、堅牢なエラー処理が不可欠です。
-
API 呼び出しを try/catch で囲む: サーバー上で OpenAI の呼び出しを try/catch で囲みます。問題が発生した場合は、適切なエラーレスポンスを返します。
-
クライアント側のエラー状態: レスポンスがエラーを示している場合に対処します。
try {
await sendMessage({ text: input });
} catch (error) {
console.error("Failed to send message:", error);
}
-
ユーザーへのフィードバック: 問題が発生した場合は、必ずユーザーに知らせます。チャット内にエラーをインラインで表示します。たとえば、「申し訳ありません。問題が発生しました。もう一度お試しください。」という特別な「system」メッセージを表示します。
-
再試行の仕組み: 「もう一度試す」ボタンで、ユーザーが再試行できるようにすることを検討します。
-
検証エラー: API を呼び出す前にクライアント側で検証します。入力が空の場合は送信を無効にするか、長さの上限を超える入力を切り詰めます。
UI の構築:コンポーネントとスタイル設定のパターン
チャットメッセージコンポーネント: 単一のメッセージバブルを描画する ChatMessage コンポーネントを作成します。ロールに応じてスタイルを変えます。
function ChatMessage({ role, content }) {
const isUser = role === 'user';
return (
<div className={`flex ${isUser ? 'justify-end' : 'justify-start'} mb-2`}>
<div className={`max-w-xl px-4 py-2 rounded-lg ${
isUser ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-900'
}`}>
{content}
</div>
</div>
);
}
入力コンポーネント:
function InputBox({ value, onChange, onSubmit, disabled }) {
return (
<form onSubmit={onSubmit} className="flex gap-2">
<input
type="text"
value={value}
onChange={onChange}
disabled={disabled}
className="flex-1 border rounded px-3 py-2"
placeholder="Type your message..."
/>
<button type="submit" disabled={disabled} className="bg-blue-500 text-white px-4 py-2 rounded">
Send
</button>
</form>
);
}
コンポーネントの組み合わせ:
function ChatInterface() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();
return (
<div className="flex flex-col h-screen max-w-2xl mx-auto p-4">
<div className="flex-1 overflow-y-auto">
{messages.map((msg, i) => (
<ChatMessage key={i} role={msg.role} content={msg.content} />
))}
</div>
<InputBox
value={input}
onChange={handleInputChange}
onSubmit={handleSubmit}
disabled={isLoading}
/>
</div>
);
}
関心事を分離することで、UI パーツのテストや交換が簡単になります。ロジック(useChat)は表示コンポーネントから切り離されています。
Vercel AI Elements(チャット UI の事前構築済みコンポーネント)
Vercel の AI Elements ライブラリでは、AI チャットインターフェース専用に設計された、すぐに使える React コンポーネントが提供されています。
- Conversation:自動スクロール機能を備えた、メッセージ一覧を描画するコンテナーです。
- Prompt:チャットプロンプト向けに最適化された入力コンポーネントです。
- TypingIndicator:AI が「考え中」である場合や、レスポンスをストリーミング中である場合に表示されます。
- ErrorBoundary/ErrorMessage:エラーを適切に処理して表示します。
import { Conversation, Prompt, TypingIndicator } from '@vercel/ai-elements';
function ChatApp() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();
return (
<div className="h-screen flex flex-col">
<Conversation messages={messages} />
{isLoading && <TypingIndicator />}
<Prompt
value={input}
onChange={handleInputChange}
onSubmit={handleSubmit}
/>
</div>
);
}
すべてを組み合わせる
-
バックエンド API ルート: Next.js のルートハンドラーを使う場合も、別の Express サーバーを使う場合も、メッセージを受信して AI モデルを呼び出し、レスポンスをストリーミングで返すエンドポイントを作成します。
-
状態管理: Vercel AI SDK の
useChatフック(または独自にuseStateを使用した実装)で会話の状態を管理します。 -
ストリーミング: 応答性の高い UX を実現するため、サーバーとクライアントの両方でストリーミングを有効にします。
-
デバウンスとレート制限: オートコンプリートなどの機能では、API 呼び出しにデバウンスを適用します。チャットでは、レスポンスのストリーミング中に入力を無効にします。
-
エラー処理: API 呼び出しを try/catch で囲み、エラー発生時にユーザーへフィードバックを提供し、再試行の仕組みを検討します。
-
再利用可能なコンポーネント: データ取得ロジックから切り離された、表示用コンポーネント(
ChatMessage、InputBox)を構築します。本番環境向けにすぐ使えるコンポーネントとして、AI Elements の利用も検討します。 -
スタイル設定: TailwindCSS(またはお好みのスタイル設定手段)を使い、すっきりとしたレスポンシブなチャットインターフェースを作成します。
アーキテクチャの比較:Next.js と Vite
| 項目 | Next.js | Vite + Node Backend |
|---|---|---|
| API ルート | 組み込み(pages/api/ または app/api/) | 別途 Express/Node サーバーが必要 |
| ストリーミング | Edge Runtime でネイティブサポート | res.write() による手動実装 |
| デプロイ | Vercel(最適化済み)またはセルフホスト | フロントエンド(静的)とバックエンドを別々にデプロイ |
| 複雑さ | 低い(オールインワン) | 高い(2 つのコードベース) |
| 柔軟性 | フレームワークの規約 | 完全な制御 |
多くの AI チャットアプリケーションでは、統合された API ルートとストリーミングサポートにより、Next.js を使うと開発者エクスペリエンスがシンプルになります。ただし、既存の Vite/React アプリがある場合や、より多くの制御を望む場合は、ここで説明したパターンを別のバックエンドと組み合わせてうまく活用できます。