JavaScriptのみでChatGPT APIのリアルタイム出力を行うシンプルなチャットボット

## 概要

たった1個のHTMLファイルだけで爆速の回答を行うチャットボットを実装したいと思います。
デフォルトのChatGPT APIでは回答のボリュームが大きければ大きいほど、待ち時間が長くなります。それは回答を一括で表示するためです。「こんにちは」ならレスポンスは1秒程度ですが、「〜の情報を詳しく教えて」といった質問では10秒以上待たされる理由はそれです。

ここではChatGPT本家の様に、ChatGPT APIでも回答を1文字ずつ出力を実現するHTMLファイルを公開しています。ブラウザをリフレッシュしない限り継続した会話も可能なので、しりとりでも遊べます。
日本語のChatGPT APIからのリアルタイム出力させる情報がまだまだ少ないことと、自分の手元にもっとお手軽に使えるものが欲しかったので作成しました。

参考動画
<br>
<iframe width="560" height="315" src="https://www.youtube.com/embed/40XW-yH45-E" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

## 注意
・こちらはあくまで開発学習の一環として作られたものです。**Javascript内にAPIキーを記述する仕様**なので、<font color="red">**公開サイトでは絶対に利用しないでください**</font>。自分用、あるいは社内や信頼できる身内だけで楽しみましょう。

・環境変数や別ファイル、サーバサイドにAPIキーを移動させて隠しても、実行時にキーが漏れます。公開サイトにて使用したい場合は別のサーバサイドプログラムを利用いただくか、OpenAI側でAPIキーへのアクセス制限機能が搭載されることを期待しましょう。

[!! Caution !!]
This is a learning tool for development, and you will write the API key in Javascript, so <font color="red">**please do not use it on public websites**</font>. It is for your own use, or enjoy it only within your company or trusted relatives.
Even if you hide the API key by moving it to an environment variable, a separate file or the server side, the key will be leaked at runtime. If you want to use it on a public site, please use other server-side programme or hope that OpenAI will provide a function to restrict access to the API key.

## ファイル

index.htmlと、そのスタイルシートstyles.cssだけで動きます。インターネットに接続されていればローカルでも動きます。

「ここにAPIキーを入力」と書かれている場所を自身で発行したAPIキーに書き換え、Google Chromeからindex.htmlを開いてください。

```index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
  <title>ChatGPT chatbot</title>
  <link rel="stylesheet" href="styles.css">
</head>

<body>
  <form id="chat-form" autocomplete="off">
    <input type="text" id="chat-input" placeholder="質問を入力してEnterで送信" />
    <div id="chat-window">
      <pre id="chat-history"></pre>
  </form>
  </div>
</body>

</html>
<script>
  let messageHistory = [];

  async function appendAssistantResponse(assistantMessage) {
    messageHistory.push({ 'role': 'assistant', 'content': assistantMessage });
  }

  $('#chat-form').on('submit', async function (event) {
    event.preventDefault();
    const userMessage = $('#chat-input').val();
    $('#chat-history').append('<p class="you">' + userMessage + '</p>');

    messageHistory.push({ 'role': 'user', 'content': userMessage });

    const formData = $(this).serialize();
    const url = 'https://api.openai.com/v1/chat/completions';
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ここにAPIキーを入力',
      },
      body: JSON.stringify({
        'model': 'gpt-3.5-turbo',
        'stream': true,
        'messages': messageHistory,
      }),
    });

    if (!response.ok) {
      console.error('Error:', await response.text());
      return;
    }

    $("#chat-input").val("");
    $("#chat-input").focus();

    const reader = response.body.getReader();
    const textDecoder = new TextDecoder();
    let buffer = '';

    while (true) {
      const { value, done } = await reader.read();

      if (done) {
        break;
      }

      buffer += textDecoder.decode(value, { stream: true });

      while (true) {
        const newlineIndex = buffer.indexOf('\n');
        if (newlineIndex === -1) {
          break;
        }

        const line = buffer.slice(0, newlineIndex);
        buffer = buffer.slice(newlineIndex + 1);

        if (line.startsWith('data:')) {

          if (line.includes('[DONE]')) {
            $('#chat-history').append('<hr>');
            return;
          }

          const jsonData = JSON.parse(line.slice(5));

          if (jsonData.choices && jsonData.choices[0].delta && jsonData.choices[0].delta.content) {
            const assistantMessage = jsonData.choices[0].delta.content;
            $('#chat-history').append('' + assistantMessage + '');
            await appendAssistantResponse(assistantMessage);
          }

        }

      }
    }
  });
</script>
<script>
  const chatWindow = document.getElementById('chat-window');
  function scrollChatWindow() {
    const chatWindowHeight = chatWindow.clientHeight;
    const chatWindowScrollHeight = chatWindow.scrollHeight;
    const chatWindowTextHeight = chatWindowScrollHeight - chatWindow.scrollTop;
    if (chatWindowTextHeight > chatWindowHeight) {
      chatWindow.scrollTop = chatWindowScrollHeight;
    }
  }
  chatWindow.addEventListener('DOMNodeInserted', scrollChatWindow);
</script>
```


スタイルシートは結構適当です。適宜調整してください。

```styles.css
body {
	text-align:center;
    align-items: center;
    min-height: 100vh;
    margin: 0;
    font-family: sans-serif;
}
#chat-window {
	margin:0px auto 15px auto;
	text-align:left;
    width: 90%;
    height: 600px;
    border: 1px solid #ccc;
    border-top: none;
    overflow-y: auto;
    padding: 10px;
    box-sizing:border-box;
}
#chat-history {
	white-space: pre-wrap;
    max-height: 760px;
    box-sizing:border-box;
    line-height:1.5em;
    font-size:12pt;
}
#chat-history hr {
	border-top: 1px dashed #8c8b8b;
}
#chat-input {
	background-color:#efefef;
    width: 90%;
    height: 60px;
    border: 1px solid #ccc;
    outline: none;
    margin-top: 30px;
    padding: 10px;
    box-sizing:border-box;
    font-size: 16px; 
    max-height: 100%; 
}
.you {
	color:#FF6600;
	margin:0px;
	padding-top:5px;
	padding-bottom:5px;
}
```

## 追記（2023/04/12）
~~・PHPをエンドポイントに設定して扱えるスクリプトを後日用意します。お待ちください。~~
会社の方針で、PHPスクリプトを販売する事となりました。PHP大好き・Python苦手な私個人としてはPHPでの活用方法をもっと広められればいいなって思ったのですが、申し訳無いです。
Qiitaでは規約で直接的な宣伝が出来ないため、私の個人サイトから辿っていただければと思います。
https://aokikotori.com/

## 最後に

シンプルに、手軽に使えるWebアプリの情報がありましたらお寄せください。
会員登録が必要だったり、ライブラリのインストールが必要だったりで、「すぐ使える」ものが少ないこと、それからまだまだ日本語の情報が少ないので、みなさんの学習に繋がると嬉しいです。

私が以前制作したもの：
ChatGPT APIをHTML+PHPから実行するスクリプト
https://qiita.com/kotonoha0109/items/b1b1b48520f7ee137c5e
（こちらはリアルタイム出力では無いですが、APIキーはブラウザ上からは漏れません）