【docker】簡易的なチャットボットのReactアプリをdocker-composeで動かす

# 概要

Chatbotを作成するためのAPIである「A3RT Talk API」とReactを利用した簡易的なチャットボットアプリを作成し、それをdocker-composeで動かします。

- 動作確認
```
$ docker ps
CONTAINER ID   IMAGE                   COMMAND                  CREATED       STATUS       PORTS                                       NAMES
c3f1a4a5cced   chatbot-sample:latest   "docker-entrypoint.s…"   3 hours ago   Up 3 hours   0.0.0.0:3000->3000/tcp, :::3000->3000/tcp   docker-compose_app_1
$ 
```

- サンプル画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307261/576ab1f5-b018-c033-167a-8a8f8e6a973d.png)

## 事前準備

### API KEYの発行

リクルートから提供されているチャットボットを作成するためのAPIである「A3RT Talk API」の「API KEY発行」に進みます。メールアドレス入力やプライバシーポリシーの同意後、確認メールが送信されます。確認後、「API KEY発行完了のお知らせ」のメールを受領します。<br>
[A3RT Talk API](https://a3rt.recruit.co.jp/product/talkAPI/)

### ユーザー/ボットの画像

[いらすとや](https://www.irasutoya.com/search?q=%E7%84%A1%E6%96%99)から入手しました。

## コード作成

### src/App.js

```typescript:App.js
import React, { useState, useEffect } from 'react';
import './App.css';

const Chatbot = () => {
  const [input, setInput] = useState('');
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    setMessages([{ text: '何か文章を書いてね', sender: 'bot' }]);
  }, []);

  const handleInputChange = (event) => {
    setInput(event.target.value);
  }

  const handleSubmit = async (event) => {
    event.preventDefault();
    setMessages(messages => [...messages, { text: input, sender: 'user' }]);
    setInput('');
    try {
      const response = await fetch(process.env.REACT_APP_API_ENDPOINT, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: new URLSearchParams({
          apikey: process.env.REACT_APP_API_KEY,
          query: input
        })
      });
      const data = await response.json();
      if (data.status === 0) {
        setMessages(messages => [...messages, { text: data.results[0].reply, sender: 'bot' }]);
      } else {
        setMessages(messages => [...messages, { text: 'Error: Unable to generate response', sender: 'bot' }]);
      }
    } catch (error) {
      setMessages(messages => [...messages, { text: 'Error: Failed to fetch data from API', sender: 'bot' }]);
    }
  }

  return (
    <div className="chatbot">
      <ul className="messages">
        {messages.map((message, index) => (
          <li key={index} className={message.sender}>
            <img src={`/${message.sender}.png`} alt={message.sender} />
            <span>{message.text}</span>
          </li>
        ))}
      </ul>
      <form onSubmit={handleSubmit}>
        <input type="text" value={input} onChange={handleInputChange} />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

export default Chatbot;
```

### App.css

```css:App.css
body {
  background-color: lightblue;
}

.chatbot {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.messages {
  flex-grow: 1;
  overflow-y: scroll;
}

.messages::-webkit-scrollbar {
  width: 12px;
}

.messages::-webkit-scrollbar-track {
  background-color: transparent;
}

.messages::-webkit-scrollbar-thumb {
  background-color: #00bfff;
  border-radius: 6px;
}

.messages li {
  display: flex;
  align-items: center;
}

.messages li img {
  width: 32px;
  height: 32px;
}

.messages li span {
  margin-left: 8px;
  border-radius: 16px;
  padding: 8px;
}

.messages li.user {
  justify-content: flex-end;
}

.messages li.user span {
  background-color: lightgreen;
}

.messages li.bot span {
  background-color: #00bfff;
}

form {
  display:flex;
}

form input[type="text"]{
   flex-grow :1;
   border-radius :16px;
   padding :8px;
   margin-right :8px;
   border :1px solid gray;
   outline :none;
   box-shadow :0px 0px 10px rgba(0,0,0,0.1);
   background-color :#f0fff0;
}
form button{
   background-color :#ff6961;
   border-radius :16px;
   padding :8px;
   border :none;
   outline :none;
   box-shadow :0px 0px 10px rgba(0,0,0,0.1);
}
```

### .env

```
REACT_APP_API_ENDPOINT=<A3RT Talk APIのエンドポイント>
REACT_APP_API_KEY=<取得したAPI KEY>
```

### その他
public/配下にユーザー画像のuser.png、ボット画像のbot.pngを配置します。

## ローカル上での動作確認

yarn startを実行し、http://localhost:3000 にアクセスします。テキストボックスに適当な文章を入力後「send」をクリックすることでボットが返答することを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307261/9639d68a-e83b-8160-ddd8-90b43aedc1bf.png)

## Dockerfile作成

軽量なLinuxあるalpine Linuxをベースとしたnode16のDockerImageを使用してDockerImageを作成します。

```
FROM node:16-alpine3.17
COPY . /app
WORKDIR /app
RUN npm install --production
EXPOSE 3000
CMD ["npm", "start"]
```

## DockerImage作成

chatbot-sampleのDockerImageを作成します。

```
$ docker build -t chatbot-sample:latest .
$ docker image ls
REPOSITORY       TAG       IMAGE ID       CREATED       SIZE
chatbot-sample   latest    6c11b1dc726a   3 hours ago   514MB
```

## docker-compose.yml作成

REACT_APP_API_KEY及びREACT_APP_API_ENDPOINTの環境変数の値は別途「.env」に記載します。

```
version: '3.3'
services:
  app:
    image: "chatbot-sample:latest"
    environment:
      - REACT_APP_API_KEY=${REACT_APP_API_KEY}
      - REACT_APP_API_ENDPOINT=${REACT_APP_API_ENDPOINT}
    ports:
      - 3000:3000
```

## DockerImageを利用した動作確認

docker-compose upにてchatbot-sampleのコンテナが立ち上がったか、また http://localhost:3000 に接続してチャットボットが利用できるか確認します。

```
$ docker-compose up
$ docker ps
CONTAINER ID   IMAGE                   COMMAND                  CREATED       STATUS          PORTS
                  NAMES
c3f1a4a5cced   chatbot-sample:latest   "docker-entrypoint.s…"   3 hours ago   Up 11 minutes   0.0.0.0:3000->3000/tcp, :::3000->3000/tcp   docker-compose_app_1
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307261/05075529-f776-7f45-9dba-c7482f341dad.png)

## 最後に

「A3RT Talk API」を使用することで簡易的なチャットボットUI画面を作成することができます。またdocker-composeを利用することで、様々な環境の際による違い(nodeのバージョン差異など)が発生することなくチャットボットUI画面を利用することができます。