📋 アンケート作成(先生用)

質問を自由に作って保存すると、児童に配る共有URLが発行されます。あとから編集して保存しなおすこともできます。

📖 回答の確認・所見プロンプト作成へ / 🧑‍🎓 回答フォームを見る

このURLを児童に共有してください(Google Classroomなど)

📝 アンケートに答えよう

名前を入力してから、質問に答えてね。

👩‍🏫 先生はこちら
🎉
回答を送りました!ありがとう。

📖 回答確認・所見プロンプト作成(先生用)

児童ごとの回答を確認し、所見に使いたい内容を選んで、生成AI(ChatGPTやClaudeなど)にわたすプロンプトを作成します。実名はこの画面にだけ表示され、コピーされるプロンプトには「呼び方」だけが入ります。

✏️ 質問編集へ / 🧑‍🎓 回答フォームを見る
文字数のめやす

所見作成アンケート

Google Apps Script デプロイ用ファイル一式
クラス共有版(児童の回答をGoogleスプレッドシートに集約する機能つき)を Google Apps Scriptにデプロイするための3つのファイルです。 それぞれの「コピー」ボタンで個別にコピーし、 ③ README.txt の手順に沿って script.google.com に貼り付けてください。
① index.html ② Code.gs ③ README.txt(手順)
1
index.html
児童が触る画面本体(左の「🎮 実際の画面」で動作を確認できます)
<!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>
2
Code.gs
Googleスプレッドシートへの書き込みを行うバックエンド処理
/**
 * 「所見作成アンケート」 - アンケートの質問保存・回答収集を行う機能
 *
 * 使い方(デプロイ手順)は 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
README.txt(デプロイ手順)
script.google.com への貼り付け方・公開方法
「所見作成アンケート」 スプレッドシート送信版 - デプロイ手順
====================================================

このフォルダには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を共有してください