YovStudio

article 第 6 章:その微調整、人間の出番です

公開日: 2025-07-14更新日: 2025-07-28著者: Yov in YovStudio

今回やること

前回はいくつか細かい課題に対応してみましたが、ChatGPT に渡したプロンプトでは意図がうまく伝わらず、あまり期待通りに課題を解決できませんでしたね。

今回もプロンプトを作って ChatGPT に改善してもらっても良いのですが、細かい部分なのでプロンプトにもより詳細に指示を記述する必要があり手間がかかります。特に色味の調整、配置の調整といった「人の目で見て気になる、微細なズレや違和感」は AI に分かりやすく具体的に伝えるのが難しいため、また AI に同じような修正を繰り返させてしまう恐れもあります。

将来的には分かりませんが、「色味のように微妙なニュアンスが求められる部分」は現時点ではやはり人間が得意とするところですので、ここは介入してあげるのが良いと思います。

何も AI に全部を作ってもらう必要はないのです。お互いの得意とするところを掛け合わせて、AI との共創を実現しましょう。

介入ポイント

以下に列挙する、プロンプトで指示を出すのが難しそうな細かい箇所に介入していきます。
ただ、修正規模によっては 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);
      --neutral-light: rgba(0, 0, 0, 0.05);
      --neutral-dark: rgba(255, 255, 255, 0.08);
    }

    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: 4px;
      position: relative;
    }
    .month-labels {
      display: flex;
      gap: 4px;
      padding-left: 36px;
      font-size: 0.8rem;
      margin-bottom: 4px;
    }
    .month-labels span {
      width: 16px;
      text-align: center;
    }
    .day-col {
      display: flex;
      flex-direction: column;
      gap: 4px;
    }
    .days-with-labels {
      display: flex;
    }
    .day-labels {
      display: flex;
      flex-direction: column;
      gap: 4px;
      margin-right: 4px;
      font-size: 0.75rem;
      padding-top: 16px;
    }
    .day {
      width: 16px;
      height: 16px;
      border-radius: 3px;
      cursor: pointer;
      background: var(--neutral-light);
      box-sizing: border-box;
      transition: transform 0.1s, border 0.2s;
    }
    body.dark .day {
      background: var(--neutral-dark);
    }
    .day:hover {
      transform: scale(1.3);
    }
    .day.selected {
      border: 2px solid #000;
    }
    body.dark .day.selected {
      border: 2px solid #fff;
    }
    .legend {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 0.5rem;
      margin: 1rem;
    }
    .mood-row {
      display: flex;
      gap: 0.5rem;
    }
    .mood-icon {
      font-size: 2rem;
      cursor: pointer;
      opacity: 0.6;
    }
    .mood-icon.active {
      opacity: 1;
    }
    .theme-selector {
      margin-left: auto;
    }
    #selectedDateDisplay {
      font-size: 1rem;
      font-weight: bold;
    }
  </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">
    <div id="selectedDateDisplay">日付を選択してください</div>
    <div class="mood-row">
      <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>
  </div>

  <script>
    const calendarEl = document.getElementById('calendar');
    const themeSelect = document.getElementById('themeColor');
    const toggleModeBtn = document.getElementById('toggleMode');
    const moodIcons = document.querySelectorAll('.mood-icon');
    const selectedDateDisplay = document.getElementById('selectedDateDisplay');

    let currentTheme = localStorage.getItem('theme') || 'green';
    let selectedDate = new Date().toISOString().split('T')[0];
    let isDarkMode = localStorage.getItem('mode') === 'dark';

    if (isDarkMode) document.body.classList.add('dark');
    themeSelect.value = currentTheme;
    toggleModeBtn.textContent = isDarkMode ? 'Light Mode' : 'Dark Mode';

    let moodData = JSON.parse(localStorage.getItem('moodData') || '{}');

    function getColorScale(mood, theme, isDark) {
      const palette = {
        light: {
          green: ['#cce8da','#98d1b6','#63bc92','#33a66e','#198f56'],
          blue:  ['#d1f2f9','#9edbf0','#68c6e6','#30b0dc','#008ac1'],
          red:   ['#ffd6d6','#ff9c9c','#ff6c6c','#ff3c3c','#d10000'],
          purple:['#e6d9f0','#d1aee4','#b77dd7','#9f49cb','#7d2ea6'],
          orange:['#ffeacc','#ffd099','#ffb566','#ff9a33','#cc6f00']
        },
        dark: {
          green: ['#1a2f25','#246647','#2ca37a','#34dca0','#6fffc8'],
          blue:  ['#12333e','#14627c','#168bb5','#18b1dc','#68e0f5'],
          red:   ['#3d1e1e','#842323','#d33434','#f95858','#ffaaaa'],
          purple:['#2f1e35','#562b63','#813a94','#a856c0','#ce9ef0'],
          orange:['#3f2c1a','#7c4c00','#b36a00','#ff8c1a','#ffc180']
        }
      };
      return palette[isDark ? 'dark' : 'light'][theme][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.includes(new Date().getFullYear().toString())) years.push(new Date().getFullYear().toString());
      years = years.sort((a, b) => b - a);

      const dayLabels = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];

      years.forEach(year => {
        const section = document.createElement('div');
        section.className = 'year-section';
        section.innerHTML = `<h2>${year}</h2>`;

        const monthLabels = document.createElement('div');
        monthLabels.className = 'month-labels';

        const weekRow = document.createElement('div');
        weekRow.className = 'week-row';

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

        const labelDays = document.createElement('div');
        labelDays.className = 'day-labels';
        dayLabels.forEach(d => {
          const label = document.createElement('div');
          label.textContent = d;
          labelDays.appendChild(label);
        });

        const dayWrapper = document.createElement('div');
        dayWrapper.className = 'days-with-labels';
        dayWrapper.appendChild(labelDays);

        let lastMonth = -1;
        for (let d = new Date(firstSunday); d <= endDate; d.setDate(d.getDate() + 7)) {
          const col = document.createElement('div');
          col.className = 'day-col';

          const cellMonth = d.getMonth();
          if (lastMonth !== cellMonth) {
            const label = document.createElement('span');
            label.textContent = `${cellMonth + 1}月`;
            monthLabels.appendChild(label);
            lastMonth = cellMonth;
          } else {
            const blank = document.createElement('span');
            blank.textContent = '';
            monthLabels.appendChild(blank);
          }

          for (let i = 0; i < 7; i++) {
            const cellDate = new Date(d);
            cellDate.setDate(cellDate.getDate() + i);
            if (cellDate.getFullYear().toString() !== year) continue;
            const key = formatDateKey(cellDate);
            const day = document.createElement('div');
            day.className = 'day';

            const mood = moodData[key];
            const isDark = document.body.classList.contains('dark');
            if (mood) {
              day.style.background = getColorScale(mood, currentTheme, isDark);
            }
            if (selectedDate === key) {
              day.classList.add('selected');
            }

            day.title = key;
            day.onclick = () => openMoodSelector(key);
            col.appendChild(day);
          }
          dayWrapper.appendChild(col);
        }

        section.appendChild(monthLabels);
        section.appendChild(dayWrapper);
        calendarEl.appendChild(section);
      });

      selectedDateDisplay.textContent = selectedDate;
    }

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

    themeSelect.onchange = () => {
      currentTheme = themeSelect.value;
      localStorage.setItem('theme', currentTheme);
      buildCalendar();
    };

    toggleModeBtn.onclick = () => {
      document.body.classList.toggle('dark');
      const isDark = document.body.classList.contains('dark');
      toggleModeBtn.textContent = isDark ? 'Light Mode' : 'Dark Mode';
      localStorage.setItem('mode', isDark ? 'dark' : 'light');
      buildCalendar();
    };

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

ファイルはひとつだけで、中身は CSS と JavaScript を含む HTML ファイルです。 行数はおよそ 350 ですね。

少し構造を分かりやすくまとめてみます。<!-- -->で囲まれた部分に概要を記述しています。

<!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>
      <!-- レイアウトや色などのスタイル定義(CSS) 省略 -->
    </style>
  </head>
  <body>
    <header class="glass">
      <!-- タイトルやテーマカラー選択などのヘッダ部分(HTML) 省略 -->
    </header>

    <!-- カレンダー部分(HTML) 原文まま JavaScriptで構築されるため空っぽ -->
    <div class="calendar" id="calendar"></div>

    <div class="legend glass">
      <!-- 気分入力エリア(HTML) 省略 -->
    </div>

    <script>
      <!-- 各種イベント処理やカレンダー構築などのスクリプト定義(JavaScript) 省略 -->
    </script>
  </body>
</html>

今回手を加えていくのは<style>の中と<script>の中です。

style の中身をざっくり把握

ChatGPT が生成したコードはクラス名がわかりやすく、カレンダーまわりのデザインに関わってくるスタイルはこのあたりだと目星がつきました。

    <style>
      :root {
        (省略)
        --neutral-light: rgba(0, 0, 0, 0.05);
        --neutral-dark: rgba(255, 255, 255, 0.08);
      }

      (省略)

      .calendar {
        (省略)
      }
      .year-section {
        (省略)
      }
      .week-row {
        (省略)
      }
      .month-labels {
        (省略)
      }
      .month-labels span {
        (省略)
      }
      .day-col {
        (省略)
      }
      .days-with-labels {
        (省略)
      }
      .day-labels {
        (省略)
      }
      .day {
        (省略)
        background: var(--neutral-light);
        (省略)
      }
      body.dark .day {
        background: var(--neutral-dark);
      }

      (省略)

    </style>

script の中身をざっくり把握

こちらも ChatGPT の命名が分かりやすく、getColorScale() で気分の色を定義していて、 buildCalendar()でカレンダーの構築を行っていることが読み取れます。buildCalendar() 内ではループが 3 つほど入れ子になっていて、「年ループ - 週ループ - 曜日ループ」の処理をしているようです。

    <script>
      (省略)

      function getColorScale(mood, theme, isDark) {
        const palette = {
          light: {
            green:  ['#cce8da', '#98d1b6', '#63bc92', '#33a66e', '#198f56'],
            blue:   ['#d1f2f9', '#9edbf0', '#68c6e6', '#30b0dc', '#008ac1'],
            red:    ['#ffd6d6', '#ff9c9c', '#ff6c6c', '#ff3c3c', '#d10000'],
            purple: ['#e6d9f0', '#d1aee4', '#b77dd7', '#9f49cb', '#7d2ea6'],
            orange: ['#ffeacc', '#ffd099', '#ffb566', '#ff9a33', '#cc6f00'],
          },
          dark: {
            green:  ['#1a2f25', '#246647', '#2ca37a', '#34dca0', '#6fffc8'],
            blue:   ['#12333e', '#14627c', '#168bb5', '#18b1dc', '#68e0f5'],
            red:    ['#3d1e1e', '#842323', '#d33434', '#f95858', '#ffaaaa'],
            purple: ['#2f1e35', '#562b63', '#813a94', '#a856c0', '#ce9ef0'],
            orange: ['#3f2c1a', '#7c4c00', '#b36a00', '#ff8c1a', '#ffc180'],
          },
        };
        return palette[isDark ? 'dark' : 'light'][theme][mood - 1];
      }

      (省略)

      function buildCalendar() {
        (省略)

        years.forEach((year) => {
          (年単位のループ 一部省略)

          let lastMonth = -1;
          for (let d = new Date(firstSunday); d <= endDate; d.setDate(d.getDate() + 7)) {
            (週単位の横軸ループ 省略)

            for (let i = 0; i < 7; i++) {
              (週内のSun〜Sat縦軸ループ 省略)
            }

            (省略)
          }

          (省略)
        });

        (省略)
      }

      (省略)

    </style>

感覚的な部分の調整を行う

なるほど、カレンダーまわりの構造はざっくり把握できました。

ちなみにですが、(省略)となっている部分はいちいち細かく書いていたら記事が長くなりすぎるというメタ的な理由もありますが、私が現時点では中身をじっくり読んでいないことを意味しています。
もちろん必要になればじっくり読みますが、常に全文を隅々まで真面目に読むのではなく、どこでどんな処理をしているか、ざっくり把握することを重視していますよ。

それではいよいよコードの修正をしていきましょう。もし修正内容が分からなくても焦ったり不安になったりしなくて良いです。今は「こうやって調整するんだな」と雰囲気を楽しんでください。
修正後のコード全文は、ひと通り修正箇所の説明をしたあとに載せてあります。

曜日ラベルの位置ズレ解消

曜日ラベルの位置ズレに関わる部分は、style.day-labels です。
line-height で行の高さを固定し、padding-top による無駄な余白を削除することでズレを解消します。
gapfont-size にも微調整を加えてバランスを整えています。

  .day-labels {
    display: flex;
    flex-direction: column;
-   gap: 4px;
+   gap: 3px; /* 余白をやや小さく */
    margin-right: 4px;
-   font-size: 0.75rem;
+   font-size: 0.65rem; /* 曜日ラベルをやや小さく */
-   padding-top: 16px; /* 曜日ラベルの上に無駄な空白が存在していたため削除 */
+   line-height: 16px; /* 曜日ラベルの高さを固定 */
  }

月ラベルの位置ズレ解消、不要なラベルの削除

月ラベルの位置ズレ解消に関わる部分は、stylemonth-labelsmonth-labels span です。 月ラベルの要素の横幅を 16px で固定しつつ、余白の調整を行っています。
また、月ラベルが縦書きではなく横書きになるよう、折り返さないように設定しています。

  .month-labels {
    display: flex;
-   gap: 4px;
+   gap: 3px; /* 余白をやや小さく */
-   padding-left: 36px;
+   padding-left: 30px; /* 月ラベルの開始位置をカレンダーの開始位置と揃える */
-   font-size: 0.8rem;
+   font-size: 0.65rem; /* やや小さく */
-   margin-bottom: 4px; /* 不要なため削除 */
+   word-break: keep-all; /* 月ラベルを折り返さない */
  }
  .month-labels span {
-   width: 16px; /* 横幅を固定するため min-width, max-width を使用する */
+   min-width: 16px; /* ブラウザの幅で伸び縮みしないように固定する */
+   max-width: 16px; /* ブラウザの幅で伸び縮みしないように固定する */
    text-align: center;
  }

また、最初の 1 月の前にある 12 月は、実際には表示される年とは関係ないので削除します。このラベルを削除するため scriptbuildCalendar() 内の週単位ループ処理を修正しています。

  let lastMonth = -1;
  for (let d = new Date(firstSunday); d <= endDate; d.setDate(d.getDate() + 7)) {
    const col = document.createElement('div');
    col.className = 'day-col';

    const cellMonth = d.getMonth();
-   if (lastMonth !== cellMonth) {
+   const isFirstLoop = lastMonth === -1; /* 最初のループか */
+   const isMonthChanged = lastMonth !== cellMonth; /* 月が変わったか */
+   const isJanuary = cellMonth === 0; /* 1月か */
+   if (isMonthChanged && (!isFirstLoop || isJanuary)) { /* 最初の12月ラベルは表示しない */
      const label = document.createElement('span');
      label.textContent = `${cellMonth + 1}月`;
      monthLabels.appendChild(label);
-     lastMonth = cellMonth; /* if 文の外に移動する */
    } else {
      const blank = document.createElement('span');
      blank.textContent = '';
      monthLabels.appendChild(blank);
    }
+   lastMonth = cellMonth; /* if 文の条件によらず lashMonth を更新する  */

マスの横方向の余白追加

マスの横方向の余白に関わるのは、style.day-col です。
余白を追加しています。

  .day-col {
    display: flex;
    flex-direction: column;
+   gap: 3px; /* 余白を追加 */
  }

カレンダー両端の、削除されてしまったマスの復活

script の曜日ループに、違う year ならスキップする処理があるので、これを削除します。

  for (let i = 0; i < 7; i++) {
    const cellDate = new Date(d);
    cellDate.setDate(cellDate.getDate() + i);
-   if (cellDate.getFullYear().toString() !== year) continue; /* 年が異なってもスキップせず表示するため削除 */
    const key = formatDateKey(cellDate);
    const day = document.createElement('div');
    day.className = 'day';
    (省略)
  }

色味調整

カレンダーのマスのデフォルト色は :root にて定義されているので、透明度を調整します。

  :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);
-   --neutral-light: rgba(0, 0, 0, 0.05);
+   --neutral-light: rgba(0, 0, 0, 0.06); /* やや不透明に */
-   --neutral-dark: rgba(255, 255, 255, 0.08);
+   --neutral-dark: rgba(255, 255, 255, 0.06); /* やや透明に */
  }

続いて気分の色は、<script>getColorScale() に定義されているので見えづらい色を調整します。

  function getColorScale(mood, theme, isDark) {
    const palette = {
      light: {
-       green:  ['#cce8da', '#98d1b6', '#63bc92', '#33a66e', '#198f56'],
+       green:  ['#b0e8c3', '#98d1b6', '#63bc92', '#33a66e', '#198f56'],
        blue:   ['#d1f2f9', '#9edbf0', '#68c6e6', '#30b0dc', '#008ac1'],
        red:    ['#ffd6d6', '#ff9c9c', '#ff6c6c', '#ff3c3c', '#d10000'],
        purple: ['#e6d9f0', '#d1aee4', '#b77dd7', '#9f49cb', '#7d2ea6'],
        orange: ['#ffeacc', '#ffd099', '#ffb566', '#ff9a33', '#cc6f00'],
      },
      dark: {
-       green:  ['#1a2f25', '#246647', '#2ca37a', '#34dca0', '#6fffc8'],
+       green:  ['#243a2c', '#246647', '#2ca37a', '#34dca0', '#6fffc8'],
-       blue:   ['#12333e', '#14627c', '#168bb5', '#18b1dc', '#68e0f5'],
+       blue:   ['#22435e', '#24729c', '#269bd5', '#28c1fc', '#68f0ff'],
        red:    ['#3d1e1e', '#842323', '#d33434', '#f95858', '#ffaaaa'],
-       purple: ['#2f1e35', '#562b63', '#813a94', '#a856c0', '#ce9ef0'],
+       purple: ['#38234f', '#562b63', '#813a94', '#a856c0', '#ce9ef0'],
        orange: ['#3f2c1a', '#7c4c00', '#b36a00', '#ff8c1a', '#ffc180'],
      },
    };
    return palette[isDark ? 'dark' : 'light'][theme][mood - 1];
  }

その他

細かいのですが、若干月ラベルとカレンダーのマスの間の余白が大きいので、年カレンダー内の要素間 gap をカレンダーの gap と統一しておきます。プロンプト化するのが難しそうな内容なので、このタイミングで修正しています。

  .year-section {
    display: flex;
    flex-direction: column;
-   gap: 0.5rem;
+   gap: 3px; /* カレンダーの gap と統一 */
  }

今回の調整は以上です。

ソースコード全文も折りたたみで載せておきます。

ソースコード全文はこちら
<!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);
        --neutral-light: rgba(0, 0, 0, 0.06);
        --neutral-dark: rgba(255, 255, 255, 0.06);
      }

      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: 3px;
      }
      .week-row {
        display: flex;
        overflow-x: auto;
        gap: 4px;
        position: relative;
      }
      .month-labels {
        display: flex;
        gap: 3px;
        padding-left: 30px;
        font-size: 0.65rem;
        word-break: keep-all;
      }
      .month-labels span {
        min-width: 16px;
        max-width: 16px;
        text-align: center;
      }
      .day-col {
        display: flex;
        flex-direction: column;
        gap: 3px;
      }
      .days-with-labels {
        display: flex;
        gap: 3px;
      }
      .day-labels {
        display: flex;
        flex-direction: column;
        gap: 3px;
        margin-right: 4px;
        font-size: 0.65rem;
        line-height: 16px;
      }
      .day {
        width: 16px;
        height: 16px;
        border-radius: 3px;
        cursor: pointer;
        background: var(--neutral-light);
        box-sizing: border-box;
        transition:
          transform 0.1s,
          border 0.2s;
      }
      body.dark .day {
        background: var(--neutral-dark);
      }
      .day:hover {
        transform: scale(1.3);
      }
      .day.selected {
        border: 2px solid #000;
      }
      body.dark .day.selected {
        border: 2px solid #fff;
      }
      .legend {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 0.5rem;
        margin: 1rem;
      }
      .mood-row {
        display: flex;
        gap: 0.5rem;
      }
      .mood-icon {
        font-size: 2rem;
        cursor: pointer;
        opacity: 0.6;
      }
      .mood-icon.active {
        opacity: 1;
      }
      .theme-selector {
        margin-left: auto;
      }
      #selectedDateDisplay {
        font-size: 1rem;
        font-weight: bold;
      }
    </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">
      <div id="selectedDateDisplay">日付を選択してください</div>
      <div class="mood-row">
        <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>
    </div>

    <script>
      const calendarEl = document.getElementById('calendar');
      const themeSelect = document.getElementById('themeColor');
      const toggleModeBtn = document.getElementById('toggleMode');
      const moodIcons = document.querySelectorAll('.mood-icon');
      const selectedDateDisplay = document.getElementById('selectedDateDisplay');

      let currentTheme = localStorage.getItem('theme') || 'green';
      let selectedDate = new Date().toISOString().split('T')[0];
      let isDarkMode = localStorage.getItem('mode') === 'dark';

      if (isDarkMode) document.body.classList.add('dark');
      themeSelect.value = currentTheme;
      toggleModeBtn.textContent = isDarkMode ? 'Light Mode' : 'Dark Mode';

      let moodData = JSON.parse(localStorage.getItem('moodData') || '{}');

      function getColorScale(mood, theme, isDark) {
        const palette = {
          light: {
            green:  ['#b0e8c3', '#98d1b6', '#63bc92', '#33a66e', '#198f56'],
            blue:   ['#d1f2f9', '#9edbf0', '#68c6e6', '#30b0dc', '#008ac1'],
            red:    ['#ffd6d6', '#ff9c9c', '#ff6c6c', '#ff3c3c', '#d10000'],
            purple: ['#e6d9f0', '#d1aee4', '#b77dd7', '#9f49cb', '#7d2ea6'],
            orange: ['#ffeacc', '#ffd099', '#ffb566', '#ff9a33', '#cc6f00'],
          },
          dark: {
            green:  ['#243a2c', '#246647', '#2ca37a', '#34dca0', '#6fffc8'],
            blue:   ['#22435e', '#24729c', '#269bd5', '#28c1fc', '#68f0ff'],
            red:    ['#3d1e1e', '#842323', '#d33434', '#f95858', '#ffaaaa'],
            purple: ['#38234f', '#562b63', '#813a94', '#a856c0', '#ce9ef0'],
            orange: ['#3f2c1a', '#7c4c00', '#b36a00', '#ff8c1a', '#ffc180'],
          },
        };
        return palette[isDark ? 'dark' : 'light'][theme][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.includes(new Date().getFullYear().toString())) years.push(new Date().getFullYear().toString());
        years = years.sort((a, b) => b - a);

        const dayLabels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

        years.forEach((year) => {
          const section = document.createElement('div');
          section.className = 'year-section';
          section.innerHTML = `<h2>${year}</h2>`;

          const monthLabels = document.createElement('div');
          monthLabels.className = 'month-labels';

          const weekRow = document.createElement('div');
          weekRow.className = 'week-row';

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

          const labelDays = document.createElement('div');
          labelDays.className = 'day-labels';
          dayLabels.forEach((d) => {
            const label = document.createElement('div');
            label.textContent = d;
            labelDays.appendChild(label);
          });

          const dayWrapper = document.createElement('div');
          dayWrapper.className = 'days-with-labels';
          dayWrapper.appendChild(labelDays);

          let lastMonth = -1;
          for (let d = new Date(firstSunday); d <= endDate; d.setDate(d.getDate() + 7)) {
            const col = document.createElement('div');
            col.className = 'day-col';

            const cellMonth = d.getMonth();
            const isFirstLoop = lastMonth === -1;
            const isMonthChanged = lastMonth !== cellMonth;
            const isJanuary = cellMonth === 0;
            if (isMonthChanged && (!isFirstLoop || isJanuary)) {
              const label = document.createElement('span');
              label.textContent = `${cellMonth + 1}月`;
              monthLabels.appendChild(label);
            } else {
              const blank = document.createElement('span');
              blank.textContent = '';
              monthLabels.appendChild(blank);
            }
            lastMonth = cellMonth;

            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';

              const mood = moodData[key];
              const isDark = document.body.classList.contains('dark');
              if (mood) {
                day.style.background = getColorScale(mood, currentTheme, isDark);
              }
              if (selectedDate === key) {
                day.classList.add('selected');
              }

              day.title = key;
              day.onclick = () => openMoodSelector(key);
              col.appendChild(day);
            }
            dayWrapper.appendChild(col);
          }

          section.appendChild(monthLabels);
          section.appendChild(dayWrapper);
          calendarEl.appendChild(section);
        });

        selectedDateDisplay.textContent = selectedDate;
      }

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

      themeSelect.onchange = () => {
        currentTheme = themeSelect.value;
        localStorage.setItem('theme', currentTheme);
        buildCalendar();
      };

      toggleModeBtn.onclick = () => {
        document.body.classList.toggle('dark');
        const isDark = document.body.classList.contains('dark');
        toggleModeBtn.textContent = isDark ? 'Light Mode' : 'Dark Mode';
        localStorage.setItem('mode', isDark ? 'dark' : 'light');
        buildCalendar();
      };

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

カレンダーが整った

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

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

── カレンダー部分の見た目が整いましたね!まさにかゆいところに手が届いた感じです。

さて、人の手での調整はいかがでしたか?
修正した箇所はもしかしたら多く見えたかもしれませんが、ロジックに関する部分の修正はわずかだったと思います。
AI が作ってくれたソースに対し、人の手で細かい調整を行う、というイメージが少し湧いたでしょうか。

人間と AI の役割分担

今回は ChatGPT が生成したカレンダーのコードに対して、人の手を介入させて細かな調整を行いました。 テーマカラーの色味や、曜日・月ラベルの位置、マスの配置など、いずれも「感覚的な違和感」を解消するためのものでした。

こういった部分は、プロンプトで正確に伝えることが難しく、AI に頼りすぎると修正のループに陥る可能性もあります。 一方、構造やロジックを AI に任せて、人間は仕上げの「見た目」や「心地よさ」を調整する——これは非常に現実的で、実践的な役割分担です。

このようにして AI との共創の形が少しずつ見えてきたのではないでしょうか。

そして次回は AI の出番です。人の手で整えたソースに、さらに磨きをかけてもらいましょう。残っている課題への対処と、見た目のブラッシュアップを AI に依頼してみますよ。

おまけ: ChatGPT に最新のソースコードを渡しておく

今回、人の手でソースコードに修正を加えましたが、まだこのことを ChatGPT は知りません。 しっかりと ChatGPT にもどんな修正をしたのか共有しておきましょう。

次回、古いソースに対して更新をかけられても困りますからね。

私は以下のプロンプトを送信しました。ChatGPT には canvas というコード共有・保持領域があり、その領域にソースコードを保持しているので、そちらにもちゃんと反映してもらうようにしましょう。

私は一度目のプロンプトで canvas への反映を指示し忘れていたので 2 つプロンプトを送信しています。
みなさんも ChatGPT にコードを渡すときは、意図したソースがきちんと反映されているか、念のため canvas にアップデートされたかを確認しておくと安心ですよ。

  • 1 回目のプロンプト
作成してもらった「MOOD TRACKER」アプリに、一部細かい調整を行いました。主な修正点は以下の通りです。
・マスのデフォルト色や、気分の色味の調整
・曜日ラベルの配置ズレの修正
・月ラベルの配置ズレの修正及び不要なラベルの削除
・年末年始の境界で、異なる年のマスも表示するように修正
・マスの横方向の余白追加

以後の調整や改善は以下のコードに対して行ってください。

<<ソースコード全文(省略)>>
  • 2回目のプロンプト
以下のコードを、canvasにもそのまま反映してください。
※このコードを一切変更せずにそのままアップデートしてください。

<<ソースコード全文(省略)>>