YovStudio

article 第 3 章:プロンプトを磨いてみた

公開日: 2025-06-29著者: Yov in YovStudio

前回の反省をもとに再チャレンジ

前回、以下のざっくりしたプロンプトで ChatGPT にお願いしてみました。

気分を記録して、カレンダーっぽい感じで色分けして
表示できるWebアプリって作れますか?
色の濃さで気分の違いを見せられると嬉しいです。
名前は MOOD TRACKER でお願いします!
あと、ダークモード/ライトモードの切り替えと、
テーマカラー(気分の色)の切り替え機能もつけてほしいです。

結果、返ってきたのは React 製のコードでした。
「おお、すごい!」となった一方で、「そのまま動かない…」「Reactってなに?」と戸惑う場面もありましたね。

そこからわかったのは、プロンプトの曖昧さが、出力結果のズレにつながるということ。
今回はその反省を踏まえて、より具体的なプロンプトを作って再チャレンジしてみます。

「ざっくり」を「具体的」に

プロンプトに盛り込むべきポイントは、主に次の3つです。

  • 使用する技術や実行環境(HTML/CSS/JavaScript で、ローカルで動かせるようにしてほしいなど)
  • 欲しい機能の粒度(どんなカレンダー?気分はどう記録?など)
  • 妥協したくない要素(今回の軸となる GitHub 風ヒートマップなど)

まずは、土台となる仕様をちゃんと伝えることで、出てくるコードの精度がぐっと上がります。
そのうえで、細かい要望や演出が不足しているようなら後から追加していく作戦にしましょう。

今回使うプロンプト

1 話で紹介したイメージ画像から、なるべく具体的に要望を書いてみました。
細かい要望も一気に書いているので、ざっくり版プロンプトとくらべると少し長くなってしまいましたね。

今回はリトライなので、このプロンプトを前回の続きではなく、新しいチャットで送ってみますよ。

「MOOD TRACKER」という、日々の気分を記録するWebアプリを作ってください。  
以下の前提を必ず守り、機能をすべて実装してください。

## 前提
- ローカルで実行できるように、HTML、CSS、JavaScriptだけで実装してください。Reactなどのフレームワークは使わないでください。
- データの保存はブラウザのローカルストレージを使ってください。
- コードは省略せず、フルで提供してください。

## 機能
- GitHub風の1年カレンダーを表示し、各日のマスにその日の気分を緑系の5段階グラデーションの色で表示する。
- カレンダーは曜日を縦軸に(Mon〜Sun)、週単位を横軸にして横スクロールできる形式で表示する。
- 最新の年が一番上に来るように、年ごとのカレンダーを縦に並べて表示する。
- 気分が登録されている最も古い年から現在の年まで表示する。
- 気分が1度も登録されていない場合、現在の年だけ表示する。
- 気分は5段階で記録し、1日1回登録できる。
- 気分の記録は、顔の表情が異なるアイコンを5つ並べて選べる。
- 顔の色もその気分に対応した色で表示する。
- 過去の気分を変更できる。
- ダークモードとライトモードの切り替えができる。
- カレンダーのマスの色(テーマカラー)を他の主要色に変更できる。
- アプリ全体の見た目は、ガラスのような透明感と光のグラデーションが感じられるような雰囲気とする。

ChatGPT からの返答

今回は 20 秒前後で「コード」と「チャットの回答」が返ってきました。
返ってきた内容は以下の通りです。

※補足:同じプロンプトでも、ChatGPT は状況によって違った回答を返すことがあります。自分で試してみたら全然違う結果になった、というのもよくあることです。その違いも含めて楽しんでもらえればと思います。

コード

コードは長いので折りたたんでいます。
コード内に含まれるコメントも含め、すべて ChatGPT が作成したものです。

コードはこちら
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>MOOD TRACKER</title>
  <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined" rel="stylesheet">
  <style>
    :root {
      --bg-light: #f0f0f0;
      --bg-dark: #1e1e2f;
      --text-light: #333;
      --text-dark: #eee;
      --glass-bg: rgba(255, 255, 255, 0.15);
      --glass-border: rgba(255, 255, 255, 0.3);
      --theme-green: #00ff66;
      --theme-blue: #00ccff;
      --theme-red: #ff3366;
      --theme-purple: #cc66ff;
      --theme-orange: #ff9900;
    }

    body {
      margin: 0;
      font-family: sans-serif;
      background: var(--bg-light);
      color: var(--text-light);
      transition: background 0.3s, color 0.3s;
    }
    body.dark {
      background: var(--bg-dark);
      color: var(--text-dark);
    }

    .glass {
      backdrop-filter: blur(12px);
      background: var(--glass-bg);
      border: 1px solid var(--glass-border);
      border-radius: 12px;
      padding: 1rem;
      box-shadow: 0 4px 12px rgba(0,0,0,0.2);
    }

    header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 1rem;
    }
    .controls {
      display: flex;
      gap: 1rem;
    }
    select, button {
      padding: 0.4rem 0.8rem;
      border-radius: 6px;
      border: none;
      font-size: 1rem;
    }
    .calendar {
      display: flex;
      flex-direction: column;
      gap: 2rem;
      padding: 1rem;
    }
    .year-section {
      display: flex;
      flex-direction: column;
      gap: 0.5rem;
    }
    .week-row {
      display: flex;
      overflow-x: auto;
      gap: 2px;
    }
    .day-col {
      display: flex;
      flex-direction: column;
      gap: 2px;
    }
    .day {
      width: 16px;
      height: 16px;
      border-radius: 3px;
      cursor: pointer;
      transition: transform 0.1s;
    }
    .day:hover {
      transform: scale(1.3);
    }
    .legend {
      display: flex;
      justify-content: center;
      gap: 0.5rem;
      margin: 1rem 0;
    }
    .mood-icon {
      font-size: 2rem;
      cursor: pointer;
      opacity: 0.6;
    }
    .mood-icon.active {
      opacity: 1;
    }
    .theme-selector {
      margin-left: auto;
    }
  </style>
</head>
<body>
  <header class="glass">
    <h1>MOOD TRACKER</h1>
    <div class="controls">
      <select id="themeColor">
        <option value="green">Green</option>
        <option value="blue">Blue</option>
        <option value="red">Red</option>
        <option value="purple">Purple</option>
        <option value="orange">Orange</option>
      </select>
      <button id="toggleMode">Dark Mode</button>
    </div>
  </header>

  <div class="calendar" id="calendar"></div>

  <div class="legend glass">
    <span class="mood-icon" data-mood="1">😞</span>
    <span class="mood-icon" data-mood="2">😐</span>
    <span class="mood-icon" data-mood="3">🙂</span>
    <span class="mood-icon" data-mood="4">😊</span>
    <span class="mood-icon" data-mood="5">😁</span>
  </div>

  <script>
    const calendarEl = document.getElementById('calendar');
    const themeSelect = document.getElementById('themeColor');
    const toggleModeBtn = document.getElementById('toggleMode');
    const moodIcons = document.querySelectorAll('.mood-icon');
    let currentTheme = 'green';
    let moodData = JSON.parse(localStorage.getItem('moodData') || '{}');

    function getColorScale(mood, theme) {
      const base = {
        green: ['#e6f4ea','#c2e7d2','#7bdcb5','#34c38f','#19936c'],
        blue:  ['#e0f7fa','#b2ebf2','#4dd0e1','#00acc1','#00838f'],
        red:   ['#ffe5e5','#ffb3b3','#ff8080','#ff4d4d','#e60000'],
        purple:['#f3e5f5','#e1bee7','#ce93d8','#ba68c8','#9c27b0'],
        orange:['#fff3e0','#ffe0b2','#ffcc80','#ffb74d','#ff9800'],
      }[theme];
      return base[mood - 1];
    }

    function formatDateKey(date) {
      return date.toISOString().split('T')[0];
    }

    function buildCalendar() {
      calendarEl.innerHTML = '';
      let allDates = Object.keys(moodData);
      let years = [...new Set(allDates.map(d => d.split('-')[0]))];
      if (years.length === 0) years = [new Date().getFullYear().toString()];
      years = years.sort((a, b) => b - a);
      years.forEach(year => {
        const section = document.createElement('div');
        section.className = 'year-section';
        section.innerHTML = `<h2>${year}</h2>`;
        const weeks = document.createElement('div');
        weeks.className = 'week-row';

        const startDate = new Date(`${year}-01-01`);
        const endDate = new Date(`${year}-12-31`);
        const firstMonday = new Date(startDate);
        while (firstMonday.getDay() !== 1) firstMonday.setDate(firstMonday.getDate() - 1);

        for (let d = new Date(firstMonday); d <= endDate; d.setDate(d.getDate() + 7)) {
          const col = document.createElement('div');
          col.className = 'day-col';
          for (let i = 0; i < 7; i++) {
            const cellDate = new Date(d);
            cellDate.setDate(cellDate.getDate() + i);
            const key = formatDateKey(cellDate);
            const day = document.createElement('div');
            day.className = 'day';
            if (cellDate.getFullYear().toString() === year) {
              const mood = moodData[key];
              if (mood) day.style.background = getColorScale(mood, currentTheme);
            }
            day.title = key;
            day.onclick = () => openMoodSelector(key);
            col.appendChild(day);
          }
          weeks.appendChild(col);
        }
        section.appendChild(weeks);
        calendarEl.appendChild(section);
      });
    }

    function openMoodSelector(dateKey) {
      moodIcons.forEach(icon => {
        icon.onclick = () => {
          const mood = parseInt(icon.dataset.mood);
          moodData[dateKey] = mood;
          localStorage.setItem('moodData', JSON.stringify(moodData));
          buildCalendar();
        };
      });
    }

    themeSelect.onchange = () => {
      currentTheme = themeSelect.value;
      buildCalendar();
    };

    toggleModeBtn.onclick = () => {
      document.body.classList.toggle('dark');
      toggleModeBtn.textContent = document.body.classList.contains('dark') ? 'Light Mode' : 'Dark Mode';
    };

    buildCalendar();
  </script>
</body>
</html>

チャットの回答

「MOOD TRACKER」のWebアプリをHTML/CSS/JavaScriptのみでフル実装しました。
以下の特徴を備えています:
・GitHub風カレンダー:曜日が縦、週が横。横スクロールで閲覧。
・気分記録:😞〜😁 の5段階で1日1回登録可能。
・ローカルストレージ保存:データは自動的に保存・復元されます。
・過去の記録変更可能
・複数年対応:最も古い記録年から現在まで表示。
・ダーク/ライトモード切替
・テーマカラー変更:緑系以外にもブルー、レッド、パープル、オレンジあり。
・ガラス風デザイン:glassmorphism風の見た目を実現。

そのまま .html ファイルとして保存し、ブラウザで開けば動作します。動作確認や改良したい点があれば、お気軽にどうぞ!

実際のやり取り

前回と同様に、興味のある方向けに ChatGPT とのやり取りのリンクを共有したいところですが、後続の内容に関わるネタバレを含むため、今後は控えておきます。

最低限って感じ…?

さて、今回は細かめに指示をしたことですし、期待できるかもしれません。
さっそく出来映えを拝ませてもらいましょう!

とりあえず動きを見たい方は下記リンクからどうぞ。
動作確認用デモページ

実際に手元で動かしてみたい方は、下記からダウンロード後、zip ファイルを展開して中に入っている index.html を実行してみてください。
zipファイルをダウンロード

── むむむ…これは…。さわってみた感じ、100 点満点中 30 点といったところでしょうか。プロンプトで伝えたことはたしかに守られていそうなので、その点は賞賛に値します。ただ残念ながらいろいろとユーザーに対する優しさがなく、実用レベルに至っていないですね。

致命的に優しさが足りてないポイントを挙げます。

  • カレンダーが存在することが分からない
  • カレンダーの横軸と縦軸に曜日や月のラベルがないため、表示されている部分がいつなのかわからない
  • いつの気分を入力をしているか分からない
  • 初期表示で顔文字を選択しても何も起きない
  • ライトモードだと😞の色がかなり見えづらい
  • ダークモードへの切り替えやテーマカラー変更が保存されない

ユーザー体験を想像することの大切さ

今回生成されたコードから気づいたのは、「伝えた仕様を満たすことと、使って気持ちいいものになることは全然別だ」ということです。

「どこを見ればいいかわからない」「操作しても反応がない」といった体験は、自然に「ユーザーのことを考えていない、つまり優しさの不足」として伝わってしまうのです。
人間が使う以上、「優しさ」を設計することがとても大切になります。

たとえば今回のアプリも、

  • 日付や曜日がラベルとして表示されていたら?
  • 入力の対象日がちゃんと明示されていたら?
  • カレンダーが存在しているとすぐ気づけたら?

それだけでずいぶん印象が変わるはずです。 ChatGPT にそれを気づかせるためには、プロンプトの中で 「人の感覚」や「体験」の部分まで表現してあげる必要があるのかもしれませんね。

仕様は伝わった、でも完成ではない

プロンプトを詳細にすれば、ChatGPT は高い精度でコードを出力してくれます。 でもその「精度の高さ」は、書かれた仕様の解釈としての正しさであって、実際に使いやすいかどうかはまた別の話です。

今回の学びを活かして、次回は人に優しくするための追加指示を、ChatGPT に出していきます。 ベースのコードは今回出来上がったので、次は ChatGPT とさらにチャットを続けて、たとえば「カレンダーに曜日ラベルをつけて」といった小さな追加要望をどうやって伝えていくか、「差分で伝える」プロンプトの工夫を試してみましょう。