TypeScriptとReactで作るAIチャットボット: OpenAI APIを活用した実装ガイド

こんにちは！この記事では、TypeScriptとReactを使って、OpenAI APIを活用したシンプルなAIチャットボットを実装する方法を解説します。実際のコード例と共に、ステップバイステップで進めていきます。

## 概要

今回作成するのは以下の機能を持つチャットボットです：
- ユーザーの入力に対してAIが応答する
- 会話履歴を保持し、文脈を理解した応答が可能
- TypeScriptの型安全性を活かした実装
- ReactとTailwind CSSによるモダンなUI

## 前提条件

- Node.js (v14以上)
- React (v18推奨)
- TypeScript
- OpenAI APIキー

## 1. プロジェクトのセットアップ

まずはプロジェクトをセットアップします。create-react-appを使用することも可能ですが、今回はViteを使用します。

```bash
npm create vite@latest my-ai-chatbot -- --template react-ts
cd my-ai-chatbot
npm install
```

次に必要なパッケージをインストールします：

```bash
npm install openai axios tailwindcss @tailwindcss/forms
```

Tailwind CSSの設定：

```bash
npx tailwindcss init -p
```

`tailwind.config.js`を以下のように編集：

```javascript
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [
    require('@tailwindcss/forms'),
  ],
}
```

`src/index.css`に以下を追加：

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

## 2. 型定義

TypeScriptの利点を活かすため、まずは型定義を行います。`src/types/index.ts`を作成します：

```typescript
export interface Message {
  id: string;
  content: string;
  role: 'user' | 'assistant';
  timestamp: Date;
}

export interface ChatState {
  messages: Message[];
  isLoading: boolean;
  error: string | null;
}
```

## 3. OpenAI APIとの連携サービス

APIとの連携部分を作成します。`src/services/openai.ts`を作成：

```typescript
import axios from 'axios';
import { Message } from '../types';

const API_KEY = 'YOUR_OPENAI_API_KEY'; // 環境変数から取得する方が安全です
const API_URL = 'https://api.openai.com/v1/chat/completions';

export const generateResponse = async (messages: Message[]): Promise<string> => {
  try {
    const formattedMessages = messages.map(msg => ({
      role: msg.role,
      content: msg.content
    }));

    const response = await axios.post(
      API_URL,
      {
        model: 'gpt-3.5-turbo',
        messages: formattedMessages,
        temperature: 0.7,
      },
      {
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${API_KEY}`
        }
      }
    );

    return response.data.choices[0].message.content;
  } catch (error) {
    console.error('Error generating AI response:', error);
    throw new Error('AIレスポンスの生成に失敗しました');
  }
};
```

## 4. チャットコンポーネントの実装

次に、チャットUIを実装します。`src/components/ChatInterface.tsx`を作成：

```tsx
import React, { useState, useRef, useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { Message, ChatState } from '../types';
import { generateResponse } from '../services/openai';

const ChatInterface: React.FC = () => {
  const [chatState, setChatState] = useState<ChatState>({
    messages: [],
    isLoading: false,
    error: null
  });
  
  const [input, setInput] = useState<string>('');
  const messagesEndRef = useRef<HTMLDivElement>(null);

  // 新しいメッセージが追加されたら自動スクロール
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [chatState.messages]);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    if (!input.trim()) return;
    
    // ユーザーメッセージを追加
    const userMessage: Message = {
      id: uuidv4(),
      content: input,
      role: 'user',
      timestamp: new Date()
    };
    
    setChatState(prev => ({
      ...prev,
      messages: [...prev.messages, userMessage],
      isLoading: true,
      error: null
    }));
    
    setInput('');
    
    try {
      // AIの応答を取得
      const aiResponse = await generateResponse([...chatState.messages, userMessage]);
      
      // AIメッセージを追加
      const assistantMessage: Message = {
        id: uuidv4(),
        content: aiResponse,
        role: 'assistant',
        timestamp: new Date()
      };
      
      setChatState(prev => ({
        ...prev,
        messages: [...prev.messages, assistantMessage],
        isLoading: false
      }));
    } catch (error) {
      setChatState(prev => ({
        ...prev,
        isLoading: false,
        error: 'AIの応答取得に失敗しました'
      }));
    }
  };

  return (
    <div className="flex flex-col h-screen max-w-2xl mx-auto p-4">
      <div className="bg-white rounded-lg shadow-md overflow-hidden flex flex-col h-full">
        <div className="bg-blue-600 text-white p-4">
          <h1 className="text-xl font-bold">AI Chat Assistant</h1>
        </div>
        
        <div className="flex-1 overflow-y-auto p-4 space-y-4">
          {chatState.messages.length === 0 && (
            <div className="text-center text-gray-500 my-8">
              AIアシスタントに質問してみましょう
            </div>
          )}
          
          {chatState.messages.map((message) => (
            <div
              key={message.id}
              className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
            >
              <div
                className={`max-w-[80%] rounded-lg p-3 ${
                  message.role === 'user'
                    ? 'bg-blue-500 text-white rounded-br-none'
                    : 'bg-gray-200 text-gray-800 rounded-bl-none'
                }`}
              >
                {message.content}
              </div>
            </div>
          ))}
          
          {chatState.isLoading && (
            <div className="flex justify-start">
              <div className="bg-gray-200 text-gray-800 rounded-lg rounded-bl-none p-3 max-w-[80%]">
                <div className="flex space-x-2">
                  <div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce"></div>
                  <div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }}></div>
                  <div className="w-2 h-2 bg-gray-500 rounded-full animate-bounce" style={{ animationDelay: '0.4s' }}></div>
                </div>
              </div>
            </div>
          )}
          
          {chatState.error && (
            <div className="bg-red-100 text-red-800 p-3 rounded-lg text-center">
              {chatState.error}
            </div>
          )}
          
          <div ref={messagesEndRef} />
        </div>
        
        <form onSubmit={handleSubmit} className="border-t p-4">
          <div className="flex space-x-2">
            <input
              type="text"
              value={input}
              onChange={(e) => setInput(e.target.value)}
              placeholder="メッセージを入力..."
              className="flex-1 border-gray-300 rounded-full focus:ring-blue-500 focus:border-blue-500"
              disabled={chatState.isLoading}
            />
            <button
              type="submit"
              disabled={chatState.isLoading || !input.trim()}
              className="bg-blue-600 text-white px-4 py-2 rounded-full font-medium hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
            >
              送信
            </button>
          </div>
        </form>
      </div>
    </div>
  );
};

export default ChatInterface;
```

## 5. App.tsxの編集

最後に、`src/App.tsx`を編集してチャットインターフェースを表示します：

```tsx
import ChatInterface from './components/ChatInterface';

function App() {
  return (
    <div className="min-h-screen bg-gray-100">
      <ChatInterface />
    </div>
  );
}

export default App;
```

## 6. 実行と動作確認

以上の実装が完了したら、以下のコマンドでアプリケーションを起動します：

```bash
npm run dev
```

ブラウザで`http://localhost:5173`にアクセスすると、AIチャットボットが表示されます。

## 7. 発展的な機能

基本的な機能の実装が完了したら、以下のような機能を追加することも検討できます：

- 会話履歴の永続化（LocalStorageやFirebase等を使用）
- ユーザー認証
- 複数の会話スレッド対応
- プロンプトのカスタマイズ
- 応答の生成途中でのストリーミング表示

## まとめ

この記事では、TypeScriptとReactを使用して、OpenAI APIを活用したシンプルなAIチャットボットを実装する方法を紹介しました。TypeScriptの型安全性とReactのコンポーネント指向は、複雑なアプリケーション開発でも構造を整理しやすくする利点があります。

さらに知識を深めたい方は、OpenAIのドキュメントを参照するとともに、プロンプトエンジニアリングについても学習することをおすすめします。AIの可能性を最大限に引き出すには、適切な指示を与えることが重要です。

## 参考リンク

- [OpenAI API リファレンス](https://platform.openai.com/docs/api-reference)
- [React 公式ドキュメント](https://reactjs.org/docs/getting-started.html)
- [TypeScript 公式ドキュメント](https://www.typescriptlang.org/docs/)

実装中に何か問題に遭遇した場合や、質問があればコメントでお知らせください！