発問と選択肢を自由に決められます。できあがった共有URLを児童生徒に配ると、選んだ答えに応じてハートの色が変わる画面が開きます。
このURLを児童生徒に共有してください(Google Classroomなど)
URLが正しくコピーされていないようです。先生にもう一度リンクを送ってもらってください。
③ README.txt の手順に沿って script.google.com に貼り付けてください。
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>気持ちの温度計</title>
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
button:focus-visible, input:focus-visible, [tabindex]:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
html, body { margin: 0; padding: 0; }
body {
font-family: "Hiragino Sans", "Yu Gothic", system-ui, sans-serif;
background: #f3f6fb;
color: #1f2937;
padding: 24px 16px 40px;
}
[hidden] { display: none !important; }
.setup-card, .answer-wrap {
background: #fff;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
padding: 26px 22px;
max-width: 620px;
margin: 0 auto;
}
h1 { font-size: 19px; margin: 0 0 8px; text-align: center; }
.lead { font-size: 12.5px; color: #64748b; line-height: 1.7; text-align: center; margin: 0 0 18px; }
.field-label { font-size: 12.5px; font-weight: 700; color: #475569; display: block; margin-bottom: 6px; }
#questionInput {
width: 100%; padding: 12px 14px; font-size: 15px; border-radius: 12px; border: 2px solid #cbd5e1;
font-family: inherit; margin-bottom: 16px;
}
#questionInput:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px #dbeafe; }
.setting-block { margin-bottom: 16px; }
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; }
.opt-btn {
font-size: 13px; font-weight: 700; padding: 8px 16px; border-radius: 999px; border: 2px solid #cbd5e1;
background: #fff; color: #1f2937; cursor: pointer; transition: all 0.15s ease-out;
}
.opt-btn.active { border-color: #2563eb; background: #eff6ff; color: #1d4ed8; }
.opt-btn:hover { border-color: #2563eb; }
#choiceInputs { display: flex; flex-direction: column; gap: 8px; margin-bottom: 18px; }
.choice-input-row { display: flex; align-items: center; gap: 8px; }
.color-swatch { flex-shrink: 0; width: 20px; height: 20px; border-radius: 50%; }
.choice-input {
flex: 1 1 auto; padding: 9px 12px; font-size: 13.5px; border-radius: 10px; border: 2px solid #cbd5e1;
font-family: inherit;
}
.choice-input:focus { outline: none; border-color: #2563eb; }
.preview-block { margin-bottom: 18px; }
#previewArea { background: #f8fafc; border-radius: 14px; padding: 12px 4px 4px; }
#makeUrlBtn {
width: 100%; padding: 14px; font-size: 16px; font-weight: 700; border: none; border-radius: 12px;
background: #2563eb; color: #fff; cursor: pointer; transition: all 0.15s ease-out;
}
#makeUrlBtn:hover { background: #1d4ed8; }
#urlResult { margin-top: 16px; text-align: center; }
#urlResult p { font-size: 12.5px; color: #475569; margin: 0 0 8px; }
#shareUrl {
word-break: break-all; font-family: monospace; font-size: 11.5px; font-weight: 600;
background: #f1f5f9; border: 2px dashed #cbd5e1; border-radius: 10px; padding: 12px; color: #1d4ed8;
margin-bottom: 10px;
}
.url-actions { display: flex; gap: 8px; }
.url-actions button {
flex: 1; padding: 10px; font-size: 12.5px; font-weight: 700; border: none; border-radius: 10px;
cursor: pointer; transition: all 0.15s ease-out;
}
#copyUrlBtn { background: #e0e7ff; color: #3730a3; }
#copyUrlBtn:hover { background: #c7d2fe; }
#previewOpenBtn { background: #dcfce7; color: #15803d; }
#previewOpenBtn:hover { background: #bbf7d0; }
#backToSetupBtn {
font-size: 12px; font-weight: 700; padding: 7px 14px; border: none; border-radius: 999px;
background: #e2e8f0; color: #334155; cursor: pointer; transition: all 0.15s ease-out;
margin-bottom: 14px;
}
#backToSetupBtn:hover { background: #cbd5e1; }
.question-text { font-size: 18px; font-weight: 700; text-align: center; margin: 4px 0 18px; line-height: 1.6; }
.heart-wrap { display: flex; justify-content: center; margin-bottom: 22px; }
.heart-wrap svg { width: min(50vw, 220px); height: auto; }
.heart-path { fill: #e2e8f0; transition: fill 0.3s ease-out; }
@keyframes heartPulse {
0% { transform: scale(1); }
30% { transform: scale(1.18); }
60% { transform: scale(0.94); }
100% { transform: scale(1); }
}
.heart-wrap.pulse svg { animation: heartPulse 0.5s ease-out; transform-origin: center; }
.choice-row { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-bottom: 20px; }
.choice-btn {
padding: 12px 18px; font-size: 14px; font-weight: 700; border-radius: 999px; border: 3px solid #cbd5e1;
background: #fff; cursor: pointer; transition: all 0.15s ease-out;
}
.send-block { max-width: 380px; margin: 0 auto; text-align: center; }
.send-row { display: flex; gap: 8px; margin-bottom: 8px; }
#nameInput {
flex: 1 1 auto; padding: 10px 12px; font-size: 14px; border-radius: 10px; border: 2px solid #cbd5e1;
font-family: inherit;
}
#nameInput:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px #dbeafe; }
#sendBtn {
flex-shrink: 0; padding: 10px 18px; font-size: 13.5px; font-weight: 700; border: none; border-radius: 10px;
background: #16a34a; color: #fff; cursor: pointer; transition: all 0.15s ease-out;
}
#sendBtn:hover:not(:disabled) { background: #15803d; }
#sendBtn:disabled { background: #cbd5e1; cursor: default; }
#sendStatus { font-size: 12.5px; font-weight: 700; min-height: 16px; }
</style>
</head>
<body>
<div id="app">
<section id="setup-screen" hidden>
<div class="setup-card">
<h1>💗 気持ちの温度計(先生の設定)</h1>
<p class="lead">発問と選択肢を自由に決められます。できあがった共有URLを児童生徒に配ると、選んだ答えに応じてハートの色が変わる画面が開きます。</p>
<label class="field-label" for="questionInput">発問</label>
<input type="text" id="questionInput" placeholder="例:今日の学習はどのくらい分かりましたか?" maxlength="80">
<div class="setting-block">
<span class="field-label">選択肢の数</span>
<div class="btn-row" id="choiceCountRow">
<button class="opt-btn" data-count="3" type="button">3個</button>
<button class="opt-btn" data-count="4" type="button">4個</button>
<button class="opt-btn active" data-count="5" type="button">5個</button>
<button class="opt-btn" data-count="6" type="button">6個</button>
<button class="opt-btn" data-count="7" type="button">7個</button>
</div>
</div>
<div class="setting-block">
<span class="field-label">選択肢のことば(自由に書きかえてOK)</span>
<div id="choiceInputs"></div>
</div>
<div class="preview-block">
<span class="field-label">プレビュー(タップしてためせます)</span>
<div id="previewArea"></div>
</div>
<button id="makeUrlBtn" type="button">🔗 共有URLを作る</button>
<div id="urlResult" hidden>
<p>このURLを児童生徒に共有してください(Google Classroomなど)</p>
<div id="shareUrl"></div>
<div class="url-actions">
<button id="copyUrlBtn" type="button">📋 コピーする</button>
<button id="previewOpenBtn" type="button">👀 この内容で試す</button>
</div>
</div>
</div>
</section>
<section id="answer-screen" hidden>
<div class="answer-wrap">
<button id="backToSetupBtn" type="button" hidden>⚙️ 設定を作り直す</button>
<div id="answerContent"></div>
<div class="send-block" id="sendBlock" hidden>
<div class="send-row">
<input type="text" id="nameInput" placeholder="なまえを入力してね">
<button id="sendBtn" type="button">📤 送る</button>
</div>
<div id="sendStatus"></div>
</div>
</div>
</section>
<section id="link-error-screen" hidden>
<div class="setup-card">
<h1>🤔 リンクが正しくありません</h1>
<p class="lead">URLが正しくコピーされていないようです。先生にもう一度リンクを送ってもらってください。</p>
</div>
</section>
</div>
<script>
(function(){
const setupScreen = document.getElementById('setup-screen');
const answerScreen = document.getElementById('answer-screen');
const linkErrorScreen = document.getElementById('link-error-screen');
const questionInput = document.getElementById('questionInput');
const choiceCountRow = document.getElementById('choiceCountRow');
const choiceInputsEl = document.getElementById('choiceInputs');
const previewArea = document.getElementById('previewArea');
const makeUrlBtn = document.getElementById('makeUrlBtn');
const urlResult = document.getElementById('urlResult');
const shareUrlEl = document.getElementById('shareUrl');
const copyUrlBtn = document.getElementById('copyUrlBtn');
const previewOpenBtn = document.getElementById('previewOpenBtn');
const backToSetupBtn = document.getElementById('backToSetupBtn');
const answerContent = document.getElementById('answerContent');
const sendBlock = document.getElementById('sendBlock');
const nameInput = document.getElementById('nameInput');
const sendBtn = document.getElementById('sendBtn');
const sendStatus = document.getElementById('sendStatus');
const HEART_PATH = "M16 29C16 29 0 18.5 0 8.7C0 3.9 3.9 0 8.7 0C11.6 0 14.2 1.4 16 3.6C17.8 1.4 20.4 0 23.3 0C28.1 0 32 3.9 32 8.7C32 18.5 16 29 16 29Z";
const DEFAULT_LABELS = {
3: ["そう思わない", "どちらでもない", "そう思う"],
4: ["全くそう思わない", "そう思わない", "そう思う", "とてもそう思う"],
5: ["全くそう思わない", "あまりそう思わない", "どちらでもない", "少しそう思う", "とてもそう思う"],
6: ["全くそう思わない", "そう思わない", "あまりそう思わない", "少しそう思う", "そう思う", "とてもそう思う"],
7: ["全くそう思わない", "そう思わない", "あまりそう思わない", "どちらでもない", "少しそう思う", "そう思う", "とてもそう思う"],
};
function colorForIndex(i, total){
const hue = total <= 1 ? 210 : 210 - (210 * i / (total - 1));
return 'hsl(' + Math.round(hue) + ', 70%, 50%)';
}
function encodeConfig(question, choices){
const payload = { v: 1, q: question, c: choices };
try {
return btoa(unescape(encodeURIComponent(JSON.stringify(payload))));
} catch(e){
return null;
}
}
function decodeConfig(code){
try {
const json = decodeURIComponent(escape(atob(code)));
const payload = JSON.parse(json);
if(!payload || typeof payload.q !== 'string' || !payload.q.trim()) return null;
if(!Array.isArray(payload.c) || payload.c.length < 2) return null;
if(!payload.c.every((c) => typeof c === 'string' && c.trim().length > 0)) return null;
return payload;
} catch(e){
return null;
}
}
// 質問+ハート+選択肢のUIを組み立てる。プレビューと回答画面の両方で使う共通部品。
function buildHeartUI(question, choices, opts){
opts = opts || {};
const wrap = document.createElement('div');
const q = document.createElement('div');
q.className = 'question-text';
q.textContent = question;
wrap.appendChild(q);
const heartWrap = document.createElement('div');
heartWrap.className = 'heart-wrap';
heartWrap.innerHTML = '<svg viewBox="0 0 32 29"><path class="heart-path" d="' + HEART_PATH + '"></path></svg>';
const heartPath = heartWrap.querySelector('.heart-path');
wrap.appendChild(heartWrap);
const choiceRow = document.createElement('div');
choiceRow.className = 'choice-row';
const buttons = [];
choices.forEach((label, i) => {
const color = colorForIndex(i, choices.length);
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'choice-btn';
btn.textContent = label;
btn.style.borderColor = color;
btn.style.color = color;
btn.addEventListener('click', () => {
heartPath.setAttribute('fill', color);
heartWrap.classList.remove('pulse');
void heartWrap.offsetWidth;
heartWrap.classList.add('pulse');
buttons.forEach((b, bi) => {
const bColor = colorForIndex(bi, choices.length);
if(bi === i){
b.style.background = bColor;
b.style.color = '#fff';
} else {
b.style.background = '#fff';
b.style.color = bColor;
}
});
if(opts.onSelect) opts.onSelect(i, label, color);
});
buttons.push(btn);
choiceRow.appendChild(btn);
});
wrap.appendChild(choiceRow);
return wrap;
}
// ---------------- 設定画面(先生) ----------------
let choiceCount = 5;
function currentChoiceLabels(){
return Array.from(choiceInputsEl.querySelectorAll('.choice-input')).map((el) => el.value.trim());
}
function renderChoiceInputs(){
choiceInputsEl.innerHTML = '';
const defaults = DEFAULT_LABELS[choiceCount];
for(let i = 0; i < choiceCount; i++){
const row = document.createElement('div');
row.className = 'choice-input-row';
const swatch = document.createElement('span');
swatch.className = 'color-swatch';
swatch.style.background = colorForIndex(i, choiceCount);
const input = document.createElement('input');
input.type = 'text';
input.className = 'choice-input';
input.value = defaults[i];
input.maxLength = 20;
input.addEventListener('input', updatePreview);
row.appendChild(swatch);
row.appendChild(input);
choiceInputsEl.appendChild(row);
}
updatePreview();
}
function updatePreview(){
const question = questionInput.value.trim() || '(ここに発問が表示されます)';
const choices = currentChoiceLabels().map((c) => c || '(選択肢)');
previewArea.innerHTML = '';
previewArea.appendChild(buildHeartUI(question, choices));
urlResult.hidden = true;
}
choiceCountRow.addEventListener('click', (e) => {
const btn = e.target.closest('.opt-btn');
if(!btn) return;
choiceCount = Number(btn.dataset.count);
choiceCountRow.querySelectorAll('.opt-btn').forEach((b) => b.classList.toggle('active', b === btn));
renderChoiceInputs();
});
questionInput.addEventListener('input', updatePreview);
function shareUrlFor(question, choices){
const code = encodeConfig(question, choices);
if(!code) return null;
return window.location.origin + window.location.pathname + '#c=' + encodeURIComponent(code);
}
makeUrlBtn.addEventListener('click', () => {
const question = questionInput.value.trim();
const choices = currentChoiceLabels();
if(!question){
questionInput.focus();
return;
}
if(choices.some((c) => !c)){
choiceInputsEl.querySelectorAll('.choice-input').forEach((el) => {
if(!el.value.trim()) el.focus();
});
return;
}
const url = shareUrlFor(question, choices);
if(!url) return;
shareUrlEl.textContent = url;
urlResult.hidden = false;
});
copyUrlBtn.addEventListener('click', () => {
const text = shareUrlEl.textContent;
if(navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(text).then(() => {
copyUrlBtn.textContent = '✅ コピーしました';
setTimeout(() => { copyUrlBtn.textContent = '📋 コピーする'; }, 1500);
}).catch(() => {});
}
});
previewOpenBtn.addEventListener('click', () => {
const question = questionInput.value.trim() || '(発問が入力されていません)';
const choices = currentChoiceLabels().map((c) => c || '(選択肢)');
showAnswerScreen({ q: question, c: choices }, { fromPreview: true });
});
backToSetupBtn.addEventListener('click', () => {
answerScreen.hidden = true;
setupScreen.hidden = false;
});
// ---------------- 回答画面(児童生徒) ----------------
function safeGet(key){ try { return localStorage.getItem(key); } catch(e){ return null; } }
function safeSet(key, val){ try { localStorage.setItem(key, val); } catch(e){} }
const NAME_KEY = 'heart-thermometer-name';
nameInput.value = safeGet(NAME_KEY) || '';
let currentSelection = null;
function showAnswerScreen(config, opts){
opts = opts || {};
setupScreen.hidden = true;
linkErrorScreen.hidden = true;
answerScreen.hidden = false;
backToSetupBtn.hidden = !opts.fromPreview;
answerContent.innerHTML = '';
currentSelection = null;
sendBlock.hidden = true;
sendStatus.textContent = '';
sendBtn.disabled = false;
answerContent.appendChild(buildHeartUI(config.q, config.c, {
onSelect: (idx, label, color) => {
currentSelection = { idx: idx, label: label, color: color, question: config.q, count: config.c.length };
sendBlock.hidden = false;
sendStatus.textContent = '';
sendBtn.disabled = false;
},
}));
}
sendBtn.addEventListener('click', () => {
if(!currentSelection) return;
const name = nameInput.value.trim();
if(!name){
sendStatus.textContent = 'なまえを入力してね';
sendStatus.style.color = '#C0392B';
nameInput.focus();
return;
}
safeSet(NAME_KEY, name);
if(typeof google === 'undefined' || !google.script || !google.script.run){
sendStatus.textContent = 'この機能はGoogleスプレッドシート連携版として配信された環境でのみ使えます。';
sendStatus.style.color = '#C0392B';
return;
}
sendBtn.disabled = true;
sendStatus.textContent = '送信中…';
sendStatus.style.color = '#475569';
google.script.run
.withSuccessHandler(function(){
sendStatus.textContent = '送信しました!ありがとう。';
sendStatus.style.color = '#4F8A4B';
})
.withFailureHandler(function(){
sendStatus.textContent = '送信に失敗しました。もう一度試してね';
sendStatus.style.color = '#C0392B';
sendBtn.disabled = false;
})
.sendHeartResult({
name: name,
question: currentSelection.question,
choiceLabel: currentSelection.label,
choiceIndex: currentSelection.idx,
choiceCount: currentSelection.count,
});
});
// ---------------- 起動時の分岐 ----------------
function init(){
const hash = window.location.hash || '';
if(hash.indexOf('#c=') === 0){
const config = decodeConfig(decodeURIComponent(hash.slice(3)));
if(config){
showAnswerScreen(config);
return;
}
linkErrorScreen.hidden = false;
return;
}
setupScreen.hidden = false;
renderChoiceInputs();
}
init();
})();
</script>
</body>
</html>
/**
* 「気持ちの温度計」 - みんなの回答をスプレッドシートに送る機能
*
* 使い方(デプロイ手順)は README.txt を参照してください。
*/
function doGet(e) {
return HtmlService.createTemplateFromFile('index')
.evaluate()
.setTitle('気持ちの温度計')
.addMetaTag('viewport', 'width=device-width, initial-scale=1')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function getOrCreateSpreadsheet_() {
const props = PropertiesService.getScriptProperties();
const savedId = props.getProperty('SPREADSHEET_ID');
if (savedId) {
try {
return SpreadsheetApp.openById(savedId);
} catch (err) {
// 保存されていたIDが無効な場合は作り直す
}
}
const ss = SpreadsheetApp.create('気持ちの温度計 - みんなの回答');
const sheet = ss.getSheets()[0];
sheet.appendRow(['送信日時', 'なまえ', '発問', '選んだ選択肢', '選択肢の番号(0始まり)', '選択肢の総数']);
sheet.setFrozenRows(1);
props.setProperty('SPREADSHEET_ID', ss.getId());
return ss;
}
/**
* フロント側から google.script.run 経由で呼ばれる。
* data = {
* name: string, question: string, choiceLabel: string,
* choiceIndex: number, choiceCount: number
* }
*/
function sendHeartResult(data) {
if (!data || !data.name) {
throw new Error('なまえがありません');
}
const ss = getOrCreateSpreadsheet_();
const sheet = ss.getSheets()[0];
sheet.appendRow([
new Date(),
data.name.toString().slice(0, 40),
(data.question || '').toString().slice(0, 200),
(data.choiceLabel || '').toString().slice(0, 40),
data.choiceIndex,
data.choiceCount,
]);
return { ok: true };
}
「気持ちの温度計」 スプレッドシート送信版 - デプロイ手順
====================================================
このフォルダには3つのファイルがあります。
・index.html … 児童が触る画面(オフライン版に「なまえ」欄と「送る」ボタンを追加したもの)
・Code.gs … スプレッドシートにデータを送るためのバックエンド処理
・README.txt … このファイル
【1】Google Apps Script プロジェクトを作る
1. Google ドライブを開く(学校アカウントでログイン)
2. 「新規」→「その他」→「Google Apps Script」
(見当たらない場合は https://script.google.com/ から「新しいプロジェクト」)
3. プロジェクト名を「気持ちの温度計」などにする
【2】コードを貼り付ける
1. 左側のファイル一覧にある「Code.gs」を開き、中身をすべて削除
2. 渡した Code.gs の内容をすべて貼り付けて保存(Ctrl+S)
3. 左側の「+」→「HTML」を選び、ファイル名を必ず「index」にする(拡張子は自動でつきます)
4. 渡した index.html の内容をすべて貼り付けて保存
【3】ウェブアプリとして公開する
1. 右上の「デプロイ」→「新しいデプロイ」
2. 種類の選択(歯車マーク)で「ウェブアプリ」を選ぶ
3. 「次のユーザーとして実行」→ 自分(先生のアカウント)
4. 「アクセスできるユーザー」→ 学校のドメイン内の全員
(例:kita9.ed.jp 内の全員、など。学校のGoogle Workspace設定に合わせてください)
5. 「デプロイ」をクリックし、表示されたウェブアプリのURLをコピーする
6. 初回のみ、権限の承認画面が出るので許可する
【4】児童に配布する
・コピーしたURLをブックマークやGoogle Classroom等で共有すれば、
Chromebookのブラウザからそのまま使えます。
【5】スプレッドシートの確認
・誰かが初めて「送る」を押したときに、自動で
「気持ちの温度計 - みんなのふりかえり」という
スプレッドシートがドライブに作成されます。
・以降の送信は、そのシートに1人1行ずつ追加されていきます。
・共有設定(誰が見られるか)は、作成後にドライブから
先生が調整してください(デフォルトは先生のみ閲覧可)。
【困ったときは】
・貼り付けたコードを直接書き換えても大丈夫です。次に相談するときは
「Code.gsの◯◯を直したい」のように伝えてもらえれば調整できます。