AI Smart Todo App ハンズオン
1. 講座の全体像と本日のゴール
本日のハンズオンでは、特別なソフトウェアのインストールや難しい事前準備は一切行いません。
ブラウザ(Google Chrome 等)だけで、**「世界に一つだけの自分専用 AI Web アプリケーション」**を構築します。
使用するツール
-
Google スプレッドシート
タスクの保存場所(データベース) -
Google Apps Script(GAS)
アプリの頭脳(プログラムの実行) -
Google AI Studio(Gemini API)
AI の頭脳(タスクを自動分解するエンジン) -
Web アプリ UI
ブラウザで操作できる入力・一覧画面
2. 事前準備(ハンズオン開始前に行うこと)
① Google アカウントの準備
ブラウザ(Google Chrome など)で、ご自身の Google アカウントにログインした状態にしておきます。
② 無料の AI API キーの取得(Google AI Studio)
AI 機能を動かすための「パスワード(API キー)」を各自で無料発行します。
数クリックで取得できます。
- ブラウザで Google AI Studio にアクセスします。
- 画面右上または中央の 「Sign in(ログイン)」 をクリックし、Google アカウントでログインします。
- 画面左側のメニューにある 「Get API key」 をクリックします。
- 画面上部にある 「Create API key」 ボタンをクリックし、「Create API key in new project」 を選択します。
- 生成された
AIza...から始まる長い文字列(API キー)をコピーし、必ずメモ帳などに保存してください。
注意
API キーは外部に公開しないでください。GitHub に直接書き込まず、後述する GAS のスクリプトプロパティへ保存します。
3. スプレッドシートと GAS の準備
- 新規の Google スプレッドシートを作成し、ファイル名を任意の名称に変更します。
- 画面左下のシート名を半角で
Todoに変更します。 - メニューの 「拡張機能」 → 「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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
</script>
</body>
</html>
`);
}
5. API キーの設定(スクリプトプロパティ)
API キーをソースコードへ直接書き込むのではなく、GAS の スクリプトプロパティに保存します。
- GAS エディタの左メニューにある 「プロジェクトの設定(歯車アイコン)」 をクリックします。
- 下にスクロールし、「スクリプト プロパティを追加」 をクリックします。
- 以下を入力して保存します。
| 項目 | 設定値 |
|---|---|
| プロパティ | GEMINI_API_KEY |
| 値 | 事前にコピーした API キー(AIza...) |
重要
API キーは GitHub、SNS、チャット、スクリーンショットなどに公開しないでください。
6. ウェブアプリとしての公開(デプロイ)
- GAS エディタ画面の右上にある 「デプロイ」 → 「新しいデプロイ」 をクリックします。
- 歯車アイコン(種類の選択)から 「ウェブアプリ」 を選択します。
- 各項目を以下のように設定します。
| 項目 | 設定内容 |
|---|---|
| 説明 | v1.0 AI Todo App |
| 次のユーザーとして実行 | 自分(Me) |
| アクセスできるユーザー | 全員(Anyone) |
- 「デプロイ」 ボタンをクリックします。
- 初回のみアクセス権の承認画面が表示されます。
- 必要な権限を確認し、Google アカウントで承認します。
- 発行された 「ウェブアプリの URL」 をコピーします。
- URL をブラウザで開き、アプリが正常に動作することを確認します。
🎉 完成
これで、
Google スプレッドシート + Google Apps Script + Gemini API
を使った、自分専用の AI Smart Todo App が完成です。
タスクを入力すると、
- Gemini がタスクを分析
- 具体的な実行ステップを3つ生成
- Google スプレッドシートへ保存
- Web アプリ上に一覧表示
- 完了・未完了の切り替え
- 不要なタスクの削除
までをブラウザだけで行えます。
このハンズオンで体験できること
- Web アプリケーションの基本
- データベース的なデータ保存
- Google Apps Script によるバックエンド処理
- 外部 API の利用
- Gemini を使った生成 AI 機能
- HTML / CSS / JavaScript による UI
- Web アプリのデプロイ
- URL を使ったアプリ公開
特別な開発環境がなくても、Google アカウントとブラウザだけで AI アプリを作り、実際に公開するところまで体験できます。