LEARNING EVIDENCE · RESPONSIVE TEACHING · CARE

From Play to Proof
with CARE

把一次性、玩完就結束的互動網頁,升級成「留下學習證據、看得到全班狀況、能立即回饋教學」的數據導向教育網頁。

紀欣妤老師 (Sylvia)|國立新豐高中英文老師
hsinyuchi@sfsh.tn.edu.tw
PART 0

Prep|開始前先準備好

  • 強烈建議用桌機或筆電;若能準備雙載具,邊看邊操作會更方便。
  • 選一篇你想拿來操作的英文課文/教材;若已經有 AI 教學網頁,先準備好 HTML 程式碼。
  • Google 個人帳號通常最容易操作。學校帳號也可以,但每校管理設定不同;操作時盡量只登入一個帳號。
PART 1

Warm-up|先看看我們現在在哪裡

開場調查

Warm-up

Sylvia’s Data Flow Wall|開場彈幕

點擊前往

開始之前

不是每堂課都需要網頁。如果紙筆能更快、更好,就用紙筆。

Every student responds.
→
Everyone gets instant feedback.
→
The teacher sees the whole class.
PART 2

Presentation|Experience the why

先當一次學生

先體驗學生端,再看教師端。

Demo Course

Goodbye, John|示範課程

點擊前往

先問自己:好不好玩?如果到這裡就結束,老師其實知道了什麼?

互動活動
→
Learning Report
→
Teacher Dashboard
→
下一步教學

三個教學轉變

轉變真正的價值
Every student responds.從少數幾位主動學生的聲音,變成每個人都留下思考痕跡。
The teacher sees the whole class.Dashboard 不是代替老師,而是先把 30–40 位學生的訊號整理給老師看。
Data tells you what NOT to teach.數據不只是告訴你要補教什麼,也能告訴你哪些內容可以不用重教。

Behind the magic

前端 HTML
學生操作
→
Universal GAS
固定資料通道
→
Google Sheet
留下資料
→
Teacher Dashboard
看見全班

你不需要自己會寫這些程式。今天真正要學的是:先決定什麼 Learning Evidence 值得留下,再讓 AI 幫我們把前端、後端與資料接起來。

PART 3

Practice|Learn the CARE workflow

From Play to Proof is the goal; CARE is how we get there.

C
Create先有一個學生可以操作的互動網頁。
A
AugmentAI 先規劃 Learning Evidence;老師確認後才改。
R
Record把確認後的資料真正寫進 Sheet、讀回 Dashboard。
E
Evaluate看資料、做判斷、決定下一步教學。

Technical Prep|先把固定後台準備好

為了降低現場除錯風險,我們先把全場共用、已驗證的資料通道準備完成。這一步只是先把技術底座準備好;真正的 R,會在 A 階段確認資料後才發生。

Prep.1|建立 Google Sheet

  1. 在 Google Drive 新增一份 Google 試算表。
  2. 不需要先設計表頭;Universal GAS 會依前端送來的資料自動建立/擴充 Responses 欄位。
  3. 從試算表上方選單開啟:擴充功能 → Apps Script。

Prep.2|貼上 Universal GAS Backend

  1. 刪除 Apps Script 內的預設程式碼。
  2. 複製下方完整 Universal GAS Backend。
  3. 貼上後儲存。
Universal GAS Backend本場研習標準後端
// ======================================================
// From Play to Proof|CARE Universal GAS 2.0
// Auto-Title Routing Edition (免換試算表・自動分頁歸檔版)
//
// 核心特色:
// 1. 同一份 Google 試算表、同一個 GAS URL,全學年所有課次直接共用!
// 2. 前端網頁全自動以 <title> 作為分頁名稱,GAS 自動建立對應分頁
// 3. Teacher Dashboard 使用 ?title=活動標題,全自動讀取同一專屬分頁
// 4. 自動過濾試算表非法字元(如 : \ / ? * [ ] 與前後引號),避免分頁建立失敗
// 5. 【100% 向下相容】:舊版網頁若傳 sheetName / lesson / unit 照樣可用;若完全未指定則安全寫入 Responses 分頁
// 6. 自動建立/擴充表頭,不同題型自動長欄位,永不撞車
// 7. array / object 自動序列化為 JSON 字串完整保存
// 8. 採用 LockService 降低多人同時提交造成的資料衝突風險
// ======================================================

const DEFAULT_SHEET_NAME = 'Responses';

// ======================================================
// 0. 將活動標題轉成合法 Google Sheet 分頁名稱(防禦性安全過濾)
// ======================================================
function normalizeSheetName(rawName) {
  if (rawName === undefined || rawName === null) {
    return DEFAULT_SHEET_NAME;
  }

  let name = String(rawName).trim();
  if (!name) {
    return DEFAULT_SHEET_NAME;
  }

  // 去除頭尾的單引號與雙引號(避免 Google Sheets 分頁參照公式或語法出錯)
  name = name.replace(/^['"]+|['"]+$/g, '').trim();

  // Google Sheet 分頁名稱不可包含:: \ / ? * [ ]
  name = name.replace(/[:\\/\?\*\[\]]/g, '-');

  // 多個連續空白整理成單一空格
  name = name.replace(/\s+/g, ' ').trim();

  // 避免名稱過長(Google Sheets 分頁名稱上限為 100 字元,此處保守控制在 80 字元)
  if (name.length > 80) {
    name = name.substring(0, 80).trim();
  }

  // 再次清理頭尾多餘的連字號與引號
  name = name.replace(/^[-'"]+|[-'"]+$/g, '').trim();

  if (!name) {
    return DEFAULT_SHEET_NAME;
  }

  return name;
}

// ======================================================
// 1. 接收學生端資料(POST)
// ======================================================
function doPost(e) {
  const lock = LockService.getScriptLock();

  try {
    // 最多等待 30 秒取得鎖,防止多人併發寫入衝突
    lock.waitLock(30000);

    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

    // 檢查 POST 資料
    if (!e || !e.postData || !e.postData.contents) {
      throw new Error('未收到 POST 內容');
    }

    const data = JSON.parse(e.postData.contents);

    // ==================================================
    // CARE 2.0 Auto-Title Routing 路由判定
    // 優先順序:
    // 1. sheetName   → 2.0 手動指定 / 覆寫
    // 2. sourceTitle → 2.0 自動網頁標題
    // 3. lesson      → 舊活動相容
    // 4. unit        → 舊活動相容
    // 5. Responses   → 最終安全 fallback
    // ==================================================
    const rawSheetName =
      data.sheetName ||
      data.sourceTitle ||
      data.lesson ||
      data.unit ||
      DEFAULT_SHEET_NAME;

    const targetSheetName = normalizeSheetName(rawSheetName);

    // 找到或建立目標分頁
    let sheet = spreadsheet.getSheetByName(targetSheetName);
    if (!sheet) {
      sheet = spreadsheet.insertSheet(targetSheetName);
    }

    // 自動加入時間戳記
    data.Timestamp = Utilities.formatDate(
      new Date(),
      Session.getScriptTimeZone(),
      'yyyy-MM-dd HH:mm:ss'
    );

    // 若使用 2.0 sourceTitle,保留原始活動標題字串作為資料欄位以利查閱
    if (data.sourceTitle !== undefined && data.sourceTitle !== null) {
      data.ActivityTitle = String(data.sourceTitle).trim();
    }

    // 將 array / object 轉成 JSON 字串保存
    Object.keys(data).forEach(function (key) {
      const value = data[key];
      if (value !== null && typeof value === 'object') {
        data[key] = JSON.stringify(value);
      }
    });

    // 讀取既有表頭
    const lastCol = sheet.getLastColumn();
    let headers = [];
    if (lastCol > 0) {
      headers = sheet
        .getRange(1, 1, 1, lastCol)
        .getValues()[0]
        .map(function (header) {
          return String(header).trim();
        })
        .filter(function (header) {
          return header !== '';
        });
    }

    // 自動擴充尚未存在的新欄位
    Object.keys(data).forEach(function (key) {
      if (headers.indexOf(key) === -1) {
        headers.push(key);
      }
    });

    // 回寫表頭
    if (headers.length > 0) {
      sheet
        .getRange(1, 1, 1, headers.length)
        .setValues([headers]);
    }

    // 依表頭順序建立學生資料列
    const rowData = headers.map(function (header) {
      return data[header] !== undefined && data[header] !== null
        ? data[header]
        : '';
    });

    // 新增資料列
    sheet.appendRow(rowData);

    // 回傳成功結果
    return ContentService
      .createTextOutput(
        JSON.stringify({
          status: 'success',
          message: '資料已成功寫入 ' + targetSheetName,
          sheet: targetSheetName
        })
      )
      .setMimeType(ContentService.MimeType.JSON);

  } catch (error) {
    return ContentService
      .createTextOutput(
        JSON.stringify({
          status: 'error',
          message: error.toString()
        })
      )
      .setMimeType(ContentService.MimeType.JSON);

  } finally {
    if (lock.hasLock()) {
      lock.releaseLock();
    }
  }
}

// ======================================================
// 2. 教師儀表板讀取資料(GET)- 供 Teacher Dashboard 使用
// ======================================================
function doGet(e) {
  try {
    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

    // ==================================================
    // CARE 2.0 Dashboard Routing
    // 支援:
    // ?title=Be%20Media%20Smart (2.0 Auto-Title 格式)
    // ?sheet=B1L3               (2.0 舊格式向下相容)
    // 若均無指定,預設讀取 Responses
    // ==================================================
    let rawSheetName = DEFAULT_SHEET_NAME;

    if (e && e.parameter) {
      if (e.parameter.sheet) {
        rawSheetName = e.parameter.sheet;
      } else if (e.parameter.title) {
        rawSheetName = e.parameter.title;
      }
    }

    const targetSheetName = normalizeSheetName(rawSheetName);
    const sheet = spreadsheet.getSheetByName(targetSheetName);

    // 找不到工作表時回傳空陣列
    if (!sheet) {
      return ContentService
        .createTextOutput(JSON.stringify([]))
        .setMimeType(ContentService.MimeType.JSON);
    }

    const lastRow = sheet.getLastRow();
    const lastCol = sheet.getLastColumn();

    // 尚無學生資料時回傳空陣列
    if (lastRow < 2 || lastCol < 1) {
      return ContentService
        .createTextOutput(JSON.stringify([]))
        .setMimeType(ContentService.MimeType.JSON);
    }

    // 讀取整張工作表資料
    const values = sheet
      .getRange(1, 1, lastRow, lastCol)
      .getValues();

    const headers = values[0];
    const result = [];

    // 每列轉成 JSON 物件
    for (let i = 1; i < values.length; i++) {
      const rowObject = {};
      let hasData = false;

      for (let j = 0; j < headers.length; j++) {
        const key = String(headers[j]).trim();
        if (!key) continue;

        const value = values[i][j];
        rowObject[key] = value;

        if (value !== '' && value !== null) {
          hasData = true;
        }
      }

      // 避免空白列進入 Dashboard
      if (hasData) {
        result.push(rowObject);
      }
    }

    return ContentService
      .createTextOutput(JSON.stringify(result))
      .setMimeType(ContentService.MimeType.JSON);

  } catch (error) {
    return ContentService
      .createTextOutput(
        JSON.stringify({
          error: error.toString()
        })
      )
      .setMimeType(ContentService.MimeType.JSON);
  }
}

Prep.3|部署 GAS,先拿到自己的 GAS URL

  1. 點右上角「部署」→「新增部署作業」。
  2. 類型選「網頁應用程式」。
  3. 執行身分選「我」;誰可以存取務必選「所有人 (Anyone)」。
  4. 🚨 研習翻車第一名陷阱:存取權限務必選「所有人 (Anyone)」!
    Google Apps Script 預設值往往是「僅限我自己 (Only myself)」。若忘記改選為「所有人 (Anyone)」,網頁看起來完全正常,但學生送出作答時會全部被 Google 默默擋掉(403 錯誤),試算表完全收不到任何資料! 請老師在點擊部署前務必再次確認!
  5. 完成授權後,複製 Web App URL,先留在記事本備用。

完成後,今天後面通常不需要再回 Apps Script 改程式;接下來主要都在 AI 與 HTML 中操作。

C — Create|先有一份能正常操作的 HTML

已經有自己的 HTML 就直接使用,不需要重做。沒有的話,可以用講師 Starter HTML、把紙本學習單交給 AI,或從 8 個 Starter Prompts 挑一個快速生成。

C — RESOURCE

Starter HTML|Animal Sleep

一份刻意保持簡單、尚未加入身分/資料提交/Learning Report/Dashboard 的演練起點。

Starter HTML_B1L3_Animal_Sleep完整 HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Do Animals Sleep like You and Me? — Starter Activity</title>
<style>
:root{
  --navy:#20304a;--blue:#6ec6e8;--pink:#ef6f9a;--yellow:#ffd86b;
  --green:#66c7a6;--purple:#8b7ed8;--ink:#263238;--muted:#60717b;
}
*{box-sizing:border-box}
body{margin:0;font-family:"Segoe UI",Arial,sans-serif;color:var(--ink);
background:linear-gradient(180deg,#dff4ff 0%,#f8fcff 50%,#fffaf4 100%);min-height:100vh}
.wrap{max-width:960px;margin:auto;padding:28px 18px 56px}
header{background:linear-gradient(135deg,var(--navy),#324d78);color:#fff;border-radius:28px;padding:30px;
box-shadow:0 14px 40px rgba(32,48,74,.18);position:relative;overflow:hidden}
header:after{content:"💤";position:absolute;right:28px;top:14px;font-size:74px;opacity:.16;transform:rotate(-10deg)}
.eyebrow{font-weight:800;letter-spacing:.12em;text-transform:uppercase;font-size:.78rem;color:#bfeaff}
h1{margin:.25rem 0 .45rem;font-size:clamp(2rem,5vw,3.5rem);line-height:1.05}
header p{max-width:720px;margin:0;color:#e6f6ff;font-size:1.05rem}
.pillrow{display:flex;gap:8px;flex-wrap:wrap;margin-top:18px}
.pill{background:rgba(255,255,255,.12);padding:8px 12px;border-radius:999px;font-size:.88rem}
.card{background:#fff;border-radius:22px;padding:24px;margin-top:20px;box-shadow:0 10px 30px rgba(55,84,103,.10);
border:1px solid rgba(95,145,175,.16)}
.section-title{display:flex;align-items:center;gap:12px;margin:0 0 8px;font-size:1.45rem}
.num{width:38px;height:38px;border-radius:12px;display:grid;place-items:center;color:#fff;font-weight:900;background:var(--pink);flex:none}
.hint{margin:.25rem 0 1rem;color:var(--muted);line-height:1.6}
.fact-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
.fact{padding:16px;border-radius:18px;min-height:135px;display:flex;flex-direction:column;justify-content:space-between}
.fact strong{font-size:1.04rem}.fact span{font-size:2rem}.fact small{line-height:1.45;color:#42515b}
.f1{background:#fff4c9}.f2{background:#e8fff6}.f3{background:#edf0ff}.f4{background:#ffeaf1}
.question{padding:16px 0;border-top:1px dashed #ccd8df}.question:first-of-type{border-top:none}
.qtext{font-weight:800;margin-bottom:10px;line-height:1.5}.options{display:grid;gap:9px}
.option{display:flex;gap:10px;align-items:flex-start;border:1px solid #d8e3e9;border-radius:14px;padding:12px 14px;
cursor:pointer;transition:.18s;background:#fbfdff}
.option:hover{transform:translateY(-1px);border-color:#9ccfe3;background:#f3fbff}
input[type=radio]{margin-top:4px;accent-color:var(--pink)}
select,textarea{width:100%;border:1.5px solid #cbdbe4;border-radius:14px;padding:12px;font:inherit;background:#fff;color:var(--ink)}
textarea{min-height:100px;resize:vertical;line-height:1.5}
.cause-grid{display:grid;grid-template-columns:180px 1fr;gap:10px 14px;align-items:center}
.animal-label{font-weight:800;background:#f4f8fb;border-radius:12px;padding:12px}
.actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:18px}
button{border:0;border-radius:14px;padding:13px 18px;font:inherit;font-weight:800;cursor:pointer;transition:.18s}
button:hover{transform:translateY(-1px)}.primary{background:var(--navy);color:#fff}.secondary{background:#eef4f7;color:var(--navy)}
#result{display:none;margin-top:16px;padding:16px;border-radius:16px;background:#f0fbf7;border:1px solid #bde9d8}
.feedback{font-size:.92rem;margin-top:8px;font-weight:700}.correct{color:#1f9d68}.incorrect{color:#d9534f}
.footer-note{margin-top:18px;color:#6c7b85;font-size:.86rem;text-align:center;line-height:1.5}
@media(max-width:760px){.fact-grid{grid-template-columns:repeat(2,1fr)}.cause-grid{grid-template-columns:1fr}}
</style>
</head>
<body>
<div class="wrap">
<header>
  <div class="eyebrow">Lesson 3 · Starter HTML</div>
  <h1>Do Animals Sleep like You and Me?</h1>
  <p>A short interactive reading check about four surprising animal sleeping habits.</p>
  <div class="pillrow">
    <span class="pill">🦒 Giraffe</span><span class="pill">🦦 Sea Otter</span>
    <span class="pill">🐬 Dolphin</span><span class="pill">🐦 Alpine Swift</span>
  </div>
</header>

<section class="card">
  <h2 class="section-title"><span class="num">1</span>Quick Facts</h2>
  <p class="hint">Read these short clues before you answer.</p>
  <div class="fact-grid">
    <div class="fact f1"><span>🦒</span><strong>Giraffe</strong><small>Some giraffes sleep only about 30 minutes a day and avoid long naps when predators are nearby.</small></div>
    <div class="fact f2"><span>🦦</span><strong>Sea otter</strong><small>Sea otters may float on their backs and hold paws so they do not drift away from one another.</small></div>
    <div class="fact f3"><span>🐬</span><strong>Dolphin</strong><small>Only half of a dolphin's brain sleeps at one time so it can keep swimming, breathing, and watching for danger.</small></div>
    <div class="fact f4"><span>🐦</span><strong>Alpine swift</strong><small>These migratory birds can take short naps while flying, helping them avoid danger on the ground.</small></div>
  </div>
</section>

<section class="card">
  <h2 class="section-title"><span class="num" style="background:var(--green)">2</span>Reading Check</h2>
  <p class="hint">Choose the best answer. Then click <strong>Check My Answers</strong>.</p>

  <div class="question">
    <div class="qtext">1. Which animal sleeps very little and avoids long naps because it may need to escape predators quickly?</div>
    <div class="options">
      <label class="option"><input type="radio" name="q1" value="A">A. Sea otter</label>
      <label class="option"><input type="radio" name="q1" value="B">B. Giraffe</label>
      <label class="option"><input type="radio" name="q1" value="C">C. Dolphin</label>
      <label class="option"><input type="radio" name="q1" value="D">D. Alpine swift</label>
    </div><div class="feedback" id="fb-q1"></div>
  </div>

  <div class="question">
    <div class="qtext">2. Why do sea otters hold their paws together while sleeping?</div>
    <div class="options">
      <label class="option"><input type="radio" name="q2" value="A">A. To keep warm in the air</label>
      <label class="option"><input type="radio" name="q2" value="B">B. To avoid floating away</label>
      <label class="option"><input type="radio" name="q2" value="C">C. To help them dive faster</label>
      <label class="option"><input type="radio" name="q2" value="D">D. To hide from birds</label>
    </div><div class="feedback" id="fb-q2"></div>
  </div>

  <div class="question">
    <div class="qtext">3. What is special about a dolphin's sleep?</div>
    <div class="options">
      <label class="option"><input type="radio" name="q3" value="A">A. It sleeps only in groups.</label>
      <label class="option"><input type="radio" name="q3" value="B">B. It sleeps only on land.</label>
      <label class="option"><input type="radio" name="q3" value="C">C. Half of its brain stays awake.</label>
      <label class="option"><input type="radio" name="q3" value="D">D. It never sleeps.</label>
    </div><div class="feedback" id="fb-q3"></div>
  </div>

  <div class="question">
    <div class="qtext">4. What common idea best explains these animals' unusual sleeping habits?</div>
    <div class="options">
      <label class="option"><input type="radio" name="q4" value="A">A. They want to sleep longer than humans.</label>
      <label class="option"><input type="radio" name="q4" value="B">B. They are trying to survive and avoid danger.</label>
      <label class="option"><input type="radio" name="q4" value="C">C. They all live in the same environment.</label>
      <label class="option"><input type="radio" name="q4" value="D">D. They all eat the same food.</label>
    </div><div class="feedback" id="fb-q4"></div>
  </div>
</section>

<section class="card">
  <h2 class="section-title"><span class="num" style="background:var(--purple)">3</span>Cause → Effect</h2>
  <p class="hint">Choose the effect that matches each animal's situation.</p>
  <div class="cause-grid">
    <div class="animal-label">🦒 Giraffe</div>
    <select id="ce1"><option value="">Choose an effect...</option><option value="A">It avoids long naps.</option><option value="B">It locks paws with others.</option><option value="C">Half of its brain stays awake.</option><option value="D">It takes short naps in flight.</option></select>
    <div class="animal-label">🦦 Sea otter</div>
    <select id="ce2"><option value="">Choose an effect...</option><option value="A">It avoids long naps.</option><option value="B">It locks paws with others.</option><option value="C">Half of its brain stays awake.</option><option value="D">It takes short naps in flight.</option></select>
    <div class="animal-label">🐬 Dolphin</div>
    <select id="ce3"><option value="">Choose an effect...</option><option value="A">It avoids long naps.</option><option value="B">It locks paws with others.</option><option value="C">Half of its brain stays awake.</option><option value="D">It takes short naps in flight.</option></select>
    <div class="animal-label">🐦 Alpine swift</div>
    <select id="ce4"><option value="">Choose an effect...</option><option value="A">It avoids long naps.</option><option value="B">It locks paws with others.</option><option value="C">Half of its brain stays awake.</option><option value="D">It takes short naps in flight.</option></select>
  </div>
  <div class="feedback" id="fb-ce"></div>
</section>

<section class="card">
  <h2 class="section-title"><span class="num" style="background:var(--pink)">4</span>Think & Reflect</h2>
  <p class="hint">There is no single correct answer here.</p>
  <label for="reflection"><strong>Which animal sleeping habit surprised you the most? Why?</strong></label>
  <textarea id="reflection" placeholder="I was most surprised by... because..."></textarea>
</section>

<section class="card">
  <h2 class="section-title"><span class="num" style="background:var(--yellow);color:#5d4c00">5</span>Finish</h2>
  <div class="actions">
    <button class="primary" onclick="checkAnswers()">✅ Check My Answers</button>
    <button class="secondary" onclick="resetActivity()">↻ Reset</button>
  </div>
  <div id="result"></div>
</section>

<div class="footer-note">Workshop starter page — intentionally simple: no student identity, no cloud submission, no learning report, and no teacher dashboard yet.</div>
</div>

<script>
const answerKey={q1:"B",q2:"B",q3:"C",q4:"B"};
const causeEffectKey={ce1:"A",ce2:"B",ce3:"C",ce4:"D"};

function getRadioValue(name){
  const s=document.querySelector(`input[name="${name}"]:checked`);
  return s?s.value:"";
}
function checkAnswers(){
  let correct=0,answered=0;
  Object.entries(answerKey).forEach(([q,key])=>{
    const value=getRadioValue(q),fb=document.getElementById(`fb-${q}`);
    if(value) answered++;
    if(value===key){correct++;fb.textContent="✓ Correct";fb.className="feedback correct";}
    else if(!value){fb.textContent="Please choose an answer.";fb.className="feedback incorrect";}
    else{fb.textContent=`Not quite. The best answer is ${key}.`;fb.className="feedback incorrect";}
  });
  let ceCorrect=0;
  Object.entries(causeEffectKey).forEach(([id,key])=>{
    const value=document.getElementById(id).value;
    if(value) answered++;
    if(value===key){ceCorrect++;correct++;}
  });
  const ceFb=document.getElementById("fb-ce");
  if(ceCorrect===4){ceFb.textContent="✓ Great! All four matches are correct.";ceFb.className="feedback correct";}
  else{ceFb.textContent=`${ceCorrect}/4 matches are correct. Review the facts and try again.`;ceFb.className="feedback incorrect";}

  const reflection=document.getElementById("reflection").value.trim();
  const result=document.getElementById("result");
  result.style.display="block";
  result.innerHTML=`<strong>Objective score: ${correct} / 8</strong><br>Answered objective items: ${answered} / 8`+
    (reflection?"":"<br><small>Tip: add your reflection before you finish.</small>");
  result.scrollIntoView({behavior:"smooth",block:"center"});
}
function resetActivity(){
  document.querySelectorAll('input[type="radio"]').forEach(el=>el.checked=false);
  document.querySelectorAll('select').forEach(el=>el.value="");
  document.getElementById("reflection").value="";
  document.querySelectorAll('.feedback').forEach(el=>{el.textContent="";el.className="feedback";});
  const result=document.getElementById("result");result.style.display="none";result.innerHTML="";
  window.scrollTo({top:0,behavior:"smooth"});
}
</script>
</body>
</html>
C — RESOURCE

8 個高中英文網頁 Starter Prompts

沒有現成 HTML 時,挑一個最接近你課堂需求的起點(已精選高穩定度題型)。

① 單字複習網頁Starter Prompt
請根據我提供的英文教材,製作一個適合高中生使用的互動式單字複習網頁。

請包含:
1. 重要單字與中文解釋
2. 單字例句
3. 點擊可查看意思
4. 10 題單字選擇或配對活動
5. 作答後立即顯示正確答案與簡短回饋

請產生完整單一 HTML 程式碼,CSS 與 JavaScript 都包含在同一份檔案中。
網頁需適合電腦、Chromebook 與平板操作。
② 閱讀理解互動學習單Starter Prompt
請將我提供的英文文章製作成高中英文互動閱讀網頁。

請保留原文,並加入:
1. 重要單字或片語點擊提示
2. 段落重點提示
3. 5–8 題閱讀理解題
4. 包含主旨、細節、推論等不同層次
5. 學生作答後立即看到回饋
6. 最後加入一題簡短 Reflection

請產生完整、可直接使用的單一 HTML 程式碼。
③ 文法練習網頁Starter Prompt
請根據我提供的文法教材,製作一個高中英文文法互動練習網頁。

流程為:

文法重點簡短說明

→ 範例

→ 引導練習

→ 獨立練習

→ 即時回饋

請避免只有大量選擇題,可以混合:

- 選擇

- 填空

- 句型改寫

- 找錯

- 造句

請產生完整單一 HTML 程式碼,適合學生自行操作。
④ Exit Ticket|離場卷Starter Prompt
請根據今天的課程內容,設計一個 5 分鐘可以完成的英文課 Exit Ticket 網頁。

內容包含:

1. 2 題知識理解題

2. 1 題應用題

3. 1 題「今天最清楚的是什麼?」

4. 1 題「還有什麼地方不懂?」

畫面簡潔,不需要複雜動畫。

學生完成後請在畫面上整理出他的回答摘要。

請產生完整單一 HTML 程式碼。
⑤ Vocabulary Mission|單字任務Starter Prompt
請把我提供的高中英文單字與課文內容設計成一個「Vocabulary Mission」互動任務網頁。

學生需要完成 3 個小任務:

Mission 1:單字配對

Mission 2:情境選字

Mission 3:用線索破解最後密碼

難度適合高中生,不要太幼稚。

每完成一關立即給簡短英文或中文回饋。

請產生完整單一 HTML 程式碼。
⑥ Choose Your Response|情境選擇Starter Prompt
請根據我提供的英文主題,設計一個情境式英文互動網頁。

學生會遇到 4–6 個情境,每個情境需要選擇最適合的英文回應。

選完後:

- 告訴學生這個回答是否恰當

- 解釋語氣或用法

- 讓學生進入下一個情境

最後顯示學生的選擇結果與簡短回顧。

請產生完整單一 HTML 程式碼。
⑦ Speaking Warm-up|口說前情境鷹架Starter Prompt
請根據我提供的英文單元主題,製作一個高中英文口說前暖身網頁。

學生先選擇或輸入自己想說的內容,再透過網頁完成簡短的英文情境任務。

請提供:

1. 情境說明

2. 2–3 個 sentence starters

3. 可使用的關鍵字

4. 一個簡短任務

5. 最後請學生整理出自己準備對真人同學說的英文

目的不是讓 AI 取代真人口說,而是作為真人 pair work 前的鷹架。

請產生完整單一 HTML 程式碼。
⑧ What Would You Do?|沒有標準答案的討論型Starter Prompt
請根據我提供的英文文本或主題,製作一個「What Would You Do?」互動網頁。

學生閱讀 3–5 個與文本相關的情境後,需要做選擇並簡短說明原因。

部分題目可以沒有唯一正確答案。

目標是讓學生:

- 理解文本

- 做出判斷

- 表達個人觀點

- 為後續小組討論做準備

最後整理學生所有選擇與文字回答。

請產生完整單一 HTML 程式碼。

A — Augment|先規劃,再讓 AI 動手

這一步先不要寫程式。如果您使用一般 AI,把以下三樣材料放進同一個 AI 對話,並搭配 12 字黃金指令:

  1. 自己的 HTML 網頁程式碼(例如 Starter HTML 或您自製的單元網頁)
  2. CARE Universal Upgrade Prompt(下方核心提示詞)
  3. 剛才取得的自己的 GAS Web App URL(第二步複製的網址)

(💡 備註:萬用後台 Code.gs 已經在第二步部署完成,升級前端時完全不需要再貼給 AI,省去繁瑣代碼複製!)

這份 Prompt 會幫你做什麼?

功能白話版
保留原本活動不會為了加新功能,就把你好不容易做好的網頁整個重做。
判斷哪些學習資料值得留下AI 先幫你想:哪些學生表現真的值得記錄,而不是什麼都收。
老師先確認AI 第一回合只提規劃;你說「可以,照這個做」之後才開始改。
產生學生個人學習報告學生做完不只看到分數,也能回顧自己的作答與學習。
沒有後台也能完成即使資料暫時送不出去,學生仍可完成活動、看到報告並下載成果。
有後台時自動收資料學生作答可送進 Google Sheet,老師不用逐份整理。
整理全班學習狀況建立 Teacher Dashboard,快速看出哪些題目或觀念需要處理。
自動檢查與防呆盡量保護原本已成功的功能;出錯時只修必要部分。
🌟 兩種升級途徑 | CHOOSE YOUR PATH

途徑 A:直接使用 Sylvia 專屬客製化 Gem(最推薦、零代碼)

講師已將完整的 CARE 2.0 規範與萬用後台邏輯封裝進專屬 Gemini Gem!老師完全不需要複製長篇 Prompt 或後台代碼,直接點開專用 Gem,把 HTML 丟給它,一秒完成升級!

⚡ 【12 字黃金指令】(免貼後台代碼,通用所有 AI)

萬一以後不用 Gem、或習慣使用 ChatGPT / Claude / 一般 Gemini,只要在對話框貼上以下「12 字黃金指令」,並附上 (1) 你的 HTML (2) CARE Prompt (3) 你的 GAS URL 三樣材料,AI 就會完全按照 CARE 規範嚴謹升級:

請依照 CARE 規範,幫我升級這份教學網頁。
A — CORE RESOURCE

CARE Universal Upgrade Prompt 2.0(Auto-Title Routing 版)

可直接貼給一般 AI,也可日後自行封裝成 Gem、Custom GPT、Skill 或交給 Agent。

CARE Universal Upgrade Prompt 2.0 (Auto-Title)核心 Prompt
用途:把既有教學 HTML 升級成能留下 Learning Evidence、產生個人 Learning Report、自動依網頁標題寫入 Google Sheet 專屬分頁,並支援 Teacher Dashboard 的「雙棲智慧教學網頁」。這份 Prompt 不依賴 Gem,可直接貼給 ChatGPT、Gemini、Claude 等一般 AI,也可自行封裝成 Gem/Skill 或交給 AI Agent 使用。

# From Play to Proof|CARE 教學網頁升級助手 2.0
## Auto-Title Routing × Universal GAS 2.0 × Learning Evidence

## 1. 角色與任務
你是一位專精於教育科技、高中教學活動設計、HTML/CSS/JavaScript、Google Apps Script、Google Sheets、Google Sites、Learning Analytics、Formative Assessment 與 Teacher Dashboard 設計的教育網頁升級助手。
你的主要服務對象是:沒有程式設計背景、但已經使用 AI 產生教學網頁的高中職教師。
你的任務不是從零重新設計另一個網站,而是在盡量保留原始教學網頁的教學目標、核心玩法、題目內容、活動流程與視覺風格的前提下,把它升級成能留下 Learning Evidence 的雙棲智慧教學網頁。

## 2. 核心升級目標
所有新增功能都必須服務於:留下有意義的學習證據,並支援教師做下一步教學判斷。不要為了蒐集資料而蒐集資料。

## 3. 雙棲架構|沒有 GAS 也能完成
同一個網頁必須保留兩條使用路徑:
A|沒有 GAS/GAS 暫時失敗:學生仍可完成活動 → 看到個人 Learning Report → 下載 PNG 學習成果 → 上傳 Google Classroom/LMS。
B|有 GAS:學生完成活動 → 資料送至 Google Sheet → 老師由右上角 🔒 進入 Teacher Dashboard → 讀取全班 KPI、圖表與 Student Response Wall → 調整下一步教學。
原則:GAS 是即時資料回收路徑,不是學生完成活動的入場券。

## 4. Preserve First|Augment, don't replace
除非原本功能明顯故障,否則不要:
• 刪除原活動或任意改寫題目與正確答案
• 改變主要教學流程或重做成完全不同的活動
• 破壞既有遊戲、配對、拖曳、閱讀、作答、計時、回饋或動畫
• 任意改變原本視覺風格
原則:保留原作,增加 Learning Evidence 能力。

## 5. 前端與發布原則
最終學生網頁原則上維持一個完整、可直接使用的 self-contained HTML;HTML、CSS、JavaScript 放在同一份檔案中。
預設必須能在以下環境合理運作:Google Sites iframe、一般學校網路、Chromebook、Windows、平板、手機。不要建立必須使用 Node.js、npm 或 build 才能執行的架構。
核心操作嚴禁依賴瀏覽器原生 prompt()、alert()、confirm();請改用 HTML Modal、Toast、Notice Box、Inline Message 或 Inline Validation。Teacher Login 必須使用自訂 HTML Modal。

## 6. 固定兩階段工作流程
不要一收到 HTML 就直接輸出新版程式碼!
第一階段:只分析與規劃。即使使用者已同時提供 Universal GAS Backend 與 GAS Web App URL,也不得因此提前修改程式。
第二階段:老師確認後,才正式產生完整 HTML。

## 7. 第一階段|先理解原網頁
先分析:學習目標、學生實際操作、可自動評分內容、不適合自動評分內容、值得留下的學生行為、能幫教師做教學判斷的資料、適合的 KPI、適合的圖表、值得進入 Student Response Wall 的質性回答。
請主動做教育判斷。除非真的無法判斷,不要把資料欄位設計工作全部丟回給老師。

## 8. 第一階段固定回覆格式
請依序回覆:
1. 原本值得保留的設計
2. 建議留下的 Learning Evidence
3. 個人 Learning Report 應呈現什麼
4. Teacher Dashboard 建議的 3~6 個 KPI
5. 建議圖表,以及每張圖回答的教學問題
6. Student Response Wall:哪些質性回答值得保留,順序必須依學生前端實際出現順序
7. 【2.0 新增必填・分頁名稱預告】預計採用的自動活動名稱與試算表分頁名稱:【XXXX】(AI 必須從原始 HTML 內容提取具體課名或主題,例如「B1L3_Animal_Sleep_Interactive」,嚴禁使用 Document、Worksheet、Learning Activity 等無意義泛稱。老師若想自訂可在此步驟直接指定修改。)
8. Google Sheet 最後大致會留下哪些資料
最後只問老師:「如果這份規劃可以,請回覆『可以,照這個做。』;若要增刪資料或調整分頁名稱,請現在告訴我。」第一階段不要輸出完整 HTML,也不要修改我提供的 Universal GAS Backend。

## 9. Teacher Checkpoint
只有老師明確確認後,才進入第二階段。若老師提出增刪需求,先更新規劃並再次確認。

## 10. 第二階段|正式升級 HTML
老師確認後,請產生完整、可直接使用的單一 HTML,而不是零散修改片段。
升級內容至少包括:必要學生身分欄位、Learning Evidence 收集、適合的自動評量、個人 Learning Report、PNG 下載備援、Teacher Dashboard、KPI、必要圖表、Student Response Wall、Refresh Data、Close Dashboard、安全的 Loading/Empty/Error 狀態。

## 11. Auto-Title Routing|活動名稱自動判讀
本系統採用 CARE Universal GAS 2.0 的 Auto-Title Routing。正常情況下,老師不需要另外輸入課次、活動代碼、sheetName,也不需要修改 JavaScript。
【嚴禁泛稱標題】產出之 HTML <title> 必須依教材內容設定具唯一辨識度的具體名稱(如 <title>B1L3_Animal_Sleep_Interactive</title>),嚴禁使用 Document、Worksheet 等無意義名稱,以免造成不同活動的分頁撞名覆蓋。
優先順序:1. 原始 HTML 的 <title>;2. 頁面主要 <h1>;3. 最主要的活動標題;4. 根據整份 HTML 教學內容判斷適合的活動名稱。

## 12. Auto-Title 前端規則
同一份 HTML 必須建立一個共用函式取得活動標題(Single Source of Truth):優先使用 document.title.trim(),若無有效 title 再使用主要 h1。
POST 與 Teacher Dashboard GET 必須使用完全相同的活動標題來源!不得讓 POST 與 GET 各自重新猜測不同名稱。
學生送資料時,payload 必須包含:sourceTitle(值為自動判讀之活動標題)。
Teacher Dashboard 讀取資料時,使用:GAS_URL + '?title=' + encodeURIComponent(getActivityTitle())。(注意:必須強制使用 encodeURIComponent,確保特殊符號與中文不被 URL 截斷)。

## 13. Universal GAS 2.0 相容
本研習提供一份標準 CARE Universal GAS Backend 2.0。請把它視為固定標準後端:不要重寫、不要改造、不要自行另外產生 Code.gs。
學生提交資料:POST → GAS URL,payload 至少包含 sourceTitle。
Teacher Dashboard:GET → GAS_URL?title=活動標題。
Universal GAS 2.0 會自動整理合法分頁名稱、建立分頁、自動擴充表頭、寫入資料,並在 GET 時讀取同一活動分頁。

## 14. 向下相容原則
CARE Universal GAS 2.0 同時支援:2.0 sourceTitle、2.0 sheetName、舊活動 lesson / unit,以及完全未指定時自動回退至 Responses。因此舊活動 100% 不會壞掉。

## 15. Payload 與 canonical key
請依活動建立結構清楚的 JSON payload。同一資料概念使用一致的 canonical key(如 className, seatNumber, studentName, score, accuracy, duration, retryCount, reflection, textResponses)。前端送出的欄位名稱必須和 Teacher Dashboard 讀取時使用的名稱一致。array/object 可以直接放入 payload,Universal GAS 會將其轉成 JSON 字串保存。

## 16. 自動評量原則
單選、多選、是非、配對、拖曳、填空、具明確答案的文法句型題可自動評量;記錄總分、正確率、各區塊得分、各題正誤、選項分布、作答時間與 retryCount。
Reflection、SEL、個人意見、創意寫作、開放式閱讀回答等無標準答案內容,不要硬判正誤;保留學生原文,呈現在 Learning Report、Google Sheet 與 Student Response Wall。

## 17. 個人 Learning Report
Learning Report 不可只顯示總分。包含班級、座號、姓名、作答時間、總分、正確率、各區塊表現、答題摘要、自評、Reflection、開放式回答等重要學習證據。必須支援下載 PNG;即使 GAS 未設定或失敗,學生仍能看報告與下載 PNG。

## 18. 質性資料
若活動有多個質性回答,請保留各自清楚欄位;若原架構適合也可建立 textResponses 作為備援。Student Response Wall 的題目順序必須和學生端實際出現順序完全一致。

## 19. 提交與 no-cors
若前端使用 fetch + mode:'no-cors' 送資料,前端無法可靠驗證伺服器 response status,因此只可顯示「資料已送出」,不要在未驗證時宣稱「已成功寫入試算表」。Submit 後必須防止重複連點、顯示送出中狀態;提交失敗不得破壞學生端既有活動與 Learning Report。

## 20. Teacher Dashboard
右上角固定提供清楚可辨識的 🔒 Teacher。點擊後立即顯示自訂 Teacher Login Modal,預設示範密碼 teacher。登入後先打開 Dashboard 外框顯示「正在讀取資料……」,再呼叫 GET。若讀取失敗仍保持開啟並顯示錯誤,不要讓老師以為按鈕壞掉。具備 Refresh Data 與 Close Dashboard。

## 21. Dashboard 內容原則
KPI 只選 3~6 個真正有教學意義的指標(如提交人數、平均得分、平均正確率、核心題型正確率、平均作答時間)。圖表只畫能回答實際教學診斷問題的內容。Student Response Wall 顯示名稱要使用老師看得懂的題目標籤,不要只顯示 q1Rewrite 等底層 key。

## 22. Dashboard 容錯
部分資料錯誤不等於整個 Dashboard 故障。某 KPI 沒資料顯示 —;某題無文字顯示「目前無資料」;圖表資料不足顯示「資料不足」;doGet 失敗顯示「資料讀取失敗」。避免 undefined/null crash。Chart.js 重畫前先 destroy 舊 instance。

## 23. End-to-End Self Check
輸出前逐項內部檢查:學生作答 → payload → Universal GAS doPost → Google Sheet → doGet → Teacher Dashboard。確認原活動正常、PNG 可下載、無 GAS 能完成、payload 包含 sourceTitle、GAS_URL 設定清楚、POST 與 GET 使用同一活動標題且經 encodeURIComponent、🔒 入口與密碼正常、質性回答對應正確。

## 24. JavaScript 健全性
確認所有 getElementById 對應 id 存在且無重複、按鈕有 handler、無未定義函式或變數重複宣告、Modal 與 Dashboard 可正常開關、Google Sites iframe 不會被原生 Dialog 阻塞。

## 25. 除錯原則|Minimum Necessary Change
若後續故障,不要立刻整份重寫。先定位問題在 UI → event → payload → POST → GAS → Sheet → GET → mapping → Dashboard render 哪一層,做最小必要修改。已成功功能不要重寫;若 Sheet 已收到資料,不要優先重寫提交流程;若 GAS URL 已設定好,不得任意移除。

## 26. GAS URL 串接規則
若老師一開始已提供 GAS URL,第一階段先不改程式;第二階段直接放入正確位置。若第二階段後才補上 URL,只做最小必要修改。不得要求老師自己搜尋程式碼修改網址以外的技術參數。

## 27. 第二階段輸出規則
正式輸出完整單一 HTML。不要輸出新的 Code.gs,因為使用研習提供的 CARE Universal GAS Backend 2.0。完成後只給老師真正需要的下一步,一次一個步驟。

## 28. 最終品質標準
最終成果讓老師只需處理:部署一次 GAS 取得 URL → 提供 HTML+CARE Prompt 3.0+GAS URL 給 AI → 確認第一階段規劃 → 測試一筆 → 放入 Google Sites。老師不需處理課次代碼、sheetName、路由或分頁建立。

## 29. 可攜與封裝
本 CARE Universal Prompt 是核心規則,不依賴任何單一 AI 產品。可直接貼給一般 AI,也可封裝成 Gem、Custom GPT、Skill 或 AI Agent。使用 AI Agent 時,亦不得跳過第一階段與 Teacher Checkpoint。

## 30. CARE 2.0 一句話原則
先看懂 → 先規劃 → 我確認 → 再全部做。
老師只提供 GAS URL;活動名稱、資料分頁與 Dashboard 路由由 AI 與 Universal GAS 2.0 自動處理!

Universal GAS Backend 2.0|本場研習標準資料後台 Code.gs(Auto-Title Routing 版)
這份 Universal GAS Backend 2.0 是本場研習的標準後端。老師終身只需要部署這一次,同一個 GAS 網址與 Google 試算表直接共用!它具備 Auto-Title Routing 功能,會自動依網頁標題建立獨立分頁,並自動過濾非法字元、自動建立與擴充表頭、物件自動轉 JSON、支援 LockService 併發防護。同時 100% 向下相容 2.0 (sheetName) 與 1.0 (Responses)。

// ======================================================
// From Play to Proof|CARE Universal GAS 2.0
// Auto-Title Routing Edition (免換試算表・自動分頁歸檔版)
//
// 核心特色:
// 1. 同一份 Google 試算表、同一個 GAS URL,全學年所有課次直接共用!
// 2. 前端網頁全自動以 <title> 作為分頁名稱,GAS 自動建立對應分頁
// 3. Teacher Dashboard 使用 ?title=活動標題,全自動讀取同一專屬分頁
// 4. 自動過濾試算表非法字元(如 : \ / ? * [ ] 與前後引號),避免分頁建立失敗
// 5. 【100% 向下相容】:舊版網頁若傳 sheetName / lesson / unit 照樣可用;若完全未指定則安全寫入 Responses 分頁
// 6. 自動建立/擴充表頭,不同題型自動長欄位,永不撞車
// 7. array / object 自動序列化為 JSON 字串完整保存
// 8. 採用 LockService 降低多人同時提交造成的資料衝突風險
// ======================================================

const DEFAULT_SHEET_NAME = 'Responses';

// ======================================================
// 0. 將活動標題轉成合法 Google Sheet 分頁名稱(防禦性安全過濾)
// ======================================================
function normalizeSheetName(rawName) {
  if (rawName === undefined || rawName === null) {
    return DEFAULT_SHEET_NAME;
  }

  let name = String(rawName).trim();
  if (!name) {
    return DEFAULT_SHEET_NAME;
  }

  // 去除頭尾的單引號與雙引號(避免 Google Sheets 分頁參照公式或語法出錯)
  name = name.replace(/^['"]+|['"]+$/g, '').trim();

  // Google Sheet 分頁名稱不可包含:: \ / ? * [ ]
  name = name.replace(/[:\\/\?\*\[\]]/g, '-');

  // 多個連續空白整理成單一空格
  name = name.replace(/\s+/g, ' ').trim();

  // 避免名稱過長(Google Sheets 分頁名稱上限為 100 字元,此處保守控制在 80 字元)
  if (name.length > 80) {
    name = name.substring(0, 80).trim();
  }

  // 再次清理頭尾多餘的連字號與引號
  name = name.replace(/^[-'"]+|[-'"]+$/g, '').trim();

  if (!name) {
    return DEFAULT_SHEET_NAME;
  }

  return name;
}

// ======================================================
// 1. 接收學生端資料(POST)
// ======================================================
function doPost(e) {
  const lock = LockService.getScriptLock();

  try {
    // 最多等待 30 秒取得鎖,防止多人併發寫入衝突
    lock.waitLock(30000);

    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

    // 檢查 POST 資料
    if (!e || !e.postData || !e.postData.contents) {
      throw new Error('未收到 POST 內容');
    }

    const data = JSON.parse(e.postData.contents);

    // ==================================================
    // CARE 2.0 Auto-Title Routing 路由判定
    // 優先順序:
    // 1. sheetName   → 2.0 手動指定 / 覆寫
    // 2. sourceTitle → 2.0 自動網頁標題
    // 3. lesson      → 舊活動相容
    // 4. unit        → 舊活動相容
    // 5. Responses   → 最終安全 fallback
    // ==================================================
    const rawSheetName =
      data.sheetName ||
      data.sourceTitle ||
      data.lesson ||
      data.unit ||
      DEFAULT_SHEET_NAME;

    const targetSheetName = normalizeSheetName(rawSheetName);

    // 找到或建立目標分頁
    let sheet = spreadsheet.getSheetByName(targetSheetName);
    if (!sheet) {
      sheet = spreadsheet.insertSheet(targetSheetName);
    }

    // 自動加入時間戳記
    data.Timestamp = Utilities.formatDate(
      new Date(),
      Session.getScriptTimeZone(),
      'yyyy-MM-dd HH:mm:ss'
    );

    // 若使用 2.0 sourceTitle,保留原始活動標題字串作為資料欄位以利查閱
    if (data.sourceTitle !== undefined && data.sourceTitle !== null) {
      data.ActivityTitle = String(data.sourceTitle).trim();
    }

    // 將 array / object 轉成 JSON 字串保存
    Object.keys(data).forEach(function (key) {
      const value = data[key];
      if (value !== null && typeof value === 'object') {
        data[key] = JSON.stringify(value);
      }
    });

    // 讀取既有表頭
    const lastCol = sheet.getLastColumn();
    let headers = [];
    if (lastCol > 0) {
      headers = sheet
        .getRange(1, 1, 1, lastCol)
        .getValues()[0]
        .map(function (header) {
          return String(header).trim();
        })
        .filter(function (header) {
          return header !== '';
        });
    }

    // 自動擴充尚未存在的新欄位
    Object.keys(data).forEach(function (key) {
      if (headers.indexOf(key) === -1) {
        headers.push(key);
      }
    });

    // 回寫表頭
    if (headers.length > 0) {
      sheet
        .getRange(1, 1, 1, headers.length)
        .setValues([headers]);
    }

    // 依表頭順序建立學生資料列
    const rowData = headers.map(function (header) {
      return data[header] !== undefined && data[header] !== null
        ? data[header]
        : '';
    });

    // 新增資料列
    sheet.appendRow(rowData);

    // 回傳成功結果
    return ContentService
      .createTextOutput(
        JSON.stringify({
          status: 'success',
          message: '資料已成功寫入 ' + targetSheetName,
          sheet: targetSheetName
        })
      )
      .setMimeType(ContentService.MimeType.JSON);

  } catch (error) {
    return ContentService
      .createTextOutput(
        JSON.stringify({
          status: 'error',
          message: error.toString()
        })
      )
      .setMimeType(ContentService.MimeType.JSON);

  } finally {
    if (lock.hasLock()) {
      lock.releaseLock();
    }
  }
}

// ======================================================
// 2. 教師儀表板讀取資料(GET)- 供 Teacher Dashboard 使用
// ======================================================
function doGet(e) {
  try {
    const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();

    // ==================================================
    // CARE 2.0 Dashboard Routing
    // 支援:
    // ?title=Be%20Media%20Smart (2.0 Auto-Title 格式)
    // ?sheet=B1L3               (2.0 舊格式向下相容)
    // 若均無指定,預設讀取 Responses
    // ==================================================
    let rawSheetName = DEFAULT_SHEET_NAME;

    if (e && e.parameter) {
      if (e.parameter.sheet) {
        rawSheetName = e.parameter.sheet;
      } else if (e.parameter.title) {
        rawSheetName = e.parameter.title;
      }
    }

    const targetSheetName = normalizeSheetName(rawSheetName);
    const sheet = spreadsheet.getSheetByName(targetSheetName);

    // 找不到工作表時回傳空陣列
    if (!sheet) {
      return ContentService
        .createTextOutput(JSON.stringify([]))
        .setMimeType(ContentService.MimeType.JSON);
    }

    const lastRow = sheet.getLastRow();
    const lastCol = sheet.getLastColumn();

    // 尚無學生資料時回傳空陣列
    if (lastRow < 2 || lastCol < 1) {
      return ContentService
        .createTextOutput(JSON.stringify([]))
        .setMimeType(ContentService.MimeType.JSON);
    }

    // 讀取整張工作表資料
    const values = sheet
      .getRange(1, 1, lastRow, lastCol)
      .getValues();

    const headers = values[0];
    const result = [];

    // 每列轉成 JSON 物件
    for (let i = 1; i < values.length; i++) {
      const rowObject = {};
      let hasData = false;

      for (let j = 0; j < headers.length; j++) {
        const key = String(headers[j]).trim();
        if (!key) continue;

        const value = values[i][j];
        rowObject[key] = value;

        if (value !== '' && value !== null) {
          hasData = true;
        }
      }

      // 避免空白列進入 Dashboard
      if (hasData) {
        result.push(rowObject);
      }
    }

    return ContentService
      .createTextOutput(JSON.stringify(result))
      .setMimeType(ContentService.MimeType.JSON);

  } catch (error) {
    return ContentService
      .createTextOutput(
        JSON.stringify({
          error: error.toString()
        })
      )
      .setMimeType(ContentService.MimeType.JSON);
  }
}

第一回合|AI 先規劃,不改程式

AI 先讀懂原始活動,只提出值得留下的 Learning Evidence、Learning Report、Teacher Dashboard、圖表與質性回答規劃。這時不應輸出新版 HTML,也不應修改 Universal GAS。

Teacher Checkpoint|老師確認後再做

規劃合理就只要回覆:「可以,照這個做。」 如果有不想收的資料或缺少的重要證據,先在這一步調整。

第二回合|AI 直接產生已串好 GAS 的完整 HTML

確認後,AI 才依同一份 CARE Prompt 產生完整新版 HTML,並直接使用你已提供的 GAS Web App URL。

R — Record|只測一筆,不要一次全班上

R 的目標,是把 A 階段已確認的 Learning Evidence 真正接進資料流。後端已經準備好,這裡不再回頭部署;只驗證資料有沒有真的走通。

  1. 開啟完成版 HTML,確認原本活動、Learning Report、右上角 Teacher 入口都正常。
  2. 以學生身分只送 1 筆測試資料。
  3. 確認 Google Sheet 出現資料,而且欄位符合剛才確認的 Learning Evidence。
  4. 打開 Teacher Dashboard,確認 KPI、圖表與開放式回答都能讀回同一筆資料。
Safety Checkpoint通過標準
1|HTML可以正常開啟與操作。
2|Google Sheet收得到 1 筆測試資料。
3|Teacher Dashboard讀得到同一筆資料。
Alternative Route|今天不想碰 GAS,也沒關係
學生完成 → Learning Report → Download PNG → Classroom/LMS → 課後再用 AI/NotebookLM 整理。
GAS 是即時資料回收的進階路徑,不是使用教學網頁的入場券。

E — Evaluate & Respond|看到資料後,下一步怎麼教?

打開 Dashboard,問自己:「看到這些資料後,我下一步會怎麼教?」

層次用途
課堂當下直接看 Teacher Dashboard,處理「現在我要怎麼教?」
課後分析把 Sheet 資料交給 AI,處理「這批資料還告訴我什麼?」
E — ANALYZE

Learning Evidence Analyzer Prompt

不只摘要數字,而是找出已掌握、需要處理、可能迷思、可少教什麼,以及下一步教學建議。

Learning Evidence Analyzer課堂學習資料分析助手
Learning Evidence Analyzer|課堂學習資料分析助手
你是一位協助教師進行形成性評量(formative assessment)的教學分析助手,請分析這份學生學習資料。
## 分析原則
1. 不要只摘要數據,要找出「對下一步教學有意義的模式」。
2. 不要捏造資料中不存在的資訊,若資料不足,請直接說資料不足,不要猜測。
3. 請清楚區分:
- 資料直接顯示的事實(Evidence)
- 根據資料做出的可能推論(Possible interpretation)
4. 不要只找學生不會的地方,也要指出:
- 已經掌握、可能不需要再花時間重教的內容
- 學生共同的優勢
5. 若有開放式回答或 Reflection,請整理:
- 常見想法
- 有代表性的不同觀點
- 可能的迷思或困惑
- 值得帶回全班討論的主題
6. 若資料不足以做某項判斷,請直接說「目前資料不足」,不要猜測。
## 請用以下格式回答
### 1. Quick Snapshot|快速總覽
用 3–5 點告訴我這一班目前最值得注意的狀況。
### 2. What They Already Know|已掌握
列出學生整體表現良好、可能不需要重新詳細教學的內容。
### 3. What Needs Attention|需要處理
依重要程度列出 1–3 個最值得處理的學習問題,並提供資料證據。
### 4. Possible Misconceptions|可能的迷思
若某些錯誤呈現共同模式,說明可能的原因。
請標示這是「推論」,不是已證實事實。
### 5. Student Voices|學生想法
若資料有開放式回答,整理主要觀點、差異與值得全班討論的內容。
### 6. Who May Need Support|可能需要額外支持
若資料足夠,指出哪些學生或哪些類型的學生可能需要:
- 再教一次
- 額外鷹架
- 個別確認
- 延伸挑戰
不要進行人格或能力標籤。
### 7. What NOT to Reteach|可以少教什麼
明確指出哪些內容學生已大致掌握,可以快速帶過或不必重新完整講解。
### 8. Next Teaching Moves|下一步教學建議
請給我最多 3 個具體、可立即執行的教學行動。
每一項說明:
- 為什麼值得做
- 適合全班/小組/個別
- 建議花多少時間
### 9. Teacher To-do List|教師待辦
最後列出一份簡短待辦清單,依優先順序分成:
- 🔴 下一堂課前一定處理
- 🟡 有時間再處理
- 🟢 暫時不用處理
### 10. One-sentence Insight|一句話洞察
最後請用一句話回答:
「如果我是這個班的老師,看到這份資料後,下一步最值得做的是什麼?」
---
以下是我的學生學習資料:
【請把 Google Sheet 資料、CSV、表格或學生回答貼在這裡】
E — VISUALIZE

Classroom Data Visualization Assistant Prompt

讓 AI 判斷什麼值得畫、用什麼圖表,以及每張圖能回答什麼教學問題。

Classroom Data Visualization Assistant形成性評量資料視覺化
你是一位協助教師分析形成性評量資料的資料視覺化助手。我會提供學生學習資料。請先分析資料結構,再判斷哪些圖表真正有助於教師理解學生學習狀況。
請遵守以下原則:
1. 不要為了畫圖而畫圖。
2. 每張圖都必須能回答一個明確的教學問題。
3. 不要捏造缺少的資料。
4. 計算比例或平均值前先確認分母與缺漏值。
5. 清楚區分原始資料與 AI 推論。
6. 優先使用容易讓老師理解的圖表:
- Bar chart
- Grouped bar chart
- Distribution chart
- Scatter plot
7. 除非資料真的適合 part-to-whole,否則避免使用 pie chart。
8. 若開放式文字無法合理分類,不要強迫視覺化。
請先告訴我:
### A. What is worth visualizing?
列出最多 3 個值得視覺化的問題,並說明原因。
### B. Recommended charts
每一張圖請提供:
- 圖表名稱
- 要回答的教學問題
- 圖表類型
- X 軸
- Y 軸
- 分組方式
- 需要使用哪些資料欄位
### C. Create the charts
依照上述建議製作圖表。
### D. Teaching Insight
每張圖最後用一句話回答:「老師看到這張圖後,最值得採取什麼行動?」
若資料不足以產生某張圖,請直接說明,不要猜測。
PART 4

Production|Use it for real

Production 的目標不是再寫一次程式,而是讓另一個人真的使用,而且老師真的收到 Learning Evidence。

Step 1|Publish

R 的三個 Safety Checkpoints 都通過後,再把完成版 HTML 放到 Google Sites。

  1. 開啟 Google Sites,進入要放學習單的頁面。
  2. 使用「嵌入程式碼」貼上 AI 產出的完整 HTML。
  3. 預覽正常後插入並發布。

Google Sites 是免費、穩定、低門檻的發布方式;未來也可以學習 Netlify、GitHub Pages 等其他方式。

Step 2|One real response

把網站交給一位夥伴老師實際作答,再確認:學生端 → Sheet → Dashboard 整條資料流在正式發布環境中也成功。

Step 3|Production Check

完成標準不是「我有一份程式碼」;而是另一個人真的可以使用這個網站,而且老師真的看得到 Learning Evidence。

PART 5

Wrap-up|What survives when tools change?

回到最初的問題

When is technology worth the trouble?

當它能讓原本看不見的學習變得看得見,而且那些資訊真的會改變我的下一步教學時。

What’s Next?|From Proof to Agent

階段 流程 白話說明
Today|今天 Technical Prep 技術準備 → Existing HTML 現有網頁 → CARE Prompt → Teacher Approval 老師確認 → Ready-to-use HTML 完成版網頁 → Test 測試 → Evaluate 判讀 今天我們把流程拆開來做,先學會每一層在做什麼,也保留老師最後確認 Learning Evidence 的角色。
Next|之後 CARE Prompt → Gem/Skill/AI Agent 熟悉流程後,可以把同一份 CARE Prompt 封裝成 Gem 或 Skill,或交給 AI Agent 協助完成更多修改、串接、測試與部署工作。

CARE 決定「應該做什麼」;Prompt、Skill、Gem、Agent 只是不同的執行方式。

工具會改變,但工作流程應該留下來。
The tools will change. The workflow should survive.

REFERENCE

Reference Links|課後延伸

🌟 研習限定課後大禮包 | POST-WORKSHOP CHALLENGE

英語歌曲互動網頁版學習單:Count on Me

這是一份為臺灣高中職 A1~A2 英語課堂設計的旗艦互動學習單!具備 10 題行內三選一聽力挖空、單字語音朗讀 (TTS)、雙語鷹架句型引導 (Sentence Starters) 與成果認證小卡。
💡 課後進階挑戰:請嘗試將今天學到的 CARE 2.0 工作流 套用到這份學習單上,為它接上萬用 Google Sheets 後台,打造屬於你的第二個學習歷程收集神器!

🎵 課後挑戰教材:Count on Me 互動學習單原始碼 HTML5 獨立單檔

完整單一 HTML 檔案,包含所有 CSS、Tailwind、JavaScript、聽力題庫與自製認證小卡機制。下載後直接以瀏覽器開啟即可離線使用或進行二次創作。

<!DOCTYPE html>

<html lang="zh-TW">

<head>

  <meta charset="UTF-8">

  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <title>🎧 英語歌曲互動學習單 | Bruno Mars - Count on Me</title>

  <!-- Tailwind CSS -->

  <script src="https://cdn.tailwindcss.com"></script>

  <!-- Canvas Confetti -->

  <script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>

  <!-- html2canvas -->

  <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>

  <!-- Google Fonts -->

  <link rel="preconnect" href="https://fonts.googleapis.com">

  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

  <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Noto+Sans+TC:wght@400;500;700;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">



  <script>

    tailwind.config = {

      theme: {

        extend: {

          colors: {

            brand: {

              50: '#fef7ee',

              100: '#fdedd6',

              200: '#fbd7ac',

              500: '#f59e0b',

              600: '#d97706',

              700: '#b45309'

            },

            accent: {

              blue: '#3b82f6',

              indigo: '#6366f1',

              emerald: '#10b981',

              rose: '#f43f5e'

            }

          },

          fontFamily: {

            sans: ['"Plus Jakarta Sans"', '"Noto Sans TC"', 'sans-serif'],

            mono: ['"JetBrains Mono"', 'monospace']

          }

        }

      }

    }

  </script>



  <style>

    body {

      background-color: #fafaf9;

      background-image: radial-gradient(#e7e5e4 1px, transparent 1px);

      background-size: 24px 24px;

    }



    /* Inline Popover Cloze Styling */

    .lyric-line {

      position: relative;

      z-index: 1;

    }



    .lyric-line.active-line {

      z-index: 500 !important;

    }



    .cloze-blank {

      position: relative;

      z-index: 2;

      display: inline-flex;

      align-items: center;

      justify-content: center;

      min-width: 90px;

      padding: 2px 10px;

      margin: 0 3px;

      border-bottom: 2.5px dashed #f59e0b;

      color: #78350f;

      font-weight: 700;

      font-size: 0.95em;

      cursor: pointer;

      border-radius: 6px;

      background-color: #fef3c7;

      transition: all 0.15s ease;

      user-select: none;

    }



    .cloze-blank:hover {

      background-color: #fde68a;

      border-bottom-color: #d97706;

      transform: translateY(-1px);

    }



    .cloze-blank.has-popover {

      z-index: 1000 !important;

      background-color: #fde68a;

      box-shadow: 0 0 0 2px #d97706;

    }



    .cloze-blank.selected {

      background-color: #e0e7ff;

      color: #3730a3;

      border-bottom: 2.5px solid #6366f1;

    }



    .cloze-blank.answered {

      cursor: default;

    }



    .cloze-blank.correct {

      background-color: #dcfce7;

      color: #15803d;

      border-bottom: 2.5px solid #22c55e;

      text-decoration: none;

    }



    .cloze-blank.wrong {

      background-color: #fee2e2;

      color: #b91c1c;

      border-bottom: 2.5px solid #ef4444;

    }



    /* Popover Container */

    .inline-popover {

      position: absolute;

      top: calc(100% + 8px);

      left: 50%;

      transform: translateX(-50%);

      display: inline-flex;

      gap: 6px;

      background: #ffffff;

      padding: 6px;

      border-radius: 12px;

      box-shadow: 0 12px 28px -4px rgba(0, 0, 0, 0.25), 0 8px 12px -6px rgba(0, 0, 0, 0.15);

      border: 1.5px solid #cbd5e1;

      z-index: 1000;

      min-width: max-content;

      animation: popFade 0.15s cubic-bezier(0.16, 1, 0.3, 1);

    }



    /* Pointer arrow pointing to blank */

    .inline-popover::before {

      content: '';

      position: absolute;

      top: -6px;

      left: 50%;

      transform: translateX(-50%);

      border-width: 0 6px 6px 6px;

      border-style: solid;

      border-color: transparent transparent #cbd5e1 transparent;

    }



    .inline-popover::after {

      content: '';

      position: absolute;

      top: -4.5px;

      left: 50%;

      transform: translateX(-50%);

      border-width: 0 5px 5px 5px;

      border-style: solid;

      border-color: transparent transparent #ffffff transparent;

    }



    /* Smart upward popover when close to screen bottom */

    .inline-popover.pop-up {

      top: auto;

      bottom: calc(100% + 8px);

      animation: popFadeUp 0.15s cubic-bezier(0.16, 1, 0.3, 1);

    }



    .inline-popover.pop-up::before {

      top: auto;

      bottom: -6px;

      border-width: 6px 6px 0 6px;

      border-color: #cbd5e1 transparent transparent transparent;

    }



    .inline-popover.pop-up::after {

      top: auto;

      bottom: -4.5px;

      border-width: 5px 5px 0 5px;

      border-color: #ffffff transparent transparent transparent;

    }



    @keyframes popFade {

      from { opacity: 0; transform: translate(-50%, -6px); }

      to { opacity: 1; transform: translate(-50%, 0); }

    }



    @keyframes popFadeUp {

      from { opacity: 0; transform: translate(-50%, 6px); }

      to { opacity: 1; transform: translate(-50%, 0); }

    }



    .pop-opt-btn {

      font-size: 0.9rem;

      padding: 5px 12px;

      background-color: #f8fafc;

      border-radius: 8px;

      cursor: pointer;

      color: #334155;

      font-weight: 700;

      transition: all 0.12s ease;

      border: 1px solid #cbd5e1;

    }



    .pop-opt-btn:hover {

      background-color: #4f46e5;

      color: #ffffff;

      border-color: #4f46e5;

      transform: scale(1.05);

    }



    /* Gamified Visual Effects */

    @keyframes float {

      0% { transform: translateY(0px); opacity: 0; }

      50% { opacity: 1; }

      100% { transform: translateY(-100vh); opacity: 0; }

    }



    .balloon {

      position: fixed;

      bottom: -50px;

      animation: float 4s ease-in infinite;

      z-index: 60;

      pointer-events: none;

    }



    @keyframes rain {

      0% { transform: translateY(-100vh); }

      100% { transform: translateY(100vh); }

    }



    .rain-drop {

      position: fixed;

      top: -50px;

      animation: rain 2s linear infinite;

      z-index: 60;

      pointer-events: none;

    }



    .grayscale-mode {

      filter: grayscale(85%);

      transition: filter 1s ease;

    }



    .shake {

      animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both;

    }



    @keyframes shake {

      10%, 90% { transform: translate3d(-1px, 0, 0); }

      20%, 80% { transform: translate3d(2px, 0, 0); }

      30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }

      40%, 60% { transform: translate3d(4px, 0, 0); }

    }

  </style>

</head>

<body class="text-slate-800 antialiased selection:bg-amber-200">



  <!-- Container -->

  <div class="max-w-4xl mx-auto px-4 py-8 relative z-10">



    <!-- Header Section -->

    <header class="bg-gradient-to-r from-amber-500 via-orange-500 to-amber-600 rounded-3xl p-6 md:p-8 text-white shadow-xl shadow-amber-500/20 mb-8 relative overflow-hidden">

      <div class="absolute -right-10 -bottom-10 opacity-15 text-white pointer-events-none">

        <svg class="w-64 h-64" fill="currentColor" viewBox="0 0 24 24"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>

      </div>



      <div class="flex flex-wrap justify-between items-center gap-4 relative z-10">

        <div>

          <div class="inline-flex items-center gap-2 bg-white/20 backdrop-blur-md px-3 py-1 rounded-full text-xs font-bold tracking-wide uppercase mb-2">

            <span>✨ 課堂延伸主題:情意素養與人際互助 (True Friendship)</span>

          </div>

          <h1 class="text-2xl md:text-4xl font-extrabold tracking-tight">

            Bruno Mars - Count on Me

          </h1>

          <p class="text-amber-100 text-sm md:text-base font-medium mt-1">

            🎧 英語歌曲互動網頁學習單 | 10 題行內三選一聽力挑戰

          </p>

        </div>



        <!-- Timer Card -->

        <div class="bg-white/15 backdrop-blur-md border border-white/30 rounded-2xl px-5 py-3 text-center min-w-[120px]">

          <div class="text-[11px] font-bold uppercase tracking-wider text-amber-100">作答時間 (Timer)</div>

          <div id="live-timer" class="text-2xl font-black font-mono tracking-wider">00:00</div>

        </div>

      </div>

    </header>



    <!-- Video Player Section -->

    <section class="bg-white rounded-3xl p-4 md:p-6 shadow-md border border-slate-200 mb-8">

      <div class="flex flex-wrap justify-between items-center mb-4 gap-2">

        <div class="flex items-center gap-2">

          <span class="w-3 h-3 rounded-full bg-red-500 animate-ping"></span>

          <h2 class="text-lg font-bold text-slate-800">🎬 官方歌詞版 MV (Music Video Player)</h2>

        </div>

        <!-- YouTube Fallback Button -->

        <a href="https://www.youtube.com/watch?v=6k8cpUkKK4c" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-1.5 text-xs font-bold text-red-600 bg-red-50 hover:bg-red-100 px-3.5 py-1.5 rounded-full transition border border-red-200">

          <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z"/></svg>

          ▶️ 若無法直接播放,點此開啟 YouTube 觀看

        </a>

      </div>



      <div class="relative w-full aspect-video rounded-2xl overflow-hidden shadow-inner bg-black">

        <iframe 

          class="w-full h-full"

          src="https://www.youtube-nocookie.com/embed/6k8cpUkKK4c?rel=0&enablejsapi=1" 

          title="Bruno Mars - Count on Me (Official Lyric Video)" 

          frameborder="0" 

          allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" 

          referrerpolicy="strict-origin-when-cross-origin"

          allowfullscreen>

        </iframe>

      </div>

      <p class="text-xs text-slate-400 mt-2 text-center">💡 點擊播放後,請邊聽邊對照下方歌詞,並點選空格填入正確單字!</p>

    </section>



    <!-- Quiz Area Section -->

    <section id="quiz-area" class="bg-white rounded-3xl p-5 md:p-8 shadow-md border border-slate-200 mb-8 relative">

      

      <!-- Sticky Progress Bar -->

      <div class="sticky top-4 bg-white/95 backdrop-blur-md p-3.5 rounded-2xl shadow-lg z-30 border border-slate-200 mb-6">

        <div class="flex justify-between items-center mb-2">

          <div class="flex items-center gap-2">

            <span class="bg-amber-100 text-amber-800 text-xs font-bold px-2.5 py-1 rounded-md">

              🎵 聽力三選一 (10 題,每題 10 分)

            </span>

            <span class="text-xs text-slate-500 font-medium">手機點擊空格即可作答</span>

          </div>

          <div class="text-sm font-bold text-slate-600">

            作答進度:<span id="progress-text" class="text-indigo-600 font-mono text-base font-extrabold">0/10</span>

          </div>

        </div>

        <div class="w-full bg-slate-100 rounded-full h-2.5 overflow-hidden">

          <div id="progress-bar" class="bg-gradient-to-r from-amber-500 to-indigo-600 h-2.5 rounded-full transition-all duration-300" style="width: 0%"></div>

        </div>

      </div>



      <!-- Lyrics Container -->

      <div id="lyrics-container" class="space-y-3.5 text-slate-800 leading-relaxed text-base md:text-lg">

        <!-- Rendered by JavaScript -->

      </div>



      <!-- Lyrics Quick Actions -->

      <div class="mt-8 text-center flex flex-wrap justify-center gap-3">

        <button onclick="checkAnswersOnly()" class="inline-flex items-center gap-2 bg-slate-100 hover:bg-slate-200 text-slate-700 font-bold text-sm py-2.5 px-5 rounded-xl border border-slate-300 transition">

          <span>✅ 即時對答案 (Check Answers)</span>

        </button>

        <a href="#reflection-section" class="inline-flex items-center gap-2 bg-amber-50 hover:bg-amber-100 text-amber-800 font-bold text-sm py-2.5 px-5 rounded-xl border border-amber-200 transition">

          <span>⬇️ 繼續往下學習與填寫反思</span>

        </a>

      </div>

    </section>



    <!-- Vocabulary & Pronunciation Section -->

    <section class="bg-white rounded-3xl p-6 md:p-8 shadow-md border border-slate-200 mb-8">

      <div class="flex items-center gap-2 mb-4">

        <span class="p-2 rounded-xl bg-indigo-50 text-indigo-600 font-bold">📚</span>

        <div>

          <h2 class="text-xl font-bold text-slate-800">重點字彙與片語庫 (Vocabulary & Collocations)</h2>

          <p class="text-xs text-slate-500">點擊 🔊 按鈕可直接聆聽標準美式發音</p>

        </div>

      </div>



      <div class="grid grid-cols-1 md:grid-cols-2 gap-4">

        <!-- Vocab 1 -->

        <div class="p-4 rounded-2xl bg-slate-50 border border-slate-200 hover:border-indigo-300 transition">

          <div class="flex justify-between items-center mb-1">

            <span class="text-lg font-bold text-indigo-700">count on</span>

            <button onclick="speakWord('count on')" class="p-1.5 rounded-lg bg-indigo-100 text-indigo-700 hover:bg-indigo-200 text-xs font-bold transition flex items-center gap-1">

              🔊 發音

            </button>

          </div>

          <p class="text-xs text-slate-500 mb-2"><span class="font-semibold text-slate-700">(phr.)</span> 依靠、指望、信賴</p>

          <div class="text-xs text-slate-600 space-y-1 mb-2">

            <p><span class="font-bold text-amber-600">常用搭配:</span>count on sb for help (尋求某人協助)、count on sb to V (指望某人去做...)</p>

          </div>

          <div class="bg-white p-2.5 rounded-xl border border-slate-200 text-xs">

            <div class="flex justify-between items-start">

              <span class="text-slate-700 italic">"You can always count on true friends when you are in trouble."</span>

              <button onclick="speakWord('You can always count on true friends when you are in trouble.')" class="text-slate-400 hover:text-indigo-600 ml-1">🔊</button>

            </div>

            <span class="text-slate-500 block mt-1">(當你身陷困境時,你永遠可以依靠真正的朋友。)</span>

          </div>

        </div>



        <!-- Vocab 2 -->

        <div class="p-4 rounded-2xl bg-slate-50 border border-slate-200 hover:border-indigo-300 transition">

          <div class="flex justify-between items-center mb-1">

            <span class="text-lg font-bold text-indigo-700">guide</span>

            <button onclick="speakWord('guide')" class="p-1.5 rounded-lg bg-indigo-100 text-indigo-700 hover:bg-indigo-200 text-xs font-bold transition flex items-center gap-1">

              🔊 發音

            </button>

          </div>

          <p class="text-xs text-slate-500 mb-2"><span class="font-semibold text-slate-700">(v. / n.)</span> 指引、引導;指南、導遊</p>

          <div class="text-xs text-slate-600 space-y-1 mb-2">

            <p><span class="font-bold text-amber-600">常用搭配:</span>guide sb through sth (引領某人度過難關)、a tour guide (導遊)</p>

          </div>

          <div class="bg-white p-2.5 rounded-xl border border-slate-200 text-xs">

            <div class="flex justify-between items-start">

              <span class="text-slate-700 italic">"The kind teacher guided the lost student back to the classroom."</span>

              <button onclick="speakWord('The kind teacher guided the lost student back to the classroom.')" class="text-slate-400 hover:text-indigo-600 ml-1">🔊</button>

            </div>

            <span class="text-slate-500 block mt-1">(善良的老師引導迷路的學生回到教室。)</span>

          </div>

        </div>



        <!-- Vocab 3 -->

        <div class="p-4 rounded-2xl bg-slate-50 border border-slate-200 hover:border-indigo-300 transition">

          <div class="flex justify-between items-center mb-1">

            <span class="text-lg font-bold text-indigo-700">remind</span>

            <button onclick="speakWord('remind')" class="p-1.5 rounded-lg bg-indigo-100 text-indigo-700 hover:bg-indigo-200 text-xs font-bold transition flex items-center gap-1">

              🔊 發音

            </button>

          </div>

          <p class="text-xs text-slate-500 mb-2"><span class="font-semibold text-slate-700">(v.)</span> 提醒、使想起</p>

          <div class="text-xs text-slate-600 space-y-1 mb-2">

            <p><span class="font-bold text-amber-600">常用搭配:</span>remind sb of sth (使某人想起某事)、remind sb to V (提醒某人去做某事)</p>

          </div>

          <div class="bg-white p-2.5 rounded-xl border border-slate-200 text-xs">

            <div class="flex justify-between items-start">

              <span class="text-slate-700 italic">"Please remind me to submit my English assignment tomorrow."</span>

              <button onclick="speakWord('Please remind me to submit my English assignment tomorrow.')" class="text-slate-400 hover:text-indigo-600 ml-1">🔊</button>

            </div>

            <span class="text-slate-500 block mt-1">(明天請提醒我繳交英文作業。)</span>

          </div>

        </div>



        <!-- Vocab 4 -->

        <div class="p-4 rounded-2xl bg-slate-50 border border-slate-200 hover:border-indigo-300 transition">

          <div class="flex justify-between items-center mb-1">

            <span class="text-lg font-bold text-indigo-700">supposed to</span>

            <button onclick="speakWord('supposed to')" class="p-1.5 rounded-lg bg-indigo-100 text-indigo-700 hover:bg-indigo-200 text-xs font-bold transition flex items-center gap-1">

              🔊 發音

            </button>

          </div>

          <p class="text-xs text-slate-500 mb-2"><span class="font-semibold text-slate-700">(adj. phr.)</span> 應當、理應、本該</p>

          <div class="text-xs text-slate-600 space-y-1 mb-2">

            <p><span class="font-bold text-amber-600">常用搭配:</span>be supposed to V (理應做某事)、not supposed to V (按理不該做某事)</p>

          </div>

          <div class="bg-white p-2.5 rounded-xl border border-slate-200 text-xs">

            <div class="flex justify-between items-start">

              <span class="text-slate-700 italic">"We are supposed to respect and help each other in class."</span>

              <button onclick="speakWord('We are supposed to respect and help each other in class.')" class="text-slate-400 hover:text-indigo-600 ml-1">🔊</button>

            </div>

            <span class="text-slate-500 block mt-1">(在課堂上我們理應彼此尊重、互相幫助。)</span>

          </div>

        </div>



        <!-- Vocab 5 -->

        <div class="p-4 rounded-2xl bg-slate-50 border border-slate-200 hover:border-indigo-300 transition md:col-span-2">

          <div class="flex justify-between items-center mb-1">

            <span class="text-lg font-bold text-indigo-700">fall asleep</span>

            <button onclick="speakWord('fall asleep')" class="p-1.5 rounded-lg bg-indigo-100 text-indigo-700 hover:bg-indigo-200 text-xs font-bold transition flex items-center gap-1">

              🔊 發音

            </button>

          </div>

          <p class="text-xs text-slate-500 mb-2"><span class="font-semibold text-slate-700">(phr.)</span> 入睡、睡著 (注意:asleep 為形容詞,不可與 sleep 混淆)</p>

          <div class="text-xs text-slate-600 space-y-1 mb-2">

            <p><span class="font-bold text-amber-600">常用搭配:</span>fall fast asleep (熟睡)、struggle to fall asleep (輾轉難以入眠)</p>

          </div>

          <div class="bg-white p-2.5 rounded-xl border border-slate-200 text-xs">

            <div class="flex justify-between items-start">

              <span class="text-slate-700 italic">"After an exhausting training session, he fell asleep right away."</span>

              <button onclick="speakWord('After an exhausting training session, he fell asleep right away.')" class="text-slate-400 hover:text-indigo-600 ml-1">🔊</button>

            </div>

            <span class="text-slate-500 block mt-1">(在筋疲力盡的訓練之後,他立刻就睡著了。)</span>

          </div>

        </div>

      </div>

    </section>



    <!-- Grammar & Sentence Pattern Section -->

    <section class="bg-white rounded-3xl p-6 md:p-8 shadow-md border border-slate-200 mb-8">

      <div class="flex items-center gap-2 mb-4">

        <span class="p-2 rounded-xl bg-amber-50 text-amber-600 font-bold">🎯</span>

        <div>

          <h2 class="text-xl font-bold text-slate-800">高中核心句型解析 (Target Sentence Patterns)</h2>

          <p class="text-xs text-slate-500">掌握高中大考高頻文法架構</p>

        </div>

      </div>



      <div class="space-y-4">

        <!-- Pattern 1 -->

        <div class="p-4 rounded-2xl bg-amber-50/50 border border-amber-200">

          <h3 class="font-bold text-amber-900 text-base mb-1">

            📌 句型一:If + S + V (現在簡單式), S + will / can + V (原形動詞) ...

          </h3>

          <p class="text-xs text-slate-600 mb-2">

            【文法考點】此為高中核心「真實條件句 (First Conditional)」。用以描述未來可能發生之情境。<strong>注意:If 子句須用現在式代替未來式</strong>,主要子句則使用助動詞 will / can。

          </p>

          <div class="space-y-1 text-xs">

            <p class="font-mono bg-white p-2 rounded-lg border border-amber-200 text-slate-700">

              🎵 歌詞典範:If you ever <span class="text-amber-700 font-bold">find</span> yourself lost in the dark, I <span class="text-indigo-700 font-bold">will be</span> the light to guide you.

            </p>

            <p class="font-mono bg-white p-2 rounded-lg border border-amber-200 text-slate-700">

              📝 升學造句:If it <span class="text-amber-700 font-bold">rains</span> tomorrow, our class picnic <span class="text-indigo-700 font-bold">will be</span> canceled.

            </p>

          </div>

        </div>



        <!-- Pattern 2 -->

        <div class="p-4 rounded-2xl bg-indigo-50/50 border border-indigo-200">

          <h3 class="font-bold text-indigo-900 text-base mb-1">

            📌 句型二:find + oneself + 受詞補語 (形容詞 / 介系詞片語 / 分詞) ...

          </h3>

          <p class="text-xs text-slate-600 mb-2">

            【文法考點】表示<strong>「不知不覺中發現自己身處於某種狀況或情緒」</strong>。受詞為反身代名詞 (oneself),補語可用形容詞、現在分詞 (-ing) 或介系詞片語。

          </p>

          <div class="space-y-1 text-xs">

            <p class="font-mono bg-white p-2 rounded-lg border border-indigo-200 text-slate-700">

              🎵 歌詞典範:If you ever find <span class="text-indigo-700 font-bold">yourself stuck</span> in the middle of the sea... (stuck 為形容詞表受困)

            </p>

            <p class="font-mono bg-white p-2 rounded-lg border border-indigo-200 text-slate-700">

              📝 升學造句:After studying late at night, she found <span class="text-indigo-700 font-bold">herself falling</span> asleep at her desk. (分詞表持續動作)

            </p>

          </div>

        </div>

      </div>

    </section>



    <!-- Appreciation Section -->

    <section class="bg-gradient-to-br from-slate-900 via-slate-900 to-indigo-950 border border-slate-800 text-white rounded-3xl p-6 md:p-8 shadow-xl mb-8">

      <div class="flex items-center gap-3 mb-4">

        <span class="text-2xl">🎧</span>

        <h2 class="text-xl font-black text-white">聽懂這首歌:青春共鳴與背後寓意 (Behind the Song)</h2>

      </div>



      <!-- 歌手檔案與榮譽戰績卡 -->

      <div class="bg-slate-800/90 rounded-2xl p-4 md:p-5 mb-5 border border-slate-700/80 flex flex-col md:flex-row md:items-center justify-between gap-3 shadow-md">

        <div class="flex items-center gap-3">

          <span class="text-3xl">🏆</span>

          <div>

            <h3 class="text-sm md:text-base font-black text-amber-400">【歌手檔案與超狂戰績】Bruno Mars 火星人布魯諾</h3>

            <p class="text-xs text-slate-300 mt-0.5">榮獲 15 座葛萊美獎、史上首位擁有 6 首鑽石認證單曲的超級流行天王</p>

          </div>

        </div>

        <div class="inline-flex items-center text-[11px] font-bold text-emerald-400 bg-emerald-950/80 px-3.5 py-1.5 rounded-full border border-emerald-800/60 self-start md:self-auto shrink-0">

          🎸 美國 RIAA 4 白金認證 + 全球畢業季友誼第一神曲

        </div>

      </div>



      <!-- 寫給高中職生的對話式賞析短文 -->

      <div class="space-y-3 text-sm md:text-base leading-relaxed text-slate-300">

        <p>一把簡單溫暖的烏克麗麗,加上磁性動人的嗓音,這首歌是全世界公認最純粹的友誼贊歌。在高中最難熬的時刻——可能是被考卷狠狠打擊的挫折、跟家人吵架的委屈、或是失戀的眼淚,最棒的解藥往往不是大道理,而是朋友的一句『我在』。</p>

        <p>歌詞唱道:『If you ever find yourself stuck in the middle of the sea, I'll sail the world to find you.』真正的友誼不需要華麗的承諾,而是在你掉眼淚時默默遞上衛生紙,在你跌倒時伸出手說『數到一二三,我就在你身邊』。聽完這首歌,不妨轉過頭,給坐在你旁邊的好朋友一個擊掌或一句發自內心的感謝吧!</p>

      </div>

    </section>



    <!-- Reflection & Rating Section (Bilingual Scaffolding for A1-A2) -->

    <section id="reflection-section" class="bg-white rounded-3xl p-6 md:p-8 shadow-md border border-slate-200 mb-8 scroll-mt-6">

      <div class="flex items-center gap-2 mb-4">

        <span class="p-2 rounded-xl bg-emerald-50 text-emerald-600 font-bold">✍️</span>

        <div>

          <h2 class="text-xl font-bold text-slate-800">Part 3: Reflection & Rating (學生自我反思與推薦)</h2>

          <p class="text-xs text-slate-500">

            💡 Feel free to answer in English or Chinese! (歡迎用英文或中文作答,寫出真實想法最重要!)

          </p>

        </div>

      </div>



      <div class="space-y-6">

        <!-- 1-5 Star Range Slider -->

        <div class="p-4 rounded-2xl bg-slate-50 border border-slate-200">

          <label class="block text-sm font-bold text-slate-800 mb-1">

            1. Song Rating (歌曲推薦指數,請拉動 1 ~ 5 星):

          </label>

          <p class="text-xs text-slate-500 mb-2">How much do you like this song? (你有多喜歡這首歌曲?)</p>

          <div class="flex items-center gap-4">

            <input 

              type="range" 

              id="rating-slider" 

              min="1" 

              max="5" 

              value="5" 

              class="w-full h-2.5 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-amber-500"

              oninput="updateRatingDisplay(this.value)"

            >

            <div id="rating-badge" class="font-extrabold text-amber-600 bg-amber-100 px-3 py-1 rounded-xl text-sm min-w-[200px] text-center">

              ⭐⭐⭐⭐⭐ (5/5) 絕世神曲!

            </div>

          </div>

        </div>



        <!-- Reflection Q1 -->

        <div>

          <label class="block text-sm font-bold text-slate-800 mb-1">

            2. Why do you give this rating? (你給予這個星等的原因是什麼?):

          </label>

          <p class="text-xs text-slate-500 mb-2">

            Tell us about the melody, rhythm, or your feelings. (說說你對旋律、節奏或聽完的感受)

          </p>

          <!-- Scaffolding Box -->

          <div class="bg-amber-50/70 border border-amber-200 rounded-xl p-2.5 mb-2 text-xs text-slate-600">

            <span class="font-bold text-amber-800">💡 Sentence Starter (你可以這樣開頭):</span>

            <div class="mt-1 space-y-0.5 font-mono text-[11px]">

              <p>• <strong>I like this song because...</strong> (我喜歡這首歌,因為...)</p>

              <p>• <strong>The melody is... and it makes me feel...</strong> (旋律很...,讓我覺得...)</p>

              <p class="text-slate-500">📝 Example: <span class="text-slate-700 italic">"I like this song because the melody is warm and sweet."</span> (我喜歡這首歌,因為旋律很溫暖甜蜜。)</p>

            </div>

          </div>

          <input 

            type="text" 

            id="student-reason" 

            placeholder="I like this song because the melody is warm. / 旋律很溫暖放鬆,聽完心情很好..." 

            class="w-full p-3 text-sm rounded-xl border border-slate-300 focus:outline-none focus:border-amber-500 transition"

          >

        </div>



        <!-- Reflection Q2 -->

        <div>

          <label class="block text-sm font-bold text-slate-800 mb-1">

            3. Which lyric line touched you the most? What did you learn about friendship? (哪句歌詞最打動你?關於友誼你學到了什麼?):

          </label>

          <p class="text-xs text-slate-500 mb-2">

            Pick one line you like and share your thought. (挑選一句你最喜歡的歌詞並分享心得)

          </p>

          <!-- Scaffolding Box -->

          <div class="bg-indigo-50/70 border border-indigo-200 rounded-xl p-2.5 mb-2 text-xs text-slate-600">

            <span class="font-bold text-indigo-800">💡 Sentence Starter (你可以這樣開頭):</span>

            <div class="mt-1 space-y-0.5 font-mono text-[11px]">

              <p>• <strong>My favorite line is "..." because...</strong> (我最喜歡這句歌詞,因為...)</p>

              <p>• <strong>From this song, I learned that a good friend...</strong> (從這首歌中,我學到一個好朋友會...)</p>

              <p class="text-slate-500">📝 Example: <span class="text-slate-700 italic">"I like 'I'll be there.' It reminds me to help my friends."</span> (我最喜歡「我會守候在你身旁」,提醒我要主動幫助朋友。)</p>

            </div>

          </div>

          <textarea 

            id="student-quote" 

            rows="3" 

            placeholder="My favorite line is 'You can count on me.' It reminds me to be a good friend. / 最喜歡 'You can count on me',提醒我朋友需要時要挺身而出..." 

            class="w-full p-3 text-sm rounded-xl border border-slate-300 focus:outline-none focus:border-amber-500 transition"

          ></textarea>

        </div>

      </div>

    </section>



    <!-- Final Submission Section (Page Footer) -->

    <section class="text-center py-4 mb-12">

      <div class="bg-gradient-to-r from-amber-500 via-orange-500 to-amber-600 rounded-3xl p-6 md:p-10 text-white shadow-xl shadow-amber-500/25 relative overflow-hidden">

        <div class="absolute -right-6 -bottom-6 opacity-15 pointer-events-none">

          <svg class="w-48 h-48" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>

        </div>

        <div class="relative z-10">

          <div class="inline-flex items-center gap-2 bg-white/20 backdrop-blur-md px-3.5 py-1 rounded-full text-xs font-bold uppercase tracking-wider mb-3">

            <span>🎓 學習任務總結算 | 108課綱學習歷程數位佐證</span>

          </div>

          <h3 class="text-2xl md:text-3xl font-black mb-2 tracking-tight">

            恭喜完成《Count on Me》英語歌曲學習單!

          </h3>

          <p class="text-amber-100 text-xs md:text-sm max-w-xl mx-auto mb-6 leading-relaxed">

            您已完成 10 題聽力挖空、5 大核心單字發音、高中大考文法句型,並完成了自我反思。<br class="hidden sm:inline">

            請點擊下方按鈕結算成績,領取收錄完整反思的<strong>專屬學習歷程成果認證卡</strong>!

          </p>

          <button onclick="submitQuiz()" class="inline-flex items-center gap-3 bg-white hover:bg-amber-50 text-amber-900 font-black text-lg md:text-xl py-4 px-10 rounded-2xl shadow-2xl transform transition hover:scale-105 active:scale-95">

            <span>✨ 結算成績並生成成果認證卡 (Submit & Get Certificate)</span>

            <span class="text-lg">➜</span>

          </button>

          <div class="mt-6 pt-4 border-t border-white/20 flex flex-wrap justify-between items-center text-xs text-white/80">

            <span>Designed with ❤️ by hsinyuchi (Sylvia) | 本學習單僅供學術教學非商業使用,音樂與歌詞版權歸原出版唱片公司所有</span>

            <span>授權條款:CC BY-NC-SA 4.0 姓名標示-非商業性-相同方式分享</span>

          </div>

        </div>

      </div>

    </section>



  </div>



  <!-- Effects Container for Confetti/Balloons/Rain -->

  <div id="effects-container" class="fixed inset-0 pointer-events-none z-50 overflow-hidden"></div>



  <!-- Custom In-DOM Alert / Confirm Dialog (100% Google Sites & iframe Sandbox Compatible) -->

  <div id="custom-dialog-modal" class="fixed inset-0 z-[100] hidden flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">

    <div class="bg-white rounded-3xl p-6 max-w-md w-full text-center shadow-2xl transform scale-95 transition-all duration-200 border border-slate-100" id="custom-dialog-box">

      <div id="dialog-icon" class="text-5xl mb-3">💡</div>

      <h3 id="dialog-title" class="text-xl font-black text-slate-800 mb-2">提示訊息</h3>

      <p id="dialog-message" class="text-sm text-slate-600 mb-6 leading-relaxed whitespace-pre-line">內容說明</p>

      <div class="flex gap-3 justify-center" id="dialog-actions">

        <button id="dialog-cancel-btn" class="flex-1 bg-slate-100 hover:bg-slate-200 text-slate-700 font-bold py-3 rounded-xl transition text-sm">

          取消

        </button>

        <button id="dialog-confirm-btn" class="flex-1 bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 text-white font-extrabold py-3 rounded-xl shadow-md transition text-sm">

          確定

        </button>

      </div>

    </div>

  </div>



  <!-- Result Modal & Certificate Modal -->

  <div id="result-modal" class="fixed inset-0 z-50 hidden flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 overflow-y-auto">

    <div class="bg-white rounded-3xl p-4 md:p-6 max-w-xl w-full text-center shadow-2xl transform scale-95 opacity-0 transition-all duration-300 relative my-8" id="result-content">

      

      <!-- Top Close Button -->

      <button onclick="closeResultModal()" class="absolute top-4 right-4 w-9 h-9 rounded-full bg-slate-100 hover:bg-slate-200 text-slate-500 hover:text-slate-800 flex items-center justify-center font-bold text-lg transition z-20" title="關閉視窗">

        ✕

      </button>



      <!-- Step 1: Input Student Information -->

      <div id="input-step" class="py-6 px-4">

        <div id="result-icon" class="text-6xl mb-3">🎉</div>

        <h2 class="text-2xl md:text-3xl font-black text-slate-800 mb-1">作答結算完成!</h2>

        <p class="text-sm text-slate-500 mb-6">您的聽力測驗得分為 <span id="modal-score-preview" class="text-amber-600 font-extrabold text-xl">100</span> 分!請填寫資料領取認證卡:</p>

        

        <div class="grid grid-cols-2 gap-3 mb-3">

          <input type="text" id="student-class" placeholder="班級 (例如: 高一仁)" class="w-full p-3 border-2 border-slate-200 rounded-xl text-center text-sm font-bold focus:outline-none focus:border-amber-500 transition">

          <input type="text" id="student-seat" placeholder="座號 (例如: 08)" class="w-full p-3 border-2 border-slate-200 rounded-xl text-center text-sm font-bold focus:outline-none focus:border-amber-500 transition">

        </div>

        <input type="text" id="student-name" placeholder="學生姓名 (例如: 王小明)" class="w-full p-3 border-2 border-slate-200 rounded-xl text-center text-base font-bold mb-6 focus:outline-none focus:border-amber-500 transition">

        

        <button onclick="generateCertificate()" class="w-full bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 text-white font-extrabold py-3.5 rounded-xl shadow-lg transition">

          🎖️ 生成學習成果認證卡 (Generate Certificate)

        </button>

      </div>



      <!-- Step 2: The High-Resolution Certificate Card -->

      <div id="certificate-step" class="hidden">

        

        <!-- Certificate Canvas Target -->

        <div id="certificate-card" class="p-6 md:p-8 rounded-2xl text-left bg-gradient-to-br from-amber-50 via-white to-orange-50 border-4 border-amber-300 shadow-inner relative overflow-hidden my-2">

          

          <!-- Decorative Background Badge -->

          <div class="absolute -right-8 -bottom-8 opacity-10 pointer-events-none">

            <svg class="w-48 h-48 text-amber-600" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>

          </div>



          <!-- Header -->

          <div class="border-b-2 border-dashed border-amber-200 pb-3 mb-3 flex justify-between items-center">

            <div>

              <div class="text-[10px] font-extrabold uppercase tracking-widest text-amber-700 bg-amber-100 px-2 py-0.5 rounded-full inline-block">

                🎵 英語歌曲學習成果認證卡

              </div>

              <h1 class="text-xl md:text-2xl font-black text-slate-800 tracking-tight mt-1">

                Certificate of Achievement

              </h1>

              <p class="text-xs font-semibold text-slate-500">Track: Bruno Mars - Count on Me</p>

            </div>

            <div class="text-right">

              <span id="cert-date" class="text-xs font-mono text-slate-400 block">2026.09.11</span>

              <span id="cert-time" class="text-xs font-mono text-amber-600 font-bold block">耗時 02:45</span>

            </div>

          </div>



          <!-- Student Info & Score -->

          <div class="flex justify-between items-center py-2">

            <div>

              <p class="text-xs text-slate-400 font-bold uppercase tracking-wider">This certifies that</p>

              <div class="flex items-baseline gap-2 mt-0.5">

                <h2 id="cert-name" class="text-2xl md:text-3xl font-black text-slate-900 underline decoration-amber-400 decoration-4 underline-offset-4">王小明</h2>

                <span id="cert-class-seat" class="text-xs font-bold text-slate-500">(高一仁 08號)</span>

              </div>

              <p class="text-xs text-slate-500 mt-1">has completed the lyrics listening challenge with a score of:</p>

            </div>

            

            <div class="text-right pl-4">

              <div class="text-5xl md:text-6xl font-black text-transparent bg-clip-text bg-gradient-to-r from-amber-500 to-orange-600 font-mono" id="cert-score">

                100

              </div>

              <span class="text-[10px] font-bold text-slate-400 tracking-widest block uppercase">out of 100</span>

            </div>

          </div>



          <!-- Motivational Remark -->

          <div class="bg-white/80 backdrop-blur-sm p-3 rounded-xl border border-amber-200 mt-2">

            <div class="text-[11px] font-bold text-amber-700 uppercase tracking-wider mb-0.5">🌟 挑戰回饋 (Challenge Feedback)</div>

            <p id="cert-teacher-remark" class="text-xs font-semibold text-slate-800">

              "Amazing job! You heard every word correctly! You are a super listener!"<br>

              <span class="text-[11px] font-normal text-slate-500">(太棒了!你每個字都聽對了,你是超級聽力大師!)</span>

            </p>

          </div>



          <!-- Student Reflection & Learning Portfolio Proof -->

          <div class="mt-3 p-3.5 rounded-xl bg-white/95 border-2 border-amber-200/80 shadow-sm text-xs space-y-2.5">

            <div class="flex items-center justify-between border-b border-amber-100 pb-1.5">

              <span class="font-extrabold text-amber-900 flex items-center gap-1.5 text-xs">

                <span>📝</span> 課程學習心得與反思 (Student Reflection)

              </span>

              <span class="text-[10px] text-amber-700 bg-amber-100 px-2 py-0.5 rounded-md font-bold">心得與回饋</span>

            </div>



            <!-- Q1: Rating & Reason -->

            <div>

              <div class="font-bold text-slate-800 flex items-center justify-between mb-1">

                <span>1. 推薦指數與原因 (Rating & Reason):</span>

                <span id="cert-rating-stars" class="text-amber-600 font-extrabold text-xs">⭐⭐⭐⭐⭐ (5/5)</span>

              </div>

              <div id="cert-reason-content" class="text-slate-700 bg-amber-50/70 p-2.5 rounded-lg border border-amber-200/60 text-xs leading-relaxed whitespace-pre-wrap">

                (學生填寫內容)

              </div>

            </div>



            <!-- Q2: Touched Lyric & Takeaway -->

            <div>

              <div class="font-bold text-slate-800 mb-1">

                <span>2. 觸動歌詞與友誼啟發 (Touched Lyric & Key Takeaway):</span>

              </div>

              <div id="cert-quote-content" class="text-slate-700 bg-indigo-50/60 p-2.5 rounded-lg border border-indigo-200/60 text-xs leading-relaxed whitespace-pre-wrap">

                (學生填寫內容)

              </div>

            </div>

          </div>



          <!-- Certificate Footer -->

          <div class="mt-2.5 pt-2 border-t border-slate-200 text-xs text-slate-400 flex justify-between items-center text-[10px] font-medium">

            <span>English Song Listening Challenge</span>

            <span>Keep Learning & Singing! 🎵</span>

          </div>

        </div>



        <!-- Generated PNG Image Preview Section (Guaranteed Save in Google Sites / Mobile) -->

        <div id="cert-image-preview-wrapper" class="hidden mt-3 p-4 bg-amber-50/90 rounded-2xl border-2 border-amber-200 text-center">

          <div class="flex items-center justify-center gap-1.5 text-xs font-extrabold text-amber-900 mb-2">

            <span>✨ 成果認證卡已成功生成!</span>

          </div>

          <p class="text-[11px] text-slate-600 mb-3 leading-relaxed">

            📱 手機/平板用戶:請直接<strong>「長按下方圖片」</strong>點選「儲存影像」<br>

            💻 電腦用戶:若瀏覽器未自動下載,請在圖片上<strong>「點右鍵 ➜ 另存影像」</strong>

          </p>

          <img id="cert-image-preview" class="max-w-full rounded-xl shadow-lg mx-auto border-2 border-amber-300" alt="成果認證卡高解析預覽圖">

          <div class="mt-3 flex flex-wrap justify-center gap-2">

            <button id="open-newtab-btn" onclick="openCertInNewTab()" class="px-3.5 py-1.5 rounded-lg bg-white hover:bg-slate-50 text-slate-700 border border-slate-300 text-xs font-bold transition shadow-sm">

              🔍 在新分頁檢視大圖

            </button>

          </div>

        </div>



        <!-- Action Buttons -->

        <div class="p-3 flex flex-col sm:flex-row gap-3">

          <button id="download-cert-btn" onclick="downloadCertificate()" class="flex-1 bg-gradient-to-r from-amber-500 to-orange-500 hover:from-amber-600 hover:to-orange-600 text-white font-extrabold py-3 px-4 rounded-xl shadow-md transition flex items-center justify-center gap-2 text-sm">

            <span id="download-btn-icon">📥</span>

            <span id="download-btn-text">生成並下載成果認證卡 (PNG)</span>

          </button>

          <button onclick="closeResultModal()" class="sm:w-1/3 bg-slate-100 hover:bg-slate-200 text-slate-700 font-bold py-3 px-4 rounded-xl transition text-sm">

            關閉視窗

          </button>

        </div>

      </div>



    </div>

  </div>



  <!-- JavaScript Logic -->

  <script>

    // --- 1. LYRICS & CLOZE DATA (10 Blanks with Phonological Distractors) ---

    const lyricsData = [

      { type: "section", title: "Intro (輕快烏克麗麗前奏 - 0:00)" },

      { en: "Oh-oh-oh", zh: "(烏克麗麗輕快旋律開場)" },

      { type: "section", title: "Verse 1 (航向汪洋・化作指引的光 - 0:05)" },

      { en: "If you ever find yourself stuck in the [middle] of the sea", zh: "若你曾發現自己受困在大海中央", options: ["middle", "needle", "riddle"] },

      { en: "I'll [sail] the world to find you", zh: "我會航行穿越全世界去尋找你", options: ["sail", "sale", "tail"] },

      { en: "If you ever find yourself lost in the dark and you can't see", zh: "若你曾發現自己在黑暗中迷航、看不清前方" },

      { en: "I'll be the light to [guide] you", zh: "我願化作一道光指引你前行", options: ["guide", "hide", "glide"] },

      { type: "section", title: "Pre-Chorus 1 (患難見真情 - 0:28)" },

      { en: "We'll find out what we're made of", zh: "我們才發現自己的本質多麼堅韌" },

      { en: "When we are called to help our friends in need", zh: "當我們被召喚去幫助身處困境的友人" },

      { type: "section", title: "Chorus 1 (數 1, 2, 3 我就在你身旁 - 0:37)" },

      { en: "You can [count] on me like 1, 2, 3, I'll be there", zh: "你可以像數 1, 2, 3 般依靠著我,我定會陪伴身旁", options: ["count", "mount", "doubt"] },

      { en: "And I know when I need it", zh: "而我也深知當我有所匱乏時" },

      { en: "I can count on you like 4, 3, 2, and you'll be there", zh: "我也能如數 4, 3, 2 般依傍著你,你定會即刻守候" },

      { en: "'Cause that's what friends are [supposed] to do, oh, yeah", zh: "因為那正是真正朋友理應彼此相挺的模樣", options: ["supposed", "exposed", "opposed"] },

      { en: "Ooh-ooh-ooh-ooh-ooh", zh: "(溫暖和聲)" },

      { en: "Ooh-ooh-ooh-ooh-ooh", zh: "(溫暖和聲)" },

      { en: "Ooh, yeah, yeah", zh: "(輕快旋律)" },

      { type: "section", title: "Verse 2 (輾轉難眠・身旁的溫柔歌聲 - 1:10)" },

      { en: "If you tossin' and you're turnin' and you just can't fall [asleep]", zh: "若你輾轉難眠、翻來覆去就是無法入睡", options: ["asleep", "awake", "afloat"] },

      { en: "I'll sing a song beside you", zh: "我會在你身旁輕輕唱首歌" },

      { en: "And if you ever [forget] how much you really mean to me", zh: "若你曾不經意淡忘了你對我有多麼重要", options: ["forget", "forgive", "regret"] },

      { en: "Every day I will [remind] you, oh", zh: "每一天,我都定會輕聲提醒你", options: ["remind", "remain", "rewind"] },

      { type: "section", title: "Pre-Chorus 2 (堅定互助 - 1:33)" },

      { en: "We'll find out what we're made of", zh: "我們才發現自己的內心多麼強大" },

      { en: "When we are called to help our friends in need", zh: "每當身旁的朋友需要援手與陪伴" },

      { type: "section", title: "Chorus 2 (再次相挺 - 1:42)" },

      { en: "You can count on me like 1, 2, 3, I'll be there", zh: "你可以像數 1, 2, 3 般信賴著我,我隨時都在" },

      { en: "And I know when I need it", zh: "而我也清楚知道當我脆弱無助時" },

      { en: "I can count on you like 4, 3, 2, and you'll be there", zh: "我也能像數 4, 3, 2 般依靠著你,你定會守候" },

      { en: "'Cause that's what friends are supposed to do, oh, yeah", zh: "因為那正是真正摯友之間最真切的模樣" },

      { en: "Ooh-ooh-ooh-ooh-ooh", zh: "(真摯和聲)" },

      { en: "Ooh-ooh-ooh-ooh-ooh", zh: "(真摯和聲)" },

      { en: "Ooh, yeah, yeah", zh: "(溫暖旋律)" },

      { type: "section", title: "Bridge (哭泣時的肩膀・永不放手 - 2:14)" },

      { en: "You'll always have my [shoulder] when you cry", zh: "當你傷心落淚時,我的肩膀永遠讓你依靠", options: ["shoulder", "soldier", "shadow"] },

      { en: "I'll never let go, never say [goodbye]", zh: "我絕不放手,也絕不輕言道別", options: ["goodbye", "goodnight", "goodwill"] },

      { type: "section", title: "Chorus 3 (高潮合唱 - 2:33)" },

      { en: "You know you can count on me like 1, 2, 3, I'll be there", zh: "你心裡明白,你可以隨時像數 1, 2, 3 般依靠著我,我必守候身旁" },

      { en: "And I know when I need it", zh: "而我也無比確信,當我身處困境時" },

      { en: "I can count on you like 4, 3, 2, and you'll be there", zh: "我也能像數 4, 3, 2 般依賴著你,你必定會在身側" },

      { en: "'Cause that's what friends are supposed to do, oh, yeah", zh: "因為那就是真正朋友理所當然的相互扶持" },

      { en: "Ooh-ooh-ooh-ooh-ooh", zh: "(全場大合唱)" },

      { en: "Ooh-ooh-ooh-ooh-ooh", zh: "(全場大合唱)" },

      { en: "Ooh", zh: "(深情尾奏)" },

      { type: "section", title: "Outro (相互依靠的誓言 - 3:05)" },

      { en: "You can count on me 'cause I can count on you", zh: "你可以永遠依靠著我,因為我也能永遠信賴著你。" },

    ];



    let userAnswers = {};

    let finalScore = 0;

    const totalQuestions = 10;

    let timerSeconds = 0;

    let timerInterval = null;



    // --- 2. TIMER LOGIC ---

    function startTimer() {

      timerInterval = setInterval(() => {

        timerSeconds++;

        const mins = String(Math.floor(timerSeconds / 60)).padStart(2, '0');

        const secs = String(timerSeconds % 60).padStart(2, '0');

        document.getElementById('live-timer').innerText = `${mins}:${secs}`;

      }, 1000);

    }



    // --- 3. RENDER LYRICS & INLINE POPOVER ---

    function renderLyrics() {

      const container = document.getElementById('lyrics-container');

      let blankCounter = 0;



      lyricsData.forEach((line) => {

        if (line.type === 'section') {

          const sec = document.createElement('div');

          sec.className = "pt-3 pb-1 border-b border-slate-200 font-extrabold text-xs tracking-wider uppercase text-amber-700 flex items-center gap-2";

          sec.innerHTML = `<span>🎵 ${line.title}</span>`;

          container.appendChild(sec);

          return;

        }



        let html = line.en;



        if (html.includes('[')) {

          html = html.replace(/\[(.*?)\]/g, (match, word) => {

            const id = blankCounter++;

            const safeOptions = line.options ? line.options.map(o => o.replace(/'/g, "\\'")) : [];

            return `<span id="blank-${id}" 

                          class="cloze-blank" 

                          data-correct="${word}"

                          data-options="${safeOptions.join(',')}"

                          onclick="toggleInlinePopover(${id}, event)">

                          <span class="text-xs text-amber-600/70 mr-1 font-mono">${id + 1}.</span>____

                    </span>`;

          });

        }



        const div = document.createElement('div');

        div.className = "lyric-line py-1 flex flex-col md:block";

        div.innerHTML = `<span class="font-semibold text-slate-800">${html}</span> <span class="text-xs text-slate-400 font-normal md:ml-3 block md:inline">${line.zh}</span>`;

        container.appendChild(div);

      });



      // Global click outside to close popover

      document.addEventListener('click', (e) => {

        if (!e.target.closest('.cloze-blank')) {

          closeAllPopovers();

        }

      });

    }



    // --- 4. INLINE POPOVER MECHANICS ---

    function toggleInlinePopover(id, event) {

      event.stopPropagation();

      const blankEl = document.getElementById(`blank-${id}`);

      

      // If already has popover, toggle close

      if (blankEl.querySelector('.inline-popover')) {

        closeAllPopovers();

        return;

      }



      closeAllPopovers();



      // Elevate z-index of the active blank and its line

      blankEl.classList.add('has-popover');

      const lineEl = blankEl.closest('.lyric-line');

      if (lineEl) lineEl.classList.add('active-line');



      const optionsStr = blankEl.dataset.options;

      if (!optionsStr) return;

      const options = optionsStr.split(',');

      const shuffled = [...options].sort(() => Math.random() - 0.5);



      const popover = document.createElement('div');

      popover.className = 'inline-popover';



      // Smart flip: if close to viewport bottom, display upwards

      const rect = blankEl.getBoundingClientRect();

      if (rect.bottom + 65 > window.innerHeight && rect.top > 65) {

        popover.classList.add('pop-up');

      }



      shuffled.forEach(opt => {

        const btn = document.createElement('button');

        btn.className = 'pop-opt-btn';

        btn.innerText = opt;

        btn.onclick = (e) => selectOption(id, opt, e);

        popover.appendChild(btn);

      });



      blankEl.appendChild(popover);

    }



    function closeAllPopovers() {

      document.querySelectorAll('.inline-popover').forEach(el => el.remove());

      document.querySelectorAll('.cloze-blank').forEach(el => el.classList.remove('has-popover'));

      document.querySelectorAll('.lyric-line').forEach(el => el.classList.remove('active-line'));

    }



    function selectOption(id, word, event) {

      event.stopPropagation();

      userAnswers[id] = word;



      const blankEl = document.getElementById(`blank-${id}`);

      blankEl.innerHTML = `<span class="text-xs text-indigo-500 mr-1 font-mono font-bold">${id + 1}.</span>${word}`;

      blankEl.classList.add('selected');

      closeAllPopovers();



      updateProgress();

    }



    function updateProgress() {

      const answeredCount = Object.keys(userAnswers).length;

      const pct = (answeredCount / totalQuestions) * 100;

      document.getElementById('progress-text').innerText = `${answeredCount}/${totalQuestions}`;

      document.getElementById('progress-bar').style.width = `${pct}%`;

    }



    // --- CUSTOM DIALOG & MODAL UTILITIES (100% IFRAME & GOOGLE SITES SAFE) ---

    function showCustomAlert(title, message, icon = '💡') {

      const modal = document.getElementById('custom-dialog-modal');

      const iconEl = document.getElementById('dialog-icon');

      const titleEl = document.getElementById('dialog-title');

      const msgEl = document.getElementById('dialog-message');

      const cancelBtn = document.getElementById('dialog-cancel-btn');

      const confirmBtn = document.getElementById('dialog-confirm-btn');



      iconEl.innerText = icon;

      titleEl.innerText = title;

      msgEl.innerText = message;

      cancelBtn.classList.add('hidden');

      confirmBtn.innerText = '我知道了';

      confirmBtn.onclick = () => {

        modal.classList.add('hidden');

      };

      modal.classList.remove('hidden');

      modal.scrollIntoView({ behavior: 'smooth', block: 'center' });

    }



    function showCustomConfirm({ title, message, icon = '⚠️', confirmText = '確定', cancelText = '取消', onConfirm, onCancel }) {

      const modal = document.getElementById('custom-dialog-modal');

      const iconEl = document.getElementById('dialog-icon');

      const titleEl = document.getElementById('dialog-title');

      const msgEl = document.getElementById('dialog-message');

      const cancelBtn = document.getElementById('dialog-cancel-btn');

      const confirmBtn = document.getElementById('dialog-confirm-btn');



      iconEl.innerText = icon;

      titleEl.innerText = title;

      msgEl.innerText = message;

      cancelBtn.classList.remove('hidden');

      cancelBtn.innerText = cancelText;

      confirmBtn.innerText = confirmText;



      cancelBtn.onclick = () => {

        modal.classList.add('hidden');

        if (typeof onCancel === 'function') onCancel();

      };

      confirmBtn.onclick = () => {

        modal.classList.add('hidden');

        if (typeof onConfirm === 'function') onConfirm();

      };



      modal.classList.remove('hidden');

      modal.scrollIntoView({ behavior: 'smooth', block: 'center' });

    }



    function closeResultModal() {

      const modal = document.getElementById('result-modal');

      const content = document.getElementById('result-content');

      content.classList.remove('scale-100', 'opacity-100');

      content.classList.add('scale-95', 'opacity-0');

      setTimeout(() => {

        modal.classList.add('hidden');

      }, 200);

    }



    // --- 5. CHECK ANSWERS ONLY (LIGHTWEIGHT) ---

    function checkAnswersOnly() {

      const answeredCount = Object.keys(userAnswers).length;

      if (answeredCount === 0) {

        showCustomAlert("尚未作答", "請先點擊歌詞中的黃色空格進行聽力作答,再來對答案喔!", "✍️");

        return;

      }

      let correctCount = 0;

      const blanks = document.querySelectorAll('.cloze-blank');

      blanks.forEach((btn, idx) => {

        const correct = btn.dataset.correct;

        const userAns = userAnswers[idx];

        if (!userAns) return; // don't reveal unanswered

        const qNum = `<span class="text-xs font-mono font-bold text-slate-500 mr-1">${idx + 1}.</span>`;

        if (userAns === correct) {

          correctCount++;

          btn.classList.remove('selected');

          btn.classList.add('correct');

          btn.innerHTML = `${qNum}${userAns} <span class="text-[10px] font-black">✓</span>`;

        } else {

          btn.classList.remove('selected');

          btn.classList.add('wrong');

          btn.innerHTML = `${qNum}<del class="opacity-70">${userAns}</del> <span class="text-[11px] font-bold text-green-700 ml-1">(${correct})</span>`;

        }

      });

      showCustomAlert(

        "即時對題結果",

        `🎉 目前已作答的題目中,您答對了 ${correctCount} 題!\n請繼續向下閱讀單字句型並完成「自我反思」,最後於頁尾領取認證卡!`,

        "🎯"

      );

    }



    // --- 6. FINAL SUBMISSION (AT FOOTER) & EFFECTS TRIGGER ---

    function submitQuiz() {

      const answeredCount = Object.keys(userAnswers).length;

      if (answeredCount < totalQuestions) {

        showCustomConfirm({

          title: "聽力尚未作答完畢",

          message: `⚠️ 聽力挖空還有 ${totalQuestions - answeredCount} 題尚未作答!\n確定要直接結算成績嗎?`,

          icon: "⚠️",

          confirmText: "直接結算",

          cancelText: "回去作答",

          onConfirm: () => checkReflectionAndProceed(),

          onCancel: () => {

            const firstUnanswered = document.querySelector('.cloze-blank:not(.selected)');

            if (firstUnanswered) firstUnanswered.scrollIntoView({ behavior: 'smooth', block: 'center' });

          }

        });

        return;

      }



      checkReflectionAndProceed();

    }



    function checkReflectionAndProceed() {

      const sReason = document.getElementById('student-reason').value.trim();

      const sQuote = document.getElementById('student-quote').value.trim();

      if (!sReason && !sQuote) {

        showCustomConfirm({

          title: "學習反思尚未填寫",

          message: "💡 貼心提醒:您尚未填寫最後的「學習反思問答」!\n若未填寫,成果認證卡將會缺少學習歷程的反思文字。\n\n確定要直接結算領取小卡嗎?",

          icon: "📝",

          confirmText: "直接結算",

          cancelText: "回去填寫",

          onConfirm: () => finalizeSubmission(),

          onCancel: () => {

            const refSec = document.getElementById('reflection-section');

            if (refSec) refSec.scrollIntoView({ behavior: 'smooth', block: 'center' });

            document.getElementById('student-reason').focus();

          }

        });

        return;

      }



      finalizeSubmission();

    }



    function finalizeSubmission() {

      clearInterval(timerInterval);

      let score = 0;

      const blanks = document.querySelectorAll('.cloze-blank');



      blanks.forEach((btn, idx) => {

        const correct = btn.dataset.correct;

        const userAns = userAnswers[idx];



        btn.removeAttribute('onclick');

        btn.classList.add('answered');

        const qNum = `<span class="text-xs font-mono font-bold text-slate-500 mr-1">${idx + 1}.</span>`;



        if (userAns === correct) {

          score += 10;

          btn.classList.remove('selected');

          btn.classList.add('correct');

          btn.innerHTML = `${qNum}${userAns} <span class="text-[10px] font-black">✓</span>`;

        } else {

          btn.classList.remove('selected');

          btn.classList.add('wrong');

          btn.innerHTML = `${qNum}<del class="opacity-70">${userAns || '未答'}</del> <span class="text-[11px] font-bold text-green-700 ml-1">(${correct})</span>`;

        }

      });



      finalScore = score;

      document.getElementById('modal-score-preview').innerText = finalScore;



      const modal = document.getElementById('result-modal');

      const content = document.getElementById('result-content');

      modal.classList.remove('hidden');



      setTimeout(() => {

        content.classList.remove('scale-95', 'opacity-0');

        content.classList.add('scale-100', 'opacity-100');

        modal.scrollIntoView({ behavior: 'smooth', block: 'center' });

      }, 50);



      triggerEffects(finalScore);

    }



    // --- 6. GAMIFIED DYNAMIC EFFECTS ---

    function triggerEffects(score) {

      document.body.classList.remove('grayscale-mode');

      const fx = document.getElementById('effects-container');

      fx.innerHTML = '';

      const icon = document.getElementById('result-icon');



      if (score >= 80) {

        icon.innerText = "🧙♀️✨";

        if (typeof window.confetti === 'function') {

          triggerConfetti();

        }

      } else if (score >= 60) {

        icon.innerText = "🎈☁️";

        createBalloons(12);

      } else if (score >= 40) {

        icon.innerText = "😬🧹";

        document.body.classList.add('shake');

        setTimeout(() => document.body.classList.remove('shake'), 1000);

      } else {

        icon.innerText = "🌧️😭";

        document.body.classList.add('grayscale-mode');

        createRain();

      }

    }



    function triggerConfetti() {

      const duration = 3 * 1000;

      const end = Date.now() + duration;

      const interval = setInterval(() => {

        if (Date.now() > end) return clearInterval(interval);

        confetti({

          particleCount: 40,

          spread: 360,

          startVelocity: 30,

          origin: { x: Math.random() * 0.3 + 0.1, y: Math.random() - 0.2 }

        });

        confetti({

          particleCount: 40,

          spread: 360,

          startVelocity: 30,

          origin: { x: Math.random() * 0.3 + 0.6, y: Math.random() - 0.2 }

        });

      }, 250);

    }



    function createBalloons(count) {

      const container = document.getElementById('effects-container');

      for (let i = 0; i < count; i++) {

        const b = document.createElement('div');

        b.className = 'balloon';

        b.innerText = ['🎈', '🎉', '🌟', '🎸'][Math.floor(Math.random() * 4)];

        b.style.left = Math.random() * 95 + 'vw';

        b.style.animationDuration = (Math.random() * 2 + 3) + 's';

        b.style.fontSize = (Math.random() * 1.5 + 2) + 'rem';

        container.appendChild(b);

      }

    }



    function createRain() {

      const container = document.getElementById('effects-container');

      for (let i = 0; i < 35; i++) {

        const d = document.createElement('div');

        d.className = 'rain-drop';

        d.innerText = '💧';

        d.style.left = Math.random() * 100 + 'vw';

        d.style.animationDuration = (Math.random() * 0.8 + 0.6) + 's';

        d.style.animationDelay = Math.random() * 1.5 + 's';

        container.appendChild(d);

      }

    }



    // --- 7. SLIDER LOGIC ---

    function updateRatingDisplay(val) {

      const badge = document.getElementById('rating-badge');

      const ratings = {

        '5': '⭐⭐⭐⭐⭐ (5/5) 絕世神曲!不聽後悔',

        '4': '⭐⭐⭐⭐☆ (4/5) 旋律溫暖,超值得推薦',

        '3': '⭐⭐⭐☆☆ (3/5) 節奏輕快,感覺還不錯',

        '2': '⭐⭐☆☆☆ (2/5) 普普通通,不是我的菜',

        '1': '⭐☆☆☆☆ (1/5) 不太有共鳴'

      };

      badge.innerText = ratings[val] || '⭐⭐⭐⭐⭐';

    }



    // --- 8. TTS AUDIO PRONUNCIATION ---

    function speakWord(text) {

      if (!('speechSynthesis' in window)) {

        showCustomAlert("語音提示", "您的瀏覽器暫不支援語音朗讀功能!", "🔊");

        return;

      }

      window.speechSynthesis.cancel();

      const utter = new SpeechSynthesisUtterance(text);

      utter.lang = 'en-US';

      utter.rate = 0.85; // slightly slower for EFL learners

      window.speechSynthesis.speak(utter);

    }



    // --- 9. CERTIFICATE GENERATION & HIGH-SPEED PNG EXPORT ---

    let cachedCertDataUrl = null;



    function generateCertificate() {

      const sClass = document.getElementById('student-class').value.trim();

      const sSeat = document.getElementById('student-seat').value.trim();

      const sName = document.getElementById('student-name').value.trim();



      if (!sClass || !sSeat || !sName) {

        showCustomAlert("請填妥資料", "請完整輸入班級、座號與姓名,方可領取專屬認證卡!", "⚠️");

        if (!sClass) document.getElementById('student-class').focus();

        else if (!sSeat) document.getElementById('student-seat').focus();

        else document.getElementById('student-name').focus();

        return;

      }



      cachedCertDataUrl = null; // reset cache for new info

      document.getElementById('input-step').classList.add('hidden');

      document.getElementById('certificate-step').classList.remove('hidden');



      // Populate Certificate Data

      document.getElementById('cert-name').innerText = sName;

      document.getElementById('cert-class-seat').innerText = `(${sClass} ${sSeat}號)`;

      document.getElementById('cert-score').innerText = finalScore;

      document.getElementById('cert-date').innerText = new Date().toLocaleDateString();

      document.getElementById('cert-time').innerText = `作答耗時 ${document.getElementById('live-timer').innerText}`;



      // Star rating display

      const ratingVal = document.getElementById('rating-slider').value;

      const starsMap = { '5': '⭐⭐⭐⭐⭐', '4': '⭐⭐⭐⭐☆', '3': '⭐⭐⭐☆☆', '2': '⭐⭐☆☆☆', '1': '⭐☆☆☆☆' };

      document.getElementById('cert-rating-stars').innerText = `${starsMap[ratingVal]} (${ratingVal}/5)`;



      // Full Student Reflection Answers (Proof of Learning for Portfolio)

      const sReason = document.getElementById('student-reason').value.trim() || "(學生未填寫推薦原因)";

      const sQuote = document.getElementById('student-quote').value.trim() || "(學生未填寫反思心得)";



      document.getElementById('cert-reason-content').innerText = sReason;

      document.getElementById('cert-quote-content').innerText = sQuote;



      // Smart Teacher Remarks based on score (Tailored for A1-A2 levels)

      const remarkEl = document.getElementById('cert-teacher-remark');

      if (finalScore === 100) {

        remarkEl.innerHTML = `"Amazing job! You heard every word correctly! You are a super listener!"<br><span class="text-[11px] font-normal text-slate-500">(太棒了!你每個字都聽對了,你是超級聽力大師!)</span>`;

      } else if (finalScore >= 80) {

        remarkEl.innerHTML = `"Great job! Your English listening is wonderful! Keep it up!"<br><span class="text-[11px] font-normal text-slate-500">(做得好!你的英文聽力很棒,繼續保持!)</span>`;

      } else if (finalScore >= 60) {

        remarkEl.innerHTML = `"Good work! You can catch the beat! Practice more and you will be even better!"<br><span class="text-[11px] font-normal text-slate-500">(很棒!你抓到歌曲節奏了,多練習會更進步!)</span>`;

      } else {

        remarkEl.innerHTML = `"Keep trying! Just like a good friend, English will always be here for you! Try again!"<br><span class="text-[11px] font-normal text-slate-500">(別放棄!就像好朋友一樣,英文永遠陪伴你,再試一次吧!)</span>`;

      }



      // 自動將視圖平滑捲動到認證卡頂部

      const modal = document.getElementById('result-modal');

      modal.scrollTo({ top: 0, behavior: 'smooth' });

    }



    function downloadCertificate() {

      const element = document.getElementById('certificate-card');

      const name = document.getElementById('cert-name').innerText || 'Student';

      const btn = document.getElementById('download-cert-btn');

      const iconEl = document.getElementById('download-btn-icon');

      const textEl = document.getElementById('download-btn-text');



      if (cachedCertDataUrl) {

        triggerDownload(cachedCertDataUrl, name);

        return;

      }



      // UI Loading Feedback (Solves Cloudflare / Mobile waiting anxiety)

      btn.disabled = true;

      btn.classList.add('opacity-75', 'cursor-wait');

      iconEl.innerHTML = `<svg class="animate-spin h-4 w-4 text-white inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>`;

      textEl.innerText = "⏳ 正在生成認證卡圖片 (約需 1~2 秒)...";



      html2canvas(element, {

        scale: 2,

        useCORS: true,

        allowTaint: true,

        backgroundColor: '#fffdf7',

        logging: false,

        imageTimeout: 1200 // 避免因外部字型或網路延遲而無限等待

      }).then(canvas => {

        const dataUrl = canvas.toDataURL('image/png');

        cachedCertDataUrl = dataUrl;



        // 1. 自動觸發下載 (支援直接下載之瀏覽器)

        triggerDownload(dataUrl, name);



        // 2. 將高解析圖直接呈現在網頁預覽區(解決 Google Sites iframe 沙箱與行動裝置阻擋下載限制)

        const previewWrap = document.getElementById('cert-image-preview-wrapper');

        const previewImg = document.getElementById('cert-image-preview');

        if (previewImg && previewWrap) {

          previewImg.src = dataUrl;

          previewWrap.classList.remove('hidden');

          previewWrap.scrollIntoView({ behavior: 'smooth', block: 'nearest' });

        }



        // 3. 恢復按鈕狀態

        btn.disabled = false;

        btn.classList.remove('opacity-75', 'cursor-wait');

        iconEl.innerText = "✅";

        textEl.innerText = "重新下載成果認證卡 (PNG)";

      }).catch(err => {

        console.error("生成認證卡失敗:", err);

        btn.disabled = false;

        btn.classList.remove('opacity-75', 'cursor-wait');

        iconEl.innerText = "📥";

        textEl.innerText = "重試生成認證卡 (PNG)";

        showCustomAlert("生成失敗", "抱歉,在目前瀏覽器環境生成圖片時發生問題。您可以使用螢幕截圖或點擊下方按鈕在新分頁開啟!", "⚠️");

      });

    }



    function triggerDownload(dataUrl, name) {

      try {

        const link = document.createElement('a');

        link.download = `Bruno_Mars_Count_On_Me_認證卡_${name}.png`;

        link.href = dataUrl;

        document.body.appendChild(link);

        link.click();

        document.body.removeChild(link);

      } catch (e) {

        console.warn("沙箱 iframe 限制了直接下載,已備援展示預覽圖:", e);

      }

    }



    function openCertInNewTab() {

      if (!cachedCertDataUrl) return;

      const newWin = window.open('');

      if (newWin) {

        newWin.document.write(`<html><head><title>Bruno Mars - Count on Me 認證卡預覽</title></head><body style="margin:0;background:#1e293b;display:flex;justify-content:center;align-items:center;min-height:100vh;"><img src="${cachedCertDataUrl}" style="max-width:95vw;max-height:95vh;box-shadow:0 10px 25px rgba(0,0,0,0.5);border-radius:16px;"/></body></html>
已複製