8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-06-11

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

前回の記事はこちら

仕事が増えてしまいました…。

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

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

使用画面
image.png
使用方法
・左上で業務を入力し、日付と優先度(高、中、低)を選択し追加ボタンを押すと、下の欄に業務が追加されます。
・完了した場合は左の完了を押すと業務に斜線が入ります。
・右の編集、削除ボタンで登録済の業務の編集、削除ができます。
・指示して AI くんボタンを押すと優先度を考慮しやるべき業務TOP5を選出してくれます。あと労ってくれます。

使ったツール・技術

・ChatGPT:アプリのコード作成・修正、アイデアの壁打ち

・Dify:AI によるタスク優先度の指示機能を実装(Gemini API を利用)

・CodePen:試作・動作確認

・HTMLファイル:外部 API 制限の回避・最終的なアプリ動作環境

ChatGPTでプロトタイプ開発

まず、ChatGPT に以下のプロンプトを送信してアプリの原型コードを生成してもらいました。

スクリーンショット 2025-06-09 234820.png

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

スクリーンショット 2025-06-10 224755.png

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

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

スクリーンショット 2025-06-09 235319.png
しっかり要望通りのものができました!
(先ほどのメッセージの後に締切3日前のものを赤文字表示する要望も伝えました。)

スクリーンショット 2025-06-10 230030.png
ここまでなんと30分! コードの知識なんて全くない私がこんな簡単に、一瞬でアプリを作ってしまいました…。

Dify と連携して AI 指示を受け取る

次にタスクが多いときに AI が優先度 TOP5 を返してくれる機能Dify を利用して追加します。

ここで Dify の外部 API リクエストを行うため、アプリをローカルの HTML ファイルに移行しました。

まず ChatGPT に以下のように指示し、先ほど作成したアプリに Dify に Todo の情報を伝えるための機能を追加し、Dify API と連携させるコードを生成してもらいました。

スクリーンショット 2025-06-10 001744.png

ChatGPTが送ってくれたコード

"YOUR_DIFY_API_KEY"と書いてあるところを自身が用意した API キーに書き換えます。API キーの取得は後程…

コード
<!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>

指示して AI くんボタンが追加されました!
スクリーンショット 2025-06-10 233734.png

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

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

Dify の全体像
スクリーンショット 2025-06-10 235526.png
シンプル!

まず Dify トップ画面からスタジオタブでアプリを作成最初から作成
アプリタイプはチャットフローを選択しを名前入力して作成します。
image.png
続いてノードの設定です。
開始ノードで入力フィールドを追加します。
変数名はアプリから送るデータのキー名と同じものを入力(ここでは todo_list )フィールドタイプは大量のテキストを受け取ることができる「段落」を設定します。最大長はアプリから送るデータの文字数に対応できるように調整します。

image.png

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

スクリーンショット 2025-06-11 002321.png

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

スクリーンショット 2025-06-11 005714.png

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

アプリの使用画面
スクリーンショット 2025-06-11 235449.png

未完了の業務のみを抽出し、優先度順に Todo を表示してくれます。
しっかり労ってもくれています。

まとめ

今回は ChatGPT や Dify を活用し Todo 管理アプリを作成してみました。
ChatGPT にアプリの概要を伝えるだけで、最初のコードをスピーディに作成でき、さらに修正依頼も即座に反映してもらえるなんて驚きしました。

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

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

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

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?