Prompt API で Vercel AI SDK UI と AI 要素を使用する

公開日: 2026 年 7 月 16 日

Vercel AI SDK で組み込みの Prompt API を使用するでは、generateText()streamText()、ハイブリッド コード、 Output.object() を使用した構造化出力という 4 つのコア生成 プリミティブについて説明しました。これらはすべて @browser-ai/core を基盤としています。今回は、よりインタラクティブなものを作成します。ブラウザで完全に実行されるフル ストリーミング チャット UI です。Prompt API が使用できない場合は、クラウドモデルに自動的にフォールバックします。

作成する内容

次の処理を行う React チャット インターフェース。

  • Vercel AI SDK の useChat フックを使用して、マルチターンの会話をストリーミングします。
  • バックエンド サーバーを必要とせずに、ブラウザ でモデルループを実行します。
  • Prompt API が使用できない場合は、Gemini 2.5 Flash に自動的にフォールバックします。
  • アシスタントの返信を GitHub フレーバーのマークダウンとしてレンダリングし、ストリーミング中に不完全なトークンを処理します。
  • 最初のトークンを待機している間、「思考中…」のきらめきを表示します。
  • 新しいメッセージに自動スクロールします。上にスクロールした場合は、一番下までスクロールするボタンが表示されます。

追加の依存関係

ai@browser-ai/core@ai-sdk/google に加えて、チャット UI には React、AI SDK の React バインディング、いくつかのマークダウン パッケージが必要です。

npm install react react-dom @ai-sdk/react
npm install react-markdown remark-gfm harden-react-markdown
npm install -D @types/react @types/react-dom

UI の場合は、Tailwind CSS、コンポーネント ユーティリティ、アイコン用の Lucide も追加します。

npm install -D tailwindcss postcss autoprefixer
npm install clsx tailwind-merge lucide-react

投票構成: マルチエントリ ビルド

プロジェクトにはすでに index.html があります。2 つ目のエントリ ポイントとして chat.html を追加し、両方をビルドするように Vite を構成します。

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';

export default defineConfig({
  plugins: [react()],
  resolve: { alias: { '@': resolve(__dirname, './src') } },
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'index.html'),
        chat: resolve(__dirname, 'chat.html'),
      },
    },
  },
});

chat.html は最小限で、<div id="root"> とスクリプトタグのみです。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Built-in AI Chatbot</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/chat.tsx"></script>
  </body>
</html>

モデルの自動選択

チャットボットでは、アプリが自動的に選択を行います。まず組み込みモデルを試して、Prompt API が使用できない場合はクラウドにフォールバックします。

これは、React がマウントされる前のモジュールの読み込み時に行われるため、ユーザーが最初のメッセージを入力するまでにエージェントの準備が整います。

const agentPromise: Promise<ToolLoopAgent> = (async () => {
  const builtIn = browserAI();
  let model: any = builtIn;

  if (typeof builtIn.availability === 'function') {
    const availability = await builtIn.availability();
    if (availability === 'unavailable') {
      const { createGoogleGenerativeAI } = await import('@ai-sdk/google');
      model = createGoogleGenerativeAI({ apiKey })('gemini-2.5-flash');
    } else if (availability === 'downloadable') {
      await builtIn.createSessionWithProgress(() => {});
    }
  }

  return new ToolLoopAgent({ model, instructions: 'You are a helpful assistant.' });
})();

新しい要素は ToolLoopAgent です。これは、任意のモデル上でマルチターンの会話ループを管理する AI SDK の抽象化です。モデルとシステム プロンプトを受け取り、内部でやり取りを処理します。

エージェントを useChat に接続する

@ai-sdk/react パッケージの useChat フックは通常、HTTP エンドポイントと通信します。ブラウザ側の推論には、代わりに DirectChatTransport を使用します。サーバーを使用せずに、ブラウザで ToolLoopAgent ループを完全に実行します。

const transport = useMemo(() => new DirectChatTransport({ agent }), [agent]);
const { messages, sendMessage, status, stop } = useChat({ transport });

DirectChatTransport は会話の状態を保持するため、useMemo は重要です。安定した参照である必要があります。レンダリングごとに再作成すると、会話がリセットされます。

useChat は次の機能を提供します。

  • Messages: 会話全体を UIMessage[] として表示します。各メッセージには roleparts 配列が含まれます。
  • sendMessage({ text }): 新しいユーザー ターンを送信し、レスポンスのストリーミングを開始します。
  • Status: 'idle' | 'submitted' | 'streaming' | 'error'
  • Stop: 実行中の生成をキャンセルします。

メッセージをレンダリングする

各メッセージには parts 配列があります。このチャットボットでは、type: 'text' 部分のみを処理します。ユーザー メッセージは右揃えのバブルとして表示されます。アシスタント メッセージは左揃えでアイコンが表示されます。

const ChatMessage = ({ message, isStreaming }: { message: UIMessage; isStreaming: boolean }) => {
  const isUser = message.role === 'user';

  const textParts = message.parts.map((part, i) => {
    if (part.type !== 'text') return null;
    if (isUser) return <span key={i}>{part.text}</span>;
    return <Response key={i} parseIncompleteMarkdown={isStreaming}>{part.text}</Response>;
  });

  if (isUser) {
    return (
      <div className="flex flex-col items-end gap-2 animate-fade-up">
        <MessageContent className="w-fit max-w-[min(80%,56ch)] ...">
          {textParts}
        </MessageContent>
      </div>
    );
  }

  return (
    <div className="flex items-start gap-3">
      <AIIcon />
      <MessageContent className="text-[13px] leading-[1.65]">{textParts}</MessageContent>
    </div>
  );
};

MessageContentResponseAI 要素です。 これらは、shadcn スタイルのソース コンポーネントであり、npm からインストールするのではなく、 プロジェクトにコピーします。Response は、GitHub フレーバーのマークダウン(テーブル、タスクリスト、取り消し線)用の react-markdown と、AI 出力のリンクと画像をサニタイズする remark-gfmharden-react-markdown をラップします。

メッセージがまだストリーミングされている間、parseIncompleteMarkdown プロパティは true です。ストリーミング中に、モデルが **bold を書き込んでも まだ ** を閉じない場合があり、リテラル アスタリスクとしてレンダリングされるぶら下がりトークンが残ります。parseIncompleteMarkdown は、開いている **__`~~ を閉じ、ぶら下がり [ リンクの開始を切り捨てます。これにより、レンダリングされた出力は増分チャンクごとにクリーンな状態になります。

「思考中…」の状態

メッセージの送信から最初のトークンの受信までの間、status'submitted' です。この期間中、アプリはアニメーションのきらめきを表示します。

{status === 'submitted' && messages.at(-1)?.role !== 'assistant' && (
  <ThinkingMessage />
)}

条件 messages.at(-1)?.role !== 'assistant' は、アシスタント メッセージのストリーミングが開始された後にきらめきが 再表示されないようにします。

ThinkingMessageShimmer コンポーネントを使用します。これは、移動するグラデーションを持つ <span> で、background-clip: text を使用して「思考中…」のテキストにスイープ ハイライト効果を与えます。

自動スクロール

新しいコンテンツが到着すると、アプリは一番下までスクロールしますが、ユーザーがすでに一番下にいる場合に限ります。会話中に読んでいるものからスクロールすると、煩わしくなります。

const [isAtBottom, setIsAtBottom] = useState(true);

useEffect(() => {
  if (isAtBottom) endRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, status, isAtBottom]);

const handleScroll = () => {
  const el = containerRef.current;
  if (!el) return;
  setIsAtBottom(el.scrollHeight - el.scrollTop - el.clientHeight < 50);
};

isAtBottom が false の場合は、フローティングの一番下までスクロールするボタンが表示され、ユーザーが一番下に戻るとフェードアウトします。

入力エリア

入力時にテキストエリアの高さが auto にリセットされ、scrollHeight に設定されるため、入力時に自動的にサイズが変更されます。Enter キー(Shift+Enter ではない)で送信されます。レスポンスがストリーミングされている間、[送信] ボタンは stop() を呼び出す [停止] ボタンに置き換えられます。

<textarea
  onInput={(e) => {
    const el = e.currentTarget;
    el.style.height = 'auto';
    el.style.height = `${el.scrollHeight}px`;
  }}
  onKeyDown={(e) => {
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      if (input.trim() && !isStreaming) {
        sendMessage({ text: input });
        setInput('');
      }
    }
  }}
/>;
{
  isStreaming ? (
    <Button variant="outline" onClick={stop}>
      Stop
    </Button>
  ) : (
    <Button type="submit" disabled={!input.trim()}>
      Send
    </Button>
  );
}

読み込み状態でマウントする

agentPromise は非同期であるため、Chat をレンダリングする前に待機します。App ラッパーは Promise を解決し、その間にスピナーを表示します。

function App() {
  const [agent, setAgent] = (useState < ToolLoopAgent) | (null > null);

  useEffect(() => {
    agentPromise.then(setAgent);
  }, []);

  if (!agent) {
    return (
      <div className="flex h-dvh items-center justify-center">
        <Loader size={20} />
      </div>
    );
  }

  return <Chat agent={agent} />;
}

エージェントが解決すると(組み込みモデルがすぐに起動するか、モデルのダウンロードを待機するか)、スピナーが消え、チャット UI がマウントされます。

デモ

ライブデモ は、ブラウザで完全に実行されるフル機能のチャットボットです。メッセージを入力して Enter キーを押します。Prompt API が使用可能な場合、返信はネットワーク リクエストなしでオンデバイス モデルから直接ストリーミングされます。ブラウザが Prompt API をサポートしていない場合は、Gemini 2.5 Flash に自動的にフォールバックします。リスト内の項目を説明したり、コード スニペットを作成したり、マークダウン形式を使用したりしてみてください。レスポンスは、書式設定されたコードブロック、テーブル、インライン コードでレンダリングされます。

AI アシスタントとの会話を示すチャット インターフェース。テキスト入力フィールドとストリーミング応答領域が表示されている。

まとめ

この 2 つの記事では、Vercel AI SDK でブラウザの組み込み Prompt API を使用して実現できることの全範囲について説明しました。生の生成プリミティブから、洗練されたストリーミング チャット インターフェースまでです。

Vercel AI SDK で組み込みの Prompt API を使用するでは、ストリーミングなしのテキスト生成とストリーミング テキスト生成に generateText()streamText() を使用する方法、Output.object() で構造化 JSON 出力をリクエストする方法、生成ロジックを変更せずに実行時に組み込みモデルとクラウド プロバイダを選択するハイブリッド コードを作成する方法について説明しました。

このドキュメントでは、これらの同じビルディング ブロックを使用して、完全な React UI でラップしました。会話ループを管理する ToolLoopAgent、ブラウザでレスポンスを直接ストリーミングする DirectChatTransport を使用した useChat、マークダウン レスポンスをクリーンにレンダリングする AI 要素コンポーネントです。Prompt API が使用できない場合は、自動的にクラウドにフォールバックします。

その結果、バックエンドを必要とせずにブラウザで完全に動作する 2 つのデモが作成されました。