0
0

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アプリ開発ハンズオン徹底解説マニュアル

0
Posted at

AI Smart Todo App ハンズオン

1. 講座の全体像と本日のゴール

本日のハンズオンでは、特別なソフトウェアのインストールや難しい事前準備は一切行いません。

ブラウザ(Google Chrome 等)だけで、**「世界に一つだけの自分専用 AI Web アプリケーション」**を構築します。

使用するツール

  1. Google スプレッドシート
    タスクの保存場所(データベース)

  2. Google Apps Script(GAS)
    アプリの頭脳(プログラムの実行)

  3. Google AI Studio(Gemini API)
    AI の頭脳(タスクを自動分解するエンジン)

  4. Web アプリ UI
    ブラウザで操作できる入力・一覧画面


2. 事前準備(ハンズオン開始前に行うこと)

① Google アカウントの準備

ブラウザ(Google Chrome など)で、ご自身の Google アカウントにログインした状態にしておきます。

② 無料の AI API キーの取得(Google AI Studio)

AI 機能を動かすための「パスワード(API キー)」を各自で無料発行します。

数クリックで取得できます。

  1. ブラウザで Google AI Studio にアクセスします。
  2. 画面右上または中央の 「Sign in(ログイン)」 をクリックし、Google アカウントでログインします。
  3. 画面左側のメニューにある 「Get API key」 をクリックします。
  4. 画面上部にある 「Create API key」 ボタンをクリックし、「Create API key in new project」 を選択します。
  5. 生成された AIza... から始まる長い文字列(API キー)をコピーし、必ずメモ帳などに保存してください。

注意
API キーは外部に公開しないでください。GitHub に直接書き込まず、後述する GAS のスクリプトプロパティへ保存します。


3. スプレッドシートと GAS の準備

  1. 新規の Google スプレッドシートを作成し、ファイル名を任意の名称に変更します。
  2. 画面左下のシート名を半角で Todo に変更します。
  3. メニューの 「拡張機能」「Apps Script」 をクリックし、GAS のエディタ画面を開きます。

4. 最終コードの実装

GAS エディタ上の既存のコードをすべて削除し、以下のコードをそのまま貼り付けて保存します。

// スプレッドシートの「Todo」シートを取得(なければ自動作成し、ヘッダーを設定)
function getSheet() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName("Todo");

  if (!sheet) {
    sheet = ss.insertSheet("Todo");
    sheet.appendRow(["ID", "日時", "タスク名", "AI分解ステップ", "ステータス"]);
  } else {
    if (sheet.getLastRow() === 0 || !sheet.getRange(1, 1).getValue()) {
      sheet.clear();
      sheet.appendRow(["ID", "日時", "タスク名", "AI分解ステップ", "ステータス"]);
    }
  }

  return sheet;
}

// 1. 全タスクを取得する関数
function getTodos() {
  const sheet = getSheet();
  const lastRow = sheet.getLastRow();

  if (lastRow <= 1) return [];

  const rows = sheet.getRange(2, 1, lastRow - 1, 5).getValues();
  const todos = [];

  for (let i = 0; i < rows.length; i++) {
    const row = rows[i];

    if (row[2]) {
      todos.push({
        rowId: i + 2,
        timestamp: row[1] ? String(row[1]) : "",
        task: String(row[2]),
        advice: String(row[3] || "AIアドバイスなし"),
        status: String(row[4] || "未完了")
      });
    }
  }

  return todos.reverse();
}

// 2. タスクを追加し、AIから分解ステップを取得する関数
function addTodoWithAI(taskName) {
  if (!taskName) return getTodos();

  const sheet = getSheet();

  const aiAdvice = getAIBreakdown(taskName);

  const id = Utilities.getUuid();
  const timestamp = Utilities.formatDate(
    new Date(),
    Session.getScriptTimeZone(),
    "yyyy/MM/dd HH:mm:ss"
  );

  sheet.appendRow([id, timestamp, taskName, aiAdvice, "未完了"]);

  return getTodos();
}

// 3. タスクを削除する関数
function deleteTodo(rowId) {
  const sheet = getSheet();
  const targetRow = Number(rowId);

  if (targetRow > 1 && targetRow <= sheet.getLastRow()) {
    sheet.deleteRow(targetRow);
  }

  return getTodos();
}

// 4. ステータスを切り替える関数(完了/未完了)
function toggleStatus(rowId) {
  const sheet = getSheet();
  const targetRow = Number(rowId);

  if (targetRow > 1 && targetRow <= sheet.getLastRow()) {
    const currentStatus = sheet.getRange(targetRow, 5).getValue();
    const newStatus = currentStatus === "完了" ? "未完了" : "完了";

    sheet.getRange(targetRow, 5).setValue(newStatus);
  }

  return getTodos();
}

// Gemini API(3.6-flash)を呼び出してタスクを分解する関数
function getAIBreakdown(taskName) {
  const apiKey = PropertiesService
    .getScriptProperties()
    .getProperty("GEMINI_API_KEY");

  if (!apiKey) {
    return "【エラー】スクリプトプロパティに GEMINI_API_KEY が設定されていません";
  }

  const url =
    `https://generativelanguage.googleapis.com/v1beta/models/gemini-3.6-flash:generateContent?key=${apiKey}`;

  const prompt =
    `以下のタスクを達成するための具体的な実行ステップを、番号付きで3つ、簡潔に日本語で教えてください。\nタスク: ${taskName}`;

  const payload = {
    contents: [
      {
        parts: [
          {
            text: prompt
          }
        ]
      }
    ]
  };

  const options = {
    method: "post",
    contentType: "application/json",
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  try {
    const response = UrlFetchApp.fetch(url, options);
    const json = JSON.parse(response.getContentText());

    if (json.candidates && json.candidates[0].content) {
      return json.candidates[0].content.parts[0].text;
    } else if (json.error) {
      return "APIエラー: " + json.error.message;
    } else {
      return "AIからの応答を取得できませんでした。";
    }
  } catch (error) {
    return "通信エラー: " + error.message;
  }
}

// 5. モダンなWebアプリの画面(UI)
function doGet() {
  return HtmlService.createHtmlOutput(`
    <!DOCTYPE html>
    <html>
      <head>
        <base target="_top">
        <meta charset="utf-8">

        <style>
          body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            max-width: 750px;
            margin: 40px auto;
            padding: 20px;
            background: #f4f6f8;
            color: #333;
          }

          .card {
            background: white;
            padding: 30px;
            border-radius: 12px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
            margin-bottom: 20px;
          }

          h2 {
            color: #1a73e8;
            margin-top: 0;
          }

          .input-group {
            display: flex;
            gap: 10px;
            margin-bottom: 15px;
          }

          input[type="text"] {
            flex: 1;
            padding: 12px;
            font-size: 16px;
            border: 1px solid #ddd;
            border-radius: 6px;
            outline: none;
          }

          input[type="text"]:focus {
            border-color: #1a73e8;
          }

          button {
            padding: 12px 20px;
            font-size: 16px;
            background: #1a73e8;
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-weight: bold;
          }

          button:hover {
            background: #1557b0;
          }

          .todo-item {
            background: #fafbfc;
            border: 1px solid #e1e4e8;
            border-radius: 8px;
            padding: 15px;
            margin-bottom: 12px;
          }

          .todo-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 8px;
          }

          .todo-title {
            font-size: 18px;
            font-weight: bold;
          }

          .todo-advice {
            background: #e8f0fe;
            padding: 10px 12px;
            border-radius: 6px;
            font-size: 14px;
            white-space: pre-wrap;
            color: #174ea6;
            margin-top: 8px;
          }

          .actions {
            display: flex;
            gap: 8px;
          }

          .btn-sm {
            padding: 6px 12px;
            font-size: 12px;
            border-radius: 4px;
            cursor: pointer;
            border: none;
            font-weight: bold;
          }

          .btn-complete {
            background: #34a853;
            color: white;
          }

          .btn-delete {
            background: #ea4335;
            color: white;
          }

          .completed {
            text-decoration: line-through;
            color: #888;
          }

          #loading {
            display: none;
            color: #1a73e8;
            font-weight: bold;
            margin-bottom: 10px;
          }
        </style>
      </head>

      <body>
        <div class="card">
          <h2>✨ AI Smart Todo App</h2>

          <p>
            やりたいタスクを入力し、青いボタンをクリックすると
            AIが即座に実行ステップに分解して追加します。
          </p>

          <div class="input-group">
            <input
              type="text"
              id="taskInput"
              placeholder="例: 週末にアプリの企画書をまとめる"
            >

            <button onclick="addTodo()">
              AIに分解して追加
            </button>
          </div>

          <div id="loading">
            🤖 AIがタスクを分解中...少々お待ちください
          </div>
        </div>

        <div class="card">
          <h3>📋 タスク一覧</h3>
          <div id="todoList">読み込み中...</div>
        </div>

        <script>
          window.onload = function() {
            loadTodos();
          };

          function loadTodos() {
            google.script.run
              .withSuccessHandler(renderTodos)
              .withFailureHandler(handleError)
              .getTodos();
          }

          function addTodo() {
            const taskInput = document.getElementById('taskInput');
            const task = taskInput.value.trim();

            if (!task) return;

            document.getElementById('loading').style.display = 'block';
            taskInput.value = '';

            google.script.run
              .withSuccessHandler(function(todos) {
                document.getElementById('loading').style.display = 'none';
                renderTodos(todos);
              })
              .withFailureHandler(handleError)
              .addTodoWithAI(task);
          }

          function deleteTodo(rowId) {
            google.script.run
              .withSuccessHandler(renderTodos)
              .withFailureHandler(handleError)
              .deleteTodo(rowId);
          }

          function toggleStatus(rowId) {
            google.script.run
              .withSuccessHandler(renderTodos)
              .withFailureHandler(handleError)
              .toggleStatus(rowId);
          }

          function renderTodos(todos) {
            const container = document.getElementById('todoList');

            if (!todos || todos.length === 0) {
              container.innerHTML =
                '<p style="color: #666;">タスクはまだありません。上のフォームから追加してみましょう!</p>';

              return;
            }

            let html = '';

            todos.forEach(item => {
              const isCompleted = item.status === "完了";

              html += \`
                <div
                  class="todo-item"
                  style="\${isCompleted ? 'opacity: 0.6;' : ''}"
                >
                  <div class="todo-header">
                    <span
                      class="todo-title \${isCompleted ? 'completed' : ''}"
                    >
                      \${escapeHtml(item.task)}
                    </span>

                    <div class="actions">
                      <button
                        class="btn-sm btn-complete"
                        onclick="toggleStatus(\${item.rowId})"
                      >
                        \${isCompleted ? '未完了に戻す' : '完了にする'}
                      </button>

                      <button
                        class="btn-sm btn-delete"
                        onclick="deleteTodo(\${item.rowId})"
                      >
                        削除
                      </button>
                    </div>
                  </div>

                  <div style="font-size: 12px; color: #666;">
                    登録日時: \${item.timestamp}
                    |
                    ステータス: \${item.status}
                  </div>

                  <div class="todo-advice">
                    💡 <strong>AI分解ステップ:</strong><br>
                    \${escapeHtml(item.advice)}
                  </div>
                </div>
              \`;
            });

            container.innerHTML = html;
          }

          function handleError(err) {
            document.getElementById('loading').style.display = 'none';
            alert("エラーが発生しました: " + err.message);
          }

          function escapeHtml(str) {
            return String(str)
              .replace(/&/g, '&amp;')
              .replace(/</g, '&lt;')
              .replace(/>/g, '&gt;')
              .replace(/"/g, '&quot;');
          }
        </script>
      </body>
    </html>
  `);
}

5. API キーの設定(スクリプトプロパティ)

API キーをソースコードへ直接書き込むのではなく、GAS の スクリプトプロパティに保存します。

  1. GAS エディタの左メニューにある 「プロジェクトの設定(歯車アイコン)」 をクリックします。
  2. 下にスクロールし、「スクリプト プロパティを追加」 をクリックします。
  3. 以下を入力して保存します。
項目 設定値
プロパティ GEMINI_API_KEY
事前にコピーした API キー(AIza...

重要

API キーは GitHub、SNS、チャット、スクリーンショットなどに公開しないでください。


6. ウェブアプリとしての公開(デプロイ)

  1. GAS エディタ画面の右上にある 「デプロイ」「新しいデプロイ」 をクリックします。
  2. 歯車アイコン(種類の選択)から 「ウェブアプリ」 を選択します。
  3. 各項目を以下のように設定します。
項目 設定内容
説明 v1.0 AI Todo App
次のユーザーとして実行 自分(Me)
アクセスできるユーザー 全員(Anyone)
  1. 「デプロイ」 ボタンをクリックします。
  2. 初回のみアクセス権の承認画面が表示されます。
  3. 必要な権限を確認し、Google アカウントで承認します。
  4. 発行された 「ウェブアプリの URL」 をコピーします。
  5. URL をブラウザで開き、アプリが正常に動作することを確認します。

🎉 完成

これで、

Google スプレッドシート + Google Apps Script + Gemini API

を使った、自分専用の AI Smart Todo App が完成です。

タスクを入力すると、

  1. Gemini がタスクを分析
  2. 具体的な実行ステップを3つ生成
  3. Google スプレッドシートへ保存
  4. Web アプリ上に一覧表示
  5. 完了・未完了の切り替え
  6. 不要なタスクの削除

までをブラウザだけで行えます。

このハンズオンで体験できること

  • Web アプリケーションの基本
  • データベース的なデータ保存
  • Google Apps Script によるバックエンド処理
  • 外部 API の利用
  • Gemini を使った生成 AI 機能
  • HTML / CSS / JavaScript による UI
  • Web アプリのデプロイ
  • URL を使ったアプリ公開

特別な開発環境がなくても、Google アカウントとブラウザだけで AI アプリを作り、実際に公開するところまで体験できます。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?