🤖

Vercel AI SDK と Next.js で Gemini 搭載のイケてるチャットアプリを作る

に公開

こんにちは!雷(らい)です。

今回は、Vercel AI SDK + Next.js でGemini搭載のイケてる(かっこいい)モダンなチャットアプリを作ります。


今回の技術スタック


APIキーを取得する

  1. https://aistudio.google.com/ にアクセスします

  2. 右上らへんにある、"Get API key" をクリックします。

  3. 右上の、"Create API Key" をクリックします。

  4. Google Cloud プロジェクトを選択し、作成します。

  5. "Create API key in existing project" を選択して"Copy"ボタンをクリックします。

  6. 完了です!


環境構築

Next.js でアプリを作る

$ bun create next-app

AI SDK をインストールする

$ bun install ai @ai-sdk/react @ai-sdk/google

.envをセットアップする

GOOGLE_GENERATIVE_AI_API_KEY=AIza...

shadcn/ui をセットアップ

$ bunx shadcn@latest init
$ bunx shadcn@latest add button input

アプリを作成する

APIを作成する (サーバー)

app/api/chat/route.ts にAPIを作成します。

import { streamText, UIMessage } from 'ai';
import { google } from '@ai-sdk/google';

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json(); //メッセージをクライアントから取得

  const result = streamText({
    model: google('gemini-2.5-flash'), // Gemini 2.5 Flash を使う
    system: 'You are a helpful assistant.', // システムプロンプト
    messages,
  });

  return result.toDataStreamResponse(); // レスポンスをクライアントへ返す
}

UIを作成する (クライアント)

app/page.tsx を編集してUIを作成します。

'use client';

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useChat } from '@ai-sdk/react';
import { Send, Bot, User, Zap } from 'lucide-react';
import { useRef, useEffect } from 'react';

export default function Page() {
  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({});
  const messagesEndRef = useRef<HTMLDivElement>(null);

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
  };

  useEffect(() => {
    scrollToBottom();
  }, [messages]);

  return (
    <main className='flex flex-col h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900'>
      {/* Header */}
      <div className='flex items-center justify-center p-6 border-b border-purple-500/20'>
        <div className='flex items-center gap-2'>
          <Zap className='w-6 h-6 text-yellow-400' />
          <h1 className='text-2xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent'>
            AI Chat
          </h1>
        </div>
      </div>

      {/* Messages */}
      <div className='flex-1 overflow-y-auto p-6 space-y-4'>
        {messages.length === 0 && (
          <div className='flex items-center justify-center h-full text-slate-400'>
            <div className='text-center'>
              <Zap className='w-12 h-12 mx-auto mb-4 text-purple-400' />
              <p className='text-lg'>Start a conversation!</p>
            </div>
          </div>
        )}
        
        {messages.map(message => (
          <div
            key={message.id}
            className={`flex gap-3 ${
              message.role === 'user' ? 'justify-end' : 'justify-start'
            }`}
          >
            <div
              className={`flex gap-3 max-w-[80%] ${
                message.role === 'user' ? 'flex-row-reverse' : 'flex-row'
              }`}
            >
              <div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
                message.role === 'user' 
                  ? 'bg-gradient-to-r from-blue-500 to-purple-600' 
                  : 'bg-gradient-to-r from-purple-500 to-pink-600'
              }`}>
                {message.role === 'user' ? (
                  <User className='w-4 h-4 text-white' />
                ) : (
                  <Bot className='w-4 h-4 text-white' />
                )}
              </div>
              
              <div
                className={`px-4 py-3 rounded-2xl ${
                  message.role === 'user'
                    ? 'bg-gradient-to-r from-blue-600 to-purple-600 text-white'
                    : 'bg-slate-800 text-slate-100 border border-slate-700'
                }`}
              >
                <p className='whitespace-pre-wrap'>{message.content}</p>
              </div>
            </div>
          </div>
        ))}
        
        {isLoading && (
          <div className='flex gap-3 justify-start'>
            <div className='flex gap-3 max-w-[80%]'>
              <div className='flex-shrink-0 w-8 h-8 rounded-full bg-gradient-to-r from-purple-500 to-pink-600 flex items-center justify-center'>
                <Bot className='w-4 h-4 text-white' />
              </div>
              <div className='px-4 py-3 rounded-2xl bg-slate-800 border border-slate-700'>
                <div className='flex space-x-1'>
                  <div className='w-2 h-2 bg-purple-400 rounded-full animate-bounce'></div>
                  <div className='w-2 h-2 bg-purple-400 rounded-full animate-bounce' style={{ animationDelay: '0.1s' }}></div>
                  <div className='w-2 h-2 bg-purple-400 rounded-full animate-bounce' style={{ animationDelay: '0.2s' }}></div>
                </div>
              </div>
            </div>
          </div>
        )}
        
        <div ref={messagesEndRef} />
      </div>

      {/* Input Form */}
      <div className='p-6 border-t border-purple-500/20'>
        <form onSubmit={handleSubmit} className='flex gap-3'>
          <div className='flex-1 relative'>
            <Input 
              name="prompt" 
              value={input} 
              onChange={handleInputChange}
              placeholder="Type your message..."
              className='pr-12 bg-slate-800 border-slate-700 text-white placeholder-slate-400 focus:border-purple-500 focus:ring-purple-500'
              disabled={isLoading}
            />
          </div>
          <Button 
            type="submit" 
            disabled={isLoading || !input.trim()}
            className='bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 disabled:opacity-50 disabled:cursor-not-allowed'
          >
            <Send className='w-4 h-4' />
          </Button>
        </form>
      </div>
    </main>
  );
}

テストしてみる

開発サーバーを起動

$ bun run dev

UIを確認

http://localhost:3000 にアクセスして確認してみましょう!

画像

このような画面が表示されていたら基本的なUIは完成です!お疲れさまでした。

出典

Discussion