Azure FunctionsとNext.jsを使ったGPT-4oのチャット画面の作成

## はじめに
[前回の記事](https://qiita.com/dushmantha492/items/ba5d959da2c65db8892c)ではFunctionsでGPT-4oのAPIを作りました。今回はNext.jsを使ってフロントエンドを構築する方法を解説します。バックエンドのコードも少し変更を加えています。

## バックエンドのコードの変更内容

1. **リクエストボディの処理の拡張**:
   - `system`、`max_tokens`、`temperature`、`top_p` のパラメータが追加されました。
   - `max_tokens`、`temperature`、`top_p` にはデフォルト値が設定しました（それぞれ1000、0、1）。

2. **Azure Open AI API呼び出しのパラメータの拡張**:
   - `max_tokens`、`temperature`、`top_p` のパラメータが `client.chat.completions.create` メソッドに追加しました。

以下が更新後のバックエンドのコードです。フロントエンドを実行する前にデプロイして置いてください：

```function_app.py
import azure.functions as func
import openai
from azurefunctions.extensions.http.fastapi import Request, StreamingResponse
import asyncio
import os
from dotenv import load_dotenv

load_dotenv()

# Azure Function App
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)

endpoint = os.getenv("AZURE_OPEN_AI_ENDPOINT")
api_key = os.getenv("AZURE_OPEN_AI_API_KEY")

# Azure Open AI
deployment = "gpt-4o"

client = openai.AsyncAzureOpenAI(
    azure_endpoint=endpoint,
    api_key=api_key,
    api_version="2024-02-01"
)

# Get data from Azure Open AI
async def stream_processor(response):
    async for chunk in response:
        if len(chunk.choices) > 0:
            delta = chunk.choices[0].delta
            if delta.content:  # Get remaining generated response if applicable
                await asyncio.sleep(0.05)
                yield delta.content

# HTTP streaming Azure Function
@app.route(route="http_trigger", methods=[func.HttpMethod.POST])
async def http_trigger(req: Request) -> StreamingResponse:
    try:
        body = await req.json()  # req.get_json() を req.json() に変更
        prompt = body.get("prompt")
        system = body.get("system")
        max_tokens = body.get("max_tokens", 1000)  # デフォルト値を1000に設定
        temperature = body.get("temperature", 0)  # デフォルト値を0に設定
        top_p = body.get("top_p", 1)  # デフォルト値を1に設定

        if not prompt:
            return func.HttpResponse("プロンプトが見つかりませんでした", status_code=400)
        if not system:
            return func.HttpResponse("システムメッセージが見つかりませんでした", status_code=401)

    except ValueError:
        return func.HttpResponse("無効なJSON形式のリクエストボディです", status_code=400)

    azure_open_ai_response = await client.chat.completions.create(
        model=deployment,
        temperature=temperature,
        max_tokens=max_tokens,
        top_p=top_p,
        messages=[
            {"role": "system", "content": system},
            {"role": "user", "content": prompt}
        ],
        stream=True
    )

    return StreamingResponse(stream_processor(azure_open_ai_response), media_type="text/event-stream")

```


## 開発環境
- Next.js
- TypeScript
- CSS
- fetch API
- Tailwind CSS
- Azure Functions
- Python 3.11

## 導入

### ステップ1: プロジェクトのセットアップ

1. *Node.js Command Promptを開きます。**
2. *フロントエンドプロジェクトを作りたいフォルダーに移動します。**
3. *プロジェクトを作成します。以下のコマンドを実行します：**
   ```bash
   npx create-next-app@latest
   ```
4. *プロンプトに従って `y` を入力し、プロジェクト名を入力します。その他の質問はそのままエンターを押します。**
5. *プロジェクトディレクトリに移動します：**
   ```bash
   cd <作成したプロジェクト>
   ```
6. *依存関係をインストールします：**
   ```bash
   npm install
   ```
7. *開発サーバーを起動します：**
   ```bash
   npm run dev
   ```
8. *VSCodeを開きます：**
   ```bash
   code .
   ```

### ステップ2: 環境変数の設定

プロジェクトのルートディレクトリに `.env.local` ファイルを作成し、以下の内容を追加します：

```env
NEXT_PUBLIC_ENDPOINT=http://localhost:7071/api/http_trigger
```

### ステップ3: フロントエンドコードの追加

`app/page.tsx` ファイルを以下のコードで置き換えます：

```page.tsx
"use client";

import { useState } from 'react';

const Home = () => {
    const [prompt, setPrompt] = useState('');
    const [system, setSystem] = useState('');
    const [maxTokens, setMaxTokens] = useState(1000);
    const [temperature, setTemperature] = useState(0);
    const [topP, setTopP] = useState(1);
    const [response, setResponse] = useState('');
    const [showParameters, setShowParameters] = useState(false);

    const handleSubmit = async (event: React.FormEvent) => {
        event.preventDefault();
        setResponse('');

        try {
            const res = await fetch(process.env.NEXT_PUBLIC_ENDPOINT!, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    prompt,
                    system,
                    max_tokens: maxTokens,
                    temperature,
                    top_p: topP
                })
            });

            if (!res.ok) {
                throw new Error('Network response was not ok');
            }

            const reader = res.body?.getReader();
            const decoder = new TextDecoder('utf-8');
            let result = '';

            if (reader) {
                while (true) {
                    const { done, value } = await reader.read();
                    if (done) break;
                    result += decoder.decode(value, { stream: true });
                    setResponse(result);
                }
            }
        } catch (error) {
            console.error('Error:', error);
            setResponse('Error occurred while processing your request.');
        }
    };

    return (
        <div className="flex flex-col items-center justify-center min-h-screen py-2 bg-gray-100">
            <div className="flex w-full max-w-4xl p-8 bg-white rounded-lg shadow-md">
                {showParameters && (
                    <div className="w-1/3 p-4 border-r border-gray-300">
                        <h2 className="mb-4 text-xl font-bold text-center">Parameters</h2>
                        <form className="flex flex-col">
                            <label htmlFor="prompt" className="mb-2 text-sm font-medium text-gray-700">System Message:</label>
                            <textarea
                                id="prompt"
                                className="p-2 mb-4 border border-gray-300 rounded-md"
                                value={prompt}
                                onChange={(e) => setPrompt(e.target.value)}
                                rows={4}
                                required
                            />
                            <label htmlFor="maxTokens" className="mb-2 text-sm font-medium text-gray-700">Max Tokens:</label>
                            <input
                                type="number"
                                id="maxTokens"
                                className="p-2 mb-4 border border-gray-300 rounded-md"
                                value={maxTokens}
                                onChange={(e) => setMaxTokens(Number(e.target.value))}
                                required
                            />
                            <label htmlFor="temperature" className="mb-2 text-sm font-medium text-gray-700">Temperature:</label>
                            <div className="flex items-center mb-4">
                                <input
                                    type="number"
                                    id="temperature"
                                    className="p-2 border border-gray-300 rounded-md w-20 mr-2"
                                    value={temperature}
                                    onChange={(e) => setTemperature(Number(e.target.value))}
                                    step="0.01"
                                    min="0"
                                    max="1"
                                    required
                                />
                                <input
                                    type="range"
                                    id="temperature-slider"
                                    className="flex-1"
                                    value={temperature}
                                    onChange={(e) => setTemperature(Number(e.target.value))}
                                    step="0.01"
                                    min="0"
                                    max="1"
                                />
                            </div>
                            <label htmlFor="topP" className="mb-2 text-sm font-medium text-gray-700">Top P:</label>
                            <div className="flex items-center mb-4">
                                <input
                                    type="number"
                                    id="topP"
                                    className="p-2 border border-gray-300 rounded-md w-20 mr-2"
                                    value={topP}
                                    onChange={(e) => setTopP(Number(e.target.value))}
                                    step="0.01"
                                    min="0"
                                    max="1"
                                    required
                                />
                                <input
                                    type="range"
                                    id="topP-slider"
                                    className="flex-1"
                                    value={topP}
                                    onChange={(e) => setTopP(Number(e.target.value))}
                                    step="0.01"
                                    min="0"
                                    max="1"
                                />
                            </div>
                        </form>
                    </div>
                )}
                <div className={`w-full ${showParameters ? 'w-2/3' : 'w-full'} p-4`}>
                    <h2 className="mb-4 text-xl font-bold text-center">Response</h2>
                    <form className="flex flex-col" onSubmit={handleSubmit}>
                        <label htmlFor="system" className="mb-2 text-sm font-medium text-gray-700">Message:</label>
                        <input
                            type="text"
                            id="system"
                            className="p-2 mb-4 border border-gray-300 rounded-md"
                            value={system}
                            onChange={(e) => setSystem(e.target.value)}
                            required
                        />
                        <button type="submit" className="p-2 mb-4 text-white bg-blue-500 rounded-md hover:bg-blue-600">Submit</button>
                    </form>
                    <button
                        onClick={() => setShowParameters(!showParameters)}
                        className="p-2 mb-4 text-white bg-gray-500 rounded-md hover:bg-gray-600"
                    >
                        {showParameters ? 'Hide Parameters' : 'Show Parameters'}
                    </button>
                    <div className="p-4 bg-gray-200 rounded-md whitespace-pre-wrap">{response}</div>
                </div>
            </div>
        </div>
    );
};

export default Home;
```

### ステップ4: CORSエラーの修正

Azure FunctionsのCORS設定で、`localhost:3000` を許可する必要があります。Azureポータルで関数アプリのCORS設定を開き、`http://localhost:3000` を追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806972/1eaaa1df-b787-f59b-b833-89324711e9bf.png)


### 実行結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806972/562cca95-6e80-c1f0-ecbb-241ed9bfeee3.png)


### まとめ

これで、Azure Functionsをバックエンドとして使用し、Next.jsを使ったフロントエンドが完成しました。フロントエンドからバックエンドにリクエストを送信し、リアルタイムでレスポンスを受け取ることができます。この記事が役に立ったら、ぜひ「いいね」や「シェア」をお願いします！