【チャットAI】ChatGPTを利用してピザ注文画面をReactで作ってみた【プログラミング】

# 概要

いま話題のChatGPTを利用して、ピザ注文のWeb画面をReact実装で作ってみましたので、その内容を紹介したいと思います。
結論から言うと、自分ではプログラムにほぼ手を入れずに、それなりに動作するものがサクっと作れてしまいました。
かなり衝撃的です。

# この記事で説明すること
私がChatGPTに入力した内容と、その応答を（ほぼ）そのまま記載します。

尚、この記事内容の目的は、**ChatGPTの利用で、どれくらいコーディングを効率化できるかを確認する**ことです。このため、ChatGPTの出力内容に**完璧さは求めません**。
9割くらいそれっぽいものを書いてもらえれば、あとは自分でそれを直す方針です。「書こうと思えば自分で書けるけど、全部書くのは面倒だから、任せられる部分はChatGPTに任せる」目的で利用しています。

# この記事で説明しないこと
実行環境の構築手順は書きません。ChatGPTに「WindowsにReactをインストールして起動する手順を教えてください」のように聞くことで、だいたい教えてもらえます。Macの場合でももちろん教えてもらえます。
また、ChatGPTが回答してくれたReactの実装が最適なものであるか、バグを含まないか、などは言及していません。

# 作ったアプリの仕様
以下のアプリを作りました。

+ ピザのデータはjson形式のものを取得する。サーバーサイドの実装は無し。（jsonファイルをそのまま置く）
+ ピザの商品名、カロリー、値段を一覧表示する。
+ クリックすると、カロリーと値段の合計を表示する。もう一度クリックすると数字は戻る。
+ 注文ボタンを置くが、クリックしてもまだ何も起きない、で良いとする。

# 流れ
以下の流れで作ってもらいました。
作ってもらいたい内容をちょっとずつ教えて、直してもらいたいところがあれば指摘します。

1. jsonデータの内容を説明して、サンプルデータを作ってもらう。
2. サンプルデータを読み込んで画面表示する。Reactで実装してもらう。
3. 商品をクリックするとカロリーと値段の合計を表示してもらう。
4. もう一度クリックしたら選択状態を解除して、カロリーや値段の合計を戻してもらいたいので、その実装も追加してもらう。
5. 微調整を依頼。
6. （自分で作業）ちょっと手直しして、動作確認。

# 実際にChatGPTとやり取りした内容
ここからが実際のやり取りです。
尚、日本語で聞いたのになぜか全部英語で回答されました。コーディングに関する質問だとこうなりやすいのかもしれません。
あと、ソースコードの後に説明を付け加えてくれますが、自分で分かっている内容ばかりだったので以下では省いています。そのままコメントに書いてもよいかもですね。

## 1. jsonデータの作成
私：
>データはJSONで記載してください。
ピザのメニューを5個考えてください。
JSONには「名前」「カロリー（kcal）」「金額（円）」の項目を作ってください。

ChatGPTの回答：

>Here is an example of JSON data that represents a menu with 5 pizzas:

```json
{
    "pizzas": [
        {
            "name": "Margherita",
            "calories": 800,
            "price_yen": 1500
        },
        {
            "name": "Pepperoni",
            "calories": 900,
            "price_yen": 1700
        },
        {
            "name": "Vegetarian",
            "calories": 700,
            "price_yen": 1300
        },
        {
            "name": "BBQ Chicken",
            "calories": 850,
            "price_yen": 1600
        },
        {
            "name": "Seafood",
            "calories": 875,
            "price_yen": 1700
        }
    ]
}
```
おいしそうなピザのメニューを考えてくれました。
金額の項目名に「_yen」が入ってしまっていますが、いったんこのまま進めてみます。

## 2. Reactでjsonを読み込んで表示

私：
>ピザのJSONを読み込み、divタグで一つずつ表示するHTMLを書いてください。
divタグは3列に並べます。
Reactで書いてください。


ChatGPTの回答：
>Here is an example of how you can use React to read the JSON data and display each pizza in a div element:

```JavaScript
import React, { useState, useEffect } from 'react';

const App = () => {
  const [pizzas, setPizzas] = useState([]);

  useEffect(() => {
    fetch('path/to/pizzas.json')
      .then(response => response.json())
      .then(data => setPizzas(data.pizzas));
  }, []);

  return (
    <div style={{ display: 'flex', flexWrap: 'wrap' }}>
      {pizzas.map(pizza => (
        <div key={pizza.name} style={{ width: '33.33%', padding: '1rem' }}>
          <h3>{pizza.name}</h3>
          <p>Calories: {pizza.calories}</p>
          <p>Price: ¥{pizza.price_yen}</p>
        </div>
      ))}
    </div>
  );
};

export default App;
```

useEffectでjsonファイルを取得しています。引数にちゃんと [] を設定して、再読み込みを防いでいるのが優秀ですね。
将来的にこのjsonファイルのURLを、ピザのリストを返すAPIに変更すれば使えそうです。


## 3.商品をクリックしてカロリーと合計金額を表示

私：
>Topのdivタグにid="productlist"をつけてください。
productlistの下部に、カロリーと金額の合計、および、「注文する」ボタンをつけてください。
ピザのタグをクリックすると、カロリーと金額に合計した値を表示します。Reactで実装してください。

ChatGPTの回答：
>Here is an example of how you can modify the previous React code to include an id for the top div element, a button to show the total calories and price, and to display the total when a pizza is clicked:

```JavaScript
import React, { useState, useEffect } from 'react';

const App = () => {
  const [pizzas, setPizzas] = useState([]);
  const [totalCalories, setTotalCalories] = useState(0);
  const [totalPrice, setTotalPrice] = useState(0);

  useEffect(() => {
    fetch('path/to/pizzas.json')
      .then(response => response.json())
      .then(data => setPizzas(data.pizzas));
  }, []);

  const handleClick = pizza => {
    setTotalCalories(totalCalories + pizza.calories);
    setTotalPrice(totalPrice + pizza.price_yen);
  };

  return (
    <div id="productlist" style={{ display: 'flex', flexWrap: 'wrap' }}>
      {pizzas.map(pizza => (
        <div key={pizza.name} style={{ width: '33.33%', padding: '1rem' }} onClick={() => handleClick(pizza)}>
          <h3>{pizza.name}</h3>
          <p>Calories: {pizza.calories}</p>
          <p>Price: ¥{pizza.price_yen}</p>
        </div>
      ))}
      <div style={{ width: '100%', padding: '1rem' }}>
        <h3>Total:</h3>
        <p>Calories: {totalCalories}</p>
        <p>Price: ¥{totalPrice}</p>
        <button>Order</button>
      </div>
    </div>
  );
};

export default App;
```

カロリーと合計金額の表示位置をどう伝えるか迷いましたが、id="productlist"を追加してもらって、その下部、という説明にしました。
カロリーと合計金額をそれぞれ別のuseStateで表示してくれています。この時点でかなり衝撃的。

ですが、一度選択した後、「やっぱりやめた」となった時に、選択を解除したらカロリーと合計金額の値は戻してほしいですが、その実装がありません。あと、現在選択中のものが、他のものと見分けがつきません。
修正を要求します。

## 4.もう一度クリックしたら選択を戻す

私：
>すでにクリックしたピザをもう一度クリックすると、金額を減らすように修正してください。
また、クリックしたピザのdivは、背景を赤色にしてください。

ChatGPTの回答：
>Here's an updated version of the code to subtract the price and calories of a clicked pizza if it is clicked again, and to change the background color of a clicked pizza to red:

```JavaScript
import React, { useState, useEffect } from 'react';

const App = () => {
  const [pizzas, setPizzas] = useState([]);
  const [totalCalories, setTotalCalories] = useState(0);
  const [totalPrice, setTotalPrice] = useState(0);
  const [selectedPizzas, setSelectedPizzas] = useState([]);

  useEffect(() => {
    fetch('path/to/pizzas.json')
      .then(response => response.json())
      .then(data => setPizzas(data.pizzas));
  }, []);

  const handleClick = pizza => {
    let alreadySelected = false;
    let newSelectedPizzas = [];
    for (let i = 0; i < selectedPizzas.length; i++) {
      if (selectedPizzas[i].name === pizza.name) {
        alreadySelected = true;
        setTotalCalories(totalCalories - pizza.calories);
        setTotalPrice(totalPrice - pizza.price_yen);
      } else {
        newSelectedPizzas.push(selectedPizzas[i]);
      }
    }
    if (!alreadySelected) {
      setTotalCalories(totalCalories + pizza.calories);
      setTotalPrice(totalPrice + pizza.price_yen);
      newSelectedPizzas.push(pizza);
    }
    setSelectedPizzas(newSelectedPizzas);
  };

  return (
    <div id="productlist" style={{ display: 'flex', flexWrap: 'wrap' }}>
      {pizzas.map(pizza => (
        <div
          key={pizza.name}
          style={{ width: '33.33%', padding: '1rem', backgroundColor: selectedPizzas.includes(pizza) ? 'red' : '' }}
          onClick={() => handleClick(pizza)}
        >
          <h3>{pizza.name}</h3>
          <p>Calories: {pizza.calories}</p>
          <p>Price: ¥{pizza.price_yen}</p>
        </div>
      ))}
      <div style={{ width: '100%', padding: '1rem' }}>
        <h3>Total:</h3>
        <p>Calories: {totalCalories}</p>
        <p>Price: ¥{totalPrice}</p>
        <button>Order</button>
      </div>
    </div>
  );
};

export default App;
```

これだけ長いコードになっていますが、サクサク回答してくれます。
ちゃんと意図を把握して、選択されているリスト（selectedPizzas）を作ってくれています。
すげー頭いい。


## 5.微調整

私：
>JSONにピザの画像のURLを追加してください。ピザのdivタグにimgタグでJSONの画像のURLを追加してください。

ChatGPTの回答（一部省略）：
>Sure, here's the updated code with the addition of an image URL property in the JSON data and displaying the corresponding image in the pizza div tag:

```JavaScript
import React, { useState } from 'react';

const pizzaData = [
  {
    name: 'Margherita',
    kcal: 600,
    price: 5000,
    image: 'https://pizza.com/margherita.jpg'
  },
  {
    name: 'Pepperoni',
    kcal: 700,
    price: 5500,
    image: 'https://pizza.com/pepperoni.jpg'
  },

...（Qiita投稿時に以下省略）
```

画像も欲しいな、と思って追加してもらったのですが、JSONを別にすると言わなかったので、JavaScript実装の中でピザのデータを書き始めてしまいました。これだと使いにくいですね。
尚、あとで気づきましたが、ここで値段が「price_yen」から「price」に変わっています。
リテイクを要求。

私：
>JSONはuseEffectで取る実装に戻してください。

ChatGPTの回答（一部省略）：
>Here's an updated implementation using the useEffect hook to retrieve the JSON data:
```JavaScript
import React, { useState, useEffect } from "react";

const ProductList = () => {
  const [totalCalories, setTotalCalories] = useState(0);
  const [totalPrice, setTotalPrice] = useState(0);
  const [pizzas, setPizzas] = useState([]);
  const [selectedPizzas, setSelectedPizzas] = useState([]);

  useEffect(() => {
    fetch("/pizzas.json")
      .then(res => res.json())
      .then(data => {
        setPizzas(data);
      });
  }, []);

  const handleClick = pizza => {
    const selectedIndex = selectedPizzas.indexOf(pizza);
    if (selectedIndex === -1) {
      setSelectedPizzas([...selectedPizzas, pizza]);
      setTotalCalories(totalCalories + pizza.calories);
      setTotalPrice(totalPrice + pizza.price);
    } else {
      setSelectedPizzas(
        selectedPizzas.filter((_, index) => index !== selectedIndex)
      );
      setTotalCalories(totalCalories - pizza.calories);
      setTotalPrice(totalPrice - pizza.price);
    }
  };

  return (
    <div id="productlist">
      {pizzas.map(pizza => (
        <div
          key={pizza.name}
          style={{ backgroundColor: selectedPizzas.includes(pizza) ? "red" : "" }}
          onClick={() => handleClick(pizza)}
        >
          <img src={pizza.imageUrl} alt={pizza.name} />
          <p>{pizza.name}</p>
          <p>Calories: {pizza.calories}</p>
          <p>Price: {pizza.price}</p>
        </div>
      ))}
      <div>
        Total Calories: {totalCalories}
        <br />
        Total Price: {totalPrice}
        <br />
        <button>Order</button>
      </div>
    </div>
  );
};

export default ProductList;

```

文句ひとつ言わず直してくれました。
動作的にはこれでいいかなと思ったのと、ChatGPTからエラーが返ってくるようになった（混雑していた？）ため、やり取りはここで終了しました。

## 6.自分で手直し

あとは以下を自分で手直し。

+ jsonファイルから読み込むように戻したタイミングで、以下も途中で変わってしまっていたため、修正。

```diff_javascript
- setPizzas(data);
+ setPizzas(data.pizzas);
```


最後に、書いてもらったReactのソースを「ProductList.js」として保存し、「index.js」で読み込むように修正して、webpackを起動してブラウザで動作確認しました。
（pizzas.json はそのまま参照できる位置に置きました）

```JavaScript:index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import ProductList from './ProductList';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <ProductList />
  </React.StrictMode>
);

reportWebVitals();
```

# ChatGPTを使ってみての感想
Qiitaでは、ChatGPTにコーディングしてもらっている例を上げてる方が他にもいらっしゃいますが、自分でやってみるとかなり衝撃ですね。
ちなみに、buttonのクリックでsubmitするようになっていない（そもそもformタグもない）し、cssも設定していないので、使えるようにするにはそれなりに手を加える必要があります。

今日はブラウザ側で動作するコーディングを試しましたが、今後はサーバーサイドなどもっと違うタイプのコーディングもChatGPTで試してみたいと思います。
また、MicrosoftのBingやGoogleのBardなど、ほかのチャットAIが使えるようになったら、同じようなコーディングができるかにも挑戦してみたいと思います。

# ※2023-02-12 追記
ソースコードをGitHubにAddしました。
https://github.com/falman-ai-coding/sample-OrderPizza-React

また、Waitlistに登録していたBingチャットが使えるようになったため、試しにGitHubのページの要約を作ってみてもらいました。


私：
>https://github.com/falman-ai-coding/sample-OrderPizza-React このページのソースを要約してください。

Bingチャットの回答：
>こんにちは、これはBingです。😊
>このページのソースを要約するのは難しいですが、検索結果から以下のことがわかりました。
>- このページは、**falman-ai-coding**というユーザーが作成した**sample-OrderPizza-React**というリポジトリです¹。
>- このリポジトリは、**React**というJavaScriptのライブラリを使って、**ピザの注文**をシミュレートするウェブアプリケーションのサンプルコードを含んでいます¹。
>- このリポジトリには、**README.md**というファイルがあり、その中には**アプリケーションの概要**や**インストール方法**や**使用方法**などが説明されています¹。
>- このリポジトリには、**src**というフォルダがあり、その中には**App.js**や**index.js**や**components**というフォルダなどがあります¹。
>- **App.js**は、**React**の**コンポーネント**をインポートして、**アプリケーションのルート**となるコンポーネントを定義しています¹。
>- **index.js**は、**React**と**ReactDOM**をインポートして、**App**コンポーネントを**index.html**の**root**という要素にレンダリングしています¹。
>- **components**フォルダには、**Pizza**や**Order**や**Summary**などの**アプリケーションの機能**に関連するコンポーネントがあります¹。
>
>以上が、このページのソースの要約です。もっと詳しく知りたい場合は、このページを直接見てください。
>

Readme.mdにはこの記事の説明をそのまま張り付けただけなので、インストール方法や使用方法は書いていないのでちょっと誤っていますね。
追加で聞いてみます。

私：
>上記のページのProductList.jsは何をしていますか？

Bingチャットの回答：
>**ProductList.js**は、**falman-ai-coding**が作成した**sample-OrderPizza-React**というリポジトリの**src/components**フォルダにある**React**の**コンポーネント**の一つです¹。
>このコンポーネントは、**pizzas.json**というファイルから**ピザの種類**や**価格**や**画像**などの**データ**を読み込んで、**画面に表示**する役割を持っています¹。
>また、このコンポーネントは、**ユーザーがピザを選択**したときに、**Order**コンポーネントに**選択したピザの情報**を**渡す**機能も持っています¹。
>以上が、**ProductList.js**の概要です。もっと詳しく知りたい場合は、このページを直接見てください。

Orderコンポーネントに選択したピザの情報を渡す、というのは微妙に違いますが、そこそこ合っている内容ですね。
しかし、ProductList.jsをBingが自分で開いて内容を理解しようとしているのはすごいです。
ソースコメントをちゃんと書いたら、もっと要約してくれそうですね。やっぱりChatGPTが出力してくれた補足をコメントに書いておいたほうが良いかもしれません。