ChatGPTでBootstrapを使ったHTMLのモックを作ってみる

画面のモックを作るツールはいろいろありますが、簡単にモックを作る方法としてChatGPTを利用する記事を見つけました。

参考 : [ChatGPTを利用して画面モックを爆速で作成する - Taste of Tech Topics](https://acro-engineer.hatenablog.com/entry/2023/10/13/150000)

「無料」で「インストールや環境構築不要」がよいので、無料版の「ChatGPT 3.5」を利用します。
また、モックツールを検索するとちょくちょく見かける「Bootstrap」も使ってみました。

# ログイン画面
まずはシンプルに「Bootstrapを使ったログイン画面のhtmlを書いて」とGhatGPTに伝えるとこんな感じになりました。

![a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1743295/119c6de2-b278-127d-e173-a865311797ab.gif)

<details><summary>GhatGPTの出力結果</summary><div>

> Bootstrapを使用して作成したログイン画面のHTMLの例を以下に示します。
> このHTMLコードでは、Bootstrapライブラリを使用してログインフォームを作成しています。フォームの入力フィールドとボタンはBootstrapのスタイルが適用され、レスポンシブデザインにも対応しています。

```html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ログイン</title>
  <!-- Bootstrap CSS -->
  <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
  <style>
    body {
      background-color: #f4f4f4;
    }
    .login-form {
      max-width: 400px;
      margin: 0 auto;
      padding: 30px 20px;
      background-color: #fff;
      border-radius: 8px;
      box-shadow: 0 0 10px rgba(0,0,0,0.1);
      margin-top: 100px;
    }
    .login-form h2 {
      text-align: center;
      margin-bottom: 30px;
    }
    .login-form input[type="text"],
    .login-form input[type="password"] {
      width: 100%;
      padding: 10px;
      margin-bottom: 20px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }
    .login-form button {
      width: 100%;
      padding: 10px;
      background-color: #007bff;
      border: none;
      border-radius: 4px;
      color: #fff;
      cursor: pointer;
    }
    .login-form button:hover {
      background-color: #0056b3;
    }
  </style>
</head>
<body>

<div class="container">
  <div class="row">
    <div class="col-md-6 offset-md-3">
      <div class="login-form">
        <h2>ログイン</h2>
        <form action="login.php" method="post">
          <div class="form-group">
            <label for="username">ユーザー名:</label>
            <input type="text" class="form-control" id="username" name="username" required>
          </div>
          <div class="form-group">
            <label for="password">パスワード:</label>
            <input type="password" class="form-control" id="password" name="password" required>
          </div>
          <button type="submit" class="btn btn-primary">ログイン</button>
        </form>
      </div>
    </div>
  </div>
</div>

<!-- Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@1.16.1/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>

</body>
</html>
```
</div></details>


## 検索画面
まずは、「本を検索する画面のHTMLを作って」と頼むととてもシンプルな検索画面が出力されました。

![a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1743295/b90d511a-0085-360c-d71d-a1741af8a924.gif)

<details><summary>GhatGPTの出力結果</summary><div>

> 以下は、本を検索する画面のHTMLの例です。
> このHTMLコードでは、Bootstrapを使用して本を検索するためのフォームが作成されています。入力フィールドと検索ボタンがあり、ユーザーがキーワードを入力して検索することができます。デザインはBootstrapのスタイルが適用されており、レスポンシブに対応しています。

```html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>本を検索する</title>
  <!-- Bootstrap CSS -->
  <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
  <style>
    body {
      background-color: #f4f4f4;
    }
    .search-form {
      max-width: 600px;
      margin: 0 auto;
      padding: 30px 20px;
      background-color: #fff;
      border-radius: 8px;
      box-shadow: 0 0 10px rgba(0,0,0,0.1);
      margin-top: 50px;
    }
    .search-form h2 {
      text-align: center;
      margin-bottom: 30px;
    }
    .search-form input[type="text"] {
      width: 100%;
      padding: 10px;
      margin-bottom: 20px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }
    .search-form button {
      width: 100%;
      padding: 10px;
      background-color: #007bff;
      border: none;
      border-radius: 4px;
      color: #fff;
      cursor: pointer;
    }
    .search-form button:hover {
      background-color: #0056b3;
    }
  </style>
</head>
<body>

<div class="container">
  <div class="row">
    <div class="col-md-8 offset-md-2">
      <div class="search-form">
        <h2>本を検索する</h2>
        <form action="search.php" method="get">
          <div class="form-group">
            <label for="keyword">キーワード:</label>
            <input type="text" class="form-control" id="keyword" name="keyword" required>
          </div>
          <button type="submit" class="btn btn-primary">検索</button>
        </form>
      </div>
    </div>
  </div>
</div>

<!-- Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@1.16.1/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>

</body>
</html>
```
</div></details>


次に「検索はタイトルと著者名にして」と検索項目を指定するとこんな感じになりました。
![a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1743295/ddbcfc26-ca63-89fc-dbb2-cff3810e56e5.gif)

<details><summary>GhatGPTの出力結果</summary><div>
> 申し訳ありません。タイトルと著者名で検索するように修正したHTMLを以下に示します。
> このHTMLコードでは、タイトルと著者名の入力フィールドが追加され、それぞれ検索の条件となります。ユーザーはこれらのフィールドにキーワードを入力し、検索ボタンをクリックして検索を実行できます。

```html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>本を検索する</title>
  <!-- Bootstrap CSS -->
  <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
  <style>
    body {
      background-color: #f4f4f4;
    }
    .search-form {
      max-width: 600px;
      margin: 0 auto;
      padding: 30px 20px;
      background-color: #fff;
      border-radius: 8px;
      box-shadow: 0 0 10px rgba(0,0,0,0.1);
      margin-top: 50px;
    }
    .search-form h2 {
      text-align: center;
      margin-bottom: 30px;
    }
    .search-form input[type="text"] {
      width: 100%;
      padding: 10px;
      margin-bottom: 20px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }
    .search-form button {
      width: 100%;
      padding: 10px;
      background-color: #007bff;
      border: none;
      border-radius: 4px;
      color: #fff;
      cursor: pointer;
    }
    .search-form button:hover {
      background-color: #0056b3;
    }
  </style>
</head>
<body>

<div class="container">
  <div class="row">
    <div class="col-md-8 offset-md-2">
      <div class="search-form">
        <h2>本を検索する</h2>
        <form action="search.php" method="get">
          <div class="form-group">
            <label for="title">タイトル:</label>
            <input type="text" class="form-control" id="title" name="title" required>
          </div>
          <div class="form-group">
            <label for="author">著者名:</label>
            <input type="text" class="form-control" id="author" name="author" required>
          </div>
          <button type="submit" class="btn btn-primary">検索</button>
        </form>
      </div>
    </div>
  </div>
</div>

<!-- Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@1.16.1/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>

</body>
</html>
```
</div></details>

「検索項目は1つ以上入力」「国会図書館のAPIで検索」「検索結果を一覧表にする」などいろいろChatGPTに伝えて以下のような画面のHTMLを出力することができました。

![a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1743295/d18f9666-9583-7037-80cd-481e3f84a0f3.gif)

<details><summary>GhatGPTの出力結果</summary><div>

```html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>国会図書館検索</title>
  <!-- Bootstrap CSS -->
  <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>

<div class="container">
  <h1 class="mt-5">国会図書館検索</h1>

  <form id="searchForm" class="mt-4 mb-3" onsubmit="submitSearch(event);">
    <div class="form-row">
      <div class="col-md-4 mb-3">
        <label for="title">タイトル:</label>
        <input type="text" class="form-control" id="title" name="title">
      </div>
      <div class="col-md-4 mb-3">
        <label for="author">著者名:</label>
        <input type="text" class="form-control" id="author" name="author">
      </div>
    </div>
    <div class="form-row text-right">
      <div class="col-md-8 mb-3">
        <button type="submit" class="btn btn-primary">検索</button>
        <button type="button" class="btn btn-secondary ml-2" onclick="clearSearch()">クリア</button>
      </div>
    </div>
  </form>

  <div id="searchResults"></div>
</div>

<!-- Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@1.16.1/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>

<script>
  function submitSearch(event) {
    event.preventDefault();

    var title = document.getElementById("title").value.trim();
    var author = document.getElementById("author").value.trim();

    // 検索条件のチェック
    if (title === "" && author === "") {
      alert("タイトルまたは著者名の少なくとも一つを入力してください。");
      return;
    }

    // 国会図書館のAPIエンドポイント
    var apiUrl = "https://ndlsearch.ndl.go.jp/api/opensearch";

    // 検索クエリの構築
    var query = "";
    if (title !== "") {
      query += "title=" + encodeURIComponent(title) + "&";
    }
    if (author !== "") {
      query += "creator=" + encodeURIComponent(author) + "&";
    }
    query += "count=10"; // 最大検索結果数を指定
    
    // APIリクエストを送信
    fetch(apiUrl + "?" + query)
      .then(response => response.text())
      .then(data => {
        // 検索結果を整形して表示
        var searchResults = document.getElementById("searchResults");
        searchResults.innerHTML = ""; // 一度中身をクリア

        // テーブル要素を作成
        var table = document.createElement("table");
        table.classList.add("table", "table-striped", "mt-3");

        // テーブルヘッダーを作成
        var thead = document.createElement("thead");
        thead.innerHTML = `
          <tr>
            <th>タイトル</th>
            <th>著者名</th>
            <th>出版日</th>
            <th>ISBN</th>
          </tr>
        `;
        table.appendChild(thead);

        // テーブルボディーを作成
        var tbody = document.createElement("tbody");

        // XMLデータをパース
        var parser = new DOMParser();
        var xmlDoc = parser.parseFromString(data, "text/xml");
        var items = xmlDoc.querySelectorAll("rss channel item");
        items.forEach(item => {
          var title = item.querySelector("title").textContent;
          var author = item.querySelector("author").textContent;
          var pubDate = item.querySelector("pubDate").textContent;

          // ISBNを取得
          var identifiers = item.querySelectorAll("identifier");
          var isbn = "";
          identifiers.forEach(identifier => {
            var xsiType = identifier.getAttribute("xsi:type");
            if (xsiType === "dcndl:ISBN") {
              isbn = identifier.textContent;
            }
          });

          var row = document.createElement("tr");
          var titleCell = document.createElement("td");
          titleCell.textContent = title;
          var authorCell = document.createElement("td");
          authorCell.textContent = author;
          var pubDateCell = document.createElement("td");
          pubDateCell.textContent = pubDate;
          var isbnCell = document.createElement("td");
          isbnCell.textContent = isbn;

          row.appendChild(titleCell);
          row.appendChild(authorCell);
          row.appendChild(pubDateCell);
          row.appendChild(isbnCell);

          tbody.appendChild(row);
        });
        table.appendChild(tbody);

        searchResults.appendChild(table);
      })
      .catch(error => {
        console.error("検索エラー:", error);
      });
  }

  function clearSearch() {
    document.getElementById("title").value = "";
    document.getElementById("author").value = "";
  }
</script>

</body>
</html>
```
</div></details>

# ファイルアップロード画面
ここでは要件をまとめてChatGPTに伝えてみました。

```text:伝えた要件
Bootstrapを使ってファイルアップロード画面をHTML出力して。

- ドラックアンドドロップでアップロードできるようにする
- アップロード後に「登録」ボタンでアップロード履歴に履歴を表示する
- 「登録」ボタンは右端に表示する
- 「登録」はファイルがアップロードされていない場合は非活性にする
- アップロード履歴を下に一覧で表示する
- アップロード履歴には「ファイル名」「アップロード日時」「状態」を表示する
- 状態には「アップロード中」「成功」「失敗」がある
- アップロード履歴にはサンプルデータを表示して
- アップロード欄には選択したファイル名を表示する
- 「登録」ボタンを押下したらアップロード欄のファイル名はクリアする
- アップロード履歴はアップロード日時の降順で表示する
- 「アップロード日時」の表示フォーマットは「YYYY-mm-dd HH:MM:DD」
```
![a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1743295/5772d00c-71b9-9f62-100a-4d0be6d8d0b0.gif)


<details><summary>GhatGPTの出力結果</summary><div>

> 以下は、要件に合わせてBootstrapを使用してファイルアップロード画面をHTMLで出力するコードです。このコードでは、ドラッグアンドドロップでファイルをアップロードし、アップロード後に「登録」ボタンを押すことでアップロード履歴にファイルの情報が表示されます。登録ボタンは右端に表示され、ファイルがアップロードされていない場合は非活性になります。アップロード履歴には「ファイル名」「アップロード日時」「状態」が表示され、状態には「アップロード中」「成功」「失敗」があります。アップロード履歴にはサンプルデータが表示され、アップロード欄には選択したファイル名が表示されます。また、「登録」ボタンを押すとアップロード欄のファイル名がクリアされ、アップロード履歴はアップロード日時の降順で表示されます。アップロード日時の表示フォーマットは「YYYY-mm-dd HH:MM:SS」です。

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>File Upload</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
  <style>
    #file-upload {
      border: 2px dashed #aaa;
      border-radius: 10px;
      padding: 20px;
      cursor: pointer;
    }
    #register-btn {
      float: right;
    }
  </style>
</head>
<body>
  <div class="container mt-5">
    <h1>File Upload</h1>
    <div id="file-upload" class="mb-3">
      <label for="file-input" class="form-label">ドラッグアンドドロップまたはクリックしてファイルをアップロード</label>
      <input type="file" class="form-control-file" id="file-input" style="display: none;">
      <div id="file-name" class="mt-2"></div>
    </div>
    <button id="register-btn" class="btn btn-primary mb-3" disabled>登録</button>
    <div id="upload-history">
      <h2>アップロード履歴</h2>
      <table class="table">
        <thead>
          <tr>
            <th scope="col">ファイル名</th>
            <th scope="col">アップロード日時</th>
            <th scope="col">状態</th>
          </tr>
        </thead>
        <tbody id="upload-history-body">
          <tr>
            <td>sample_file.txt</td>
            <td>2024-03-06 10:30:00</td>
            <td>成功</td>
          </tr>
          <!-- Sample data row, replace with dynamic content -->
        </tbody>
      </table>
    </div>
  </div>

  <script>
    const fileInput = document.getElementById('file-input');
    const registerBtn = document.getElementById('register-btn');
    const fileNameDisplay = document.getElementById('file-name');
    const uploadHistoryBody = document.getElementById('upload-history-body');

    fileInput.addEventListener('change', function () {
      if (fileInput.files.length > 0) {
        registerBtn.disabled = false;
        const fileName = fileInput.files[0].name;
        fileNameDisplay.innerHTML = `選択したファイル: ${fileName}`;
      } else {
        registerBtn.disabled = true;
        fileNameDisplay.innerHTML = '';
      }
    });

    registerBtn.addEventListener('click', function () {
      const fileName = fileInput.files[0].name;
      const currentDate = new Date().toISOString().slice(0, 19).replace('T', ' ');
      const newRow = document.createElement('tr');
      newRow.innerHTML = `
        <td>${fileName}</td>
        <td>${currentDate}</td>
        <td>アップロード中</td>
      `;
      uploadHistoryBody.insertBefore(newRow, uploadHistoryBody.firstChild);
      fileNameDisplay.innerHTML = ''; // Clear file name display
      fileInput.value = ''; // Clear file input value
      registerBtn.disabled = true; // Disable register button
    });

    // ドラッグアンドドロップイベントの処理
    const fileUpload = document.getElementById('file-upload');

    ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
      fileUpload.addEventListener(eventName, preventDefaults, false);
    });

    function preventDefaults(e) {
      e.preventDefault();
      e.stopPropagation();
    }

    ['dragenter', 'dragover'].forEach(eventName => {
      fileUpload.addEventListener(eventName, highlight, false);
    });

    ['dragleave', 'drop'].forEach(eventName => {
      fileUpload.addEventListener(eventName, unhighlight, false);
    });

    function highlight() {
      fileUpload.classList.add('border-primary');
    }

    function unhighlight() {
      fileUpload.classList.remove('border-primary');
    }

    fileUpload.addEventListener('drop', handleDrop, false);

    function handleDrop(e) {
      const dt = e.dataTransfer;
      const files = dt.files;

      fileInput.files = files;

      const fileName = files[0].name;
      fileNameDisplay.innerHTML = `選択したファイル: ${fileName}`;

      const registerBtn = document.getElementById('register-btn');
      registerBtn.disabled = false;
    }
  </script>
</body>
</html>
```
</div></details>


# ポイント
初めてChatGPTでモックのHTMLを作ってみて「ここがポイント」と思ったことは以下です。

1. コードが長いと途中でChatGPTの出力が止まることがあるので、そんな時は「続きは？」と問いかけると続きが出力される
    - [ChatGPTで出力が途中で途切れてしまうときの解決法 - Qiita](https://qiita.com/0Kizw5B6S6Zi7Da/items/035de026106965e92e0c)
1. モックの要件をまとめてからChatGPTに問いかける
    - 「検索画面」は「本を検索する画面のHTMLを作って」からひたすらChatGPTと会話して追加の要望を伝えて何度も何度もHTMLを出力しました。その過程で前に伝えたことが漏れたり、ずれたりしました。
    - なので、「要件をエディタなどにまとめる」「ChatGPTにHTMLを出力してもらう」「モック(HTML)を確認する」「要件を追記する」を繰り返すほうが良いと感じました。
1. ある程度希望のモック画面ができたら自分で微調整する
    - 細かい部分はChatGPTに伝える言葉を考えるより、JavaScriptやCSSを書いてしまったほうが早そうでした。

# さいごに
ChatGPTでHTMLを出力するのは思っていたよりも簡単でした。
ただ、「要件を文字にする能力」が問われました(というか苦戦しました)。
とはいえ、ゼロからHTMLを書くことを考えれば、かなり簡単にモックのHTMLが作成できます。

# この後の試行錯誤
https://qiita.com/chovin/items/562fa93c2620dc2623d1

https://qiita.com/chovin/items/f47d1a35198b01a03dda