Vercel AI SDK 6 : AI SDK UI – 生成型ユーザインターフェイス

生成型ユーザインターフェイス (生成型 UI) は、大規模言語モデル (LLM) がテキストの枠をこえて「UI を生成する」ことを可能にするプロセスです。
生成型 UI は、ツール呼び出しの結果を React コンポーネントに接続するプロセスで、ユーザにより魅力的で AI ネイティブなエクスペリエンスを提供します。

Vercel AI SDK 6 : AI SDK UI – 生成型ユーザインターフェイス

作成 : Masashi Okumura (@classcat.com)
作成日時 : 02/20/2026
バージョン : ai@6.0.94

* 本記事は ai-sdk.dev/docs の以下のページを参考にしています :

* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

 

Vercel AI SDK 6.x : AI SDK UI – 生成型ユーザインターフェイス

生成型 (Generative) ユーザインターフェイス (生成型 UI) は、大規模言語モデル (LLM) がテキストの枠をこえて「UI を生成する」ことを可能にするプロセスです。これは、ユーザにより魅力的で AI ネイティブなエクスペリエンスを提供します。

生成型 UI のコアはツールで、これはある場所の天気を取得するような専門タスクを遂行するためにモデルに提供される関数です。モデルは会話のコンテキストに基づいて、これらのツールをいつどのように使用するか決定できます。

生成型 UI は、ツール呼び出しの結果を React コンポーネントに接続するプロセスです。その仕組みは :

  1. モデルに、プロンプトや会話履歴をツールのセットとともに提供します。

  2. コンテキストに基づいて、モデルはツールを呼び出すか決定します。

  3. ツールが呼び出されると、それは実行されてデータが返されます。

  4. このデータは、レンダリングのために React コンポーネントに渡されます。

ツールの結果を React コンポーネントに渡すことで、魅力的でニーズに適応性の高い生成型 UI エクスペリエンスを作成できます。

 

生成型 UI チャット・インターフェイスの構築

テキストベースの会話を処理し、モデルの応答に基づいて動的に UI 要素を組み込むチャットインターフェイスを作成しましょう。

 

基本的なチャットの実装

useChat フックを使用して基本的なチャット実装から始めましょう :

app/page.tsx

'use client';

import { useChat } from '@ai-sdk/react';
import { useState } from 'react';

export default function Page() {
  const [input, setInput] = useState('');
  const { messages, sendMessage } = useChat();

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    sendMessage({ text: input });
    setInput('');
  };

  return (
    <div>
      {messages.map(message => (
        <div key={message.id}>
          <div>{message.role === 'user' ? 'User: ' : 'AI: '}</div>
          <div>
            {message.parts.map((part, index) => {
              if (part.type === 'text') {
                return {part.text};
              }
              return null;
            })}
          </div>
        </div>
      ))}

      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={e => setInput(e.target.value)}
          placeholder="Type a message..."
        />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

チャットリクエストとモデル応答を処理するため、API ルートをセットアップします :

Provider app/api/chat/route.ts

import { streamText, convertToModelMessages, UIMessage, stepCountIs } from 'ai';
import { openai } from "@ai-sdk/openai";

export async function POST(request: Request) {
  const { messages }: { messages: UIMessage[] } = await request.json();

  const result = streamText({
    model: openai("gpt-4o-mini"),
    system: 'You are a friendly assistant!',
    messages: await convertToModelMessages(messages),
    stopWhen: stepCountIs(5),
  });

  return result.toUIMessageStreamResponse();
}

この API ルートは streamText 関数を使用して、チャットメッセージを処理し、モデルのレスポンスをクライアントに返すためにストリーミングします。

 

ツールの作成

チャットインターフェイスを動的 UI 要素で拡張する前に、ツールと対応する React コンポーネントを作成する必要があります。ツールは、天気情報の取得のような、特定のアクションをモデルが遂行できるようにします。

以下の内容で、ai/tools.ts という名前の新しいファイルを作成します :

ai/tools.ts

import { tool as createTool } from 'ai';
import { z } from 'zod';

export const weatherTool = createTool({
  description: 'Display the weather for a location',
  inputSchema: z.object({
    location: z.string().describe('The location to get the weather for'),
  }),
  execute: async function ({ location }) {
    await new Promise(resolve => setTimeout(resolve, 2000));
    return { weather: 'Sunny', temperature: 75, location };
  },
});

export const tools = {
  displayWeather: weatherTool,
};

このファイルでは、weatherTool という名前のツールを作成しました。ツールは指定された場所の天気情報の取得をシミュレートします。このツールは、2 秒の遅延後にシミュレートされたデータを返します。実際のアプリケーションでは、このシミュレーションを気象サービスへの実際の API 呼び出しに置き換えます。

 

API ルートの更新

API ルートを更新して、定義したツールを含めます :

Provider app/api/chat/route.ts

import { streamText, convertToModelMessages, UIMessage, stepCountIs } from 'ai';
import { openai } from "@ai-sdk/openai";
import { tools } from '@/ai/tools';

export async function POST(request: Request) {
  const { messages }: { messages: UIMessage[] } = await request.json();

  const result = streamText({
    model: openai("gpt-4o-mini"),
    system: 'You are a friendly assistant!',
    messages: await convertToModelMessages(messages),
    stopWhen: stepCountIs(5),
    tools,
  });

  return result.toUIMessageStreamResponse();
}

ツールを定義してそれを streamText 呼び出しに追加したので、それが返す天気情報を表示するような React コンポーネントを構築しましょう。

 

UI コンポーネントの作成

components/weather.tsx という名前の新しいファイルを作成します :

components/weather.tsx

type WeatherProps = {
  temperature: number;
  weather: string;
  location: string;
};

export const Weather = ({ temperature, weather, location }: WeatherProps) => {
  return (
    <div>
      <h2>Current Weather for {location}</h2>
      <p>Condition: {weather}</p>
      <p>Temperature: {temperature}°C</p>
    </div>
  );
};

このコンポーネントは指定された場所の天気情報を表示します。それは 3 つの props: temperature, weather, location (まさに weatherTool が返すもの) を受け取ります。

 

天気コンポーネントのレンダリング

ツールと対応する React コンポーネントを作成したので、それらをチャットインターフェイスに統合しましょう。モデルが天気ツールを呼び出したとき、Weather コンポーネントをレンダリングします。

モデルがツールを呼び出したかどうか確認するには、ツール固有パーツに対する UIMessage オブジェクトの parts 配列を確認できます。AI SDK 5.0 では、ツールパーツは汎用型ではなく、型付きの命名規則: tool-${toolName} を使用します。

page.tsx ファイルを更新します。

app/page.tsx

'use client';

import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
import { Weather } from '@/components/weather';

export default function Page() {
  const [input, setInput] = useState('');
  const { messages, sendMessage } = useChat();

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    sendMessage({ text: input });
    setInput('');
  };

  return (
    <div>
      {messages.map(message => (
        <div key={message.id}>
          <div>{message.role === 'user' ? 'User: ' : 'AI: '}</div>
          <div>
            {message.parts.map((part, index) => {
              if (part.type === 'text') {
                return {part.text};
              }

              if (part.type === 'tool-displayWeather') {
                switch (part.state) {
                  case 'input-available':
                    return <div key={index}>Loading weather...</div>;
                  case 'output-available':
                    return (
                      <div key={index}>
                        <Weather {...part.output} />
                      </div>
                    );
                  case 'output-error':
                    return <div key={index}>Error: {part.errorText}</div>;
                  default:
                    return null;
                }
              }

              return null;
            })}
          </div>
        </div>
      ))}

      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={e => setInput(e.target.value)}
          placeholder="Type a message..."
        />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

この更新されたコードスニペットでは :

  1. 組み込みの input と handleInputChange の代わりに、useState を使用して手動で入力状態を管理します。

  2. handleSubmit の代わりに sendMessage を使用してメッセージを送信します。

  3. 異なるコンテンツ型を各メッセージの parts 配列で確認します。

  4. tool-displayWeather 型のツールパーツとそれらの異なる状態 (input-available, output-available, output-error) を処理します。

このアプローチは、モデルのレスポンスに基づいて UI コンポーネントを動的にレンダリングすることを可能にし、よりインタラクティブでコンテキストに応じたチャットエクスペリエンスを実現できます。

 

Generative UI アプリケーションの拡張

より多くのツールやコンポーネントを追加することでチャットアプリケーションを拡張し、よりリッチで多用途なユーザエクスペリエンスを実現できます。アプリケーションを拡張する方法は以下のとおりです :

 

より多くのツールの追加

より多くのツールを追加するには、ai/tools.ts ファイルにそれらを定義するだけです :

// Add a new stock tool
export const stockTool = createTool({
  description: 'Get price for a stock',
  inputSchema: z.object({
    symbol: z.string().describe('The stock symbol to get the price for'),
  }),
  execute: async function ({ symbol }) {
    // Simulated API call
    await new Promise(resolve => setTimeout(resolve, 2000));
    return { symbol, price: 100 };
  },
});

// Update the tools object
export const tools = {
  displayWeather: weatherTool,
  getStockPrice: stockTool,
};

次に、components/stock.tsx という名前の新しいファイルを作成します :

type StockProps = {
  price: number;
  symbol: string;
};

export const Stock = ({ price, symbol }: StockProps) => {
  return (
    <div>
      <h2>Stock Information</h2>
      <p>Symbol: {symbol}</p>
      <p>Price: ${price}</p>
    </div>
  );
};

最後に、page.tsx ファイルを更新して、新しい Stock コンポーネントを含めます :

'use client';

import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
import { Weather } from '@/components/weather';
import { Stock } from '@/components/stock';

export default function Page() {
  const [input, setInput] = useState('');
  const { messages, sendMessage } = useChat();

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    sendMessage({ text: input });
    setInput('');
  };

  return (
    <div>
      {messages.map(message => (
        <div key={message.id}>
          <div>{message.role}</div>
          <div>
            {message.parts.map((part, index) => {
              if (part.type === 'text') {
                return <span key={index}>{part.text}</span>;
              }

              if (part.type === 'tool-displayWeather') {
                switch (part.state) {
                  case 'input-available':
                    return <div key={index}>Loading weather...</div>;
                  case 'output-available':
                    return (
                      <div key={index}>
                        <Weather {...part.output} />
                      </div>
                    );
                  case 'output-error':
                    return <div key={index}>Error: {part.errorText}</div>;
                  default:
                    return null;
                }
              }

              if (part.type === 'tool-getStockPrice') {
                switch (part.state) {
                  case 'input-available':
                    return <div key={index}>Loading stock price...</div>;
                  case 'output-available':
                    return (
                      <div key={index}>
                        <Stock {...part.output} />
                      </div>
                    );
                  case 'output-error':
                    return <div key={index}>Error: {part.errorText}</div>;
                  default:
                    return null;
                }
              }

              return null;
            })}
          </div>
        </div>
      ))}

      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={input}
          onChange={e => setInput(e.target.value)}
        />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

By following this pattern, you can continue to add more tools and components, expanding the capabilities of your Generative UI application.

 

以上