AI 君が労ってくれる Todo 管理アプリを作成しました。

こんにちは。小売業で働いている takuma です。2回目の投稿になります。
前回に引き続き今回もまた、業務に役立ちそうなツールの作成に**チャレンジ**してみました。

前回の記事はこちら

https://qiita.com/07191970t/items/f4c8a88e1a56118226af

## 仕事が増えてしまいました…。
**「最近忙しい！！」** 私ごとですが春から異動、昇進しやらなくてはいけない業務が増えました。
私はこれまで裏紙などにやらなければならない業務を書いて、終わったら斜線で消してを繰り返していたのですが、最近その業務が多くなり、いちいち書くのか面倒になってしまいました。

そこで簡単に業務を登録できて、締切順や優先度順ですぐに並び替えできるようなものがあったらなと考えていたのですが、どうやら **ChatGPT** に作りたいアプリの概要を送ればコードを書き出してくれるらしい…。
というわけで今回はそれを利用して **Todo 管理アプリ**を作成しました！

**使用画面**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4080274/34e2986b-edc8-4d2f-824e-6229cf427d27.png)
**使用方法**
・左上で業務を入力し、日付と優先度（高、中、低）を選択し追加ボタンを押すと、下の欄に業務が追加されます。
・完了した場合は左の完了を押すと業務に斜線が入ります。
・右の編集、削除ボタンで登録済の業務の編集、削除ができます。
・指示して AI くんボタンを押すと優先度を考慮しやるべき業務TOP5を選出してくれます。あと労ってくれます。

## 使ったツール・技術
**・ChatGPT**：アプリのコード作成・修正、アイデアの壁打ち

**・Dify**：AI によるタスク優先度の指示機能を実装（Gemini API を利用）

**・CodePen**：試作・動作確認

**・HTMLファイル**：外部 API 制限の回避・最終的なアプリ動作環境
## ChatGPTでプロトタイプ開発
まず、ChatGPT に以下のプロンプトを送信してアプリの原型コードを生成してもらいました。

![スクリーンショット 2025-06-09 234820.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4080274/ba55e394-fe72-4a02-aa6b-a55ad89dc85f.png)

そこでもらったコードを CodePen に入力してみると

![スクリーンショット 2025-06-10 224755.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4080274/cea99923-c0a0-40b6-a65b-67ac911a68b7.png)

一回でなかなか使いやすいものができました！
こんな**簡単**にアプリができるとは…

ただこれだと一度登録した業務が編集、削除できないため褒め言葉とともに改善要望を ChatGPT に伝えると…

![スクリーンショット 2025-06-09 235319.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4080274/12a798b7-16aa-4724-9dfa-1d40a55cfaf4.png)
しっかり要望通りのものができました！
（先ほどのメッセージの後に締切3日前のものを赤文字表示する要望も伝えました。）

![スクリーンショット 2025-06-10 230030.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4080274/ffcea03f-b3e3-41c8-aa81-04b9f82c87ba.png)
ここまでなんと**30分！** コードの知識なんて全くない私がこんな簡単に、一瞬でアプリを作ってしまいました…。
## Dify と連携して AI 指示を受け取る
次に**タスクが多いときに AI が優先度 TOP5 を返してくれる機能**を **Dify** を利用して追加します。

:::note info
ここで Dify の外部 API リクエストを行うため、アプリをローカルの HTML ファイルに移行しました。
:::
 
まず ChatGPT に以下のように指示し、先ほど作成したアプリに Dify に Todo の情報を伝えるための機能を追加し、Dify API と連携させるコードを生成してもらいました。

![スクリーンショット 2025-06-10 001744.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4080274/da1eeb5e-2f9f-42bc-8c97-8e44e48b3a4c.png)
<details><summary>ChatGPTが送ってくれたコード</summary>

`"YOUR_DIFY_API_KEY"`と書いてあるところを自身が用意した API キーに書き換えます。API キーの取得は後程…
```html:コード
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>シンプルToDoリスト</title>
  <style>
    body { font-family: sans-serif; margin: 32px; background: #f9f9f9; }
    h1 { font-size: 1.5em; }
    .todo-inputs input, .todo-inputs select { margin-right: 10px; padding: 4px; }
    .todo-list { margin-top: 24px; }
    .todo-item { background: #fff; padding: 10px 12px; margin-bottom: 8px; border-radius: 8px; display: flex; align-items: center; box-shadow: 0 1px 2px #ddd;}
    .todo-item.done { opacity: 0.5; text-decoration: line-through; }
    .todo-main { flex: 1; }
    .todo-meta { font-size: 0.9em; color: #666; margin-top: 4px;}
    button.status-btn { margin-right: 8px; }
    button.delete-btn, button.edit-btn { margin-left: 8px; }
    .sort-select { margin-bottom: 16px; }
    .todo-content { font-weight: bold; font-size: 1.1em; }
    .ai-section { margin-top: 32px; padding: 16px; background: #fff; border-radius: 8px; box-shadow: 0 1px 2px #ddd;}
    #aiSummary { white-space: pre-wrap; margin-top: 12px; color: #007c1e; font-weight: bold;}
  </style>
</head>
<body>
  <h1>ToDoリスト</h1>
  <div class="todo-inputs">
    <input type="text" id="content" placeholder="ToDo内容">
    <input type="date" id="deadline">
    <select id="priority">
      <option value="高">高</option>
      <option value="中" selected>中</option>
      <option value="低">低</option>
    </select>
    <button id="addBtn" onclick="addOrUpdateTodo()">追加</button>
    <button id="cancelEditBtn" onclick="cancelEdit()" style="display:none;">キャンセル</button>
  </div>

  <div class="sort-select">
    並び替え: 
    <select id="sortBy" onchange="renderTodos()">
      <option value="priority">優先度順</option>
      <option value="created">登録日順</option>
      <option value="deadline">締切順</option>
    </select>
  </div>

  <div class="todo-list" id="todoList"></div>

  <!-- AIくんの指示エリア -->
  <div class="ai-section">
    <button onclick="instructAi()" style="font-size:1.1em;padding:7px 18px;background:#29b564;color:white;border:none;border-radius:7px;">
      指示してAIくん
    </button>
    <div id="aiSummary"></div>
  </div>

  <script>
    // localStorageからデータ読み込み
    let todos = JSON.parse(localStorage.getItem("todos") || "[]");
    let editingIndex = null; // 編集中のインデックス

    function saveTodos() {
      localStorage.setItem("todos", JSON.stringify(todos));
    }

    function addOrUpdateTodo() {
      const content = document.getElementById('content').value.trim();
      const deadline = document.getElementById('deadline').value;
      const priority = document.getElementById('priority').value;
      if (!content) {
        alert('内容を入力してください');
        return;
      }
      if (editingIndex === null) {
        // 新規追加
        const createdAt = new Date().toISOString();
        todos.push({
          content,
          deadline,
          priority,
          createdAt,
          done: false
        });
      } else {
        // 編集
        todos[editingIndex].content = content;
        todos[editingIndex].deadline = deadline;
        todos[editingIndex].priority = priority;
        editingIndex = null;
        document.getElementById("addBtn").innerText = "追加";
        document.getElementById("cancelEditBtn").style.display = "none";
      }
      saveTodos();
      document.getElementById('content').value = '';
      document.getElementById('deadline').value = '';
      document.getElementById('priority').value = '中';
      renderTodos();
    }

    function toggleDone(index) {
      todos[index].done = !todos[index].done;
      saveTodos();
      renderTodos();
    }

    function deleteTodo(index) {
      if (confirm("本当に削除しますか？")) {
        todos.splice(index, 1);
        saveTodos();
        renderTodos();
        cancelEdit();
      }
    }

    function editTodo(index) {
      document.getElementById('content').value = todos[index].content;
      document.getElementById('deadline').value = todos[index].deadline;
      document.getElementById('priority').value = todos[index].priority;
      editingIndex = index;
      document.getElementById("addBtn").innerText = "更新";
      document.getElementById("cancelEditBtn").style.display = "";
    }

    function cancelEdit() {
      editingIndex = null;
      document.getElementById('content').value = '';
      document.getElementById('deadline').value = '';
      document.getElementById('priority').value = '中';
      document.getElementById("addBtn").innerText = "追加";
      document.getElementById("cancelEditBtn").style.display = "none";
    }

    function getPriorityValue(p) {
      return p === "高" ? 1 : (p === "中" ? 2 : 3);
    }

    // 締切が3日以内なら赤く
    function getDeadlineStyle(deadline, done) {
      if (!deadline || done) return "";
      const today = new Date();
      const end = new Date(deadline);
      today.setHours(0,0,0,0);
      end.setHours(0,0,0,0);
      const diffDays = Math.ceil((end - today) / (1000 * 60 * 60 * 24));
      if (diffDays >= 0 && diffDays <= 3) {
        return "color:red; font-weight:bold;";
      }
      return "";
    }

    function renderTodos() {
      const sortBy = document.getElementById('sortBy').value;
      let sorted = [...todos];
      if (sortBy === 'priority') {
        sorted.sort((a, b) => getPriorityValue(a.priority) - getPriorityValue(b.priority));
      } else if (sortBy === 'created') {
        sorted.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
      } else if (sortBy === 'deadline') {
        sorted.sort((a, b) => {
          if (!a.deadline) return 1;
          if (!b.deadline) return -1;
          return new Date(a.deadline) - new Date(b.deadline);
        });
      }
      const list = sorted.map((todo, i) => {
        const originalIndex = todos.indexOf(todo);
        return `
        <div class="todo-item${todo.done ? ' done' : ''}">
          <button class="status-btn" onclick="toggleDone(${originalIndex})">
            ${todo.done ? '未完了' : '完了'}
          </button>
          <div class="todo-main">
            <div class="todo-content">${todo.content}</div>
            <div class="todo-meta">
              締切: <span style="${getDeadlineStyle(todo.deadline, todo.done)}">${todo.deadline || 'なし'}</span>
              | 優先度: ${todo.priority}
              | 登録日: ${todo.createdAt.slice(0, 10)}
            </div>
          </div>
          <button class="edit-btn" onclick="editTodo(${originalIndex})">編集</button>
          <button class="delete-btn" onclick="deleteTodo(${originalIndex})">削除</button>
        </div>
      `;
      }).join('');
      document.getElementById('todoList').innerHTML = list || "<div>ToDoがありません</div>";
    }

    // --- Dify AI連携：指示してAIくん ---
    async function instructAi() {
      const todos = JSON.parse(localStorage.getItem("todos") || "[]");
      const notDoneTodos = todos.filter(t => !t.done);
      if (notDoneTodos.length === 0) {
        document.getElementById('aiSummary').innerText = "未完了のタスクがありません！";
        return; }     
      const apiKey = "YOUR_DIFY_API_KEY"; // ←ここにDify APIキーを入れてね
      const url = "https://api.dify.ai/v1/chat-messages";
      document.getElementById('aiSummary').innerText = "AIくんが考え中...";

      // todo_listをJSON文字列で送る！
      const todoListText = JSON.stringify(notDoneTodos);

      const prompt =
        "todo_listには未完了ToDo項目のJSON文字列が入っています。内容・締切・優先度を考慮して「今やるべき上位5件と理由」を日本語で指示してください。必ず以下のフォーマットで出力してください：\n1. タスク内容（理由: ～）\n2. ...\ntodo_list:";

      try {
        const res = await fetch(url, {
          method: "POST",
          headers: {
            "Authorization": `Bearer ${apiKey}`,
            "Content-Type": "application/json"
          },
          body: JSON.stringify({
            inputs: { todo_list: todoListText },  // JSON文字列として送る！
            query: prompt,
            user: "user1"   // 
          })
        });
        const data = await res.json();
        document.getElementById('aiSummary').innerText =
          data.answer || data.message || "AIの指示が取得できませんでした";
      } catch (e) {
        document.getElementById('aiSummary').innerText = "AIへの接続に失敗しました";
      }
    }

    // 初期表示
    renderTodos();
  </script>
</body>
</html>
```
</details>

**指示して AI くんボタン**が追加されました！
![スクリーンショット 2025-06-10 233734.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4080274/d51bb0df-d633-45eb-ae22-c8765e6c1724.png)

続いて Dify 側の設定を行います。

Dify の概要や下準備などはこちらの記事を参考にしてください！

https://qiita.com/comware_higuchi/items/d3f489c288a06fab2973

**Dify の全体像**
![スクリーンショット 2025-06-10 235526.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4080274/75876201-f8d4-4c0a-ba8f-e2a23547ebdf.png)
**シンプル！**


まず Dify トップ画面から**スタジオ**タブで**アプリを作成**→**最初から作成**
アプリタイプは**チャットフロー**を選択しを名前入力して作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4080274/2d7e192f-5de8-4e52-9e3d-a962a949b43c.png)
続いてノードの設定です。
**開始ノード**で入力フィールドを追加します。
**変数名**はアプリから送るデータのキー名と同じものを入力（ここでは todo_list ）**フィールドタイプ**は大量のテキストを受け取ることができる「段落」を設定します。**最大長**はアプリから送るデータの文字数に対応できるように調整します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4080274/7711101c-7e9e-4081-8b66-99c018f29d89.png)

続いて **LLM ノード**の設定です。
今回、 AI モデルは **Gemini.2.0 Flash** を選択しました。
プロンプト内容は画像の通りです。
応答が Todo リストだけでは寂しかったため、労ってくれるようにしました。

![スクリーンショット 2025-06-11 002321.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4080274/1cacf3c8-50a2-4816-89c5-28b1cc5ae047.png)


**回答ノード**の設定はこれだけです！

![スクリーンショット 2025-06-11 005714.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4080274/3cc529ec-df73-43a4-9e88-446d3ab94600.png)

各ノードの設定が終わったら、画面右上の**公開する**ボタンを押しアプリをデプロイします。
最後に、サイドバーにある **API アクセス**を押した後、右上にある鍵マークがついた **API キー**ボタンを押し、そこで API キー発行を発行します。
そこで発行した API キーを Web アプリのコードに入力し完成！

**アプリの使用画面**
![スクリーンショット 2025-06-11 235449.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4080274/e45c1d4e-3d31-42fc-9c45-17f1e44ad7fb.png)

未完了の業務のみを抽出し、優先度順に Todo を表示してくれます。
**しっかり労ってもくれています。**
## まとめ
今回は ChatGPT や Dify を活用し **Todo 管理アプリ**を作成してみました。
ChatGPT にアプリの概要を伝えるだけで、最初のコードをスピーディに作成でき、さらに修正依頼も即座に反映してもらえるなんて驚きしました。

また、 Dify と組み合わせることで、ただの Todo 管理アプリを超え「 AI が指示をくれるアプリ」にできたのも、今までの自分では絶対にできなかったことですし、**次のステップ**に進めた感じがしてテンションが上がりました。

もちろん、まだ見栄えや操作性の向上、そして Dify で使うプロンプトのブラッシュアップなど課題も残っていますが、最低限実用できるレベルのものができたと感じています。
これからは、より高度な指示を AI から引き出すためのプロンプト設計の精査や、 UI  の改善をしていきたいと考えています。

**「アプリ作成なんて自分だけでは絶対にできない」** と思っていましたが、 ChatGPT をうまく活用すればあっという間に形にできちゃうことを知り、**新たな可能性**が生まれた気がします！今後もいろんなアイデアを試しながら、さらに面白いツールを作っていきたいと思います！