質問を自由に作って保存すると、児童に配る共有URLが発行されます。あとから編集して保存しなおすこともできます。
このURLを児童に共有してください(Google Classroomなど)
名前を入力してから、質問に答えてね。
児童ごとの回答を確認し、所見に使いたい内容を選んで、生成AI(ChatGPTやClaudeなど)にわたすプロンプトを作成します。実名はこの画面にだけ表示され、コピーされるプロンプトには「呼び方」だけが入ります。
③ 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, textarea:focus-visible, [tabindex]:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
html, body { margin: 0; }
body {
font-family: "Hiragino Sans", "Yu Gothic", system-ui, sans-serif;
background: #f3f6fb;
color: #1f2937;
padding: 24px 16px 48px;
}
[hidden] { display: none !important; }
.wrap { max-width: 640px; margin: 0 auto; }
h1 { font-size: 19px; margin: 0 0 4px; text-align: center; }
.lead { color: #64748b; font-size: 12.5px; text-align: center; margin: 0 0 18px; line-height: 1.7; }
.role-switch { text-align: center; margin-bottom: 16px; }
.role-switch a {
font-size: 12px; font-weight: 700; color: #64748b; text-decoration: none;
border-bottom: 1px dashed #94a3b8;
}
.role-switch a:hover { color: #2563eb; border-color: #2563eb; }
.card {
background: #fff; border-radius: 16px; box-shadow: 0 8px 24px rgba(15,23,42,0.06);
padding: 18px 20px; margin-bottom: 16px;
}
.field-label { font-size: 12.5px; font-weight: 700; color: #475569; display: block; margin-bottom: 6px; }
input[type="text"], textarea {
width: 100%; padding: 11px 13px; font-size: 14.5px; border-radius: 10px; border: 2px solid #cbd5e1;
font-family: inherit; resize: vertical;
}
input[type="text"]:focus, textarea:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px #dbeafe; }
textarea { min-height: 70px; }
/* ---- 先生用:アンケート作成 ---- */
.q-card {
border: 2px solid #e2e8f0; border-radius: 12px; padding: 14px; margin-bottom: 12px;
}
.q-card-top { display: flex; gap: 8px; align-items: flex-start; margin-bottom: 10px; }
.q-card-top input[type="text"] { flex: 1 1 auto; }
.q-type-row { display: flex; gap: 6px; margin-bottom: 10px; }
.type-btn {
font-size: 12px; font-weight: 700; padding: 7px 12px; border-radius: 999px; border: 2px solid #cbd5e1;
background: #fff; color: #1f2937; cursor: pointer; transition: all 0.15s ease-out;
}
.type-btn.active { border-color: #2563eb; background: #eff6ff; color: #1d4ed8; }
.option-row { display: flex; gap: 6px; margin-bottom: 6px; }
.option-row input { flex: 1 1 auto; padding: 8px 10px; font-size: 13px; border-radius: 8px; border: 2px solid #cbd5e1; font-family: inherit; }
.option-row input:focus { outline: none; border-color: #2563eb; }
.icon-btn {
flex-shrink: 0; width: 32px; height: 32px; border-radius: 8px; border: none;
background: #e2e8f0; color: #334155; cursor: pointer; transition: all 0.15s ease-out;
font-size: 13px;
}
.icon-btn:hover { background: #cbd5e1; }
.icon-btn.danger { background: #fde8e6; color: #c0392b; }
.icon-btn.danger:hover { background: #fbd0cc; }
.add-option-btn {
font-size: 12px; font-weight: 700; padding: 6px 12px; border-radius: 8px; border: none;
background: #eff6ff; color: #1d4ed8; cursor: pointer; transition: all 0.15s ease-out;
}
.add-option-btn:hover { background: #dbeafe; }
.q-card-actions { display: flex; gap: 6px; justify-content: flex-end; }
#addQuestionBtn {
width: 100%; padding: 12px; font-size: 13.5px; font-weight: 700; border-radius: 12px;
border: 2px dashed #93c5fd; background: #eff6ff; color: #1d4ed8; cursor: pointer; transition: all 0.15s ease-out;
margin-bottom: 16px;
}
#addQuestionBtn:hover { background: #dbeafe; }
#saveSurveyBtn {
width: 100%; padding: 13px; font-size: 15px; font-weight: 700; border-radius: 12px; border: none;
background: #2563eb; color: #fff; cursor: pointer; transition: all 0.15s ease-out;
}
#saveSurveyBtn:hover:not(:disabled) { background: #1d4ed8; }
#saveSurveyBtn:disabled { background: #cbd5e1; cursor: default; }
#saveStatus { font-size: 12.5px; font-weight: 700; text-align: center; min-height: 18px; margin-top: 10px; }
#urlResult { margin-top: 14px; 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;
}
#copyUrlBtn {
font-size: 12.5px; font-weight: 700; padding: 9px 18px; border-radius: 10px; border: none;
background: #e0e7ff; color: #3730a3; cursor: pointer; transition: all 0.15s ease-out;
}
#copyUrlBtn:hover { background: #c7d2fe; }
/* ---- 児童用:回答フォーム ---- */
.answer-q { margin-bottom: 16px; }
.answer-q .q-text { font-size: 14.5px; font-weight: 700; margin-bottom: 8px; }
.choice-row { display: flex; flex-wrap: wrap; gap: 8px; }
.choice-btn {
padding: 10px 16px; font-size: 13.5px; font-weight: 700; border-radius: 999px; border: 2px solid #cbd5e1;
background: #fff; color: #1f2937; cursor: pointer; transition: all 0.15s ease-out;
}
.choice-btn.selected { border-color: #2563eb; background: #eff6ff; color: #1d4ed8; }
#submitBtn {
width: 100%; padding: 13px; font-size: 15px; font-weight: 700; border-radius: 12px; border: none;
background: #16a34a; color: #fff; cursor: pointer; transition: all 0.15s ease-out;
}
#submitBtn:hover:not(:disabled) { background: #15803d; }
#submitBtn:disabled { background: #cbd5e1; cursor: default; }
#submitStatus { font-size: 12.5px; font-weight: 700; text-align: center; min-height: 18px; margin-top: 10px; }
#thanksCard { text-align: center; }
#thanksCard .big-emoji { font-size: 44px; margin-bottom: 8px; }
/* ---- 先生用:回答確認・所見プロンプト作成 ---- */
.setting-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
.setting-row:last-child { margin-bottom: 0; }
.setting-row input[type="text"] { padding: 8px 10px; font-size: 13.5px; }
.length-row { display: flex; gap: 6px; flex-wrap: wrap; }
.len-btn {
font-size: 12px; font-weight: 700; padding: 8px 12px; border-radius: 999px; border: 2px solid #cbd5e1;
background: #fff; color: #1f2937; cursor: pointer; transition: all 0.15s ease-out;
}
.len-btn.active { border-color: #2563eb; background: #eff6ff; color: #1d4ed8; }
#reviewStatus { font-size: 12.5px; font-weight: 700; text-align: center; color: #C0392B; min-height: 18px; margin-bottom: 10px; }
.student-card {
background: #fff; border-radius: 16px; box-shadow: 0 8px 24px rgba(15,23,42,0.06);
padding: 16px 18px; margin-bottom: 14px;
}
.student-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; }
.student-name { font-size: 14.5px; font-weight: 700; color: #1f2937; }
.label-row { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #64748b; }
.label-row input {
width: 90px; padding: 6px 8px; font-size: 12.5px; border-radius: 8px; border: 2px solid #cbd5e1; font-family: inherit;
}
.label-row input:focus { outline: none; border-color: #2563eb; }
.answer-check-row {
display: flex; align-items: flex-start; gap: 8px; padding: 8px 0; font-size: 13px; line-height: 1.6;
border-top: 1px solid #f1f5f9;
}
.answer-check-row input { margin-top: 3px; flex-shrink: 0; }
.answer-check-row .q-text-small { font-weight: 700; color: #475569; }
.make-prompt-btn {
width: 100%; margin-top: 12px; padding: 10px; font-size: 13px; font-weight: 700; border-radius: 10px;
border: none; background: #2563eb; color: #fff; cursor: pointer; transition: all 0.15s ease-out;
}
.make-prompt-btn:hover { background: #1d4ed8; }
.prompt-output { margin-top: 12px; }
.prompt-output textarea {
width: 100%; min-height: 160px; padding: 10px; font-size: 12px; border-radius: 10px; border: 2px solid #cbd5e1;
font-family: "SFMono-Regular", Consolas, Menlo, monospace; line-height: 1.6; resize: vertical;
background: #f8fafc; color: #1f2937;
}
.copy-prompt-btn {
margin-top: 8px; font-size: 12.5px; font-weight: 700; padding: 8px 16px; border-radius: 8px; border: none;
background: #e0e7ff; color: #3730a3; cursor: pointer; transition: all 0.15s ease-out;
}
.copy-prompt-btn:hover { background: #c7d2fe; }
</style>
</head>
<body>
<div class="wrap">
<section id="manage-screen" hidden>
<h1>📋 アンケート作成(先生用)</h1>
<p class="lead">質問を自由に作って保存すると、児童に配る共有URLが発行されます。あとから編集して保存しなおすこともできます。</p>
<div class="role-switch"><a href="?mode=review" id="toReviewLink">📖 回答の確認・所見プロンプト作成へ</a> / <a href="?" id="toStudentLink">🧑🎓 回答フォームを見る</a></div>
<div class="card">
<div id="questionList"></div>
<button id="addQuestionBtn" type="button">+ 質問を追加する</button>
<button id="saveSurveyBtn" type="button">保存する</button>
<div id="saveStatus"></div>
<div id="urlResult" hidden>
<p>このURLを児童に共有してください(Google Classroomなど)</p>
<div id="shareUrl"></div>
<button id="copyUrlBtn" type="button">📋 コピーする</button>
</div>
</div>
</section>
<section id="answer-screen" hidden>
<h1>📝 アンケートに答えよう</h1>
<p class="lead">名前を入力してから、質問に答えてね。</p>
<div class="role-switch"><a href="?mode=manage" id="toManageLink">👩🏫 先生はこちら</a></div>
<div class="card">
<label class="field-label" for="nameInput">なまえ</label>
<input type="text" id="nameInput" placeholder="なまえを入力してね" maxlength="30">
</div>
<div class="card" id="answerFormCard">
<div id="questionsArea"></div>
<button id="submitBtn" type="button">送信する</button>
<div id="submitStatus"></div>
</div>
<div class="card" id="thanksCard" hidden>
<div class="big-emoji">🎉</div>
<div>回答を送りました!ありがとう。</div>
</div>
</section>
<section id="review-screen" hidden>
<h1>📖 回答確認・所見プロンプト作成(先生用)</h1>
<p class="lead">児童ごとの回答を確認し、所見に使いたい内容を選んで、生成AI(ChatGPTやClaudeなど)にわたすプロンプトを作成します。実名はこの画面にだけ表示され、コピーされるプロンプトには「呼び方」だけが入ります。</p>
<div class="role-switch"><a href="?mode=manage" id="toManageLink2">✏️ 質問編集へ</a> / <a href="?" id="toStudentLink2">🧑🎓 回答フォームを見る</a></div>
<div class="card">
<div class="setting-row">
<label class="field-label" for="gradeInput">学年(プロンプトに含めます)</label>
<input type="text" id="gradeInput" placeholder="例:3年" style="max-width:140px;">
</div>
<div class="setting-row">
<span class="field-label">文字数のめやす</span>
<div class="length-row" id="lengthRow">
<button class="len-btn active" data-len="short" type="button">短め(100〜150字)</button>
<button class="len-btn" data-len="normal" type="button">標準(150〜250字)</button>
<button class="len-btn" data-len="long" type="button">長め(250〜350字)</button>
</div>
</div>
</div>
<div id="reviewStatus"></div>
<div id="studentList"></div>
</section>
</div>
<script>
(function(){
const manageScreen = document.getElementById('manage-screen');
const answerScreen = document.getElementById('answer-screen');
const SAMPLE_QUESTIONS = [
{ id: 'q1', text: '今学期がんばった教科と、その理由を教えてください', type: 'text', options: [] },
{ id: 'q2', text: 'がんばったと思う生活面は?', type: 'choice', options: ['そうじ', 'あいさつ', '係活動', '友だちとのかかわり'] },
];
function hasGas(){
return typeof google !== 'undefined' && google.script && google.script.run;
}
function loadQuestions(onDone){
if(hasGas()){
google.script.run
.withSuccessHandler((qs) => onDone(qs && qs.length ? qs : []))
.withFailureHandler(() => onDone(SAMPLE_QUESTIONS))
.getQuestions();
} else {
onDone(SAMPLE_QUESTIONS);
}
}
// ================= 先生用:アンケート作成画面 =================
const questionList = document.getElementById('questionList');
const addQuestionBtn = document.getElementById('addQuestionBtn');
const saveSurveyBtn = document.getElementById('saveSurveyBtn');
const saveStatus = document.getElementById('saveStatus');
const urlResult = document.getElementById('urlResult');
const shareUrlEl = document.getElementById('shareUrl');
const copyUrlBtn = document.getElementById('copyUrlBtn');
let questions = [];
function renderQuestionList(){
questionList.innerHTML = '';
questions.forEach((q, i) => {
const card = document.createElement('div');
card.className = 'q-card';
const top = document.createElement('div');
top.className = 'q-card-top';
const textInput = document.createElement('input');
textInput.type = 'text';
textInput.placeholder = '質問文を入力';
textInput.value = q.text;
textInput.addEventListener('input', () => { q.text = textInput.value; });
top.appendChild(textInput);
card.appendChild(top);
const typeRow = document.createElement('div');
typeRow.className = 'q-type-row';
const textTypeBtn = document.createElement('button');
textTypeBtn.type = 'button';
textTypeBtn.className = 'type-btn' + (q.type === 'text' ? ' active' : '');
textTypeBtn.textContent = '自由記述';
const choiceTypeBtn = document.createElement('button');
choiceTypeBtn.type = 'button';
choiceTypeBtn.className = 'type-btn' + (q.type === 'choice' ? ' active' : '');
choiceTypeBtn.textContent = '選択式';
textTypeBtn.addEventListener('click', () => { q.type = 'text'; renderQuestionList(); });
choiceTypeBtn.addEventListener('click', () => {
q.type = 'choice';
if(q.options.length === 0) q.options = [''];
renderQuestionList();
});
typeRow.appendChild(textTypeBtn);
typeRow.appendChild(choiceTypeBtn);
card.appendChild(typeRow);
if(q.type === 'choice'){
q.options.forEach((opt, oi) => {
const row = document.createElement('div');
row.className = 'option-row';
const optInput = document.createElement('input');
optInput.type = 'text';
optInput.placeholder = '選択肢' + (oi + 1);
optInput.value = opt;
optInput.addEventListener('input', () => { q.options[oi] = optInput.value; });
const delOptBtn = document.createElement('button');
delOptBtn.type = 'button';
delOptBtn.className = 'icon-btn danger';
delOptBtn.textContent = '×';
delOptBtn.addEventListener('click', () => {
q.options.splice(oi, 1);
renderQuestionList();
});
row.appendChild(optInput);
row.appendChild(delOptBtn);
card.appendChild(row);
});
const addOptBtn = document.createElement('button');
addOptBtn.type = 'button';
addOptBtn.className = 'add-option-btn';
addOptBtn.textContent = '+ 選択肢を追加';
addOptBtn.addEventListener('click', () => {
q.options.push('');
renderQuestionList();
});
card.appendChild(addOptBtn);
}
const actions = document.createElement('div');
actions.className = 'q-card-actions';
const upBtn = document.createElement('button');
upBtn.type = 'button';
upBtn.className = 'icon-btn';
upBtn.textContent = '↑';
upBtn.disabled = i === 0;
upBtn.addEventListener('click', () => {
if(i === 0) return;
[questions[i - 1], questions[i]] = [questions[i], questions[i - 1]];
renderQuestionList();
});
const downBtn = document.createElement('button');
downBtn.type = 'button';
downBtn.className = 'icon-btn';
downBtn.textContent = '↓';
downBtn.disabled = i === questions.length - 1;
downBtn.addEventListener('click', () => {
if(i === questions.length - 1) return;
[questions[i + 1], questions[i]] = [questions[i], questions[i + 1]];
renderQuestionList();
});
const delBtn = document.createElement('button');
delBtn.type = 'button';
delBtn.className = 'icon-btn danger';
delBtn.textContent = '🗑';
delBtn.addEventListener('click', () => {
questions.splice(i, 1);
renderQuestionList();
});
actions.appendChild(upBtn);
actions.appendChild(downBtn);
actions.appendChild(delBtn);
card.appendChild(actions);
questionList.appendChild(card);
});
urlResult.hidden = true;
}
addQuestionBtn.addEventListener('click', () => {
questions.push({ id: 'q' + (questions.length + 1), text: '', type: 'text', options: [] });
renderQuestionList();
});
function normalizeQuestionsForSave(){
return questions
.map((q, i) => ({
id: 'q' + (i + 1),
text: q.text.trim(),
type: q.type,
options: q.type === 'choice' ? q.options.map((o) => o.trim()).filter((o) => o) : [],
}))
.filter((q) => q.text && (q.type === 'text' || q.options.length >= 2));
}
saveSurveyBtn.addEventListener('click', () => {
const originalCount = questions.length;
const cleaned = normalizeQuestionsForSave();
if(cleaned.length === 0){
saveStatus.textContent = '質問を1つ以上入力してね(選択式は選択肢を2つ以上)';
saveStatus.style.color = '#C0392B';
return;
}
const droppedCount = originalCount - cleaned.length;
questions = cleaned;
renderQuestionList();
if(!hasGas()){
saveStatus.textContent = 'この機能はGoogleスプレッドシート連携版として配信された環境でのみ保存できます(プレビューでは保存されません)。';
saveStatus.style.color = '#C0392B';
return;
}
saveSurveyBtn.disabled = true;
saveStatus.textContent = '保存中…';
saveStatus.style.color = '#475569';
google.script.run
.withSuccessHandler(() => {
saveStatus.textContent = '保存しました!' + (droppedCount > 0 ? '(空の質問' + droppedCount + '件は保存されませんでした)' : '');
saveStatus.style.color = '#16a34a';
saveSurveyBtn.disabled = false;
const url = window.location.origin + window.location.pathname;
shareUrlEl.textContent = url;
urlResult.hidden = false;
})
.withFailureHandler(() => {
saveStatus.textContent = '保存に失敗しました。もう一度試してね';
saveStatus.style.color = '#C0392B';
saveSurveyBtn.disabled = false;
})
.saveQuestions(questions);
});
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(() => {});
}
});
// ================= 児童用:回答フォーム画面 =================
const nameInput = document.getElementById('nameInput');
const questionsArea = document.getElementById('questionsArea');
const answerFormCard = document.getElementById('answerFormCard');
const submitBtn = document.getElementById('submitBtn');
const submitStatus = document.getElementById('submitStatus');
const thanksCard = document.getElementById('thanksCard');
let answerQuestions = [];
let answers = {};
function renderAnswerForm(qs){
answerQuestions = qs;
answers = {};
questionsArea.innerHTML = '';
qs.forEach((q) => {
const block = document.createElement('div');
block.className = 'answer-q';
const label = document.createElement('div');
label.className = 'q-text';
label.textContent = q.text;
block.appendChild(label);
if(q.type === 'choice'){
const row = document.createElement('div');
row.className = 'choice-row';
q.options.forEach((opt) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'choice-btn';
btn.textContent = opt;
btn.addEventListener('click', () => {
answers[q.id] = opt;
row.querySelectorAll('.choice-btn').forEach((b) => b.classList.toggle('selected', b === btn));
});
row.appendChild(btn);
});
block.appendChild(row);
} else {
const ta = document.createElement('textarea');
ta.placeholder = '答えを入力してね';
ta.addEventListener('input', () => { answers[q.id] = ta.value; });
block.appendChild(ta);
}
questionsArea.appendChild(block);
});
}
submitBtn.addEventListener('click', () => {
const name = nameInput.value.trim();
if(!name){
submitStatus.textContent = 'なまえを入力してね';
submitStatus.style.color = '#C0392B';
nameInput.focus();
return;
}
const answerList = answerQuestions.map((q) => ({
questionId: q.id,
questionText: q.text,
answerText: (answers[q.id] || '').toString().trim(),
}));
if(answerList.every((a) => !a.answerText)){
submitStatus.textContent = 'すくなくとも1つは答えてね';
submitStatus.style.color = '#C0392B';
return;
}
if(!hasGas()){
submitStatus.textContent = 'この機能はGoogleスプレッドシート連携版として配信された環境でのみ使えます。';
submitStatus.style.color = '#C0392B';
return;
}
submitBtn.disabled = true;
submitStatus.textContent = '送信中…';
submitStatus.style.color = '#475569';
google.script.run
.withSuccessHandler(() => {
answerFormCard.hidden = true;
thanksCard.hidden = false;
})
.withFailureHandler(() => {
submitStatus.textContent = '送信に失敗しました。もう一度試してね';
submitStatus.style.color = '#C0392B';
submitBtn.disabled = false;
})
.submitAnswer({ name: name, answers: answerList });
});
// ================= 先生用:回答確認・所見プロンプト作成画面 =================
const reviewScreen = document.getElementById('review-screen');
const reviewStatus = document.getElementById('reviewStatus');
const studentList = document.getElementById('studentList');
const gradeInput = document.getElementById('gradeInput');
const lengthRow = document.getElementById('lengthRow');
const LENGTH_LABELS = {
short: '100〜150字程度',
normal: '150〜250字程度',
long: '250〜350字程度',
};
let lengthPref = 'short';
lengthRow.addEventListener('click', (e) => {
const btn = e.target.closest('.len-btn');
if(!btn) return;
lengthPref = btn.dataset.len;
lengthRow.querySelectorAll('.len-btn').forEach((b) => b.classList.toggle('active', b === btn));
});
function loadResponses(onDone){
if(hasGas()){
google.script.run
.withSuccessHandler((rs) => onDone(rs || []))
.withFailureHandler(() => onDone(null))
.getResponses();
} else {
onDone(null); // プレビューでは回答データが存在しないため、案内メッセージを出す
}
}
// 個人が特定されないよう、実名の代わりにこの文字列だけをプロンプトに載せる
function buildPrompt(label, grade, answers){
const lengthText = LENGTH_LABELS[lengthPref];
let prompt = 'あなたは小学校の先生です。以下の情報をもとに、通知表の所見文(担任からのコメント)を作成してください。\n\n';
prompt += '【条件】\n';
prompt += '・文体は「〜です」「〜ます」の丁寧な言葉づかいにしてください\n';
prompt += '・文字数の目安:' + lengthText + '\n';
prompt += '・よいところが伝わるように、前向きな表現でまとめてください\n';
prompt += '・個人が特定されないよう、文章の中で名前を出すときは「' + label + '」とだけ表記してください\n';
if(grade) prompt += '・対象学年:' + grade + '\n';
prompt += '\n【' + label + 'のアンケート回答(先生が選んだ内容のみ)】\n';
if(answers.length === 0){
prompt += '(内容が選ばれていません)\n';
} else {
answers.forEach((a) => {
prompt += '・' + a.questionText + ':' + a.answerText + '\n';
});
}
prompt += '\n上記の内容をもとに、所見文を2〜3パターン作成してください。';
return prompt;
}
function renderStudentList(students){
studentList.innerHTML = '';
if(students === null){
reviewStatus.textContent = 'この機能はGoogleスプレッドシート連携版として配信された環境でのみ使えます(プレビューには回答データがありません)。';
return;
}
if(students.length === 0){
reviewStatus.textContent = 'まだ回答がありません。';
return;
}
reviewStatus.textContent = '';
students.forEach((student, si) => {
const card = document.createElement('div');
card.className = 'student-card';
const head = document.createElement('div');
head.className = 'student-head';
const nameEl = document.createElement('span');
nameEl.className = 'student-name';
nameEl.textContent = student.name + '(実名・この画面にのみ表示)';
const labelRow = document.createElement('span');
labelRow.className = 'label-row';
const labelText = document.createElement('span');
labelText.textContent = '呼び方';
const labelInput = document.createElement('input');
labelInput.type = 'text';
labelInput.value = '児童' + (si + 1);
labelInput.maxLength = 20;
labelRow.appendChild(labelText);
labelRow.appendChild(labelInput);
head.appendChild(nameEl);
head.appendChild(labelRow);
card.appendChild(head);
const checks = [];
student.answers.forEach((a) => {
const row = document.createElement('label');
row.className = 'answer-check-row';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = true;
const textSpan = document.createElement('span');
const qSpan = document.createElement('span');
qSpan.className = 'q-text-small';
qSpan.textContent = a.questionText + ':';
textSpan.appendChild(qSpan);
textSpan.appendChild(document.createTextNode(a.answerText));
row.appendChild(cb);
row.appendChild(textSpan);
card.appendChild(row);
checks.push({ checkbox: cb, answer: a });
});
const makeBtn = document.createElement('button');
makeBtn.type = 'button';
makeBtn.className = 'make-prompt-btn';
makeBtn.textContent = '📝 プロンプトを作る';
card.appendChild(makeBtn);
const outputWrap = document.createElement('div');
outputWrap.className = 'prompt-output';
outputWrap.hidden = true;
const ta = document.createElement('textarea');
ta.readOnly = true;
const copyBtn = document.createElement('button');
copyBtn.type = 'button';
copyBtn.className = 'copy-prompt-btn';
copyBtn.textContent = '📋 コピーする';
outputWrap.appendChild(ta);
outputWrap.appendChild(copyBtn);
card.appendChild(outputWrap);
makeBtn.addEventListener('click', () => {
const label = labelInput.value.trim() || ('児童' + (si + 1));
const selected = checks.filter((c) => c.checkbox.checked).map((c) => c.answer);
ta.value = buildPrompt(label, gradeInput.value.trim(), selected);
outputWrap.hidden = false;
});
copyBtn.addEventListener('click', () => {
if(navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(ta.value).then(() => {
copyBtn.textContent = '✅ コピーしました';
setTimeout(() => { copyBtn.textContent = '📋 コピーする'; }, 1500);
}).catch(() => {});
}
});
studentList.appendChild(card);
});
}
// ================= 画面切り替え =================
function init(){
const params = new URLSearchParams(window.location.search);
const mode = params.get('mode');
manageScreen.hidden = true;
answerScreen.hidden = true;
reviewScreen.hidden = true;
if(mode === 'manage'){
manageScreen.hidden = false;
loadQuestions((qs) => {
questions = qs.length ? qs : [{ id: 'q1', text: '', type: 'text', options: [] }];
renderQuestionList();
});
} else if(mode === 'review'){
reviewScreen.hidden = false;
loadResponses(renderStudentList);
} else {
answerScreen.hidden = false;
loadQuestions(renderAnswerForm);
}
}
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('所見作成アンケート - データ');
props.setProperty('SPREADSHEET_ID', ss.getId());
return ss;
}
function getOrCreateSheet_(ss, name, headerRow) {
let sheet = ss.getSheetByName(name);
if (!sheet) {
sheet = ss.insertSheet(name);
sheet.appendRow(headerRow);
sheet.setFrozenRows(1);
}
return sheet;
}
function questionsSheet_() {
const ss = getOrCreateSpreadsheet_();
const sheet = getOrCreateSheet_(ss, '質問マスタ', ['質問ID', '質問文', '質問タイプ', '選択肢', '表示順']);
const defaultSheet = ss.getSheetByName('シート1');
if (defaultSheet && ss.getSheets().length > 1) {
ss.deleteSheet(defaultSheet);
}
return sheet;
}
function responsesSheet_() {
const ss = getOrCreateSpreadsheet_();
return getOrCreateSheet_(ss, '回答記録', ['回答日時', '児童名', '質問ID', '質問文', '回答内容']);
}
/**
* 現在の質問一覧を返す(表示順にならべて)。
* 児童用フォームと、先生用の編集画面の両方から呼ばれる。
*/
function getQuestions() {
const sheet = questionsSheet_();
const values = sheet.getDataRange().getValues();
const rows = values.slice(1);
return rows
.filter((r) => r[0])
.sort((a, b) => (Number(a[4]) || 0) - (Number(b[4]) || 0))
.map((r) => ({
id: r[0].toString(),
text: r[1].toString(),
type: r[2] === 'choice' ? 'choice' : 'text',
options: r[3] ? r[3].toString().split(',').map((s) => s.trim()).filter((s) => s) : [],
}));
}
/**
* 先生が編集した質問一覧をまとめて保存する(既存の質問はすべて置きかえる)。
* questions = [{ id, text, type: 'text'|'choice', options: string[] }, ...]
*/
function saveQuestions(questions) {
if (!Array.isArray(questions) || questions.length === 0) {
throw new Error('質問が空です');
}
const sheet = questionsSheet_();
const lastRow = sheet.getLastRow();
if (lastRow > 1) {
sheet.getRange(2, 1, lastRow - 1, 5).clearContent();
}
const rows = questions.map((q, i) => [
q.id || ('q' + (i + 1)),
(q.text || '').toString().slice(0, 300),
q.type === 'choice' ? 'choice' : 'text',
Array.isArray(q.options) ? q.options.join(',') : '',
i + 1,
]);
sheet.getRange(2, 1, rows.length, 5).setValues(rows);
return { ok: true };
}
/**
* 児童の回答をスプレッドシートに記録する(質問ごとに1行)。
* data = { name: string, answers: [{ questionId, questionText, answerText }, ...] }
*/
function submitAnswer(data) {
if (!data || !data.name) {
throw new Error('なまえがありません');
}
const sheet = responsesSheet_();
const now = new Date();
const answers = Array.isArray(data.answers) ? data.answers : [];
answers.forEach((a) => {
if (!a.answerText) return;
sheet.appendRow([
now,
data.name.toString().slice(0, 40),
(a.questionId || '').toString(),
(a.questionText || '').toString().slice(0, 300),
(a.answerText || '').toString().slice(0, 1000),
]);
});
return { ok: true };
}
/**
* 児童ごとにまとめた回答一覧を返す(先生用の確認画面から呼ばれる)。
* 実名を含むため、この関数の戻り値は先生の画面にだけ表示し、
* 生成AI向けのプロンプトには含めないこと(呼び方に置きかえて使う)。
*/
function getResponses() {
const sheet = responsesSheet_();
const values = sheet.getDataRange().getValues();
const rows = values.slice(1);
const byName = {};
const order = [];
rows.forEach((r) => {
const name = r[1] ? r[1].toString() : '';
if (!name) return;
if (!byName[name]) {
byName[name] = [];
order.push(name);
}
byName[name].push({
questionId: r[2] ? r[2].toString() : '',
questionText: r[3] ? r[3].toString() : '',
answerText: r[4] ? r[4].toString() : '',
});
});
return order.map((name) => ({ name: name, answers: byName[name] }));
}
「所見作成アンケート」 スプレッドシート送信版 - デプロイ手順
====================================================
このフォルダには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の◯◯を直したい」のように伝えてもらえれば調整できます。
【この教材について】
このアンケート教材は「①アンケート作成 → ②児童が回答 → ③先生が回答一覧を見る」
という一連の流れのうち、①と②の部分(アンケート作成〜回答収集まで)を実装したものです。
所見文の自動生成・回答一覧の確認画面は、今後の追加開発で対応予定です。
【使い方】
・先生用の質問編集画面:デプロイ後のURLの末尾に「?mode=manage」をつけて開きます
例)https://script.google.com/.../exec?mode=manage
・児童用の回答フォーム:「?mode=manage」なしの、そのままのURLを共有してください