CONTENT
ここから
企画タイトル、ターゲット、雰囲気などを入力する表を作る
この教材では、Google Apps ScriptのWebアプリとして、AIに指示を出すための操作画面を作ります。
前回は、スプレッドシートのセルに入力して、AIの回答をセルに表示しました。
今回は少し進んで、ブラウザで開ける専用画面を作ります。
前回:
スプレッドシートに直接入力する
今回:
Webアプリの画面から入力する
↓
GASが受け取る
↓
スプレッドシートに保存する
Google Apps ScriptのWebアプリは、doGet() または doPost() を使って、ブラウザから開けるアプリとして公開できます。HTML画面を表示する場合は、HtmlService を使ってHTMLファイルを返します。
このページで完成するもの
今回は、次のような入力フォームを作ります。
企画タイトル
ターゲット
伝えたいこと
雰囲気
使いたい画像のイメージ
動画で見せたい場面
見た人にしてほしい行動
入力して保存ボタンを押すと、スプレッドシートに1行追加されます。
Web画面に入力
↓
保存ボタンを押す
↓
GASが受け取る
↓
スプレッドシートに保存
この画面ができると、次の授業でAI APIとつなげやすくなります。
1. 今回の完成イメージ
今回作るアプリは、まだAIを呼び出しません。
まずは、AIに渡すための材料をきれいに集めます。
| 入力項目 | 何を書くか |
|---|---|
| 企画タイトル | 作りたい企画の名前 |
| ターゲット | 誰に届けたいか |
| 伝えたいこと | 何を伝えたいか |
| 雰囲気 | 明るい、かわいい、落ち着いたなど |
| 画像イメージ | どんな画像を作りたいか |
| 動画イメージ | どんな場面を動画にしたいか |
| 行動 | 見た人に何をしてほしいか |
今回のゴールは、これです。
AIに投げる前の材料を、
Web画面で入力できるようにする
2. 使うもの
今回は、次の2つのファイルで作ります。
| ファイル | 役割 |
|---|---|
Code.gs | GAS側の処理を書く |
index.html | 画面の見た目を書く |
WebアプリとしてHTML画面からGASの関数を呼び出す時は、HTML側で google.script.run を使います。google.script.run は、HTMLサービスの画面からサーバー側のApps Script関数を非同期で呼び出すための仕組みです。
3. まずスプレッドシートを作る
Googleスプレッドシートを開きます。
https://sheets.google.com/
新しいスプレッドシートを作成します。
ファイル名は、次にしてください。
AI企画入力アプリ
4. シート名を変更する
画面下のシート名を変更します。
初期状態では、たぶん「シート1」になっています。
これを次に変更します。
企画データ
シート名は、コードで使います。
間違えると保存できないので、ここは同じ名前にしてください。
5. 見出し行を作る
1行目に、次の見出しを入力します。
| セル | 入力する内容 |
|---|---|
| A1 | 登録日時 |
| B1 | 企画タイトル |
| C1 | ターゲット |
| D1 | 伝えたいこと |
| E1 | 雰囲気 |
| F1 | 画像イメージ |
| G1 | 動画イメージ |
| H1 | 行動 |
| I1 | ステータス |
これで、Webアプリから送られてきた内容を保存する場所ができます。
6. Apps Scriptを開く
スプレッドシートの上部メニューから開きます。
拡張機能
↓
Apps Script
Apps Scriptのプロジェクト名を変更します。
AI企画入力アプリ
Apps Scriptは、Google Workspaceの作業を自動化・拡張できるGoogleのクラウド型JavaScript環境です。
7. Code.gsを作る
Apps Scriptを開くと、最初から Code.gs があるはずです。
中身をすべて消して、次のコードを貼り付けてください。
const SHEET_NAME = '企画データ';
function doGet() {
return HtmlService
.createHtmlOutputFromFile('index')
.setTitle('AI企画入力アプリ')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function saveProject(formData) {
validateFormData_(formData);
const sheet = getSheet_();
sheet.appendRow([
new Date(),
formData.title,
formData.target,
formData.message,
formData.tone,
formData.imageIdea,
formData.videoIdea,
formData.action,
'未生成'
]);
return {
ok: true,
message: '保存しました。次はAI生成に進めます。'
};
}
function getRecentProjects() {
const sheet = getSheet_();
const lastRow = sheet.getLastRow();
if (lastRow <= 1) {
return [];
}
const values = sheet.getRange(2, 1, lastRow - 1, 9).getValues();
return values
.map((row, index) => {
return {
rowNumber: index + 2,
createdAt: formatDate_(row[0]),
title: row[1],
target: row[2],
message: row[3],
tone: row[4],
imageIdea: row[5],
videoIdea: row[6],
action: row[7],
status: row[8]
};
})
.reverse()
.slice(0, 5);
}
function getSheet_() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const sheet = ss.getSheetByName(SHEET_NAME);
if (!sheet) {
throw new Error('「' + SHEET_NAME + '」という名前のシートが見つかりません。');
}
return sheet;
}
function validateFormData_(formData) {
if (!formData) {
throw new Error('入力データがありません。');
}
const requiredFields = [
['title', '企画タイトル'],
['target', 'ターゲット'],
['message', '伝えたいこと'],
['tone', '雰囲気']
];
requiredFields.forEach(([key, label]) => {
if (!formData[key] || String(formData[key]).trim() === '') {
throw new Error(label + 'を入力してください。');
}
});
}
function formatDate_(date) {
if (!date) return '';
return Utilities.formatDate(
new Date(date),
Session.getScriptTimeZone(),
'yyyy/MM/dd HH:mm'
);
}
8. index.htmlを作る
Apps Scriptの左側にある「+」を押します。
+
↓
HTML
ファイル名は、次にしてください。
index
index.html が作られたら、中身をすべて消して、次のコードを貼り付けます。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<base target="_top" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI企画入力アプリ</title>
<style>
:root {
--bg: #f4f7fb;
--card: #ffffff;
--text: #17202a;
--muted: #6b7280;
--line: #dfe5ee;
--primary: #2563eb;
--primary-dark: #1d4ed8;
--danger: #dc2626;
--success: #16a34a;
--soft: #eff6ff;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: linear-gradient(180deg, #f7fbff 0%, #eef3f9 100%);
color: var(--text);
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
line-height: 1.7;
}
.page {
width: min(1080px, calc(100% - 32px));
margin: 0 auto;
padding: 32px 0 48px;
}
.hero {
padding: 28px;
margin-bottom: 20px;
border: 1px solid var(--line);
border-radius: 24px;
background: var(--card);
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08);
}
.badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
border-radius: 999px;
background: var(--soft);
color: var(--primary);
font-size: 13px;
font-weight: 700;
}
h1 {
margin: 18px 0 8px;
font-size: clamp(28px, 5vw, 44px);
letter-spacing: -0.04em;
line-height: 1.15;
}
.lead {
margin: 0;
color: var(--muted);
font-size: 16px;
}
.grid {
display: grid;
grid-template-columns: 1.1fr 0.9fr;
gap: 20px;
}
.card {
border: 1px solid var(--line);
border-radius: 24px;
background: var(--card);
box-shadow: 0 14px 32px rgba(15, 23, 42, 0.06);
overflow: hidden;
}
.card-header {
padding: 20px 22px;
border-bottom: 1px solid var(--line);
background: #fbfdff;
}
.card-header h2 {
margin: 0;
font-size: 20px;
}
.card-header p {
margin: 4px 0 0;
color: var(--muted);
font-size: 14px;
}
form {
padding: 22px;
}
label {
display: block;
margin-bottom: 14px;
font-weight: 700;
}
.hint {
display: block;
margin-top: 2px;
margin-bottom: 6px;
color: var(--muted);
font-size: 13px;
font-weight: 500;
}
input,
textarea,
select {
width: 100%;
border: 1px solid var(--line);
border-radius: 14px;
padding: 12px 14px;
background: #ffffff;
color: var(--text);
font: inherit;
outline: none;
}
textarea {
min-height: 92px;
resize: vertical;
}
input:focus,
textarea:focus,
select:focus {
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.12);
}
.row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
.actions {
display: flex;
gap: 12px;
margin-top: 20px;
}
button {
border: 0;
border-radius: 14px;
padding: 12px 18px;
cursor: pointer;
font: inherit;
font-weight: 800;
}
.primary {
background: var(--primary);
color: #ffffff;
}
.primary:hover {
background: var(--primary-dark);
}
.secondary {
background: #eef2f7;
color: #111827;
}
.message {
margin: 0 22px 22px;
padding: 12px 14px;
border-radius: 14px;
display: none;
font-weight: 700;
}
.message.success {
display: block;
background: #ecfdf5;
color: var(--success);
}
.message.error {
display: block;
background: #fef2f2;
color: var(--danger);
}
.preview {
padding: 22px;
}
.preview-box {
padding: 18px;
border-radius: 18px;
background: #f8fafc;
border: 1px dashed #cbd5e1;
}
.preview-title {
margin: 0 0 12px;
font-size: 18px;
}
.preview-item {
margin: 0 0 12px;
}
.preview-item strong {
color: var(--primary);
}
.recent {
padding: 0 22px 22px;
}
.recent-item {
padding: 14px 0;
border-top: 1px solid var(--line);
}
.recent-item:first-child {
border-top: 0;
}
.recent-title {
margin: 0;
font-weight: 800;
}
.recent-meta {
margin: 2px 0 0;
color: var(--muted);
font-size: 13px;
}
.empty {
color: var(--muted);
font-size: 14px;
}
@media (max-width: 860px) {
.grid,
.row {
grid-template-columns: 1fr;
}
.hero {
padding: 22px;
}
}
</style>
</head>
<body>
<main class="page">
<section class="hero">
<span class="badge">6-2 GAS Webアプリ</span>
<h1>スプレッドシートを<br />AIの操作画面にする</h1>
<p class="lead">
企画タイトル、ターゲット、雰囲気を入力して、AI生成のための材料を保存します。
</p>
</section>
<section class="grid">
<div class="card">
<div class="card-header">
<h2>企画情報を入力する</h2>
<p>まずはAIに渡すための材料をそろえましょう。</p>
</div>
<form id="projectForm">
<label>
企画タイトル
<span class="hint">例:放課後カフェマップ</span>
<input
id="title"
name="title"
type="text"
placeholder="企画タイトルを入力"
required
/>
</label>
<label>
ターゲット
<span class="hint">例:学校帰りに寄れる場所を探している学生</span>
<input
id="target"
name="target"
type="text"
placeholder="誰に届けたいか"
required
/>
</label>
<label>
伝えたいこと
<span class="hint">例:気軽に使える放課後の居場所を紹介する</span>
<textarea
id="message"
name="message"
placeholder="何を伝えたいか"
required
></textarea>
</label>
<div class="row">
<label>
雰囲気
<span class="hint">迷ったら「明るい」がおすすめ</span>
<select id="tone" name="tone" required>
<option value="">選択してください</option>
<option value="明るい・親しみやすい">明るい・親しみやすい</option>
<option value="おしゃれ・落ち着いた">おしゃれ・落ち着いた</option>
<option value="かわいい・やさしい">かわいい・やさしい</option>
<option value="かっこいい・シャープ">かっこいい・シャープ</option>
<option value="シンプル・分かりやすい">シンプル・分かりやすい</option>
</select>
</label>
<label>
見た人にしてほしい行動
<span class="hint">例:サイトを見る、参加する、申し込む</span>
<input
id="action"
name="action"
type="text"
placeholder="最後にしてほしい行動"
/>
</label>
</div>
<label>
画像イメージ
<span class="hint">例:夕方のカフェ、ノートとドリンク、手元のアップ</span>
<textarea
id="imageIdea"
name="imageIdea"
placeholder="どんな画像を作りたいか"
></textarea>
</label>
<label>
動画イメージ
<span class="hint">例:学校帰りにカフェへ向かう、スマホで探す</span>
<textarea
id="videoIdea"
name="videoIdea"
placeholder="どんな場面を動画にしたいか"
></textarea>
</label>
<div class="actions">
<button class="primary" type="submit" id="saveButton">
保存する
</button>
<button class="secondary" type="button" id="sampleButton">
サンプル入力
</button>
</div>
</form>
<p id="messageBox" class="message"></p>
</div>
<div class="card">
<div class="card-header">
<h2>入力内容の確認</h2>
<p>この内容が次回、AI生成の材料になります。</p>
</div>
<div class="preview">
<div class="preview-box">
<h3 class="preview-title" id="previewTitle">
まだ入力されていません
</h3>
<p class="preview-item">
<strong>ターゲット:</strong><span id="previewTarget">-</span>
</p>
<p class="preview-item">
<strong>伝えたいこと:</strong><span id="previewMessage">-</span>
</p>
<p class="preview-item">
<strong>雰囲気:</strong><span id="previewTone">-</span>
</p>
<p class="preview-item">
<strong>行動:</strong><span id="previewAction">-</span>
</p>
</div>
</div>
<div class="card-header">
<h2>最近保存した企画</h2>
<p>最新5件を表示します。</p>
</div>
<div class="recent" id="recentProjects">
<p class="empty">まだ保存データがありません。</p>
</div>
</div>
</section>
</main>
<script>
const form = document.getElementById('projectForm');
const saveButton = document.getElementById('saveButton');
const sampleButton = document.getElementById('sampleButton');
const messageBox = document.getElementById('messageBox');
const fields = {
title: document.getElementById('title'),
target: document.getElementById('target'),
message: document.getElementById('message'),
tone: document.getElementById('tone'),
imageIdea: document.getElementById('imageIdea'),
videoIdea: document.getElementById('videoIdea'),
action: document.getElementById('action')
};
const preview = {
title: document.getElementById('previewTitle'),
target: document.getElementById('previewTarget'),
message: document.getElementById('previewMessage'),
tone: document.getElementById('previewTone'),
action: document.getElementById('previewAction')
};
Object.values(fields).forEach((field) => {
field.addEventListener('input', updatePreview);
field.addEventListener('change', updatePreview);
});
sampleButton.addEventListener('click', () => {
fields.title.value = '放課後カフェマップ';
fields.target.value = '学校帰りに友達と寄れる場所を探している学生';
fields.message.value =
'勉強にも休憩にも使える、放課後の居場所を分かりやすく紹介する。';
fields.tone.value = '明るい・親しみやすい';
fields.imageIdea.value =
'夕方のカフェ、ノートとドリンク、スマホを見る手元、やわらかい自然光。';
fields.videoIdea.value =
'学校帰りにカフェへ向かい、席に座ってスマホで場所を探す流れ。';
fields.action.value = 'サイトを見て、行ってみたいカフェを探す';
updatePreview();
showMessage('サンプルを入力しました。自由に書き換えてください。', 'success');
});
form.addEventListener('submit', (event) => {
event.preventDefault();
const formData = getFormData();
setLoading(true);
showMessage('保存中です。少し待ってください。', 'success');
google.script.run
.withSuccessHandler((result) => {
setLoading(false);
showMessage(result.message, 'success');
form.reset();
updatePreview();
loadRecentProjects();
})
.withFailureHandler((error) => {
setLoading(false);
showMessage(error.message || '保存に失敗しました。', 'error');
})
.saveProject(formData);
});
function getFormData() {
return {
title: fields.title.value.trim(),
target: fields.target.value.trim(),
message: fields.message.value.trim(),
tone: fields.tone.value.trim(),
imageIdea: fields.imageIdea.value.trim(),
videoIdea: fields.videoIdea.value.trim(),
action: fields.action.value.trim()
};
}
function updatePreview() {
preview.title.textContent =
fields.title.value.trim() || 'まだ入力されていません';
preview.target.textContent = fields.target.value.trim() || '-';
preview.message.textContent = fields.message.value.trim() || '-';
preview.tone.textContent = fields.tone.value.trim() || '-';
preview.action.textContent = fields.action.value.trim() || '-';
}
function setLoading(isLoading) {
saveButton.disabled = isLoading;
saveButton.textContent = isLoading ? '保存中...' : '保存する';
}
function showMessage(text, type) {
messageBox.textContent = text;
messageBox.className = 'message ' + type;
}
function loadRecentProjects() {
google.script.run
.withSuccessHandler(renderRecentProjects)
.withFailureHandler(() => {
document.getElementById('recentProjects').innerHTML =
'<p class="empty">最近の企画を読み込めませんでした。</p>';
})
.getRecentProjects();
}
function renderRecentProjects(projects) {
const container = document.getElementById('recentProjects');
if (!projects || projects.length === 0) {
container.innerHTML =
'<p class="empty">まだ保存データがありません。</p>';
return;
}
container.innerHTML = projects
.map((project) => {
return `
<div class="recent-item">
<p class="recent-title">${escapeHtml(project.title)}</p>
<p class="recent-meta">
${escapeHtml(project.createdAt)}|${escapeHtml(project.tone || '')}|${escapeHtml(project.status || '')}
</p>
</div>
`;
})
.join('');
}
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
updatePreview();
loadRecentProjects();
</script>
</body>
</html>
9. ここまでのファイル構成
Apps Scriptの左側に、次の2つがあればOKです。
Code.gs
index.html
今回のWebアプリは、この2ファイルだけで動きます。
Code.gs
→ データを保存する処理
index.html
→ 入力画面
10. 保存する
コードを貼ったら、保存します。
Command + S
Windowsの場合は、
Ctrl + S
です。
11. 最初に実行する
Apps Scriptの上部にある関数選択で、次を選びます。
doGet
実行ボタンを押します。
初回は権限確認が出る場合があります。
権限を確認
↓
自分のGoogleアカウントを選ぶ
↓
詳細
↓
安全ではないページに移動
↓
許可
自分で作ったスクリプトなので、この教材では許可して進めます。
12. Webアプリとして公開する
次に、右上の「デプロイ」を押します。
デプロイ
↓
新しいデプロイ
種類の選択で、歯車アイコンを押します。
種類を選択
↓
ウェブアプリ
設定は次にします。
| 項目 | 設定 |
|---|---|
| 説明 | 6-2 AI企画入力アプリ |
| 次のユーザーとして実行 | 自分 |
| アクセスできるユーザー | 自分のみ |
最初は「自分のみ」で十分です。
授業で他の人にも見せたい場合は、「リンクを知っている全員」などに変更することもありますが、APIやデータ保存とつなぐ教材では、まず自分だけで動作確認するのが安全です。Apps ScriptのWebアプリは、実行ユーザーの設定によって、オーナーとして実行するか、アクセスしたユーザーとして実行するかを選べます。
13. WebアプリURLを開く
デプロイが完了すると、URLが表示されます。
ウェブアプリ URL
このURLを開くと、作った入力画面が表示されます。
画面が表示されたら成功です。
14. 入力して保存してみる
Webアプリの画面で、まず「サンプル入力」を押します。
内容が入ったら、「保存する」を押します。
成功すると、画面に次のように表示されます。
保存しました。次はAI生成に進めます。
その後、スプレッドシートを確認します。
企画データ シートに1行追加されていれば成功です。
15. 今日の完成形
今回の完成形は、これです。
Webアプリ画面
↓
企画情報を入力
↓
保存する
↓
スプレッドシートに保存
ここまでできると、次の回でAI APIにつなげやすくなります。
次の回では、保存された企画情報をもとに、
企画書を作る
画像プロンプトを作る
動画構成を作る
LP構成を作る
という形に発展できます。
16. 初学者がつまずきやすいところ
つまずき1:画面が表示されない
まず確認することは3つです。
index.html の名前が index になっているか
Code.gs の doGet があるか
デプロイ種類がウェブアプリになっているか
doGet() は、Webアプリとして画面を表示するための入り口です。
つまずき2:保存できない
シート名を確認してください。
コードでは、次の名前を使っています。
企画データ
シート名が違うと、エラーになります。
つまずき3:google.script.run が動かない
google.script.run は、Apps ScriptのHTMLサービス内で使うものです。
普通のHTMLファイルをブラウザで直接開いた場合は動きません。
必ず、デプロイされたWebアプリURLから開いてください。
つまずき4:他の人が開けない
最初は「自分のみ」にしているため、他の人は開けません。
他の人に見せたい場合は、デプロイ設定でアクセスできるユーザーを変更します。
ただし、授業ではまず自分だけで動かす方が安全です。
17. 画面を少しカスタマイズする
慣れてきたら、画面の言葉を少し変えてみます。
例えば、タイトルを変えたい場合は、index.html のここを探します。
<h1>スプレッドシートを<br />AIの操作画面にする</h1>
例えば、次のように変えられます。
<h1>自分だけの<br />AI企画アシスタント</h1>
ボタン名を変えたい場合は、ここです。
保存する
例えば、次のようにできます。
企画を登録する
最初は、色やデザインよりも、保存できることを優先してください。
18. この回で理解してほしいこと
この回で大切なのは、デザインの美しさではありません。
本当に大切なのは、次の流れです。
Web画面で入力する
↓
GASに送る
↓
スプレッドシートに保存する
この流れが分かると、AIアプリの作り方が見えてきます。
なぜなら、AI APIに送る前には、必ず「入力」が必要だからです。
よいAI出力
=
よい入力画面
+
よいプロンプト
この考え方は、かなり大切です。
19. 今日の提出物
この回の提出物は、次の3つです。
1. WebアプリURL
2. 入力画面のスクリーンショット
3. スプレッドシートに保存された行のスクリーンショット
最低限、次ができていればOKです。
Webアプリ画面から入力して、
スプレッドシートに保存できた
20. チェックリスト
| チェック | 内容 |
|---|---|
| スプレッドシートを作成した | |
| ファイル名をAI企画入力アプリにした | |
| シート名を企画データにした | |
| 見出し行を作った | |
| Apps Scriptを開いた | |
| Code.gsを貼り付けた | |
| index.htmlを作った | |
| index.htmlのコードを貼り付けた | |
| 保存した | |
| Webアプリとしてデプロイした | |
| WebアプリURLを開いた | |
| サンプル入力を押した | |
| 保存するを押した | |
| スプレッドシートに1行追加された |
まとめ
今回は、スプレッドシートをAIの操作画面にするための準備をしました。
作ったものは、次の通りです。
index.html
→ 入力フォーム
Code.gs
→ 保存処理
スプレッドシート
→ データ保存場所
まだAIは呼び出していません。
でも、AIアプリに必要な一番大事な部分ができました。
AIに何を考えてほしいかを入力する画面
次の回では、この入力内容をもとに、GASからAI APIを呼び出して、企画書を自動生成していきます。