シンプルなAIアプリ2

シンプルなAIアプリの続きです。

今回は、データの送受信処理を作成します。

ブラウザとサーバー間の送受信

ブラウザとサーバー間の送受信処理を作成していきます。

送信、受信ともに、データはJSON形式でやり取りすることにします。

ブラウザ → サーバーへは fetch()関数を使って、localhost:3456/ai-api にアクセスし、JSONデータをPOSTします。

そして、サーバー → ブラウザは fetchに対するレスポンスとしてJSONデータを送信します。

サーバー側の送受信

サーバー側のプログラムを作成します。

Gemini APIに対する処理はいったん忘れ、/ai-api にアクセスされた時に、POSTされたプロンプトをそのまま返すプログラムを作ります。

以下が、変更後のsrc/server/main.ts のコードです。

import { serveDir } from "https://deno.land/std@0.224.0/http/mod.ts";
import { IClientToServer, IServerToClient } from "../common/interface.ts";

async function handler(req: Request) {
    const url = new URL(req.url);

    if (url.pathname === "/ai-api" && req.method === "POST") {
        const { prompt } = await req.json() as IClientToServer;
        const sendData: IServerToClient = { status: 'Error', result: "プロンプトがありません。" };
        if (prompt) {
            sendData.status = 'Ok';
            sendData.result = `AI処理は未実装。以下のプロンプトを受け取りました。\n\n${prompt}`;
        }
        return new Response(JSON.stringify(sendData), {
            headers: { "content-type": "application/json" }
        });
    }

    return serveDir(req, {
        fsRoot: "public"
    })
}

Deno.serve({ port: 3456 }, handler);

ブラウザからのリクエストが /ai-api に対して行われているかチェックし、そのときはserveDir()関数での処理を行わないようにしました。

JSONデータを受け取り、プロンプトがなければエラーメッセージを返します。

プロンプトがあれば、「プロンプトを受け取りました。」とメッセージを返します。AI処理を実装した後は、この部分がGemini APIから返された結果に変わります。

共通ファイルの作成

新たに、src/common/interface.tsを作りました。

以下がそのコードです。

/**
 * クライアントからサーバに送信するJSONデータのInterface
 */
export interface IClientToServer {
    /**
     * クライアントからサーバーに送るプロンプト
     */
    prompt: string;
}

/**
 * サーバーからクライアントに送信するJSONデータのInterface
 */
export interface IServerToClient {
    /**
     * サーバーの結果
     */
    status: 'Ok' | 'Error';
    /**
     * サーバーからクライアントに送る結果
     */
    result: string;
}

interface宣言のみのファイルです。

ブラウザ側とサーバー側で、送受信するJSONデータが確実に同じフォーマットになるように、共通のinterfaceを宣言しました。

共通で使うため、src/common ディレクトリを作成して、そこに置くことにしました。

ブラウザ側の送受信

ブラウザ側のプログラムを作成します。

以下が、 src/client/script.ts のコードです。

import { IClientToServer, IServerToClient } from "../common/interface.ts";

// 送信ボタンのクリックイベントを登録する
const sendButton = document.getElementById("send-button");
if (sendButton instanceof HTMLButtonElement) {
    sendButton.addEventListener("click", async () => {
        try {
            // id="ai-prompt"のtextareaからプロンプトを得る。
            const prompt = GetPrompt();

            // プロンプトが空なら、それを表示して終了する。
            if (!prompt) {
                UpdateAiResult("プロンプトがありません");
                return;
            }

            // プロンプトをサーバーの"ai-api"に送信する。
            const sendData: IClientToServer = { prompt: prompt };
            const response = await fetch("ai-api", {
                method: "POST",
                body: JSON.stringify(sendData)
            });

            // サーバーから結果が返ってきたら、それを表示する。
            const json = await response.json() as IServerToClient;
            UpdateAiResult(json.result);
        } catch (error) {
            // エラーがあった場合は、それを表示する。
            console.error('Error:', error);
            UpdateAiResult(`エラーが発生しました: ${(error as Error).message}`);
        }
    });
}

/**
 * id="ai-prompt"からプロンプトを取得する。
 * @returns 
 */
function GetPrompt() {
    const aiPromptTextArea = document.getElementById("ai-prompt") as HTMLTextAreaElement | null;
    const prompt = aiPromptTextArea?.value ?? "";
    // 前後の空白文字を削除したあとで返す。
    return prompt.trim();
}

/**
 * id="ai-result"のテキストをresultにする。
 * @param result 
 */
function UpdateAiResult(result: string) {
    const aiResult = document.getElementById("ai-result");
    if (aiResult) {
        aiResult.innerText = result;
    }
}

コードの概要を説明します。

送信処理は、送信ボタンのクリックイベントとして実行されます。

GetPrompt()関数でtextareaからプロンプト文字列を取得します。

それが空の場合は、「プロンプトがありません」と表示します。UpdateAiResult()関数で表示します。

空でない場合は、IClientToServerのobjectにして、JSONデータに変換してサーバーの"ai-api"に送信します。

サーバーから返ってきたJSONデータを、IServerToClientのobjectにし、結果をUpdateAiResult()関数で表示します。

実際の実行例

まず、public/script.jsを作成します。作成はesbuildを使いました。以下がそのログです。

> esbuild --bundle --outdir=public --format=esm --charset=utf8 src/client/script.ts

  public/script.js  1021b 

次に、Denoサーバーを起動します。

> deno run --allow-net --allow-read src/server/main.ts
Listening on http://0.0.0.0:3456/

次に、public/index.html で、コメント化していた script.js の読み込み部分を、有効化します。

そして、ブラウザで localhost:3456/index.html にアクセスします。次のように表示されました。

プロンプトの入力した文字がそのまま表示されました。プリンプトが空のまま送信ボタンを押すと、下のようになります。

今回までの作業状況

今回の作業でcommon/interface.tsが増えました。以下に最新のディレクトリ構成と状況を記載します。

/project-root
|-- /public
|   |-- index.html
|   |-- styles.css(未着手)
|   |-- script.js(script.tsから生成される)
|-- /src
|   |-- /common
|   |   |-- interface.ts(New)
|   |-- /client
|   |   |-- script.ts(New)
|   |-- /server
|       |-- main.ts(Update:ブラウザ側との通信処理を実装)

コメント