思ってたのと違う!? AIに本当に“理想のアプリ”を作らせる方法①

当ページのリンクには広告が含まれています。
  • URLをコピーしました!
残念おパパ

AIにアプリ作ってって言えば、勝手に動くやつ出してくれるんでしょ?

──そう思っていた頃もありました。

え、これ動かないんだけど!?

ボタンどこ!?

とか、そんなカオスな結果になることもしばしば。

これ、AIが悪いんじゃなくて、こちらの伝え方がふわっとしてただけなんだと。

ということで、本シリーズでは、残念パパいのっちが2年かけて学んだ「AIに理想のアプリを作らせるコツ」を、失敗談まじりでゆる〜く解説します。

目次

AIに頼んだら「思ってたのと違う」現象

残念パパ

JavaScript、HTML、CSSで家計簿アプリを作ってください!

作りました!右側のキャンバスでプレビューできます。
ローカル保存(localStorage)、月フィルタ・検索、カテゴリ追加、収入/支出の自動集計、CSV/JSONの入出力、そして簡易グラフ(今月のカテゴリ別・月次推移)まで入っています。

残念パパ

流石、ChatGPT仕事早いな・・・どれ

ダークモードの家計簿が完成・・・

なぜ、ダークモード?

本当に動作しません。悲しい・・・

入力して登録ボタンを押したが、カテゴリを選ばないと登録でないみたいですが、そもそもカテゴリを登録する機能がない・・・。

……いや、これ、雰囲気家計簿だけど動かないし。

ボタンを押したら、エラーが出たり、レイアウトが思ってるのと違ったり、酷いとそもそも画面すら表示されなかったりします。

残念パパ

AI、全然わかってないじゃん!

……と、若かりし頃は思ったんですが、全然分かってなかったのは当方の方なんです。

だって、AIは人類の叡智の結晶かもしれませんが、エスパーではないんですよ。

曖昧すぎるとAIも困る

家計簿アプリをかっこいい感じで開発して
シンプルなデザインで家計簿作って
サクッと動く家計簿アプリを頼む

 ──これ、実はAIを一番混乱させる言葉なんです。

なぜかというと、「かっこいい」も「シンプル」も、人によって基準が違うわけで。

AI

ふむ、かっこいい……?

と考えた結果、背景が真っ黒で、文字がネオンブルーに光る家計簿を出してきたりすることがあります。

残念パパ

──夜のクラブか!

……ってツッコミたくなりますが、AIからすれば「かっこいい」要素をちゃんと入れたつもりなんですよ。

だからAIにとって大事なのは「感覚」ではなく「条件」。

「色は白と青を基調に」
「ボタンを押したら金額を足す」

といった具体的な指示がないと、正しく動けません。

次の章では、この「曖昧」をどうやって「具体」に変えていくかを、実例を交えて紹介します。

AIを“誤解させない”コツ、ここからが本番です。

曖昧を具体に変えてAIに指示する方法

じゃあ、どうやって伝えればいいの?

──ここが今回の一番のポイントです。

AIにアプリを作ってもらうときは、“お願い”ではなく“設計書”を渡すつもりで話すのがコツです。

とはいえ、いきなり設計書なんて書けませんよね。

そこで、まずは「誰が」「何を」「どう使う」の3点だけ、ざっくり決めましょう。

AIへの曖昧な指示の例(悪い例)

家計簿アプリを作ってください。
シンプルで見やすいやつがいいです。

この時点でAIの脳内はこうです。

AI

家計簿……OK。シンプル……つまり、要素少なめ? 見やすいって何? うーん、まあそれっぽく?

結果、雰囲気家計簿が再び登場します。

具体的な指示の例(良い例)

JavaScriptで家計簿アプリを作ってください。
・日付、金額、メモを入力できるフォームを作る
・追加ボタンを押すと下にリスト表示する
・合計金額を上に表示する
・見た目は白背景に青いボタンで、スマホでも操作しやすい感じに

ここまで伝えると、AIはようやく正確に動けます。
もはや「かっこよく」なんて言わなくても、自然に“ちゃんとした家計簿”が出てきます。
なぜなら、AIにとって必要なのは「雰囲気」ではなく「仕様」だからです。

いのっち流・AIへの伝え方テンプレ

  1. 目的を言う「私の家計の支出を記録したい」
  2. 機能を言う「日付・金額・メモを入力できる」
  3. 動作を言う「ボタンを押すとリストに追加される」
  4. 見た目を言う「白ベースで、スマホでも見やすく」

たったこれだけで、AIの精度が3倍くらい変わります。

たとえば、「温かみのある色合いで」+「ベージュ系で」と添えるだけで、AIはちゃんと方向性を理解してくれます。

実際にテンプレに従ってAIに指示をしてみた

今回のケースでは以下のようなプロンプトを伝えると効果的です。当サイトはJavaScriptメインのサイトなので、「HTML・CSS・JavaScript」で開発指示と、スマホ対応(レスポンシブ対応)というキーワードを足していますが、テンプレ通り指示を流してみました。

【家計簿アプリ作成のためのプロンプト】

HTML・CSS・JavaScriptで家計簿アプリを作ってください。
・日付・金額・メモ・入出金(支出/入金)を入力できるフォームを作る
・追加ボタンでリストに追記 ・合計金額を上に表示
・白背景+青ボタンでスマホ対応(レスポンシブ対応)

【出力された家計簿アプリのソースコード】

ソースコードはこちらをタップ
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>家計簿アプリ</title>
  <style>
    :root{
      --blue:#1e40af;      /* ボタン用の深い青 */
      --blue-600:#2563eb;  /* ホバー時 */
      --blue-700:#1d4ed8;
      --text:#111827;
      --muted:#6b7280;
      --bg:#ffffff;
      --red:#dc2626;
      --green:#059669;
      --border:#e5e7eb;
      --shadow: 0 8px 24px rgba(0,0,0,.08);
    }
    *{box-sizing:border-box}
    html,body{height:100%}
    body{
      margin:0; font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans JP", "Apple Color Emoji", "Segoe UI Emoji";
      color:var(--text); background:var(--bg);
    }
    .wrap{max-width: 760px; margin: 0 auto; padding: 16px;}

    header{
      position: sticky; top: 0; background: var(--bg); z-index: 5; padding: 12px 0 16px; backdrop-filter: blur(4px);
    }
    .total-card{
      border:1px solid var(--border); border-radius: 16px; padding: 16px 20px; box-shadow: var(--shadow);
      display:flex; align-items:center; justify-content:space-between; gap:12px;
    }
    .total-label{font-size:14px; color:var(--muted)}
    .total-amount{font-size: 32px; font-weight: 800; letter-spacing: .02em}

    .card{border:1px solid var(--border); border-radius:16px; padding:16px; box-shadow: var(--shadow); margin-top:16px}
    .card h2{margin:0 0 12px; font-size:18px}

    /* 入力フォーム */
    .form-grid{display:grid; gap:12px; grid-template-columns: 1fr 1fr;}
    .form-row{display:flex; flex-direction:column; gap:6px}
    .form-row label{font-size:12px; color:var(--muted)}
    input[type="date"], input[type="number"], input[type="text"], select{
      border:1px solid var(--border); border-radius:12px; padding:12px; font-size:16px; width:100%;
    }
    .actions{display:flex; gap:8px; align-items:end}
    .btn{
      appearance:none; border:0; border-radius:999px; padding:12px 18px; font-weight:700; font-size:16px; cursor:pointer;
      background: var(--blue); color:white; box-shadow: 0 2px 0 rgba(0,0,0,.08);
    }
    .btn:hover{ background: var(--blue-600)}
    .btn:active{ transform: translateY(1px)}
    .btn.ghost{ background:#f3f4f6; color:#111827}

    /* リスト */
    .table-wrap{overflow-x:auto; margin-top:8px}
    table{width:100%; border-collapse:collapse; min-width: 560px}
    thead th{font-size:12px; color:var(--muted); text-align:left; padding:10px 8px; border-bottom:1px solid var(--border);}
    tbody td{padding:12px 8px; border-bottom:1px solid var(--border)}
    tbody tr:hover{background:#fafafa}
    .amount{font-variant-numeric: tabular-nums; font-weight:700}
    .amount.income{color:var(--green)}
    .amount.expense{color:var(--red)}

    /* スマホ最適化 */
    @media (max-width: 640px){
      .form-grid{ grid-template-columns: 1fr;}
      .actions{ justify-content:flex-end }
      .total-amount{ font-size: 26px }
    }

    /* アクセシビリティ */
    .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
  </style>
</head>
<body>
  <div class="wrap">
    <header>
      <div class="total-card" role="status" aria-live="polite">
        <div>
          <div class="total-label">合計金額(入金 - 支出)</div>
          <div id="total" class="total-amount">¥0</div>
        </div>
        <button id="clearAll" class="btn ghost" title="すべて削除">全削除</button>
      </div>
    </header>

    <section class="card" aria-labelledby="formTitle">
      <h2 id="formTitle">新規入力</h2>
      <form id="entryForm" class="form-grid">
        <div class="form-row">
          <label for="date">日付</label>
          <input id="date" name="date" type="date" required />
        </div>
        <div class="form-row">
          <label for="type">入出金</label>
          <select id="type" name="type" required>
            <option value="expense">支出</option>
            <option value="income">入金</option>
          </select>
        </div>
        <div class="form-row">
          <label for="amount">金額(円)</label>
          <input id="amount" name="amount" type="number" inputmode="numeric" step="1" min="1" placeholder="1000" required />
        </div>
        <div class="form-row">
          <label for="memo">メモ</label>
          <input id="memo" name="memo" type="text" maxlength="120" placeholder="昼食、給料など" />
        </div>
        <div class="actions">
          <button class="btn" type="submit">追加</button>
        </div>
      </form>
    </section>

    <section class="card" aria-labelledby="listTitle">
      <h2 id="listTitle">履歴</h2>
      <div class="table-wrap">
        <table aria-describedby="listDesc">
          <caption id="listDesc" class="sr-only">入力した家計簿の一覧</caption>
          <thead>
            <tr>
              <th scope="col">日付</th>
              <th scope="col">入出金</th>
              <th scope="col">金額</th>
              <th scope="col">メモ</th>
              <th scope="col">操作</th>
            </tr>
          </thead>
          <tbody id="list"></tbody>
        </table>
      </div>
    </section>
  </div>

  <script>
    (function(){
      const $ = (sel) => document.querySelector(sel);
      const $$ = (sel) => document.querySelectorAll(sel);
      const form = $('#entryForm');
      const listEl = $('#list');
      const totalEl = $('#total');
      const clearBtn = $('#clearAll');
      const fmt = new Intl.NumberFormat('ja-JP', { style:'currency', currency:'JPY', maximumFractionDigits:0 });

      /** @type {{id:string,date:string,type:'income'|'expense',amount:number,memo:string}[]} */
      let entries = [];

      // ---- LocalStorage ----
      const LS_KEY = 'kakeibo_entries_v1';
      const save = () => localStorage.setItem(LS_KEY, JSON.stringify(entries));
      const load = () => {
        try {
          const raw = localStorage.getItem(LS_KEY);
          entries = raw ? JSON.parse(raw) : [];
        } catch(e){ entries = []; }
      }

      // ---- Rendering ----
      function render(){
        renderTotal();
        renderList();
      }

      function renderTotal(){
        const income = entries.filter(e=>e.type==='income').reduce((s,e)=>s+e.amount,0);
        const expense = entries.filter(e=>e.type==='expense').reduce((s,e)=>s+e.amount,0);
        const total = income - expense;
        totalEl.textContent = fmt.format(total);
      }

      function renderList(){
        if(entries.length===0){
          listEl.innerHTML = `<tr><td colspan="5" style="color:var(--muted); padding:20px">まだデータがありません。フォームから追加してください。</td></tr>`;
          return;
        }
        // 日付降順で表示
        const sorted = [...entries].sort((a,b)=> (b.date||'').localeCompare(a.date||''));
        listEl.innerHTML = sorted.map(e=>{
          const typeLabel = e.type==='income' ? '入金' : '支出';
          const cls = e.type==='income' ? 'income' : 'expense';
          return `<tr data-id="${e.id}">
            <td>${e.date||''}</td>
            <td>${typeLabel}</td>
            <td class="amount ${cls}">${fmt.format(e.amount)}</td>
            <td>${escapeHtml(e.memo||'')}</td>
            <td><button class="btn ghost btn-del" aria-label="この行を削除">削除</button></td>
          </tr>`
        }).join('');
      }

      function escapeHtml(str){
        return String(str).replace(/[&<>"]/g, s=>({ '&':'&','<':'<','>':'>','"':'"' }[s] ));
      }

      // ---- Handlers ----
      form.addEventListener('submit', (ev)=>{
        ev.preventDefault();
        const date = /** @type {HTMLInputElement} */(document.getElementById('date')).value;
        const type = /** @type {HTMLSelectElement} */(document.getElementById('type')).value;
        const amountRaw = /** @type {HTMLInputElement} */(document.getElementById('amount')).value;
        const memo = /** @type {HTMLInputElement} */(document.getElementById('memo')).value.trim();

        const amount = Number(amountRaw);
        if(!date){ alert('日付を入力してください'); return; }
        if(!(amount>0)){ alert('金額は1円以上で入力してください'); return; }
        if(type !== 'income' && type !== 'expense'){ alert('入出金を選択してください'); return; }

        const id = String(Date.now()) + Math.random().toString(36).slice(2,7);
        entries.push({ id, date, type, amount, memo });
        save();
        render();
        form.reset();
        // 直感的な入力順のため日付は維持
        document.getElementById('date').value = date;
        document.getElementById('type').value = 'expense';
      });

      listEl.addEventListener('click', (e)=>{
        const btn = e.target.closest('.btn-del');
        if(!btn) return;
        const tr = btn.closest('tr');
        const id = tr?.getAttribute('data-id');
        if(!id) return;
        if(confirm('この行を削除しますか?')){
          entries = entries.filter(x=>x.id!==id);
          save();
          render();
        }
      });

      clearBtn.addEventListener('click', ()=>{
        if(entries.length===0) return;
        if(confirm('全てのデータを削除しますか?この操作は取り消せません。')){
          entries = [];
          save();
          render();
        }
      });

      // 初期化
      (function init(){
        // 既定で今日
        const today = new Date();
        const yyyy = today.getFullYear();
        const mm = String(today.getMonth()+1).padStart(2,'0');
        const dd = String(today.getDate()).padStart(2,'0');
        document.getElementById('date').value = `${yyyy}-${mm}-${dd}`;

        load();
        render();
      })();
    })();
  </script>
</body>
</html>

以下が実際にこの指示で出来上がってきた家計簿アプリです。

試しに支出・入金をそれぞれ入れてみました。ちゃんと計算もしてくれます。

因みにプロンプトに何も書いていませんが、データの自動保存もしてくれるようです。

こういうところがAI凄いですよね。

凄い気が利く!

いろいろと足りない機能がありますが、ここから肉付けしてもいいですし、初めからある程度の条件を付けて書いていくのも悪くないですよ。

余談ですが、プロンプトをふんわりとしか伝えられないあなたは人間相手でも同じことをしていませんか?

生成AIも人間も、「やって見せ、言って聞かせてさせてみて、褒めてやらねば、動かじ」ですよ。

さて、今回はここまで!

こちらはシリーズものなので、数回に分けて説明をしていきます。

残念パパいのっちでした!

では、また!

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次