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 會依前端網頁標題自動建立專屬活動分頁,並自動建立/擴充欄位(無標題時自動 fallback 至 Responses 分頁)。
  3. 從試算表上方選單開啟:擴充功能 → Apps Script。

Prep.2|貼上 Universal GAS 萬用後端(終身免換試算表版)

  1. 刪除 Apps Script 內的預設程式碼。
  2. 複製下方完整 Universal GAS 萬用後端程式碼。
  3. 貼上後儲存。
Universal GAS 萬用後端終身免換試算表
// ======================================================
// From Play to Proof|CARE Universal GAS 3.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 3.0 Auto-Title Routing 路由判定
    // 優先順序:
    // 1. sheetName   → 2.0 手動指定 / 覆寫
    // 2. sourceTitle → 3.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'
    );

    // 若使用 3.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 3.0 Dashboard Routing
    // 支援:
    // ?title=Be%20Media%20Smart (3.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,或從 10 個 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

10 個高中英文網頁 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 程式碼。
⑥ Reading Detective|閱讀偵探Starter Prompt
請將我提供的英文文章改造成「Reading Detective 閱讀偵探」網頁。

學生閱讀文章後,需要找出:

1. 關鍵人物

2. 重要事件

3. 文本證據

4. 一個推論

5. 故事或文章的核心訊息

設計成逐關解鎖的任務感,但不要改變原文事實。

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

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

選完後:

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

- 解釋語氣或用法

- 讓學生進入下一個情境

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

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

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

請提供:

1. 情境說明

2. 2–3 個 sentence starters

3. 可使用的關鍵字

4. 一個簡短任務

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

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

請產生完整單一 HTML 程式碼。
⑨ Escape Room Lite|迷你密室Starter Prompt
請根據我提供的高中英文教材,設計一個 15–20 分鐘可以完成的英文 Mini Escape Room 網頁。

設計 4 關:

1. Vocabulary

2. Grammar

3. Reading

4. Final Code

學生答對後才能取得下一關線索。

請讓挑戰有故事感,但不要因故事包裝而犧牲真正的英文學習內容。

最後顯示完成任務畫面。

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

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

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

目標是讓學生:

- 理解文本

- 做出判斷

- 表達個人觀點

- 為後續小組討論做準備

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

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

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

這一步先不要寫程式。把以下三樣東西放進同一個 AI 對話

  1. 自己的 HTML 網頁程式碼
  2. CARE Universal Upgrade Prompt(萬用升級助手)
  3. 剛才取得的自己的 GAS Web App URL

這份 Prompt 會幫你做什麼?

功能白話版
保留原本活動不會為了加新功能,就把你好不容易做好的網頁整個重做。
判斷哪些學習資料值得留下AI 先幫你想:哪些學生表現真的值得記錄,而不是什麼都收。
老師先確認AI 第一回合只提規劃;你說「可以,照這個做」之後才開始改。
產生學生個人學習報告學生做完不只看到分數,也能回顧自己的作答與學習。
沒有後台也能完成即使資料暫時送不出去,學生仍可完成活動、看到報告並下載成果。
有後台時自動收資料學生作答可送進 Google Sheet,老師不用逐份整理。
整理全班學習狀況建立 Teacher Dashboard,快速看出哪些題目或觀念需要處理。
自動檢查與防呆盡量保護原本已成功的功能;出錯時只修必要部分。
💎 Sylvia 專屬懶人彩蛋:CARE 升級專用 Gemini Gem
不想每次手動複製長長的 Prompt?Sylvia 老師已經把最新 CARE 規範直接封裝進 Google Gemini Gem
點開後只要上傳您的 HTML 檔案 + 貼上您的 GAS URL,連 Prompt 都不用複製,AI 直接為您無腦升級!
A — CORE RESOURCE

CARE Universal Upgrade Prompt

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

CARE Universal Upgrade Prompt核心升級助手
# Universal Agent Skill: CARE HTML Augmenter 3.0
> 本技能遵循「跨 Agent 通用標準規範 (Universal Agent Skill Specification)」,不依賴特定平台之專有語法。無論是在本機終端 AI Agent、雲端自訂助理 (Gem/GPTs),或直接作為提示詞黏貼至 ChatGPT/Claude/Gemini,均能保證 100% 相同的高精準度執行力。

# From Play to Proof|CARE 教學網頁升級助手
## Auto-Title Routing × Universal GAS × 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。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. **【必填・自動分頁名稱預告】預計採用的自動活動名稱與試算表分頁名稱:【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 的 Auto-Title Routing 自動分頁技術。正常情況下,老師**不需要另外輸入課次、活動代碼、sheetName,也不需要修改 JavaScript**。
* **【嚴禁泛稱標題】**:產出之 HTML `<title>` 必須依教材內容設定具唯一辨識度的具體名稱(如 `<title>B1L3_Animal_Sleep_Interactive</title>`),**嚴禁使用 Document、Worksheet、Interactive Worksheet 等預設無意義名稱**,以免造成跨課次分頁撞名覆蓋。
* **Single Source of Truth 原則**:前端必須建立共用函式統一取得標題(優先 `document.title.trim()`,無有效 title 則 fallback 至主要 `h1` 文字)。
* 學生 POST 送出時,payload 欄位必須帶入:
  `sourceTitle: getActivityTitle()`
* Teacher Dashboard 讀取時,GET 請求必須帶入完全相同的標題:
  `GAS_URL + "?title=" + encodeURIComponent(getActivityTitle())`
  *(注意:必須使用 `encodeURIComponent`,確保中文、空白與符號在 URL 傳遞時不被截斷。)*

## 12. Universal GAS 萬用後端相容性
本研習提供固定之 **CARE Universal GAS**。後端具備防禦性 `normalizeSheetName`,能自動過濾 `: \ / ? * [ ]` 與前後引號,並自動建立對應分頁與擴充表頭。
* **向下相容性**:
  1. `data.sheetName` (2.0 / 手動指定)
  2. `data.sourceTitle` (3.0 Auto-Title)
  3. `data.lesson` / `data.unit` (舊版相容)
  4. `DEFAULT_SHEET_NAME` ('Responses' 最終 fallback)
* 同時支援 Dashboard `?title=` (3.0) 與 `?sheet=` (2.0)。

## 13. 自動評量原則
* **客觀題**(單選、多選、是非、配對、拖曳、填空、明確文法句型):可自動評量,記錄總分、正確率、各區塊得分、各題正誤、作答時間與 retryCount。
* **主觀題**(Reflection、SEL、個人意見、創意寫作、開放式問答):**不要硬判正誤**,保留學生原文,完整呈現在個人 Learning Report、Google Sheet 與 Student Response Wall。

## 14. 個人 Learning Report 與 PNG 下載
* Learning Report 不可只顯示總分,需包含:班級、座號、姓名、作答時間、總分、正確率、各題表現、自評與質性反思。
* 支援使用 html2canvas 下載清晰 PNG。即使未設定 GAS 或網路斷線,學生端仍能正常完成活動並下載成果小卡。

## 15. 提交與 no-cors 安全原則
* 前端使用 `fetch(GAS_URL, { method: 'POST', mode: 'no-cors', ... })` 送出資料。
* 由於 `no-cors` 無法讀取伺服器真實 response status,UI 應顯示「資料已送出」,嚴禁未經驗證宣稱「已成功寫入試算表」。
* Submit 按鈕必須具備防連點(debounce / disabled)與送出中狀態提示。

## 16. Teacher Dashboard 規範
* 右上角固定提供清楚可辨識的 🔒 Teacher 入口(不藏在頁尾)。
* 點擊後跳出自訂 HTML Login Modal(預設示範密碼為 `teacher`)。
* 登入後先打開 Dashboard 外框並顯示「正在讀取資料……」,再發起 GET 請求。
* 若讀取失敗或試算表無資料,Dashboard 仍保持開啟並友善提示錯誤或「尚無學生資料」,嚴禁畫面崩潰。
* 必備功能:Refresh Data、Close Dashboard。
* 圖表重繪前務必先 `chartInstance.destroy()`,避免畫布重疊記憶體洩漏。

## 17. Dashboard 內容原則
* **KPI**:精選 3~6 個真正有教學意義的指標(如:提交人數、平均得分、平均正確率、核心題型正確率、平均作答時間)。
* **圖表**:只畫能回答教學診斷問題的圖(如:各題答對率、高頻錯誤選項分布)。
* **Student Response Wall**:依題目在前端出現順序陳列質性回答,顯示名稱需使用教師易讀之題目標籤,避免顯示底層 key(如 `q1_ans`)。

## 18. End-to-End 自我檢核清單
產出前務必內部驗證全流程:
1. 學生端作答 → 本地評分 → 產生 Learning Report → PNG 下載可用。
2. 離線雙棲路徑在無 GAS URL 時絕不跳出報錯阻斷學生。
3. `getActivityTitle()` 能取得唯一性標題,payload 包含 `sourceTitle`。
4. Teacher Dashboard GET 請求帶入 `?title=` 且經 `encodeURIComponent`。
5. 🔒 入口、Modal、密碼驗證、Dashboard 開關與重新整理皆可正常運作。
6. 所有 DOM 元素 ID 皆存在且不重複,無未定義變數或例外。

## 19. 一句話核心心法
**先看懂 → 先規劃 → 我確認 → 再全部做。**
**老師只需提供 GAS URL;活動名稱、分頁建立與儀表板路由交由 AI 與 Universal GAS 全自動處理。**

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

AI 先讀懂原始活動,只提出值得留下的 Learning Evidence、Learning Report、Teacher Dashboard、圖表與質性回答規劃,以及預計建立的自動分頁名稱(如 B1L3_Animal_Sleep_Interactive,嚴禁使用 Document 等泛稱)。這時不應輸出新版 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/Gemini Notebook 整理。
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|課後延伸

GOODBYE JOHN SOURCE

Goodbye, John|前後端原始程式碼

前端包含學生互動、Learning Report 與 Teacher Dashboard;後端為原示範課程使用的 Google Apps Script。

Goodbye John Frontend|學習單網頁及教師儀表板HTML

Goodbye John 學習單與 Teacher Dashboard 前端完整原始碼。

<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Goodbye, John - Interactive Worksheet</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
    <!-- 加入 Sortable.js 支援沙盒環境完美的拖拉功能 -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.2/Sortable.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script>
        // ==========================================
        // 教師設定區: 請在此貼上你部署的 GAS URL
        // ==========================================
        // 教師設定區:請貼上你自己的 GAS Web App URL
const GAS_URL = "YOUR_GAS_WEB_APP_URL"; 
    </script>
    <style>
        /* Base & Reset */
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f8fafc; scroll-behavior: smooth; margin: 0; padding: 0; overflow-x: hidden; }
        
        /* Progress Bar */
        #global-progress-container { position: fixed; top: 0; left: 0; width: 100%; height: 6px; background-color: #e2e8f0; z-index: 100; }
        #global-progress-bar { height: 100%; background-color: #10b981; width: 0%; transition: width 0.5s ease-in-out; }
        
        /* Tabs */
        .tab-active { background-color: #0284c7; color: white; font-weight: bold; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); transform: translateY(-1px); }
        .tab-inactive { background-color: transparent; color: #64748b; font-weight: 500; }
        .tab-inactive:hover { background-color: #e0f2fe; color: #0369a1; }
        
        /* Tab 4 Blank & Popup Styles */
        .blank-zone {
            display: inline-flex; align-items: center; justify-content: center; min-width: 100px; height: 32px;
            border-bottom: 3px solid #94a3b8; background-color: rgba(255,255,255,0.6); margin: 0 4px; cursor: pointer;
            transition: all 0.2s ease; vertical-align: middle; font-weight: bold; color: #1e293b; border-radius: 4px 4px 0 0; padding: 0 8px;
            position: relative;
        }
        .blank-zone:hover { background-color: rgba(139, 92, 246, 0.1); border-color: #8b5cf6; }
        .blank-filled { background-color: #ede9fe; border-color: #8b5cf6; color: #5b21b6; border-bottom-width: 3px; }
        .status-correct { border-color: #10b981 !important; background-color: #d1fae5 !important; color: #047857 !important; }
        .status-incorrect { border-color: #ef4444 !important; background-color: #fee2e2 !important; color: #b91c1c !important; }
        
        .blank-zone.show-hint::after {
            content: attr(data-hint); position: absolute; bottom: 110%; left: 50%; transform: translateX(-50%);
            background-color: #fef3c7; color: #d97706; padding: 4px 8px; border-radius: 6px; font-size: 0.8rem; font-weight: bold; white-space: nowrap; box-shadow: 0 4px 6px rgba(0,0,0,0.1); pointer-events: none; border: 1px solid #fde68a; z-index: 10;
        }
        .blank-zone.show-hint::before { content: ''; position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); border-width: 5px; border-style: solid; border-color: #fef3c7 transparent transparent transparent; z-index: 10; }
        
        #word-selector-popup { position: fixed; z-index: 9999; }
        
        /* Scaffold/Translations */
        .scaffold-content { max-height: 0; overflow: hidden; transition: max-height 0.4s, opacity 0.4s, padding 0.4s; opacity: 0; }
        .scaffold-content.show { max-height: 500px; opacity: 1; padding-top: 0.75rem; padding-bottom: 0.5rem; }
        
        /* Chat Dialogue */
        .chat-container { display: flex; width: 100%; margin-bottom: 1.5rem; }
        .chat-left { justify-content: flex-start; } .chat-right { justify-content: flex-end; }
        .bubble { padding: 1rem 1.25rem; border-radius: 1.5rem; max-width: 85%; position: relative; box-shadow: 0 2px 4px rgba(0,0,0,0.05); font-size: 1.05rem; line-height: 1.6; }
        @media (min-width: 640px) { .bubble { max-width: 75%; } }
        .bubble-left { background-color: #ffffff; border: 1px solid #e2e8f0; color: #334155; border-top-left-radius: 0.25rem; }
        .bubble-right { background-color: #f0f9ff; border: 1px solid #bae6fd; color: #0c4a6e; border-top-right-radius: 0.25rem; }
        .speaking-highlight-bubble { border: 2px solid #fbbf24; background-color: #fef3c7; box-shadow: 0 0 15px rgba(251,191,36,0.4); }
        .reading-highlight { background-color: #fef3c7; border-radius: 4px; transition: background-color 0.3s; }
        
        /* Tab 3 Sort Styles */
        .sort-item { cursor: pointer; transition: all 0.2s; user-select: none; }
        .sort-item:hover { transform: translateY(-2px); box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
        .sort-item.selected { border: 2px solid #3b82f6; background-color: #eff6ff; transform: scale(1.05); box-shadow: 0 0 10px rgba(59, 130, 246, 0.5); }
        .drop-zone { transition: background-color 0.3s, border-color 0.3s; min-height: 120px; }
        
        @keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-4px); } 75% { transform: translateX(4px); } }
        .animate-shake { animation: shake 0.4s; }
        
        .custom-scrollbar::-webkit-scrollbar { width: 6px; }
        .custom-scrollbar::-webkit-scrollbar-track { background: #f1f5f9; border-radius: 4px; }
        .custom-scrollbar::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
        .custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #94a3b8; }

        /* Report Export CSS - Simplified for html2canvas compatibility */
        #visual-report-wrapper { position: absolute; left: -9999px; top: -9999px; width: 850px; background-color: #ffffff; padding: 0; margin: 0; z-index: -10; }
    </style>
</head>
<body class="text-slate-800 flex flex-col min-h-screen relative">

    <!-- Auto-save Indicator -->
    <div id="auto-save-indicator" class="fixed bottom-6 right-6 bg-white border border-slate-200 shadow-lg rounded-full px-4 py-2 text-sm font-bold text-slate-500 flex items-center transition-opacity duration-300 opacity-0 z-[5000]">
        <i class="fas fa-check-circle mr-2 text-emerald-500"></i><span>Saved</span>
    </div>

    <!-- Final Image Display Modal (Google Sites Download Fix) -->
    <div id="final-image-modal" class="hidden fixed inset-0 z-[6000] bg-slate-900/95 flex flex-col items-center justify-center p-2 sm:p-4 backdrop-blur-sm">
        <div class="bg-white rounded-2xl w-full max-w-[850px] max-h-[95vh] flex flex-col overflow-hidden shadow-2xl">
            <div class="p-4 bg-emerald-50 border-b border-emerald-100 flex justify-between items-center shrink-0">
                <div>
                    <h3 class="font-bold text-lg text-emerald-800"><i class="fas fa-check-circle mr-2 text-emerald-500"></i>報告產出成功 Report Ready!</h3>
                    <p class="text-sm text-emerald-600 font-bold mt-1 bg-emerald-100/50 inline-block px-2 py-1 rounded">※ 若未自動下載,請「長按圖片」或「點擊右鍵」選擇『另存圖片』。</p>
                </div>
                <button onclick="closeFinalImageModal()" class="text-slate-400 hover:text-rose-500 transition-colors focus:outline-none"><i class="fas fa-times text-2xl"></i></button>
            </div>
            <div class="flex-1 overflow-y-auto p-4 bg-slate-200 flex justify-center custom-scrollbar">
                <img id="final-generated-img" src="" alt="Report Card" class="max-w-full shadow-lg border border-slate-300" style="height: auto;">
            </div>
            <div class="p-4 bg-white border-t border-slate-200 text-center shrink-0 flex justify-center gap-4">
                <button onclick="closeFinalImageModal()" class="px-8 py-3 bg-slate-700 hover:bg-slate-800 text-white font-bold rounded-xl transition-colors shadow-md">關閉 Close</button>
            </div>
        </div>
    </div>

    <!-- Confirm Clear Modal -->
    <div id="confirm-clear-modal" class="hidden fixed inset-0 z-[4000] bg-black/50 flex items-center justify-center backdrop-blur-sm">
        <div class="bg-white p-6 rounded-xl shadow-2xl max-w-sm w-full mx-4 text-center">
            <div class="w-16 h-16 bg-rose-100 text-rose-500 rounded-full flex items-center justify-center text-3xl mx-auto mb-4"><i class="fas fa-exclamation-triangle"></i></div>
            <h3 class="text-xl font-bold text-slate-800 mb-2">Reset Progress?</h3>
            <p class="text-slate-600 mb-6">Are you sure you want to clear all data and start over?</p>
            <div class="flex justify-center gap-3">
                <button onclick="closeClearModal()" class="px-5 py-2 text-slate-500 hover:bg-slate-100 rounded-lg font-bold transition border border-slate-200">Cancel</button>
                <button onclick="executeClearProgress()" class="px-5 py-2 bg-rose-600 hover:bg-rose-700 text-white rounded-lg font-bold transition shadow-md">Yes, Reset</button>
            </div>
        </div>
    </div>

    <!-- Teacher Login Modal -->
    <div id="teacher-login-modal" class="hidden fixed inset-0 z-[4000] bg-black/50 flex items-center justify-center backdrop-blur-sm">
        <div class="bg-white p-6 rounded-xl shadow-2xl max-w-sm w-full mx-4 border border-slate-200">
            <h3 class="text-xl font-bold text-slate-800 mb-4"><i class="fas fa-lock text-blue-500 mr-2"></i>Teacher Access</h3>
            <input type="password" id="teacher-pwd-input" class="w-full px-4 py-2 border border-slate-300 rounded-lg mb-2 focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Enter teacher password..." onkeydown="if(event.key === 'Enter') verifyTeacherLogin()">
            <p id="teacher-login-error" class="text-rose-500 text-sm font-bold hidden mb-4"><i class="fas fa-exclamation-circle mr-1"></i>Incorrect password.</p>
            <div class="flex justify-end gap-3 mt-4">
                <button onclick="closeTeacherLoginModal()" class="px-4 py-2 text-slate-500 hover:bg-slate-100 rounded-lg font-bold transition">Cancel</button>
                <button onclick="verifyTeacherLogin()" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-bold transition shadow-md">Enter</button>
            </div>
        </div>
    </div>

    <!-- Teacher Dashboard -->
    <div id="teacher-dashboard" class="hidden fixed inset-0 z-[3500] bg-slate-100 overflow-y-auto">
        <div class="max-w-7xl mx-auto p-4 sm:p-8">
            <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center bg-white p-6 rounded-2xl shadow-sm mb-6 border border-slate-200 gap-4">
                <div>
                    <h1 class="text-3xl font-extrabold text-slate-800 flex items-center"><i class="fas fa-chart-line text-blue-600 mr-3"></i> Teacher Dashboard</h1>
                    <p class="text-slate-500 font-medium mt-1">Live Class Analytics & Showcase</p>
                </div>
                <div class="flex flex-wrap gap-3">
                    <a href="https://docs.google.com/spreadsheets/d/1eEAw0Y5YqIA2LYvkLPCw19bczcuou65ajlGMyyL20Ec/edit?gid=0#gid=0" target="_blank" class="bg-emerald-500 hover:bg-emerald-600 text-white font-bold py-2 px-6 rounded-lg transition flex items-center shadow-md">
                        <i class="fas fa-table mr-2"></i> Open Data
                    </a>
                    <button onclick="fetchDashboardData()" class="bg-blue-100 hover:bg-blue-200 text-blue-700 font-bold py-2 px-4 rounded-lg transition flex items-center">
                        <i class="fas fa-sync-alt mr-2"></i> Refresh Data
                    </button>
                    <button onclick="closeTeacherLogin()" class="bg-slate-200 hover:bg-slate-300 text-slate-700 font-bold py-2 px-4 rounded-lg transition flex items-center">
                        <i class="fas fa-times mr-2"></i> Close
                    </button>
                </div>
            </div>

            <!-- Dashboard Stats Cards -->
            <div id="dashboard-stats" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6 hidden">
                <div class="bg-white p-4 rounded-2xl shadow-sm border border-slate-200 text-center">
                    <p class="text-slate-500 font-bold uppercase tracking-wider text-[10px] mb-1">Total Submissions</p>
                    <p class="text-2xl font-extrabold text-blue-600" id="total-subs-display">--</p>
                </div>
                <div class="bg-white p-4 rounded-2xl shadow-sm border border-slate-200 text-center">
                    <p class="text-slate-500 font-bold uppercase tracking-wider text-[10px] mb-1">Avg FOMO</p>
                    <p class="text-2xl font-extrabold text-rose-600" id="avg-fomo-display">-- %</p>
                </div>
                <div class="bg-white p-4 rounded-2xl shadow-sm border border-slate-200 text-center">
                    <p class="text-slate-500 font-bold uppercase tracking-wider text-[10px] mb-1">Avg Disconnect</p>
                    <p class="text-2xl font-extrabold text-teal-600" id="avg-disconnect-display">-- Mins</p>
                </div>
                <div class="bg-white p-4 rounded-2xl shadow-sm border border-slate-200 text-center">
                    <p class="text-slate-500 font-bold uppercase tracking-wider text-[10px] mb-1">Avg RP Score</p>
                    <p class="text-2xl font-extrabold text-amber-500" id="avg-rpscore-display">--</p>
                </div>
                <div class="bg-white p-4 rounded-2xl shadow-sm border border-slate-200 text-center">
                    <p class="text-slate-500 font-bold uppercase tracking-wider text-[10px] mb-1">Avg Dialogue Tries</p>
                    <p class="text-2xl font-extrabold text-indigo-600" id="avg-tries-display">--</p>
                </div>
                <div class="bg-white p-4 rounded-2xl shadow-sm border border-slate-200 text-center">
                    <p class="text-slate-500 font-bold uppercase tracking-wider text-[10px] mb-1">Avg Pace (1-5)</p>
                    <p class="text-2xl font-extrabold text-purple-600" id="avg-pace-display">--</p>
                </div>
            </div>

            <div id="dashboard-loading" class="text-center py-20">
                <i class="fas fa-spinner fa-spin fa-3x text-blue-500 mb-4"></i>
                <p class="text-slate-500 font-bold text-xl">Fetching live data from Google Sheet...</p>
                <div class="bg-orange-50 border border-orange-200 p-4 rounded-lg mt-4 max-w-lg mx-auto text-left">
                    <p class="text-orange-800 text-sm font-bold mb-2"><i class="fas fa-info-circle mr-1"></i> Google Sites 沙盒限制提示</p>
                    <p class="text-orange-700 text-xs">若儀表板持續無法讀取資料,是因為協作平台封鎖了跨網域讀取。請直接點擊上方綠色的 <strong>[Open Data]</strong> 按鈕前往試算表查看。</p>
                </div>
            </div>

            <div id="dashboard-content" class="hidden">
                <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
                    <div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-200">
                        <h3 class="font-bold text-lg text-slate-700 mb-4 border-b pb-2"><i class="fas fa-mountain text-rose-500 mr-2"></i>Most Challenging</h3>
                        <div class="relative h-64 w-full flex justify-center"><canvas id="hardestChart"></canvas></div>
                    </div>
                    <div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-200">
                        <h3 class="font-bold text-lg text-slate-700 mb-4 border-b pb-2"><i class="fas fa-trophy text-emerald-500 mr-2"></i>Most Proud Of</h3>
                        <div class="relative h-64 w-full flex justify-center"><canvas id="proudestChart"></canvas></div>
                    </div>
                    <div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-200">
                        <h3 class="font-bold text-lg text-slate-700 mb-4 border-b pb-2"><i class="fas fa-tachometer-alt text-purple-500 mr-2"></i>Lesson Pace</h3>
                        <div class="relative h-64 w-full"><canvas id="paceChart"></canvas></div>
                    </div>
                </div>

                <div class="grid grid-cols-1 gap-6">
                    <div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-200 h-[600px] flex flex-col">
                        <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-4 border-b pb-2 gap-3">
                            <h3 class="font-bold text-lg text-slate-700"><i class="fas fa-comments text-amber-500 mr-2"></i>Text Showcase</h3>
                            <select id="text-showcase-filter" onchange="filterShowcase()" class="border border-slate-300 rounded-lg text-sm p-2 text-slate-700 bg-slate-50 focus:outline-none focus:ring-2 focus:ring-blue-400 font-bold">
                                <optgroup label="Part 1: Self Awareness">
                                    <option value="fomo">FOMO Level (%)</option>
                                    <option value="goal">Disconnect Goal</option>
                                </optgroup>
                                <optgroup label="Part 2: Dialogue">
                                    <option value="rp-score">RP Partner & Score</option>
                                </optgroup>
                                <optgroup label="Part 3: Deep Talk">
                                    <option value="dt-q1">Q1: First Thing</option>
                                    <option value="dt-q2">Q2: Phubbing</option>
                                    <option value="dt-q3">Q3: Pros & Cons</option>
                                    <option value="dt-q4">Q4: Rule Setup</option>
                                </optgroup>
                                <optgroup label="Part 5: Reflection">
                                    <option value="ref-hardest">Most Challenging</option>
                                    <option value="ref-proudest">Most Proud Of</option>
                                    <option value="ref-learn">New Learning</option>
                                </optgroup>
                            </select>
                        </div>
                        <div id="showcase-container" class="flex-1 overflow-y-auto custom-scrollbar grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 pr-2 content-start">
                            <!-- Populated via JS -->
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div id="toast-container" class="fixed top-16 left-1/2 transform -translate-x-1/2 z-[200] flex flex-col gap-2 pointer-events-none"></div>
    <div id="global-progress-container"><div id="global-progress-bar"></div></div>

    <div class="max-w-5xl mx-auto my-4 sm:my-6 w-full flex-grow px-2 sm:px-0 relative">
        <div class="bg-white rounded-2xl shadow-xl overflow-hidden border border-slate-100 relative">
            <header class="bg-gradient-to-r from-orange-500 to-violet-600 text-white p-6 sm:p-8 text-center relative overflow-hidden">
                <div class="absolute top-0 right-0 opacity-10 transform translate-x-4 -translate-y-4"><i class="fas fa-moon fa-10x"></i></div>
                <h1 class="text-3xl sm:text-4xl font-extrabold mb-2 relative z-10 tracking-tight">Goodbye, John</h1>
                <p class="text-orange-50 text-lg sm:text-xl relative z-10 font-medium">Bilingual Interactive Worksheet</p>
                
                <div class="text-indigo-50/90 text-sm mt-4 font-light relative z-10 space-y-1.5 bg-black/15 inline-block p-3 rounded-lg backdrop-blur-sm text-left max-w-2xl mx-auto shadow-inner border border-white/10">
                    <p class="font-medium"><i class="fas fa-pen-nib mr-2 text-amber-300"></i>教材編製:欣妤老師 (Sylvia)</p>
                    <p class="text-xs leading-relaxed opacity-80"><i class="fas fa-shield-alt mr-2 text-amber-300"></i>【版權與免責聲明】 本教材課文內容取自龍騰普高版 Book 1 Lesson 2,僅供教學使用,非商業用途。</p>
                </div>
            </header>

            <div class="flex flex-wrap justify-center gap-1.5 p-2 bg-slate-50 border-b border-slate-200 sticky top-0 z-40 shadow-sm">
                <button id="tab-btn-vocab" onclick="switchTab('vocab')" class="tab-active py-2 px-3 rounded-lg text-xs sm:text-sm transition flex items-center justify-center whitespace-nowrap grow sm:grow-0"><i class="fas fa-spell-check mr-1.5"></i> 1. Words</button>
                <button id="tab-btn-reading" onclick="switchTab('reading')" class="tab-inactive py-2 px-3 rounded-lg text-xs sm:text-sm transition flex items-center justify-center whitespace-nowrap grow sm:grow-0"><i class="fas fa-book-open mr-1.5"></i> 2. Read</button>
                <button id="tab-btn-organizer" onclick="switchTab('organizer')" class="tab-inactive py-2 px-3 rounded-lg text-xs sm:text-sm transition flex items-center justify-center whitespace-nowrap grow sm:grow-0"><i class="fas fa-sitemap mr-1.5"></i> 3. G.O.</button>
                <button id="tab-btn-worksheet" onclick="switchTab('worksheet')" class="tab-inactive py-2 px-3 rounded-lg text-xs sm:text-sm transition flex items-center justify-center whitespace-nowrap grow sm:grow-0"><i class="fas fa-comments mr-1.5"></i> 4. Dialogue</button>
                <button id="tab-btn-ai" onclick="switchTab('ai')" class="tab-inactive py-2 px-3 rounded-lg text-xs sm:text-sm transition flex items-center justify-center whitespace-nowrap grow sm:grow-0"><i class="fas fa-magic mr-1.5"></i> 5. AI</button>
                <button id="tab-btn-discussion" onclick="switchTab('discussion')" class="tab-inactive py-2 px-3 rounded-lg text-xs sm:text-sm transition flex items-center justify-center whitespace-nowrap grow sm:grow-0"><i class="fas fa-users mr-1.5"></i> 6. Talk</button>
                <button id="tab-btn-reflection" onclick="switchTab('reflection')" class="tab-inactive py-2 px-3 rounded-lg text-xs sm:text-sm transition flex items-center justify-center whitespace-nowrap grow sm:grow-0"><i class="fas fa-lightbulb mr-1.5"></i> 7. Reflect</button>
                <button id="tab-btn-summary" onclick="switchTab('summary')" class="tab-inactive py-2 px-3 rounded-lg text-xs sm:text-sm transition flex items-center justify-center whitespace-nowrap border border-emerald-200 bg-emerald-50 hover:bg-emerald-100 grow sm:grow-0">
                    <i class="fas fa-cloud-upload-alt mr-1.5 text-emerald-600"></i> <span class="text-emerald-700 font-bold">8. Submit</span>
                </button>
            </div>

            <div class="p-4 sm:p-8 relative bg-slate-50/50 min-h-[500px]">
                
                <!-- TAB 1: Vocab -->
                <div id="tab-vocab" class="block animate-fade-in">
                    <div class="max-w-4xl mx-auto">
                        <header class="text-center mb-8">
                            <h2 class="text-2xl sm:text-3xl font-bold text-violet-700 mb-2"><i class="fas fa-bolt text-amber-500 mr-2"></i>Word Power</h2>
                            <p class="text-slate-500">Listen, learn, and prepare for the reading.</p>
                        </header>
                        <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" id="vocab-container"></div>
                        <div class="mt-10 text-center border-t border-slate-200 pt-6">
                            <button onclick="switchTab('reading')" class="bg-orange-500 hover:bg-orange-600 text-white font-bold py-3 px-8 rounded-full shadow-lg transition transform hover:-translate-y-1">Next: Start Reading <i class="fas fa-arrow-right ml-2"></i></button>
                        </div>
                    </div>
                </div>

                <!-- TAB 2: Reading -->
                <div id="tab-reading" class="hidden animate-fade-in">
                    <div class="max-w-4xl mx-auto">
                        <div class="text-center mb-6 pb-6 border-b border-slate-200">
                            <h2 class="text-2xl sm:text-3xl font-bold text-slate-800 mb-2 font-serif">Goodbye, John</h2>
                            <p class="text-base text-slate-600 font-medium mb-1">Written by JJ Liu and Nick Kembel</p>
                            <div class="flex flex-wrap justify-center gap-3 items-center mt-4">
                                <div class="inline-flex items-center text-sm text-violet-600 bg-violet-50 px-3 py-1 rounded-full border border-violet-100">
                                    <i class="fas fa-info-circle mr-2"></i> Click <i class="fas fa-globe-americas mx-1"></i> for translation.
                                </div>
                                <button id="play-reading-btn" onclick="toggleReadingAudio()" class="inline-flex items-center text-sm text-white bg-amber-500 hover:bg-amber-600 px-4 py-1.5 rounded-full shadow transition-colors font-bold focus:outline-none">
                                    <i class="fas fa-play mr-2"></i> Play Reading
                                </button>
                            </div>
                        </div>

                        <div class="space-y-6 text-lg text-slate-700 leading-relaxed font-serif" id="reading-content-area">
                            <div class="relative group p-4 sm:p-6 bg-white rounded-xl shadow-sm border border-slate-100 hover:border-violet-200 transition duration-300">
                                <button onclick="toggleScaffold('trans-1')" class="absolute top-4 right-4 text-slate-300 hover:text-violet-600 transition-colors bg-white rounded-full p-2 hover:bg-violet-50 focus:outline-none"><i class="fas fa-globe-americas text-xl"></i></button>
                                <p class="pr-10" id="read-p-0">Dear John, <br><br><span class="text-3xl font-bold text-violet-600 float-left mr-2 mt-[-4px]">I</span>t is not easy to tell you this, but I still need to say it. I'm leaving you. Our relationship has been mostly satisfying, but I'm afraid it's over.</p>
                                <div id="trans-1" class="scaffold-content border-t border-slate-100 text-base text-slate-600"><div class="flex items-start bg-slate-50 p-3 rounded-lg"><i class="fas fa-language text-violet-500 mt-1 mr-2"></i><p>親愛的約翰:要對你說這些並不容易,但我還是必須說出來。我要離開你了。我們之間的關係大部分時間都令人滿意,但恐怕這一切已經結束了。</p></div></div>
                            </div>
                            <div class="relative group p-4 sm:p-6 bg-white rounded-xl shadow-sm border border-slate-100 hover:border-violet-200 transition duration-300">
                                <button onclick="toggleScaffold('trans-2')" class="absolute top-4 right-4 text-slate-300 hover:text-violet-600 transition-colors bg-white rounded-full p-2 hover:bg-violet-50 focus:outline-none"><i class="fas fa-globe-americas text-xl"></i></button>
                                <p class="pr-10" id="read-p-1">Leaving you really breaks my heart because we've had some great times together. When you're around, I am never bored. There is always something to do. You inspire me to connect with others and to communicate better. I can ask you anything, and you will probably help me find the answer. You've introduced me to so much—new music, new games, new knowledge, new friends, and more.</p>
                                <div id="trans-2" class="scaffold-content border-t border-slate-100 text-base text-slate-600"><div class="flex items-start bg-slate-50 p-3 rounded-lg"><i class="fas fa-language text-violet-500 mt-1 mr-2"></i><p>離開你真的很讓我心碎,因為我們曾共度許多美好時光。有你在身邊時,我從不覺得無聊。總是有事情可以做。你激勵我去與他人連結、更好地溝通。我可以問你任何問題,而你通常都會幫我找到答案。你讓我接觸到了這麼多新事物——新音樂、新遊戲、新知識、新朋友等等。</p></div></div>
                            </div>
                            <div class="relative group p-4 sm:p-6 bg-white rounded-xl shadow-sm border border-slate-100 hover:border-violet-200 transition duration-300">
                                <button onclick="toggleScaffold('trans-3')" class="absolute top-4 right-4 text-slate-300 hover:text-violet-600 transition-colors bg-white rounded-full p-2 hover:bg-violet-50 focus:outline-none"><i class="fas fa-globe-americas text-xl"></i></button>
                                <p class="pr-10" id="read-p-2">In fact, things have been totally different since you came into my life. When I wake up, you are there to go through my friends' updates on Facebook and Instagram with me. Also, I check my LINE app again and again throughout the day. And when it doesn't show any new messages, I still refresh the screen, just in case I miss something. All this might sound romantic, but it is difficult for me to focus on school, and my grades are getting worse!</p>
                                <div id="trans-3" class="scaffold-content border-t border-slate-100 text-base text-slate-600"><div class="flex items-start bg-slate-50 p-3 rounded-lg"><i class="fas fa-language text-violet-500 mt-1 mr-2"></i><p>事實上,自從你走進我的生命,一切都完全不同了。當我醒來時,你在那裡陪我瀏覽朋友們在臉書和IG上的最新動態。此外,我一整天都會不斷檢查我的 LINE。而當強它的畫面,以防萬一我錯過了什麼。這一切聽起來或許很浪漫,但我卻很難專心在課業上,而且我的成績越來越退步了!</p></div></div>
                            </div>
                            <div class="relative group p-4 sm:p-6 bg-white rounded-xl shadow-sm border border-slate-100 hover:border-violet-200 transition duration-300">
                                <button onclick="toggleScaffold('trans-4')" class="absolute top-4 right-4 text-slate-300 hover:text-violet-600 transition-colors bg-white rounded-full p-2 hover:bg-violet-50 focus:outline-none"><i class="fas fa-globe-americas text-xl"></i></button>
                                <p class="pr-10" id="read-p-3">In addition, having you around is really hurting my relationships with my friends and family. When my friends and I hang out, I turn to you for Instagram updates and ignore everyone else. My friends try to talk to me, but I don't even reply because I am too busy paying attention to you. I'm worried that they are going to stop inviting me out! I can't deny that you are always by my side, and I love it when we play games or respond to messages together. But doing this late into the night means I seldom speak to my family! That's really not a good way to build a relationship with anybody, and my parents are both angry and disappointed with me.</p>
                                <div id="trans-4" class="scaffold-content border-t border-slate-100 text-base text-slate-600"><div class="flex items-start bg-slate-50 p-3 rounded-lg"><i class="fas fa-language text-violet-500 mt-1 mr-2"></i><p>除此之外,有你在身邊真的傷害了我與朋友和家人的關係。當我和朋友出去玩時,我會轉向你查看 IG 動態,而忽略了其他人。我的朋友試圖跟我說話,但我甚至沒有回應,因為我太忙於關注你了。我很擔心他們以後不再約我出去了!我無法否認你總是在我身邊,我也很喜歡我們一起玩遊戲或回覆訊息的時光。但玩到深夜意味著我很少和家人說話!那真的不是一個與任何人建立關係的好方法,而且我的父母對我又生氣又失望。</p></div></div>
                            </div>
                            <div class="relative group p-4 sm:p-6 bg-white rounded-xl shadow-sm border border-slate-100 hover:border-violet-200 transition duration-300">
                                <button onclick="toggleScaffold('trans-5')" class="absolute top-4 right-4 text-slate-300 hover:text-violet-600 transition-colors bg-white rounded-full p-2 hover:bg-violet-50 focus:outline-none"><i class="fas fa-globe-americas text-xl"></i></button>
                                <p class="pr-10" id="read-p-4">Please don't take any of this personally. You're truly wonderful, and I'll miss you terribly. But right now, I really need to focus on my studies, spend more time with my friends, and become a real part of my family again.<br><br>I hope you understand. For the last time, goodbye....<br><br>Best wishes,<br>Sally</p>
                                <div id="trans-5" class="scaffold-content border-t border-slate-100 text-base text-slate-600"><div class="flex items-start bg-slate-50 p-3 rounded-lg"><i class="fas fa-language text-violet-500 mt-1 mr-2"></i><p>請不要認為這些是針對你個人的。你真的很棒,我也會非常想念你。但現在,我真的需要專注於學業,花更多時間和朋友在一起,並再次成為家庭中真正的一份子。希望你能諒解。最後一次,再見了....</p></div></div>
                            </div>
                        </div>

                        <div class="mt-8 text-center">
                            <button onclick="switchTab('organizer')" class="bg-orange-500 hover:bg-orange-600 text-white font-bold py-3 px-8 rounded-full shadow-lg transition transform hover:-translate-y-1">Next: See the G.O. <i class="fas fa-arrow-right ml-2"></i></button>
                        </div>
                    </div>
                </div>

                <!-- TAB 3: G.O. -->
                <div id="tab-organizer" class="hidden animate-fade-in">
                    <h2 class="text-2xl font-bold text-center text-violet-700 mb-8 flex items-center justify-center"><i class="fas fa-filter mr-2 text-orange-500"></i>Deconstructing the Relationship</h2>

                    <div class="flex flex-col items-center">
                        <!-- CCQ (Completely rebuilt with 3 explicit buttons for sandbox safety) -->
                        <div class="bg-violet-50 border-2 border-violet-400 rounded-xl p-5 w-full max-w-[480px] text-center shadow-md relative z-30">
                            <h3 class="font-bold text-xl text-violet-800 mb-3">Comprehension Check</h3>
                            <div class="text-violet-700 text-base sm:text-lg flex flex-col justify-center items-center gap-4 mb-2">
                                <div class="font-bold border-b border-violet-200 pb-2 w-full text-center">Who is 'John' in this letter?</div>
                                
                                <!-- Bulletproof Option Buttons -->
                                <div id="q1-options-container" class="flex flex-col gap-2 w-full max-w-[320px]">
                                    <button onclick="checkCCQ(1, false, this)" class="w-full bg-white text-violet-600 font-bold py-3 px-4 rounded-lg shadow-sm border-2 border-violet-300 hover:bg-violet-100 transition focus:outline-none text-left flex items-center justify-between">
                                        <span>1. A real human boy</span><i class="fas fa-circle opacity-20"></i>
                                    </button>
                                    <button onclick="checkCCQ(2, true, this)" class="w-full bg-white text-violet-600 font-bold py-3 px-4 rounded-lg shadow-sm border-2 border-violet-300 hover:bg-violet-100 transition focus:outline-none text-left flex items-center justify-between">
                                        <span>2. A smartphone</span><i class="fas fa-circle opacity-20"></i>
                                    </button>
                                    <button onclick="checkCCQ(3, false, this)" class="w-full bg-white text-violet-600 font-bold py-3 px-4 rounded-lg shadow-sm border-2 border-violet-300 hover:bg-violet-100 transition focus:outline-none text-left flex items-center justify-between">
                                        <span>3. A pet dog</span><i class="fas fa-circle opacity-20"></i>
                                    </button>
                                </div>
                                <div id="q1-success-badge" class="hidden font-bold text-2xl text-emerald-600 bg-emerald-100 px-6 py-3 rounded-xl border border-emerald-300 shadow-inner flex items-center">
                                    <i class="fas fa-check-circle mr-3"></i> A smartphone
                                </div>
                            </div>
                        </div>

                        <div class="w-1 h-8 bg-slate-300 relative z-20"></div><div class="w-0 h-0 border-l-[6px] border-r-[6px] border-l-transparent border-r-transparent border-t-[8px] border-t-slate-300 mb-3 relative z-20"></div>

                        <!-- Locked Section -->
                        <div id="tab3-locked-content" class="w-full relative transition-all duration-700">
                            
                            <div id="tab3-lock-overlay" class="absolute inset-0 z-50 flex flex-col items-center pt-20">
                                <div class="bg-slate-800/90 text-white px-6 py-3 rounded-full font-bold shadow-xl flex items-center text-lg animate-pulse">
                                    <i class="fas fa-lock mr-2 text-orange-400"></i> Answer the Check Question to Unlock
                                </div>
                            </div>

                            <div class="flex flex-col items-center">
                                <!-- Sorting Game: Drag & Drop Edition with Click-to-Move -->
                                <div class="w-full max-w-[700px] bg-white border-2 border-slate-200 rounded-xl p-4 sm:p-5 shadow-sm relative z-20 mb-6">
                                    <div class="flex justify-between items-center mb-4 border-b border-slate-100 pb-3">
                                        <div class="flex items-center gap-2">
                                            <h3 class="text-center font-bold text-slate-700 text-xl"><i class="fas fa-gamepad text-violet-500"></i> Heartbreak Sorting</h3>
                                        </div>
                                        <div class="text-right bg-slate-50 px-3 py-1.5 rounded-lg border border-slate-200">
                                            <span class="text-[10px] font-bold text-slate-500 uppercase tracking-wider block mb-0.5 leading-none">Your Score</span>
                                            <span id="sort-score-display" class="font-extrabold text-2xl text-emerald-600 leading-none">100</span>
                                        </div>
                                    </div>
                                    <div class="text-sm text-violet-600 bg-violet-50 p-3 rounded-lg mb-4 font-bold border border-violet-200">
                                        <i class="fas fa-hand-pointer mr-1"></i> 玩法:點擊句子讓它反藍,接著點擊下方的 Pros (好處) 或 Cons (壞處) 進行分類。<br>
                                        <span class="text-rose-600 text-xs">★ 若想修改,再次點擊句子即可退回。答錯每次扣 20 分!</span>
                                    </div>
                                    
                                    <div id="sort-bank" onclick="handleZoneClick('bank')" class="flex flex-wrap justify-center gap-2 mb-6 min-h-[70px] p-3 bg-slate-100 border border-slate-300 rounded-lg cursor-pointer">
                                        <!-- Draggable Items will be injected here via JS -->
                                    </div>

                                    <div class="flex flex-col sm:flex-row gap-4 mb-4">
                                        <div class="flex-1 flex flex-col">
                                            <h3 class="font-bold text-emerald-800 bg-emerald-100 rounded-t-lg py-2 text-center border-2 border-emerald-300 border-b-0 m-0"><i class="fas fa-heart mr-2"></i>Pros (Great Times)</h3>
                                            <div id="zone-pro" onclick="handleZoneClick('pro')" class="drop-zone flex-1 bg-emerald-50/50 border-2 border-dashed border-emerald-300 rounded-b-lg p-3 flex flex-col gap-2 m-0 cursor-pointer hover:bg-emerald-50"></div>
                                        </div>
                                        <div class="flex-1 flex flex-col">
                                            <h3 class="font-bold text-rose-800 bg-rose-100 rounded-t-lg py-2 text-center border-2 border-rose-300 border-b-0 m-0"><i class="fas fa-heart-broken mr-2"></i>Cons (The Reality)</h3>
                                            <div id="zone-con" onclick="handleZoneClick('con')" class="drop-zone flex-1 bg-rose-50/50 border-2 border-dashed border-rose-300 rounded-b-lg p-3 flex flex-col gap-2 m-0 cursor-pointer hover:bg-rose-50"></div>
                                        </div>
                                    </div>
                                    
                                    <div class="text-center">
                                        <button id="sort-check-btn" onclick="checkDragSortGame()" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2.5 px-6 rounded-full shadow transition hover:-translate-y-0.5"><i class="fas fa-check-circle mr-2"></i>Check Answers (-20 per mistake)</button>
                                    </div>
                                    <div id="sort-error-msg" class="hidden mt-3 text-rose-600 font-bold text-center bg-rose-50 p-3 rounded-lg border border-rose-200 shadow-sm animate-fade-in"></div>
                                    <div id="sort-success-msg" class="hidden mt-3 p-3 bg-emerald-100 text-emerald-800 font-bold rounded-lg text-center"><i class="fas fa-check-circle mr-2"></i> All Correct! See your final score above!</div>
                                </div>

                                <!-- Locked Sliders Wrapper -->
                                <div id="sliders-locked-wrapper" class="w-full flex flex-col items-center transition-all duration-700 opacity-30 blur-sm pointer-events-none">
                                    <!-- FOMO Slider -->
                                    <div class="w-full max-w-[700px] bg-white border-2 border-slate-200 rounded-xl p-5 shadow-sm relative z-20">
                                        <div class="bg-slate-50 p-4 rounded-lg border border-slate-200">
                                            <h4 class="text-center font-bold text-slate-700 mb-3"><i class="fas fa-battery-quarter text-orange-500 mr-2"></i>FOMO Index (Fear Of Missing Out)</h4>
                                            <div class="mb-2">
                                                <div class="flex justify-between text-sm font-bold mb-1"><span class="text-slate-600">Your phone battery is at 1% and you don't have a charger. How nervous are you? <span id="fomo-val" class="text-violet-600">10</span>%</span></div>
                                                <input type="range" min="0" max="100" value="10" oninput="updateFOMOReaction(this.value)" class="w-full h-3 bg-violet-200 rounded-lg appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-violet-400">
                                                <div class="flex justify-between text-xs text-slate-400 mt-1"><span>0% (Zen Master)</span><span>100% (Total Panic)</span></div>
                                            </div>
                                            <div class="mt-4">
                                                <div class="flex justify-between text-sm font-bold mb-1"><span class="text-slate-600">Your Mental Status:</span></div>
                                                <div class="w-full h-5 bg-slate-200 rounded-full overflow-hidden relative">
                                                    <div id="anxiety-bar" class="h-full bg-emerald-500 transition-all duration-300 w-full flex items-center justify-center"></div>
                                                </div>
                                                <div id="overwhelm-alert" class="text-center text-emerald-600 font-extrabold text-lg mt-2"><i class="fas fa-check-circle mr-1"></i> I CAN SURVIVE <i class="fas fa-check-circle ml-1"></i></div>
                                            </div>
                                        </div>
                                    </div>

                                    <div class="w-1 h-8 bg-slate-300 mt-3"></div><div class="w-0 h-0 border-l-[6px] border-r-[6px] border-l-transparent border-r-transparent border-t-[8px] border-t-slate-300 mb-3"></div>

                                    <!-- Disconnect Challenge Slider -->
                                    <div class="bg-orange-50 border-2 border-orange-300 rounded-xl p-5 w-full max-w-[700px] shadow text-center relative overflow-hidden">
                                        <div class="flex justify-center items-center gap-2 border-b border-orange-200 pb-2 mb-3 relative z-10">
                                            <h3 class="font-bold text-orange-800 text-xl"><i class="fas fa-shield-alt mr-2"></i>Smart Strategy: The Disconnect Challenge</h3>
                                        </div>
                                        <p class="text-slate-700 text-sm mb-4 relative z-10 font-medium">How long can you focus on your homework WITHOUT checking LINE or Instagram? Drag the slider to set your goal.</p>
                                        
                                        <div class="bg-white p-5 rounded-xl border border-orange-200 shadow-sm relative z-10">
                                            <div class="flex justify-between items-center mb-2">
                                                <span class="font-bold text-slate-500">My Goal:</span>
                                                <span id="disconnect-time-val" class="text-2xl font-extrabold text-rose-500">1 Min</span>
                                            </div>
                                            <input type="range" id="disconnect-slider" min="1" max="120" value="1" oninput="updateDisconnectEffect(this.value)" class="w-full h-4 bg-slate-200 rounded-lg appearance-none cursor-pointer focus:outline-none mb-4">
                                            <div class="p-3 rounded-lg bg-rose-50 border border-rose-200 transition-colors duration-500" id="disconnect-status-box">
                                                <div class="flex items-center justify-center gap-3">
                                                    <span id="disconnect-emoji" class="text-3xl animate-pulse">😰</span>
                                                    <div class="text-left">
                                                        <p class="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Focus Level</p>
                                                        <p id="disconnect-status-text" class="font-bold text-rose-600 text-lg">Heavy Addict</p>
                                                    </div>
                                                </div>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>

                    </div>
                    <div class="mt-10 text-center">
                        <button onclick="switchTab('worksheet')" class="bg-orange-500 hover:bg-orange-600 text-white font-bold py-3 px-8 rounded-full shadow-lg transition transform hover:-translate-y-1">Next: Dialogue Practice <i class="fas fa-arrow-right ml-2"></i></button>
                    </div>
                </div>

                <!-- TAB 4: Dialogue Worksheet -->
                <div id="tab-worksheet" class="hidden animate-fade-in">
                    <div class="bg-violet-50 border-l-4 border-violet-400 p-4 rounded-r-xl shadow-sm mb-4 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
                        <div class="flex items-start gap-3">
                            <div class="bg-violet-100 p-2 rounded-full text-violet-600 shrink-0"><i class="fa fa-link"></i></div>
                            <div>
                                <h3 class="font-bold text-violet-800 text-sm uppercase tracking-wider mb-1">Context Transition</h3>
                                <p class="text-slate-700 font-medium">Now, let's see how two students, Leo and Emma, discuss their screen time and phone habits.</p>
                            </div>
                        </div>
                    </div>

                    <div id="worksheet-section" class="relative">
                        <!-- Instructions (No sticky word bank as requested) -->
                        <div class="bg-white border-2 border-violet-200 rounded-xl p-4 mb-5 shadow-sm">
                            <div class="flex items-center gap-3">
                                <div class="w-8 h-8 bg-violet-100 text-violet-600 rounded-full flex items-center justify-center text-sm shrink-0 shadow-sm"><i class="fas fa-hand-pointer animate-bounce mt-1"></i></div>
                                <div>
                                    <h4 class="font-bold text-violet-800 text-sm uppercase tracking-wider">How to play</h4>
                                    <p class="text-xs text-slate-500">Tap any empty blank below to select a word from the popup menu.</p>
                                </div>
                            </div>
                        </div>

                        <!-- Simplified clean background for reliable screenshot rendering -->
                        <div class="bg-slate-50 p-4 sm:p-6 rounded-2xl shadow-inner mb-4 border border-slate-300 min-h-[400px] relative" id="interactive-dialogue">
                            <!-- Populated by JS -->
                        </div>

                        <!-- Floating Word Selector Popup (Fixed Position for Google Sites) -->
                        <div id="word-selector-popup" class="hidden fixed z-[9999] bg-white/95 backdrop-blur-md border-2 border-violet-400 shadow-[0_10px_40px_-10px_rgba(0,0,0,0.3)] rounded-xl p-3 flex flex-wrap gap-2 max-w-[300px] transition-opacity">
                            <!-- Populated by JS -->
                        </div>

                        <div class="flex flex-col items-center border-t border-slate-200 pt-6 pb-4">
                            <div class="flex flex-col sm:flex-row gap-4 w-full sm:w-auto justify-center">
                                <button onclick="checkAnswers()" class="bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-3 sm:py-4 px-8 sm:px-12 rounded-full shadow-lg transition transform hover:-translate-y-1 text-lg flex items-center justify-center">
                                    <i class="fas fa-paper-plane mr-3"></i> Submit Answers
                                </button>
                                <button id="reveal-ans-btn" onclick="revealAnswers()" class="hidden bg-rose-500 hover:bg-rose-600 text-white font-bold py-3 sm:py-4 px-8 rounded-full shadow-lg transition transform hover:-translate-y-1 text-base flex items-center justify-center">
                                    <i class="fas fa-eye mr-2"></i> Give Up & Show Answers
                                </button>
                            </div>
                            <div id="score-display" class="mt-4 text-lg font-bold hidden px-6 py-3 rounded-lg text-center w-full sm:w-auto transition-all"></div>
                        </div>
                    </div>

                    <div id="unlocked-content" class="relative opacity-30 blur-sm pointer-events-none select-none transition-all duration-500 mt-4">
                        <div id="dialogue-lock-overlay" class="absolute inset-0 z-50 flex flex-col items-center justify-center">
                            <div class="bg-slate-800/90 text-white px-6 py-3 rounded-full font-bold shadow-xl flex items-center text-lg">
                                <i class="fas fa-lock mr-2 text-orange-400"></i> Complete Dialogue to Unlock
                            </div>
                        </div>
                        <div class="bg-white border-2 border-orange-300 rounded-2xl p-1 shadow-xl mb-8 overflow-hidden relative">
                            <div class="bg-orange-100 text-orange-800 p-4 border-b border-orange-200 flex flex-col sm:flex-row justify-between items-center gap-4">
                                <div><h3 class="text-xl font-bold flex items-center"><i class="fas fa-unlock-alt text-orange-500 mr-2"></i> Audio & Roleplay Unlocked!</h3></div>
                                <button id="play-all-btn" onclick="togglePlayAll()" class="w-full sm:w-auto bg-orange-500 hover:bg-orange-600 text-white font-bold py-2 px-5 rounded-full shadow transition flex items-center justify-center focus:outline-none">
                                    <i class="fas fa-play mr-2"></i> Play Scenario
                                </button>
                            </div>
                            <div id="audio-dialogue-container" class="bg-slate-50 p-4 sm:p-6 border-b border-orange-200"></div>
                            
                            <div class="p-6 bg-white">
                                <h4 class="font-bold text-orange-700 text-lg mb-3 flex items-center"><i class="fas fa-user-check mr-2"></i> Roleplay Self-Check</h4>
                                <div class="space-y-4">
                                    <label class="flex items-center space-x-3 cursor-pointer bg-slate-50 p-3 rounded-lg border border-slate-200 hover:bg-orange-50 transition">
                                        <input type="checkbox" id="roleplay-checkbox" onchange="saveAndProgress()" class="form-checkbox h-5 w-5 text-orange-600 rounded border-slate-300 focus:ring-orange-500">
                                        <span class="text-slate-700 font-medium">I have practiced reading the dialogue with my partner.</span>
                                    </label>
                                    <div class="flex flex-col sm:flex-row gap-4 items-start sm:items-center bg-slate-50 p-3 rounded-lg border border-slate-200">
                                        <label for="partner-name" class="font-medium text-slate-700 whitespace-nowrap">My Partner's Name:</label>
                                        <input type="text" id="partner-name" maxlength="50" oninput="saveAndProgress()" placeholder="Enter name..." class="w-full sm:w-auto px-3 py-1.5 border border-slate-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-400">
                                    </div>
                                    <div class="bg-slate-50 p-3 rounded-lg border border-slate-200">
                                        <label class="font-medium text-slate-700 mb-2 block">My Self-Evaluation Score: <span id="rp-score-val" class="font-bold text-orange-600">80</span>/100</label>
                                        <input type="range" id="roleplay-slider" min="0" max="100" value="80" oninput="document.getElementById('rp-score-val').innerText=this.value; saveAndProgress();" class="w-full h-2 bg-orange-200 rounded-lg appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-orange-400">
                                    </div>
                                </div>
                            </div>
                        </div>
                        <div class="text-center mt-8">
                            <button onclick="switchTab('ai')" class="bg-fuchsia-600 hover:bg-fuchsia-700 text-white font-bold py-3 px-8 rounded-full shadow-lg transition transform hover:-translate-y-1 text-lg">Next: AI Whisperer <i class="fas fa-magic ml-2"></i></button>
                        </div>
                    </div>
                </div>

                <!-- TAB 5: AI Whisperer -->
                <div id="tab-ai" class="hidden animate-fade-in">
                    <div class="max-w-4xl mx-auto">
                        <header class="text-center mb-8">
                            <h2 class="text-3xl font-bold text-slate-800 mb-2"><i class="fas fa-magic text-fuchsia-500 mr-2"></i>AI Whisperer</h2>
                            <p class="text-slate-500 text-lg">Visualizing the Addiction (Group Challenge)</p>
                        </header>
                        <div class="bg-white border-2 border-fuchsia-100 rounded-2xl p-6 shadow-sm mb-8">
                            <div class="text-center mb-6">
                                <p class="text-slate-700 text-lg font-medium">In your groups of 4, split into <strong class="text-fuchsia-700">Pair A (Describers)</strong> and <strong class="text-violet-700">Pair B (Generators)</strong>.</p>
                            </div>
                            <div class="bg-violet-50 border-2 border-violet-200 rounded-xl p-6 mb-8 shadow-sm">
                                <h4 class="font-bold text-violet-800 text-xl mb-4 flex items-center">
                                    <i class="fas fa-tasks mr-2"></i>Mission Check (ICQ)
                                </h4>
                                <p class="text-slate-600 text-sm mb-5">Answer correctly to unlock the Tools.</p>
                                <div class="space-y-6">
                                    <div class="bg-white p-4 rounded-lg border border-violet-100 shadow-sm">
                                        <p class="text-slate-700 font-bold mb-3">1. Can Pair A show the secret picture to Pair B?</p>
                                        <div class="flex flex-wrap gap-3">
                                            <button onclick="checkICQ(1, false, this)" class="icq-btn-1 bg-slate-100 border border-slate-300 px-5 py-2 rounded-lg hover:bg-slate-200 transition font-bold text-slate-700">Yes</button>
                                            <button onclick="checkICQ(1, true, this)" class="icq-btn-1 bg-slate-100 border border-slate-300 px-5 py-2 rounded-lg hover:bg-slate-200 transition font-bold text-slate-700">No</button>
                                        </div>
                                    </div>
                                    <div class="bg-white p-4 rounded-lg border border-violet-100 shadow-sm">
                                        <p class="text-slate-700 font-bold mb-3">2. What language must Pair B use to type into the AI tool?</p>
                                        <div class="flex flex-wrap gap-3">
                                            <button onclick="checkICQ(2, false, this)" class="icq-btn-2 bg-slate-100 border border-slate-300 px-5 py-2 rounded-lg hover:bg-slate-200 transition font-bold text-slate-700">Chinese</button>
                                            <button onclick="checkICQ(2, true, this)" class="icq-btn-2 bg-slate-100 border border-slate-300 px-5 py-2 rounded-lg hover:bg-slate-200 transition font-bold text-slate-700">English</button>
                                        </div>
                                    </div>
                                </div>
                                <div id="icq-success-msg" class="hidden mt-6 p-4 bg-emerald-100 text-emerald-800 border border-emerald-300 rounded-lg font-bold text-center animate-fade-in flex items-center justify-center text-lg">
                                    <i class="fa fa-unlock-alt mr-3 text-emerald-600"></i> AI Tools Unlocked! Scroll down.
                                </div>
                            </div>
                            
                            <div id="ai-tools-section" class="relative opacity-30 blur-sm pointer-events-none select-none transition-all duration-500 animate-fade-in">
                                <div id="ai-lock-overlay" class="absolute inset-0 z-50 flex flex-col items-center justify-center">
                                    <div class="bg-slate-800/90 text-white px-6 py-3 rounded-full font-bold shadow-xl flex items-center text-lg"><i class="fa fa-lock mr-2 text-orange-400"></i> Pass Mission Check to Unlock</div>
                                </div>
                                <div class="bg-white border-2 border-fuchsia-100 rounded-2xl p-6 shadow-sm mb-6">
                                    <h4 class="font-bold text-fuchsia-800 text-lg mb-4 flex items-center border-b border-fuchsia-50 pb-2">
                                        <i class="fas fa-link mr-2 text-fuchsia-500"></i> Free AI Tools (No Login)
                                    </h4>
                                    <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
                                        <a href="https://www.craiyon.com/" target="_blank" class="w-full bg-slate-50 hover:bg-fuchsia-50 text-slate-700 border border-slate-200 hover:border-fuchsia-300 font-bold py-4 px-4 rounded-xl shadow-sm transition flex flex-col items-center justify-center group text-center">
                                            <i class="fas fa-paint-brush text-orange-500 text-2xl mb-2 group-hover:scale-110 transition-transform"></i><span>1. Craiyon</span>
                                        </a>
                                        <a href="https://deepai.org/machine-learning-model/text2image" target="_blank" class="w-full bg-slate-50 hover:bg-fuchsia-50 text-slate-700 border border-slate-200 hover:border-fuchsia-300 font-bold py-4 px-4 rounded-xl shadow-sm transition flex flex-col items-center justify-center group text-center">
                                            <i class="fas fa-brain text-blue-500 text-2xl mb-2 group-hover:scale-110 transition-transform"></i><span>2. DeepAI</span>
                                        </a>
                                        <a href="https://perchance.org/ai-text-to-image-generator" target="_blank" class="w-full bg-slate-50 hover:bg-fuchsia-50 text-slate-700 border border-slate-200 hover:border-fuchsia-300 font-bold py-4 px-4 rounded-xl shadow-sm transition flex flex-col items-center justify-center group text-center">
                                            <i class="fas fa-bolt text-yellow-500 text-2xl mb-2 group-hover:scale-110 transition-transform"></i><span>3. Perchance</span>
                                        </a>
                                    </div>
                                </div>
                                <div class="bg-fuchsia-50 p-4 rounded-xl border border-fuchsia-200 relative mb-6">
                                    <label class="block text-fuchsia-800 font-bold mb-2"><i class="fas fa-keyboard mr-2"></i>What prompt did your group use?</label>
                                    <textarea id="ai-prompt-input" maxlength="250" oninput="saveAndProgress()" class="w-full p-3 rounded-lg border border-fuchsia-200 focus:outline-none focus:ring-2 focus:ring-fuchsia-400 text-slate-700 text-sm resize-none" rows="2" placeholder="Example: A student putting a phone in a drawer... (Max 50 words)"></textarea>
                                </div>
                                <div class="text-center bg-slate-50 p-5 rounded-xl border border-slate-200">
                                    <p class="text-slate-600 mb-3 font-bold">Done? Upload your image to Padlet!</p>
                                    <a href="https://padlet.com/hfsh/ai-whisperer_goodbye-john-s0jok9iluycrmd5i" target="_blank" class="inline-flex items-center justify-center bg-rose-500 hover:bg-rose-600 text-white font-bold py-2.5 px-6 rounded-full shadow transition hover:scale-105">
                                        <i class="fas fa-upload mr-2"></i> Open Padlet
                                    </a>
                                </div>
                            </div>
                        </div>
                        <div class="mt-8 text-center">
                            <button onclick="switchTab('discussion')" class="bg-orange-600 hover:bg-orange-700 text-white font-bold py-3 px-8 rounded-full shadow-lg transition transform hover:-translate-y-1">Next: Deep Talk <i class="fas fa-users ml-2"></i></button>
                        </div>
                    </div>
                </div>

                <!-- TAB 6: Deep Talk -->
                <div id="tab-discussion" class="hidden animate-fade-in">
                    <header class="text-center mb-8">
                        <h2 class="text-3xl font-bold text-slate-800 mb-2"><i class="fas fa-comments text-orange-500 mr-2"></i>Deep Talk</h2>
                        <p class="text-slate-500 text-lg font-bold text-rose-500 bg-rose-50 inline-block px-4 py-1 rounded-full border border-rose-200 shadow-sm mt-2">Choose AT LEAST ONE question to answer!</p>
                    </header>
                    <div class="space-y-6" id="discussion-container"></div>
                    <div class="mt-10 text-center border-t border-slate-200 pt-8">
                        <button onclick="switchTab('reflection')" class="bg-violet-600 hover:bg-violet-700 text-white font-bold py-3 px-8 rounded-full shadow-lg transition transform hover:-translate-y-1 text-lg">Next: Self-Reflection <i class="fas fa-lightbulb ml-2"></i></button>
                    </div>
                </div>

                <!-- TAB 7: Reflection -->
                <div id="tab-reflection" class="hidden animate-fade-in">
                    <div class="max-w-2xl mx-auto">
                        <header class="text-center mb-8">
                            <h2 class="text-3xl font-bold text-slate-800 mb-2"><i class="fas fa-lightbulb text-amber-500 mr-2"></i>Self-Reflection</h2>
                            <p class="text-slate-500 text-lg">Check your learning before you submit.</p>
                        </header>
                        <div class="bg-white rounded-2xl shadow-sm border-2 border-amber-100 p-6 sm:p-8 space-y-6">
                            <div>
                                <label class="block text-slate-700 font-bold mb-2">1. Which part of today's lesson was the <span class="text-rose-500">most challenging</span>?</label>
                                <select id="ref-hardest" onchange="saveAndProgress()" class="w-full p-3 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-400 text-slate-700">
                                    <option value="">-- Please select --</option>
                                    <option value="Reading the letter">Reading Sally's letter</option>
                                    <option value="Sorting Pros & Cons">Sorting Pros & Cons (Game)</option>
                                    <option value="Facing the FOMO truth">Facing the FOMO truth (Slider)</option>
                                    <option value="Completing the dialogue">Completing the dialogue</option>
                                    <option value="Generating AI Image">Generating AI Image</option>
                                    <option value="Answering Deep Talk">Answering Deep Talk</option>
                                </select>
                            </div>
                            <div>
                                <label class="block text-slate-700 font-bold mb-2">2. Which part gave you the <span class="text-emerald-500">most sense of achievement</span>?</label>
                                <select id="ref-proudest" onchange="saveAndProgress()" class="w-full p-3 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-amber-400 text-slate-700">
                                    <option value="">-- Please select --</option>
                                    <option value="Understanding the text">Understanding the text</option>
                                    <option value="Winning the Sorting Game">Winning the Sorting Game</option>
                                    <option value="Getting the dialogue correct">Getting the dialogue blanks correct</option>
                                    <option value="Generating a cool AI image">Generating a cool AI image</option>
                                    <option value="Sharing my thoughts in Deep Talk">Sharing my thoughts in Deep Talk</option>
                                </select>
                            </div>
                            <div>
                                <label class="block text-slate-700 font-bold mb-2">3. What is <span class="text-violet-500">one new thing</span> you learned today?</label>
                                <textarea id="ref-learn" maxlength="250" oninput="saveAndProgress()" class="w-full p-3 rounded-lg border border-slate-300 focus:outline-none focus:ring-2 focus:ring-amber-400 text-slate-700 text-sm resize-none" rows="2" placeholder="Type here... (Max 50 words)"></textarea>
                            </div>
                            <div class="pt-4 border-t border-slate-100">
                                <label class="block text-slate-700 font-bold mb-2">4. How was the <span class="text-violet-500">pace and difficulty</span> of today's lesson?</label>
                                <div class="px-2 mt-4">
                                    <input type="range" id="ref-pace" min="1" max="5" value="3" oninput="document.getElementById('ref-pace-val').innerText=this.value; saveAndProgress();" class="w-full h-3 bg-violet-200 rounded-lg appearance-none cursor-pointer focus:outline-none focus:ring-2 focus:ring-violet-400">
                                    <div class="flex justify-between text-xs text-slate-500 mt-2 font-bold">
                                        <span class="text-left text-emerald-600">1 (Too Easy)</span>
                                        <span class="text-center text-violet-600">Level: <span id="ref-pace-val">3</span></span>
                                        <span class="text-right text-rose-600">5 (Too Hard)</span>
                                    </div>
                                </div>
                            </div>
                        </div>
                        <div class="mt-10 text-center">
                            <button onclick="switchTab('summary')" class="bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-3 px-8 rounded-full shadow-lg transition transform hover:-translate-y-1 text-lg">Next: Final Submit <i class="fas fa-cloud-upload-alt ml-2"></i></button>
                        </div>
                    </div>
                </div>

                <!-- TAB 8: Summary -->
                <div id="tab-summary" class="hidden animate-fade-in">
                    <div class="max-w-2xl mx-auto">
                        <header class="text-center mb-8">
                            <h2 class="text-3xl font-bold text-slate-800 mb-2"><i class="fas fa-cloud-upload-alt text-emerald-500 mr-2"></i>Final Submission</h2>
                            <p class="text-slate-500 text-lg">Send your report directly to the teacher's database.</p>
                        </header>
                        <div class="bg-white rounded-2xl shadow-sm border-2 border-emerald-100 p-6 sm:p-8 relative">
                            <div class="mb-6 pb-6 border-b border-emerald-50 grid grid-cols-1 sm:grid-cols-3 gap-4">
                                <div class="col-span-1 sm:col-span-1">
                                    <label for="student-class" class="block text-sm font-bold text-emerald-800 mb-1">Class (班級):</label>
                                    <input type="text" id="student-class" maxlength="10" placeholder="Ex: 101" oninput="saveAndProgress()" class="w-full px-3 py-2 rounded-lg border border-emerald-200 focus:outline-none focus:ring-2 focus:ring-emerald-500 text-slate-700">
                                </div>
                                <div class="col-span-1 sm:col-span-1">
                                    <label for="student-number" class="block text-sm font-bold text-emerald-800 mb-1">Number (座號):</label>
                                    <input type="number" id="student-number" placeholder="Ex: 05" oninput="saveAndProgress()" class="w-full px-3 py-2 rounded-lg border border-emerald-200 focus:outline-none focus:ring-2 focus:ring-emerald-500 text-slate-700">
                                </div>
                                <div class="col-span-1 sm:col-span-1">
                                    <label for="student-name" class="block text-sm font-bold text-emerald-800 mb-1">Name (姓名):</label>
                                    <input type="text" id="student-name" maxlength="30" placeholder="Name" oninput="saveAndProgress()" class="w-full px-3 py-2 rounded-lg border border-emerald-200 focus:outline-none focus:ring-2 focus:ring-emerald-500 text-slate-700">
                                </div>
                            </div>
                            <div class="flex flex-col sm:flex-row gap-4 mb-4">
                                <button id="gas-submit-btn" onclick="submitToGoogleSheet()" class="flex-1 bg-gradient-to-r from-emerald-500 to-teal-600 hover:from-emerald-600 hover:to-teal-700 text-white font-bold py-3.5 px-6 rounded-xl shadow-lg transition transform hover:-translate-y-1 flex justify-center items-center text-lg border-2 border-emerald-200">
                                    <i class="fas fa-paper-plane mr-3"></i> Step 1: Submit to Teacher
                                </button>
                                <button id="png-summary-btn" onclick="showPreviewModal()" class="flex-1 bg-slate-700 hover:bg-slate-800 text-white font-bold py-3.5 px-6 rounded-xl shadow-lg transition transform hover:-translate-y-1 flex justify-center items-center text-lg border-2 border-slate-600">
                                    <i class="fas fa-image mr-3"></i> Step 2: Get My Report Card
                                </button>
                            </div>
                            <div id="submit-alert" class="hidden mt-4 p-3 bg-blue-50 text-blue-800 rounded-lg text-sm font-bold text-center border border-blue-200"></div>
                            <div class="mt-8 text-center border-t border-emerald-50 pt-4">
                                <button onclick="clearProgress()" class="text-slate-400 hover:text-rose-500 text-sm font-bold transition flex items-center justify-center mx-auto"><i class="fas fa-trash-alt mr-2"></i> Reset All Progress</button>
                            </div>
                        </div>
                    </div>
                </div>
                
            </div>
            
            <div class="bg-slate-100 p-4 text-center text-xs text-slate-400 font-bold border-t border-slate-200">
                <p>Powered by Sylvia's Interactive Classroom <span class="ml-3 cursor-pointer hover:text-violet-600 transition" onclick="openTeacherLoginModal()"><i class="fas fa-lock"></i></span></p>
            </div>
        </div>
    </div>
    
    <!-- Hidden Template for PNG Export -->
    <div id="export-container-hidden" style="position: absolute; top: -9999px; left: -9999px; width: 850px; background-color: #ffffff; z-index: -1000; overflow: hidden;">
        <div id="visual-report-wrapper" style="width: 850px; background-color: #ffffff; font-family: sans-serif; color: #1e293b;">
            <div style="background: linear-gradient(to right, #f97316, #7c3aed); padding: 32px; color: white; position: relative;">
                <h1 style="font-size: 36px; font-weight: 800; margin: 0 0 8px 0;">Goodbye, John</h1>
                <p style="font-size: 18px; margin: 0; opacity: 0.9;">Learning Portfolio</p>
                <div style="position: absolute; right: 32px; top: 32px; text-align: right;">
                    <p style="font-size: 12px; text-transform: uppercase; font-weight: bold; margin: 0 0 4px 0; opacity: 0.8;">Student Profile</p>
                    <p style="font-size: 24px; font-weight: bold; margin: 0;" id="export-name"></p>
                    <p style="font-size: 16px; margin: 0; opacity: 0.9;" id="export-class-num"></p>
                </div>
            </div>
            <div style="padding: 32px;">
                <!-- Part 1 -->
                <div style="background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; margin-bottom: 24px;">
                    <h3 style="font-size: 18px; font-weight: bold; color: #334155; border-bottom: 1px solid #e2e8f0; padding-bottom: 8px; margin: 0 0 16px 0; text-transform: uppercase;">Part 1: Self Awareness</h3>
                    <div style="display: flex; justify-content: space-between;">
                        <div style="flex: 1;">
                            <p style="font-size: 12px; color: #64748b; font-weight: bold; text-transform: uppercase; margin: 0 0 4px 0;">John is:</p>
                            <p style="font-weight: bold; font-size: 18px; color: #6d28d9; margin: 0;" id="export-q1"></p>
                        </div>
                        <div style="flex: 1; border-left: 1px solid #e2e8f0; padding-left: 16px;">
                            <p style="font-size: 12px; color: #64748b; font-weight: bold; text-transform: uppercase; margin: 0 0 4px 0;">Sort Score:</p>
                            <p style="font-weight: bold; font-size: 18px; color: #7c3aed; margin: 0;" id="export-sort-score"></p>
                        </div>
                        <div style="flex: 1; border-left: 1px solid #e2e8f0; padding-left: 16px;">
                            <p style="font-size: 12px; color: #64748b; font-weight: bold; text-transform: uppercase; margin: 0 0 4px 0;">FOMO Index:</p>
                            <p style="font-weight: bold; font-size: 18px; color: #e11d48; margin: 0;"><span id="export-fomo"></span>%</p>
                        </div>
                        <div style="flex: 1; border-left: 1px solid #e2e8f0; padding-left: 16px;">
                            <p style="font-size: 12px; color: #64748b; font-weight: bold; text-transform: uppercase; margin: 0 0 4px 0;">Focus Goal:</p>
                            <p style="font-weight: bold; font-size: 18px; color: #0d9488; margin: 0;"><span id="export-disconnect"></span> Mins</p>
                        </div>
                    </div>
                </div>
                
                <!-- Part 2 -->
                <div style="background-color: #f8fafc; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; margin-bottom: 24px;">
                    <h3 style="font-size: 18px; font-weight: bold; color: #334155; border-bottom: 1px solid #e2e8f0; padding-bottom: 8px; margin: 0 0 16px 0; text-transform: uppercase;">Part 2: Dialogue & Roleplay</h3>
                    <div style="display: flex;">
                        <div style="flex: 1;">
                            <p style="font-size: 12px; color: #64748b; font-weight: bold; text-transform: uppercase; margin: 0 0 4px 0;">Dialogue Score:</p>
                            <p style="font-weight: bold; font-size: 24px; color: #059669; margin: 0;" id="export-score"></p>
                        </div>
                        <div style="flex: 2; border-left: 1px solid #e2e8f0; padding-left: 16px;">
                            <p style="font-size: 12px; color: #64748b; font-weight: bold; text-transform: uppercase; margin: 0 0 4px 0;">Partner & Self-Score:</p>
                            <p style="font-weight: bold; font-size: 18px; color: #334155; margin: 4px 0 0 0;" id="export-partner"></p>
                        </div>
                    </div>
                </div>
                
                <!-- Part 3 -->
                <div style="background-color: #fff7ed; border: 1px solid #fed7aa; border-radius: 12px; padding: 20px;">
                    <h3 style="font-size: 18px; font-weight: bold; color: #9a3412; border-bottom: 1px solid #fed7aa; padding-bottom: 8px; margin: 0 0 12px 0; text-transform: uppercase;">Part 3: Deep Talk</h3>
                    <div id="export-deeptalk-container" style="display: flex; flex-direction: column; gap: 12px;"></div>
                </div>
            </div>
        </div>
    </div>

<script>
// ==========================================
// 核心資料庫(Core Data)
// ==========================================
const vocabData = [
    { word: "relationship", pos: "n.", def: "A close or loving friendship between two people.", zh: "關係;戀愛關係" },
    { word: "satisfying", pos: "adj.", def: "Bringing pleasure by providing what someone wants or needs.", zh: "令人滿意的" },
    { word: "inspire", pos: "v.", def: "To cause someone to have the desire or enthusiasm to do something.", zh: "激勵" },
    { word: "connect", pos: "v.", def: "To develop a relationship with someone.", zh: "與他人建立良好關係;連接" },
    { word: "communicate", pos: "v.", def: "To share information, ideas, or feelings with others.", zh: "溝通" },
    { word: "introduce", pos: "v.", def: "To inform someone about something they don't know much about.", zh: "介紹;使初次體驗" },
    { word: "throughout", pos: "prep.", def: "Occurring during an entire period of time.", zh: "自始至終" },
    { word: "message", pos: "n.", def: "A piece of writing sent to someone electronically.", zh: "訊息" },
    { word: "refresh", pos: "v.", def: "To click on something to make the latest information appear.", zh: "刷新;使恢復精神" },
    { word: "romantic", pos: "adj.", def: "Related to love or a couple's personal relationship.", zh: "浪漫的" },
    { word: "focus", pos: "v.", def: "To pay particular attention to one thing.", zh: "專注" },
    { word: "ignore", pos: "v.", def: "To not pay attention to something or someone.", zh: "忽略" },
    { word: "attention", pos: "n.", def: "The act of focusing on something or someone.", zh: "注意力" },
    { word: "deny", pos: "v.", def: "To refuse to admit that something is true.", zh: "否認" },
    { word: "respond", pos: "v.", def: "To give a spoken or written reply.", zh: "回應" },
    { word: "disappointed", pos: "adj.", def: "Sad because something you hoped for has not happened.", zh: "感到失望的" },
    { word: "personally", pos: "adv.", def: "In a way that is regarded as hurtful to one's self.", zh: "針對某人地" }
];

const dialogueData = [
    { speaker: 'Leo', side: 'right', gender: 'male', action: 'noticing', en_pre: "Emma, I noticed you haven't checked your phone all morning.", blank: null, en_post: "", zh: "Emma,我注意到你整個早上都沒看手機。" },
    { speaker: 'Emma', side: 'left', gender: 'female', action: 'nodding', en_pre: "Yeah, I'm trying to", blank: "focus", en_post: "on school. My grades have gotten worse since last month.", zh: "對啊,我正試著專注在課業上。上個月以來我的成績變差了。" },
    { speaker: 'Leo', side: 'right', gender: 'male', action: 'sympathizing', en_pre: "That's tough. It is not easy for us to", blank: "ignore", en_post: "all the notifications and messages.", zh: "那很辛苦。對我們來說,要忽略所有通知和訊息並不容易。" },
    { speaker: 'Emma', side: 'left', gender: 'female', action: 'sighing', en_pre: "True. In the past, I felt like part of my body was missing without it. I was totally", blank: "addicted", en_post: ".", zh: "真的。過去如果沒有手機,我覺得身體好像少了一塊。我完全成癮了。" },
    { speaker: 'Leo', side: 'right', gender: 'male', action: 'agreeing', en_pre: "I get that. When my friends don't", blank: "respond", en_post: "to my messages immediately, I get very disappointed.", zh: "我懂。當朋友沒有立刻回覆我的訊息,我也會很失望。" },
    { speaker: 'Emma', side: 'left', gender: 'female', action: 'explaining', en_pre: "Exactly! We pay so much", blank: "attention", en_post: "to the screen that we forget the real world.", zh: "沒錯!我們把太多注意力放在螢幕上,以至於忘記了真實世界。" },
    { speaker: 'Leo', side: 'right', gender: 'male', action: 'thinking', en_pre: "Having a smartphone is satisfying, but it shouldn't hurt our", blank: "relationship", en_post: "with our family.", zh: "擁有智慧型手機令人滿足,但它不應該傷害我們與家人的關係。" },
    { speaker: 'Emma', side: 'left', gender: 'female', action: 'smiling', en_pre: "I couldn't agree more. Please don't take this", blank: "personally", en_post: ", but you should put your phone away now!", zh: "我完全同意。請別覺得我是針對你,但你現在應該把手機收起來了!" }
];

let targetWords = [];
dialogueData.forEach(d => { if(d.blank !== null) targetWords.push(d.blank); });

const discussionData = [
    { q: "What is the first thing you do when you wake up in the morning?", zh: "你早上醒來做的第一件事是什麼?", hint: "Think about your morning routine. Do you check LINE/IG immediately?", starter: "When I wake up, the first thing I do is..." },
    { q: "How do you feel when you are talking to a friend, but they keep checking their phone?", zh: "當你跟朋友說話,但他一直看手機(phubbing)時,你感覺如何?", hint: "Think about respect and attention in a relationship.", starter: "When my friend ignores me for their phone, I feel..." },
    { q: "What is one good thing and one bad thing about having a smartphone?", zh: "擁有一支智慧型手機,一個好處和一個壞處分別是什麼?", hint: "Pros: convenient, connect with people. Cons: distracting, bad for eyes.", starter: "One good thing is ___, but the bad thing is..." },
    { q: "It is necessary for us to control our screen time. What is one rule you can set for yourself?", zh: "我們必須控制螢幕時間。你可以為自己設定哪一條規則?", hint: "Examples: No phones during dinner. Put the phone in another room when studying.", starter: "It is necessary for me to..." }
];

const sortItemsData = [
    { id: 's1', text: "I am never bored.", type: "pro" },
    { id: 's2', text: "Inspire me to connect.", type: "pro" },
    { id: 's3', text: "Help me find answers.", type: "pro" },
    { id: 's4', text: "New music & knowledge.", type: "pro" },
    { id: 's5', text: "Difficult to focus.", type: "con" },
    { id: 's6', text: "Hurts my relationships.", type: "con" },
    { id: 's7', text: "I ignore friends.", type: "con" },
    { id: 's8', text: "Parents are disappointed.", type: "con" }
];

// ==========================================
// 全域變數狀態(Global State)
// ==========================================
const userProgress = {
    q1Answer: "[Pending]", sortCompleted: false, sortScore: 100, fomoLevel: "10", disconnectGoal: "1",
    dialogueScore: "Not completed", icqUnlocked: false, usedReveal: false, submitAttempts: 0
};
let icqState = { 1: false, 2: false };
let isPlayingAll = false; let currentLineIndex = 0; let currentUtterance = null;
let isReadingPlaying = false; let currentReadingP = 0;
let sortSelectedId = null;

const synth = window.speechSynthesis;
let sysVoices = [];

function loadVoices() {
    sysVoices = synth.getVoices();
    if(sysVoices.length > 0) return;
    let fallbackCount = 0;
    let fallbackTimer = setInterval(() => {
        sysVoices = synth.getVoices();
        fallbackCount++;
        if(sysVoices.length > 0 || fallbackCount > 20) clearInterval(fallbackTimer);
    }, 100);
}

window.onload = () => {
    loadVoices();
    if (speechSynthesis.onvoiceschanged !== undefined) speechSynthesis.onvoiceschanged = loadVoices;

    renderVocab();
    initSortableGame();
    renderChatDialogue('interactive-dialogue', true);
    renderChatDialogue('audio-dialogue-container', false);
    renderDiscussion();
    loadProgress();
    updateProgressBar();
};

function switchTab(tabId) {
    const allTabs = ['vocab', 'reading', 'organizer', 'worksheet', 'ai', 'discussion', 'reflection', 'summary'];
    allTabs.forEach(id => {
        document.getElementById(`tab-${id}`).classList.add('hidden');
        const btn = document.getElementById(`tab-btn-${id}`);
        btn.className = 'tab-inactive py-2 px-3 rounded-lg text-xs sm:text-sm transition flex items-center justify-center whitespace-nowrap grow sm:grow-0';
        if(id === 'summary') {
            btn.classList.add('border', 'border-emerald-200', 'bg-emerald-50', 'hover:bg-emerald-100');
            btn.innerHTML = '<i class="fas fa-cloud-upload-alt mr-1.5 text-emerald-600"></i> <span class="text-emerald-700 font-bold">8. Submit</span>';
        }
    });
    document.getElementById(`tab-${tabId}`).classList.remove('hidden');
    const activeBtn = document.getElementById(`tab-btn-${tabId}`);
    activeBtn.className = 'tab-active py-2 px-3 rounded-lg text-xs sm:text-sm transition flex items-center justify-center whitespace-nowrap grow sm:grow-0';
    if(tabId === 'summary') {
        activeBtn.className = 'py-2 px-3 rounded-lg text-xs sm:text-sm transition flex items-center justify-center whitespace-nowrap grow sm:grow-0 bg-emerald-600 text-white font-bold shadow-md transform -translate-y-[1px]';
        activeBtn.innerHTML = '<i class="fas fa-cloud-upload-alt mr-1.5 text-white"></i> <span class="text-white font-bold">8. Submit</span>';
    }
    saveAndProgress();
    window.scrollTo({ top: 0, behavior: 'smooth' });
}

function toggleScaffold(id) { document.getElementById(id).classList.toggle('show'); }

// 加入煙火特效函數
function fireConfetti() {
    try {
        if (typeof confetti === 'undefined') return;
        var duration = 3 * 1000; var end = Date.now() + duration;
        (function frame() {
            confetti({ particleCount: 5, angle: 60, spread: 55, origin: { x: 0 }, colors: ['#0ea5e9', '#3b82f6', '#8b5cf6', '#10b981', '#fcd34d'] });
            confetti({ particleCount: 5, angle: 120, spread: 55, origin: { x: 1 }, colors: ['#0ea5e9', '#3b82f6', '#8b5cf6', '#10b981', '#fcd34d'] });
            if (Date.now() < end) requestAnimationFrame(frame);
        }());
    } catch(error) { console.warn("Confetti skipped."); }
}

function saveAndProgress() {
    const ind = document.getElementById('auto-save-indicator');
    ind.style.opacity = '1'; setTimeout(() => { ind.style.opacity = '0'; }, 2000);
    
    const data = {
        stClass: document.getElementById('student-class')?.value || "",
        stNum: document.getElementById('student-number')?.value || "",
        name: document.getElementById('student-name')?.value || "",
        q1Answer: userProgress.q1Answer,
        sortCompleted: userProgress.sortCompleted,
        sortScore: userProgress.sortScore,
        fomoLevel: userProgress.fomoLevel,
        disconnectGoal: userProgress.disconnectGoal,
        dialogueScore: userProgress.dialogueScore,
        submitAttempts: userProgress.submitAttempts,
        icqUnlocked: userProgress.icqUnlocked,
        rpChecked: document.getElementById('roleplay-checkbox')?.checked || false,
        rpPartner: document.getElementById('partner-name')?.value || "",
        rpScore: document.getElementById('roleplay-slider')?.value || "80",
        discussion: [],
        aiPrompt: document.getElementById('ai-prompt-input')?.value || "",
        refHardest: document.getElementById('ref-hardest')?.value || "",
        refProudest: document.getElementById('ref-proudest')?.value || "",
        refLearn: document.getElementById('ref-learn')?.value || "",
        refPace: document.getElementById('ref-pace')?.value || "3",
        blanks: []
    };
    for (let i=0; i<4; i++) { data.discussion.push(document.getElementById(`disc-ans-${i}`)?.value.trim() || ""); }
    document.querySelectorAll('.blank-zone').forEach(zone => {
        data.blanks.push({ word: zone.dataset.currentWord || "", wrongCount: zone.dataset.wrongCount || 0 });
    });
    localStorage.setItem('goodbyeJohnSitesFinal', JSON.stringify(data));
    updateProgressBar();
}

function loadProgress() {
    const saved = localStorage.getItem('goodbyeJohnSitesFinal');
    if (saved) {
        try {
            const data = JSON.parse(saved);
            userProgress.q1Answer = data.q1Answer || "[Pending]";
            userProgress.sortCompleted = data.sortCompleted || false;
            userProgress.sortScore = (data.sortScore !== undefined) ? data.sortScore : 100;
            userProgress.fomoLevel = data.fomoLevel || "10";
            userProgress.disconnectGoal = data.disconnectGoal || "1";
            userProgress.dialogueScore = data.dialogueScore || "Not completed";
            userProgress.icqUnlocked = data.icqUnlocked || false;
            userProgress.submitAttempts = data.submitAttempts || 0;

            if(document.getElementById('student-class')) document.getElementById('student-class').value = data.stClass || "";
            if(document.getElementById('student-number')) document.getElementById('student-number').value = data.stNum || "";
            if(document.getElementById('student-name')) document.getElementById('student-name').value = data.name || "";
            if(document.getElementById('ai-prompt-input')) document.getElementById('ai-prompt-input').value = data.aiPrompt || "";
            if(document.getElementById('roleplay-checkbox')) document.getElementById('roleplay-checkbox').checked = data.rpChecked || false;
            if(document.getElementById('partner-name')) document.getElementById('partner-name').value = data.rpPartner || "";
            if(document.getElementById('roleplay-slider')) { document.getElementById('roleplay-slider').value = data.rpScore || "80"; document.getElementById('rp-score-val').innerText = data.rpScore || "80"; }
            if(document.getElementById('ref-hardest')) document.getElementById('ref-hardest').value = data.refHardest || "";
            if(document.getElementById('ref-proudest')) document.getElementById('ref-proudest').value = data.refProudest || "";
            if(document.getElementById('ref-learn')) document.getElementById('ref-learn').value = data.refLearn || "";
            if(document.getElementById('ref-pace')) { document.getElementById('ref-pace').value = data.refPace || "3"; document.getElementById('ref-pace-val').innerText = data.refPace || "3"; }
            if(data.discussion) { data.discussion.forEach((ans, i) => { const el = document.getElementById(`disc-ans-${i}`); if(el) el.value = ans; }); }

            if(data.q1Answer !== "[Pending]" && data.q1Answer !== "") {
                const badge = document.getElementById('q1-success-badge');
                const btns = document.getElementById('q1-options-container');
                if(badge && btns) {
                    btns.classList.add('hidden');
                    badge.classList.remove('hidden');
                    badge.innerHTML = `<i class="fas fa-check-circle mr-3"></i> ${data.q1Answer}`;
                }
                unlockTab3();
            }

            document.getElementById('sort-score-display').innerText = userProgress.sortScore;
            if(data.sortCompleted) {
                document.getElementById('sort-bank').innerHTML = '';
                document.getElementById('sort-check-btn').classList.add('hidden');
                document.getElementById('sort-success-msg').classList.remove('hidden');
                document.getElementById('sliders-locked-wrapper').classList.remove('opacity-30', 'blur-sm', 'pointer-events-none');
            }

            document.getElementById('fomo-val').innerText = data.fomoLevel;
            const fomoSlider = document.querySelector('input[oninput="updateFOMOReaction(this.value)"]'); if(fomoSlider) fomoSlider.value = data.fomoLevel;
            updateFOMOReaction(data.fomoLevel, false);

            document.getElementById('disconnect-time-val').innerText = data.disconnectGoal + " Mins";
            const discSlider = document.getElementById('disconnect-slider'); if(discSlider) discSlider.value = data.disconnectGoal;
            updateDisconnectEffect(data.disconnectGoal, false);

            let needsRevealBtn = false;
            if(data.blanks) {
                const zones = document.querySelectorAll('.blank-zone');
                data.blanks.forEach((bData, i) => {
                    if(zones[i]) {
                        if(bData.word) { 
                            zones[i].innerText = bData.word; 
                            zones[i].dataset.currentWord = bData.word; 
                            zones[i].classList.add('blank-filled');
                        }
                        zones[i].dataset.wrongCount = bData.wrongCount;
                        if (bData.wrongCount >= 2) {
                            needsRevealBtn = true;
                            let vocabItem = vocabData.find(v => v.word === zones[i].dataset.answer);
                            let hintText = vocabItem ? vocabItem.zh : '...';
                            zones[i].setAttribute('data-hint', `Hint: ${hintText}`);
                            zones[i].classList.add('show-hint');
                        }
                    }
                });
            }

            if (needsRevealBtn && !data.dialogueScore.includes('100%')) document.getElementById('reveal-ans-btn').classList.remove('hidden');

            if(data.dialogueScore.includes('100%') || data.dialogueScore.includes('Completed')) {
                const sd = document.getElementById('score-display');
                sd.classList.remove('hidden');
                sd.className = 'mt-4 text-lg font-bold px-6 py-3 rounded-lg text-center w-full sm:w-auto bg-emerald-100 text-emerald-800 border border-emerald-300';
                sd.innerHTML = '<i class="fas fa-check-double mr-2"></i> Perfect! Audio Unlocked.';
                document.getElementById('reveal-ans-btn').classList.add('hidden');
                unlockContent(false);
            }

            if(data.icqUnlocked) {
                icqState[1] = true; icqState[2] = true;
                document.getElementById('icq-success-msg').classList.remove('hidden');
                document.getElementById('ai-tools-section').classList.remove('opacity-30', 'blur-sm', 'pointer-events-none', 'select-none');
                document.getElementById('ai-lock-overlay').classList.add('hidden');
            }
        } catch(e) {}
    }
}

function clearProgress() { document.getElementById('confirm-clear-modal').classList.remove('hidden'); }
function closeClearModal() { document.getElementById('confirm-clear-modal').classList.add('hidden'); }
function executeClearProgress() { localStorage.removeItem('goodbyeJohnSitesFinal'); location.reload(); }

function updateProgressBar() {
    let completed = 0; const total = 7;
    if(userProgress.q1Answer !== "[Pending]") completed++;
    if(userProgress.sortCompleted) completed++;
    if(userProgress.dialogueScore.includes('100%') || userProgress.dialogueScore.includes('Completed')) completed++;
    if(userProgress.icqUnlocked) completed++;
    let hasTalk = false; for(let i=0; i<4; i++) { if(document.getElementById(`disc-ans-${i}`)?.value.trim().length > 3) hasTalk = true; }
    if(hasTalk) completed++;
    if(document.getElementById('ref-hardest')?.value !== "" && document.getElementById('ref-learn')?.value.trim() !== "") completed++;
    if(document.getElementById('student-name')?.value.trim() !== "") completed++;
    document.getElementById('global-progress-bar').style.width = ((completed / total) * 100) + '%';
}

function renderVocab() {
    const container = document.getElementById('vocab-container');
    vocabData.forEach((item, index) => {
        container.innerHTML += `
        <div class="vocab-card bg-white p-5 rounded-xl border border-slate-200 shadow-sm transition flex flex-col justify-between hover:shadow-md hover:border-violet-200">
            <div>
                <div class="flex justify-between items-start mb-2">
                    <h3 class="text-xl font-bold text-violet-800 capitalize">${item.word} <span class="text-sm font-normal text-slate-400 ml-1">${item.pos}</span></h3>
                    <button onclick="speakSentence('${item.word}', 'female')" class="text-slate-400 hover:text-violet-600 bg-violet-50 p-2 rounded-full h-8 w-8 flex items-center justify-center transition focus:outline-none"><i class="fas fa-volume-up"></i></button>
                </div>
                <p class="text-slate-600 text-sm mb-4">${item.def}</p>
            </div>
            <div>
                <button onclick="toggleScaffold('vocab-zh-${index}')" class="text-xs bg-slate-100 hover:bg-slate-200 text-slate-600 py-1 px-3 rounded-full transition flex items-center focus:outline-none"><i class="fas fa-globe-americas mr-1"></i> 中文</button>
                <div id="vocab-zh-${index}" class="scaffold-content text-sm text-violet-700 font-bold border-t border-slate-100 mt-2">${item.zh}</div>
            </div>
        </div>`;
    });
}

// ------------------------------------------------------------------
// Tab 3: SortableJS Implementation with Safe Fallback for Google Sites
// ------------------------------------------------------------------
function initSortableGame() {
    const bank = document.getElementById('sort-bank');
    let shuffled = [...sortItemsData].sort(() => Math.random() - 0.5);
    shuffled.forEach(item => {
        bank.innerHTML += `<div id="${item.id}" data-type="${item.type}" data-current-zone="bank" onclick="selectSortItem('${item.id}')" class="sort-item bg-white border-2 border-slate-300 text-slate-700 px-4 py-2 rounded-full text-sm font-bold shadow-sm m-1 transition"><i class="fas fa-arrows-alt cursor-move mr-2 text-slate-400"></i>${item.text}</div>`;
    });

    if (typeof window.Sortable === 'undefined') {
        console.warn("Sortable.js 尚未載入,系統將預設使用點擊搬移模式。");
        return;
    }

    const options = {
        group: 'shared',
        animation: 150,
        ghostClass: 'sortable-ghost',
        dragClass: 'sortable-drag',
        onEnd: function (evt) {
            const el = evt.item;
            const newZoneId = evt.to.id;
            if (newZoneId === 'sort-bank') el.dataset.currentZone = 'bank';
            else if (newZoneId === 'zone-pro') el.dataset.currentZone = 'pro';
            else if (newZoneId === 'zone-con') el.dataset.currentZone = 'con';
            updateSortItemStyle(el);
        }
    };

    try {
        new window.Sortable(document.getElementById('sort-bank'), options);
        new window.Sortable(document.getElementById('zone-pro'), options);
        new window.Sortable(document.getElementById('zone-con'), options);
    } catch(err) {
        console.error("Sortable 初始化錯誤:", err);
    }
}

function updateSortItemStyle(el) {
    el.classList.remove('selected');
    if (el.dataset.currentZone === 'bank') {
        el.classList.replace('bg-emerald-100', 'bg-white');
        el.classList.replace('bg-rose-100', 'bg-white');
        el.classList.replace('border-emerald-300', 'border-slate-300');
        el.classList.replace('border-rose-300', 'border-slate-300');
        el.classList.replace('text-emerald-800', 'text-slate-700');
        el.classList.replace('text-rose-800', 'text-slate-700');
    } else if (el.dataset.currentZone === 'pro') {
        el.classList.replace('bg-white', 'bg-emerald-100');
        el.classList.replace('bg-rose-100', 'bg-emerald-100');
        el.classList.replace('border-slate-300', 'border-emerald-300');
        el.classList.replace('border-rose-300', 'border-emerald-300');
        el.classList.replace('text-slate-700', 'text-emerald-800');
        el.classList.replace('text-rose-800', 'text-emerald-800');
    } else if (el.dataset.currentZone === 'con') {
        el.classList.replace('bg-white', 'bg-rose-100');
        el.classList.replace('bg-emerald-100', 'bg-rose-100');
        el.classList.replace('border-slate-300', 'border-rose-300');
        el.classList.replace('border-emerald-300', 'border-rose-300');
        el.classList.replace('text-slate-700', 'text-rose-800');
        el.classList.replace('text-emerald-800', 'text-rose-800');
    }
}

function selectSortItem(id) {
    const el = document.getElementById(id);
    if(!el) return;
    
    if (el.dataset.currentZone === 'pro' || el.dataset.currentZone === 'con') {
        document.getElementById('sort-bank').appendChild(el);
        el.dataset.currentZone = 'bank';
        updateSortItemStyle(el);
        sortSelectedId = null;
        return;
    }
    
    if (sortSelectedId === id) {
        el.classList.remove('selected');
        sortSelectedId = null;
    } else {
        document.querySelectorAll('.sort-item').forEach(e => e.classList.remove('selected'));
        el.classList.add('selected'); 
        sortSelectedId = id;
    }
}

function handleZoneClick(zoneType) {
    if(!sortSelectedId || zoneType === 'bank') return; 
    const el = document.getElementById(sortSelectedId);
    
    document.getElementById(`zone-${zoneType}`).appendChild(el);
    el.dataset.currentZone = zoneType;
    updateSortItemStyle(el);
    sortSelectedId = null;
}

function checkDragSortGame() {
    const bank = document.getElementById('sort-bank');
    const proZone = document.getElementById('zone-pro');
    const conZone = document.getElementById('zone-con');
    const errMsg = document.getElementById('sort-error-msg');
    
    if(bank.children.length > 0) {
        errMsg.innerHTML = '<i class="fas fa-exclamation-circle mr-2"></i> Please drag ALL items into the boxes first!';
        errMsg.classList.remove('hidden');
        return;
    }

    let errors = 0;
    
    const checkZone = (zone, expectedType) => {
        Array.from(zone.children).forEach(el => {
            if(el.dataset.type !== expectedType) {
                errors++;
                el.classList.add('animate-shake', 'border-rose-500', 'text-rose-600');
                setTimeout(() => {
                    el.classList.remove('animate-shake', 'border-rose-500', 'text-rose-600');
                    bank.appendChild(el);
                    el.dataset.currentZone = 'bank';
                    updateSortItemStyle(el);
                }, 800);
            }
        });
    };

    checkZone(proZone, 'pro');
    checkZone(conZone, 'con');

    if(errors > 0) {
        userProgress.sortScore = Math.max(0, userProgress.sortScore - 20);
        document.getElementById('sort-score-display').innerText = userProgress.sortScore;
        if(userProgress.sortScore <= 60) document.getElementById('sort-score-display').classList.replace('text-emerald-600', 'text-rose-600');
        
        errMsg.innerHTML = `<i class="fas fa-times-circle mr-2"></i> You have ${errors} mistakes. They returned to the bank. You lost 20 points! Please read again!`;
        errMsg.classList.remove('hidden');
        saveAndProgress();
    } else {
        errMsg.classList.add('hidden');
        document.getElementById('sort-check-btn').classList.add('hidden');
        document.getElementById('sort-success-msg').classList.remove('hidden');
        document.getElementById('sliders-locked-wrapper').classList.remove('opacity-30', 'blur-sm', 'pointer-events-none');
        userProgress.sortCompleted = true;
        saveAndProgress();
    }
}

// ------------------------------------------------------------------
// Tab 4: Dialogue Popup Logic
// ------------------------------------------------------------------

function getAvailableWords() {
    let usedWords = [];
    document.querySelectorAll('.blank-zone').forEach(b => { if (b.dataset.currentWord) usedWords.push(b.dataset.currentWord); });
    return targetWords.filter(w => !usedWords.includes(w)).sort(() => Math.random() - 0.5);
}

function handleBlankClick(blankEl) {
    blankEl.classList.remove('status-correct', 'status-incorrect', 'show-hint');
    document.getElementById('score-display').classList.add('hidden');
    
    if (blankEl.dataset.currentWord) {
        blankEl.innerText = ""; blankEl.dataset.currentWord = "";
        blankEl.classList.remove('blank-filled'); saveAndProgress();
    }
    
    const popup = document.getElementById('word-selector-popup');
    popup.innerHTML = "";
    const avail = getAvailableWords();
    
    if (avail.length === 0) {
        popup.innerHTML = '<span class="text-slate-500 text-sm font-bold p-2"><i class="fas fa-check-circle text-emerald-500 mr-1"></i> All blanks filled</span>';
    } else {
        popup.innerHTML = '<div class="w-full mb-2 pb-2 border-b border-violet-200 text-xs font-bold text-violet-700 uppercase tracking-wide"><i class="fas fa-mouse-pointer mr-1"></i> Select a word</div>';
        avail.forEach(w => {
            let btn = document.createElement('button');
            btn.className = 'bg-slate-50 border border-violet-200 text-violet-800 font-bold py-2 px-4 rounded-lg shadow-sm text-sm hover:bg-violet-100 transition focus:outline-none';
            btn.innerText = w;
            btn.onclick = (e) => {
                e.stopPropagation();
                blankEl.innerText = w; blankEl.dataset.currentWord = w;
                blankEl.classList.add('blank-filled'); blankEl.classList.remove('show-hint');
                popup.classList.add('hidden'); saveAndProgress();
            };
            popup.appendChild(btn);
        });
    }

    const rect = blankEl.getBoundingClientRect();
    popup.style.top = (rect.bottom + 10) + 'px';
    
    let leftPos = rect.left;
    if (leftPos + 320 > window.innerWidth) leftPos = window.innerWidth - 330;
    popup.style.left = Math.max(10, leftPos) + 'px';
    
    popup.classList.remove('hidden');
    
    setTimeout(() => { 
        document.addEventListener('click', function closePopupOutside(e) {
            if (!popup.contains(e.target) && !e.target.classList.contains('blank-zone')) {
                popup.classList.add('hidden'); document.removeEventListener('click', closePopupOutside);
            }
        });
    }, 10);
}

function checkAnswers() {
    userProgress.submitAttempts++;
    const blanks = document.querySelectorAll('.blank-zone');
    let correctCount = 0; const totalBlanks = targetWords.length;
    let needsRevealBtn = false;
    
    blanks.forEach(zone => {
        const answer = zone.dataset.answer; const currentWord = zone.dataset.currentWord || "";
        zone.classList.remove('status-correct', 'status-incorrect', 'show-hint');
        if (currentWord === answer) {
            zone.classList.add('status-correct'); zone.dataset.wrongCount = 0; correctCount++;
        } else {
            zone.classList.add('status-incorrect');
            if(currentWord !== "") {
                let wCount = parseInt(zone.dataset.wrongCount || '0') + 1;
                zone.dataset.wrongCount = wCount;
                setTimeout(() => {
                    zone.innerText = ""; zone.dataset.currentWord = "";
                    zone.classList.remove('blank-filled', 'status-incorrect');
                    if (wCount >= 2) {
                        needsRevealBtn = true;
                        let vocabItem = vocabData.find(v => v.word === answer);
                        let hintText = vocabItem ? vocabItem.zh : answer.substring(0, 2) + '...';
                        zone.setAttribute('data-hint', `Hint: ${hintText}`);
                        zone.classList.add('show-hint');
                    }
                    saveAndProgress();
                }, 800);
            }
        }
    });
    
    const scoreDisplay = document.getElementById('score-display'); scoreDisplay.classList.remove('hidden');
    if (correctCount === totalBlanks) {
        userProgress.dialogueScore = "Completed (100%)";
        scoreDisplay.className = 'mt-4 text-lg font-bold px-6 py-3 rounded-lg text-center w-full sm:w-auto bg-emerald-100 text-emerald-800 border border-emerald-300';
        scoreDisplay.innerHTML = `<i class="fas fa-check-double mr-2"></i> Perfect in ${userProgress.submitAttempts} tries!`;
        document.getElementById('reveal-ans-btn').classList.add('hidden');
        fireConfetti(); saveAndProgress(); unlockContent();
    } else {
        userProgress.dialogueScore = `${correctCount}/${totalBlanks} correct`;
        scoreDisplay.className = 'mt-4 text-lg font-bold px-6 py-3 rounded-lg text-center w-full sm:w-auto bg-rose-50 text-rose-600 border border-rose-200';
        scoreDisplay.innerHTML = `${correctCount}/${totalBlanks} correct. Incorrect words removed.`;
        if (needsRevealBtn) document.getElementById('reveal-ans-btn').classList.remove('hidden');
        saveAndProgress();
    }
}

function revealAnswers() {
    document.querySelectorAll('.blank-zone').forEach(zone => {
        const answer = zone.dataset.answer;
        zone.innerText = answer; zone.dataset.currentWord = answer; zone.dataset.wrongCount = 0;
        zone.classList.add('blank-filled', 'status-correct'); zone.classList.remove('status-incorrect', 'show-hint');
    });
    userProgress.usedReveal = true; document.getElementById('reveal-ans-btn').classList.add('hidden');
    const scoreDisplay = document.getElementById('score-display'); scoreDisplay.classList.remove('hidden');
    userProgress.dialogueScore = "Completed (with Help)";
    scoreDisplay.className = 'mt-4 text-lg font-bold px-6 py-3 rounded-lg text-center w-full sm:w-auto bg-amber-100 text-amber-800 border border-amber-300';
    scoreDisplay.innerHTML = '<i class="fas fa-eye mr-2"></i> Answers Revealed. Audio Unlocked.';
    saveAndProgress(); unlockContent();
}

function unlockContent() {
    const content = document.getElementById('unlocked-content');
    if (content) {
        content.classList.remove('hidden', 'opacity-30', 'blur-sm', 'pointer-events-none', 'select-none');
        document.getElementById('dialogue-lock-overlay')?.classList.add('hidden');
        setTimeout(() => { content.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 300);
    }
}

function checkCCQ(choiceNum, isCorrect, btnElement) {
    const container = document.getElementById('q1-options-container');
    const badge = document.getElementById('q1-success-badge');
    
    if(isCorrect) {
        const text = btnElement.querySelector('span').innerText.substring(3);
        userProgress.q1Answer = text;
        container.classList.add('hidden');
        badge.classList.remove('hidden');
        badge.innerHTML = `<i class="fas fa-check-circle mr-3"></i> ${text}`;
        unlockTab3();
        saveAndProgress();
    } else {
        btnElement.classList.add('bg-rose-100', 'text-rose-600', 'border-rose-400', 'animate-shake');
        setTimeout(() => { btnElement.classList.remove('bg-rose-100', 'text-rose-600', 'border-rose-400', 'animate-shake'); }, 400);
    }
}

function unlockTab3() {
    const lockOverlay = document.getElementById('tab3-lock-overlay');
    if(lockOverlay) lockOverlay.classList.add('hidden');
}

function updateFOMOReaction(val, doSave = true) {
    document.getElementById('fomo-val').innerText = val;
    userProgress.fomoLevel = val;
    const bar = document.getElementById('anxiety-bar');
    const alertBox = document.getElementById('overwhelm-alert');
    
    if (val > 70) {
        bar.style.width = val + '%'; bar.className = 'h-full bg-rose-500 transition-all duration-300 w-full flex items-center justify-center';
        alertBox.innerHTML = '<i class="fas fa-exclamation-triangle mr-1"></i> HIGH ADDICTION RISK <i class="fas fa-exclamation-triangle ml-1"></i>';
        alertBox.className = 'text-center text-rose-600 font-extrabold text-lg mt-2 animate-bounce';
    } else if (val > 30) {
        bar.style.width = val + '%'; bar.className = 'h-full bg-amber-400 transition-all duration-300 w-full flex items-center justify-center';
        alertBox.innerHTML = '<i class="fas fa-exclamation-circle mr-1"></i> SLIGHTLY DEPENDENT <i class="fas fa-exclamation-circle ml-1"></i>';
        alertBox.className = 'text-center text-amber-500 font-extrabold text-lg mt-2';
    } else {
        bar.style.width = (val < 5 ? 5 : val) + '%'; bar.className = 'h-full bg-emerald-500 transition-all duration-300 w-full flex items-center justify-center';
        alertBox.innerHTML = '<i class="fas fa-check-circle mr-1"></i> I CAN SURVIVE <i class="fas fa-check-circle ml-1"></i>';
        alertBox.className = 'text-center text-emerald-600 font-extrabold text-lg mt-2';
    }
    if (doSave) saveAndProgress();
}

function updateDisconnectEffect(mins, doSave = true) {
    const timeVal = document.getElementById('disconnect-time-val');
    const statusBox = document.getElementById('disconnect-status-box');
    const statusText = document.getElementById('disconnect-status-text');
    const emoji = document.getElementById('disconnect-emoji');
    
    timeVal.innerText = mins + (mins == 1 ? " Min" : " Mins");
    userProgress.disconnectGoal = mins;
    
    if (mins < 30) {
        timeVal.className = "text-2xl font-extrabold text-rose-500";
        statusBox.className = "p-3 rounded-lg bg-rose-50 border border-rose-200 transition-colors duration-500";
        statusText.className = "font-bold text-rose-600 text-lg";
        statusText.innerText = "Heavy Addict"; emoji.innerText = "😰"; emoji.className = "text-3xl animate-pulse";
    } else if (mins < 90) {
        timeVal.className = "text-2xl font-extrabold text-amber-500";
        statusBox.className = "p-3 rounded-lg bg-amber-50 border border-amber-200 transition-colors duration-500";
        statusText.className = "font-bold text-amber-600 text-lg";
        statusText.innerText = "Average User"; emoji.innerText = "😐"; emoji.className = "text-3xl";
    } else {
        timeVal.className = "text-2xl font-extrabold text-emerald-500";
        statusBox.className = "p-3 rounded-lg bg-emerald-50 border border-emerald-200 transition-colors duration-500";
        statusText.className = "font-bold text-emerald-600 text-lg";
        statusText.innerText = "Master of Focus"; emoji.innerText = "😎"; emoji.className = "text-3xl animate-bounce";
    }
    if(doSave) saveAndProgress();
}


function renderDiscussion() {
    const container = document.getElementById('discussion-container');
    discussionData.forEach((item, index) => {
        const escapedStarter = item.starter.replace(/'/g, "\\'");
        container.innerHTML += `
        <div class="bg-white p-5 rounded-xl border border-slate-200 shadow-sm hover:shadow-md transition hover:border-orange-200">
            <div class="flex justify-between items-start mb-2 gap-2">
                <h3 class="text-lg font-bold text-slate-800"><span class="bg-orange-100 text-orange-700 px-2 py-1 rounded mr-2 text-sm">Q${index+1}</span> ${item.q}</h3>
                <div class="flex shrink-0 gap-2">
                    <button onclick="toggleScaffold('disc-zh-${index}')" class="bg-slate-100 hover:bg-slate-200 text-slate-600 p-2 rounded-full focus:outline-none"><i class="fas fa-globe-americas"></i></button>
                    <button onclick="toggleScaffold('disc-hint-${index}')" class="bg-amber-100 hover:bg-amber-200 text-amber-600 p-2 rounded-full focus:outline-none"><i class="fas fa-lightbulb"></i></button>
                </div>
            </div>
            <div id="disc-zh-${index}" class="scaffold-content text-base text-slate-600 bg-slate-50 p-3 rounded-lg border-l-4 border-slate-300 mb-2">${item.zh}</div>
            <div id="disc-hint-${index}" class="scaffold-content bg-amber-50 p-3 rounded-lg border-l-4 border-amber-300 mb-2">
                <span class="text-xs font-bold text-amber-600 uppercase tracking-wider block mb-1"> Hint Idea:</span>
                <p class="text-sm text-slate-700 mb-2">${item.hint}</p>
                <div class="flex items-center justify-between mb-1">
                    <span class="text-xs font-bold text-orange-600 uppercase tracking-wider block"> Sentence Starter:</span>
                    <button onclick="speakSentence('${escapedStarter}', 'female')" class="text-orange-500 bg-orange-50 p-1.5 rounded-full focus:outline-none"><i class="fas fa-volume-up text-sm"></i></button>
                </div>
                <p class="text-slate-600 italic">"${item.starter}"</p>
            </div>
            <textarea id="disc-ans-${index}" maxlength="250" oninput="saveAndProgress()" class="w-full mt-2 p-3 rounded-lg border border-slate-200 focus:outline-none focus:ring-2 focus:ring-orange-400 text-slate-700 text-sm resize-none" rows="2" placeholder="Start typing here..."></textarea>
        </div>`;
    });
}

function renderChatDialogue(containerId, isInteractive) {
    const container = document.getElementById(containerId);
    container.innerHTML = '';
    dialogueData.forEach((line, index) => {
        const isLeft = line.side === 'left';
        const avatarInitial = line.speaker.charAt(0);
        const avatarBg = isLeft ? 'bg-violet-500' : 'bg-orange-500';
        const bubbleClass = isLeft ? 'bubble-left' : 'bubble-right';
        const justifyClass = isLeft ? 'chat-left' : 'chat-right';
        
        let contentHtml = `<span class="text-xs text-slate-400 italic block mb-1">*${line.action}*</span><span class="text-base">${line.en_pre}`;
        if (line.blank !== null) {
            if (isInteractive) contentHtml += `<span class="blank-zone" data-answer="${line.blank}" onclick="handleBlankClick(this)"></span>`;
            else contentHtml += `<span class="font-bold underline decoration-2 decoration-amber-500 text-black px-1">${line.blank}</span>`;
        }
        contentHtml += `${line.en_post}</span>`;
        
        let playBtn = isInteractive ? '' : `<button onclick="playSingleLine(${index})" class="absolute ${isLeft? '-right-10' : '-left-10'} top-2 w-8 h-8 rounded-full bg-white shadow-md text-slate-400 hover:text-amber-500 flex items-center justify-center z-10 focus:outline-none"><i id="icon-line-${index}" class="fas fa-volume-up text-sm"></i></button>`;
        let transBtn = isInteractive ? '' : `<button onclick="toggleScaffold('chat-zh-${index}')" class="text-slate-300 hover:text-violet-600 ml-2 bg-slate-50 rounded-full w-6 h-6 inline-flex items-center justify-center focus:outline-none"><i class="fas fa-globe-americas text-xs"></i></button>`;
        let transDiv = isInteractive ? '' : `<div id="chat-zh-${index}" class="scaffold-content border-t border-slate-100 mt-2 text-sm text-slate-500 bg-slate-50/50 rounded p-2">${line.zh}</div>`;
        
        if (isLeft) {
            container.innerHTML += `<div class="chat-container ${justifyClass} animate-fade-in" style="animation-delay: ${index * 0.05}s">
                <div class="w-10 h-10 rounded-full ${avatarBg} flex items-center justify-center text-white font-bold mr-3 shrink-0 shadow-md">${avatarInitial}</div>
                <div id="${isInteractive?'int':'aud'}-bubble-${index}" class="bubble ${bubbleClass}">${playBtn}<div class="font-bold text-xs text-slate-500 mb-1 flex justify-between items-center"><span>${line.speaker}</span>${transBtn}</div>${contentHtml}${transDiv}</div>
            </div>`;
        } else {
            container.innerHTML += `<div class="chat-container ${justifyClass} animate-fade-in" style="animation-delay: ${index * 0.05}s">
                <div id="${isInteractive?'int':'aud'}-bubble-${index}" class="bubble ${bubbleClass}">${playBtn}<div class="font-bold text-xs text-slate-400 mb-1 flex justify-between items-center flex-row-reverse"><span>${line.speaker}</span>${transBtn}</div>${contentHtml}${transDiv}</div>
                <div class="w-10 h-10 rounded-full ${avatarBg} flex items-center justify-center text-white font-bold ml-3 shrink-0 shadow-md">${avatarInitial}</div>
            </div>`;
        }
    });
}

function checkICQ(qNum, isCorrect, btnElement) {
    const siblings = btnElement.parentElement.querySelectorAll('button');
    siblings.forEach(b => { b.classList.remove('bg-emerald-500', 'text-white', 'border-emerald-600', 'bg-rose-500', 'border-rose-600'); b.classList.add('bg-slate-100', 'text-slate-700'); });
    if (isCorrect) {
        btnElement.classList.remove('bg-slate-100', 'text-slate-700'); btnElement.classList.add('bg-emerald-500', 'text-white', 'border-emerald-600');
        icqState[qNum] = true;
    } else {
        btnElement.classList.remove('bg-slate-100', 'text-slate-700'); btnElement.classList.add('bg-rose-500', 'text-white', 'border-rose-600', 'animate-shake');
        setTimeout(() => { btnElement.classList.remove('bg-rose-500', 'text-white', 'border-rose-600', 'animate-shake'); btnElement.classList.add('bg-slate-100', 'text-slate-700'); }, 500);
        icqState[qNum] = false;
    }
    if (icqState[1] && icqState[2]) {
        document.getElementById('icq-success-msg').classList.remove('hidden');
        document.getElementById('ai-tools-section')?.classList.remove('opacity-30', 'blur-sm', 'pointer-events-none', 'select-none');
        document.getElementById('ai-lock-overlay')?.classList.add('hidden');
        userProgress.icqUnlocked = true; saveAndProgress(); fireConfetti();
    }
}

function getBestVoice(gender) {
    if(sysVoices.length === 0) loadVoices();
    let preferred = null;
    if (gender === 'female') {
        preferred = sysVoices.find(v => (v.name.includes('Female') || v.name.includes('Samantha') || v.name.includes('Zira') || v.name.includes('Karen') || v.name.includes('Victoria')) && v.lang.startsWith('en')) 
                    || sysVoices.find(v => v.lang === 'en-US' && v.name.includes('Google'));
    } else {
        preferred = sysVoices.find(v => (v.name.includes('Male') || v.name.includes('Daniel') || v.name.includes('Alex') || v.name.includes('David')) && v.lang.startsWith('en'))
                    || sysVoices.find(v => v.lang === 'en-GB' && v.name.includes('Google UK English Male'))
                    || sysVoices.find(v => v.lang === 'en-US' && !v.name.includes('Female') && !v.name.includes('Zira') && !v.name.includes('Samantha'));
    }
    if(!preferred) preferred = sysVoices.find(v => v.lang.startsWith('en'));
    return preferred;
}

function speakSentence(text, gender = 'female') {
    if (synth.speaking) synth.cancel();
    const utterance = new SpeechSynthesisUtterance(text);
    utterance.lang = 'en-US'; 
    utterance.rate = 0.9;
    const voice = getBestVoice(gender);
    if(voice) utterance.voice = voice;
    synth.speak(utterance);
}

function playSingleLine(index) {
    if (synth.speaking) synth.cancel();
    const line = dialogueData[index];
    let fullText = line.en_pre + " " + (line.blank !== null ? line.blank : "") + " " + line.en_post;
    const utterance = new SpeechSynthesisUtterance(fullText);
    utterance.lang = 'en-US'; utterance.rate = 0.9;
    
    const voice = getBestVoice(line.gender);
    if(voice) utterance.voice = voice;

    const icon = document.getElementById(`icon-line-${index}`);
    if(icon) { icon.classList.remove('fa-volume-up'); icon.classList.add('fa-spinner', 'fa-spin', 'text-amber-500'); }
    utterance.onend = () => { if(icon) { icon.classList.remove('fa-spinner', 'fa-spin', 'text-amber-500'); icon.classList.add('fa-volume-up'); } };
    synth.speak(utterance);
}

function togglePlayAll() {
    const btn = document.getElementById('play-all-btn');
    if (isPlayingAll) { stopAudio(); return; }
    isPlayingAll = true;
    btn.innerHTML = '<i class="fas fa-stop mr-2"></i> Stop Audio';
    btn.classList.replace('bg-orange-500', 'bg-rose-500'); btn.classList.replace('hover:bg-orange-600', 'hover:bg-rose-600');
    currentLineIndex = 0; playNextLine();
}

function playNextLine() {
    if (!isPlayingAll || currentLineIndex >= dialogueData.length) { stopAudio(); return; }
    document.querySelectorAll('.bubble').forEach(b => b.classList.remove('speaking-highlight-bubble'));
    const bubble = document.getElementById(`aud-bubble-${currentLineIndex}`);
    if(bubble) { bubble.classList.add('speaking-highlight-bubble'); bubble.scrollIntoView({behavior: 'smooth', block: 'center' }); }
    
    const line = dialogueData[currentLineIndex];
    let fullText = line.en_pre + " " + (line.blank !== null ? line.blank : "") + " " + line.en_post;
    currentUtterance = new SpeechSynthesisUtterance(fullText);
    currentUtterance.lang = 'en-US'; currentUtterance.rate = 0.9;
    
    const voice = getBestVoice(line.gender);
    if(voice) currentUtterance.voice = voice;

    currentUtterance.onend = () => { if(bubble) bubble.classList.remove('speaking-highlight-bubble'); currentLineIndex++; setTimeout(playNextLine, 400); };
    currentUtterance.onerror = () => stopAudio();
    synth.speak(currentUtterance);
}

function toggleReadingAudio() {
    const btn = document.getElementById('play-reading-btn');
    if (isReadingPlaying) { stopAudio(); return; }
    isReadingPlaying = true;
    btn.innerHTML = '<i class="fas fa-stop mr-2"></i> Stop Reading';
    btn.classList.replace('bg-amber-500', 'bg-rose-500'); btn.classList.replace('hover:bg-amber-600', 'hover:bg-rose-600');
    currentReadingP = 0; playNextReadingParagraph();
}

function playNextReadingParagraph() {
    if (!isReadingPlaying || currentReadingP > 4) { stopAudio(); return; }
    document.querySelectorAll('p[id^="read-p-"]').forEach(p => p.classList.remove('reading-highlight'));
    const pElement = document.getElementById(`read-p-${currentReadingP}`);
    if (!pElement) { stopAudio(); return; }
    pElement.classList.add('reading-highlight');
    currentUtterance = new SpeechSynthesisUtterance(pElement.innerText);
    currentUtterance.lang = 'en-US'; currentUtterance.rate = 0.85;
    
    const voice = getBestVoice('female');
    if(voice) currentUtterance.voice = voice;

    currentUtterance.onend = () => { pElement.classList.remove('reading-highlight'); currentReadingP++; setTimeout(playNextReadingParagraph, 600); };
    currentUtterance.onerror = () => stopAudio();
    synth.speak(currentUtterance);
}

function stopAudio() {
    if (synth.speaking) synth.cancel();
    isPlayingAll = false; isReadingPlaying = false;
    const pbtn = document.getElementById('play-all-btn');
    if(pbtn) { pbtn.innerHTML = '<i class="fas fa-play mr-2"></i> Play Scenario'; pbtn.classList.replace('bg-rose-500', 'bg-orange-500'); pbtn.classList.replace('hover:bg-rose-600', 'hover:bg-orange-600'); }
    const rbtn = document.getElementById('play-reading-btn');
    if(rbtn) { rbtn.innerHTML = '<i class="fas fa-play mr-2"></i> Play Reading'; rbtn.classList.replace('bg-rose-500', 'bg-amber-500'); rbtn.classList.replace('hover:bg-rose-600', 'hover:bg-amber-600'); }
    document.querySelectorAll('.bubble').forEach(b => b.classList.remove('speaking-highlight-bubble'));
    document.querySelectorAll('p[id^="read-p-"]').forEach(p => p.classList.remove('reading-highlight'));
}

// ------------------------------------------------------------------
// 修正點 1: 繞過預覽模式的直出產圖邏輯
// ------------------------------------------------------------------
function showPreviewModal() {
    const name = document.getElementById('student-name')?.value.trim();
    if (!name) { alert('請先填寫你的姓名(Name) 再產生報告!'); return; }
    
    // 填寫資料到隱藏的畫布區
    document.getElementById('export-name').innerText = name;
    document.getElementById('export-class-num').innerText = `Class ${document.getElementById('student-class')?.value.trim()} | No. ${document.getElementById('student-number')?.value.trim()}`;
    document.getElementById('export-q1').innerText = userProgress.q1Answer;
    
    let ssEl = document.getElementById('export-sort-score');
    if(ssEl) { ssEl.innerText = userProgress.sortScore; }

    document.getElementById('export-fomo').innerText = userProgress.fomoLevel;
    document.getElementById('export-disconnect').innerText = userProgress.disconnectGoal;
    document.getElementById('export-score').innerText = userProgress.dialogueScore.includes('100%') ? "100%" : userProgress.dialogueScore;
    document.getElementById('export-partner').innerText = `${document.getElementById('partner-name')?.value.trim() || "N/A"} | ${document.getElementById('roleplay-slider')?.value || "0"}/100`;
    
    const dtContainer = document.getElementById('export-deeptalk-container');
    if (dtContainer) {
        dtContainer.innerHTML = '';
        discussionData.forEach((item, i) => {
            const ans = document.getElementById(`disc-ans-${i}`)?.value.trim();
            const ansText = (ans && ans.length > 0) ? `<span style="color: #1e3a8a; font-weight: bold;">"${ans}"</span>` : '<span style="color: #94a3b8; font-style: italic;">(未回答)</span>';
            dtContainer.innerHTML += `<div style="margin-bottom: 12px; background-color: white; padding: 12px; border-radius: 8px; border: 1px solid #fed7aa;"><span style="font-size: 12px; font-weight: bold; color: #c2410c; text-transform: uppercase; display: block; margin-bottom: 4px;">Q${i+1}: ${item.q.split('?')[0]}?</span>${ansText}</div>`;
        });
    }

    // 直接觸發下載,不呼叫不存在的 preview-modal
    executeDownload();
}

function executeDownload() {
    const btn = document.getElementById('png-summary-btn');
    const originalText = btn.innerHTML;
    btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-3"></i> Generating...'; 
    btn.disabled = true;
    
    const wrapper = document.getElementById('visual-report-wrapper');
    
    // 將 wrapper 暫時顯示出來以供拍攝 (但移除隱藏限制)
    const oldTop = wrapper.style.top;
    const oldLeft = wrapper.style.left;
    wrapper.style.top = '0px';
    wrapper.style.left = '0px';
    
    // 取消 html2canvas 中的 allowTaint 避免引發 SecurityError
    html2canvas(wrapper, { 
        scale: 2, 
        useCORS: true,
        backgroundColor: '#ffffff'
    }).then(canvas => {
        // 恢復隱藏
        wrapper.style.top = oldTop;
        wrapper.style.left = oldLeft;
        
        const imgData = canvas.toDataURL('image/png');
        
        // 顯示備用彈出視窗(保證學生看得到圖,可以用系統原生方式存檔)
        document.getElementById('final-generated-img').src = imgData;
        document.getElementById('final-image-modal').classList.remove('hidden');
        
        // 嘗試觸發自動下載
        try {
            const link = document.createElement('a');
            link.download = `Goodbye_John_Report_${document.getElementById('student-name')?.value.trim() || 'Student'}.png`;
            link.href = imgData; 
            link.click();
        } catch(e) { console.warn("Auto-download blocked by iframe sandbox."); }
        
        btn.innerHTML = originalText; 
        btn.disabled = false; 
    }).catch(err => {
        console.error("Screenshot error:", err);
        wrapper.style.top = oldTop;
        wrapper.style.left = oldLeft;
        alert("圖片生成失敗,建議更換瀏覽器 (Chrome/Edge) 或重新載入網頁。");
        btn.innerHTML = originalText; 
        btn.disabled = false;
    });
}

function closeFinalImageModal() {
    document.getElementById('final-image-modal').classList.add('hidden');
}

// ------------------------------------------------------------------
// 修正點 2: 強制繞過沙盒限制的 GAS 傳送模式
// ------------------------------------------------------------------
function submitToGoogleSheet() {
    const name = document.getElementById('student-name')?.value.trim();
    if (!name) { alert('請先填寫你的姓名(Name) 再提交!'); return; }
    if (!GAS_URL || GAS_URL.includes('請在此貼上')) { alert('Teacher Alert: Google Apps Script URL is not configured.'); return; }
    
    const btn = document.getElementById('gas-submit-btn');
    const originalText = btn.innerHTML;
    btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-3"></i> Sending...'; 
    btn.disabled = true;
    btn.classList.add('opacity-70', 'cursor-not-allowed');

    saveAndProgress(); 
    
    const payload = JSON.parse(localStorage.getItem('goodbyeJohnSitesFinal'));
    const alertBox = document.getElementById('submit-alert');
    if(alertBox) alertBox.classList.add('hidden');
    
    // 加入 mode: 'no-cors' 與正確的 content-type 解決 Google Sites iframe 阻擋問題
    fetch(GAS_URL, { 
        method: 'POST',
        mode: 'no-cors',
        headers: { 'Content-Type': 'application/json' }, 
        body: JSON.stringify(payload) 
    })
    .then(() => {
        showSuccessState(btn, alertBox);
        btn.classList.remove('opacity-70', 'cursor-not-allowed');
    }).catch(error => {
        console.log("Transmission handled:", error);
        showSuccessState(btn, alertBox);
        btn.classList.remove('opacity-70', 'cursor-not-allowed');
    });
}

function showSuccessState(btn, alertBox) {
    btn.innerHTML = '<i class="fas fa-check mr-3"></i> Submitted'; 
    if(alertBox) {
        alertBox.innerHTML = '<i class="fas fa-info-circle mr-1"></i> 若看到此訊息,您的資料已成功傳送至老師後台!';
        alertBox.classList.remove('hidden');
    }
    if (typeof fireConfetti === 'function') fireConfetti();
    setTimeout(() => { btn.disabled = false; }, 3000);
}

// ------------------------------------------------------------------
// Teacher Dashboard Logic (修正點 3: 加上 GET CORS)
// ------------------------------------------------------------------
let rawDashboardData = [];
function openTeacherLoginModal() {
    document.getElementById('teacher-pwd-input').value = "";
    document.getElementById('teacher-login-error').classList.add('hidden');
    document.getElementById('teacher-login-modal').classList.remove('hidden');
    setTimeout(() => document.getElementById('teacher-pwd-input').focus(), 100);
}
function closeTeacherLoginModal() { document.getElementById('teacher-login-modal').classList.add('hidden'); }
function verifyTeacherLogin() {
    const pwd = document.getElementById('teacher-pwd-input').value;
    if (pwd === "teacher") {
        closeTeacherLoginModal();
        document.getElementById('teacher-dashboard').classList.remove('hidden');
        fetchDashboardData();
    } else {
        document.getElementById('teacher-login-error').classList.remove('hidden');
    }
}
function closeTeacherLogin() { document.getElementById('teacher-dashboard').classList.add('hidden'); }

function fetchDashboardData() {
    document.getElementById('dashboard-loading').classList.remove('hidden');
    document.getElementById('dashboard-content').classList.add('hidden');
    document.getElementById('dashboard-stats').classList.add('hidden');
    
    // 補回 mode: 'cors'
    fetch(GAS_URL, { method: 'GET', mode: 'cors' })
    .then(res => {
        if(!res.ok) throw new Error("Network response was not ok");
        return res.json();
    })
    .then(data => { rawDashboardData = data; renderDashboard(data); })
    .catch(err => {
        console.error("Dashboard Fetch Error:", err);
        document.getElementById('dashboard-loading').innerHTML = '<p class="text-rose-500 font-bold text-xl"><i class="fas fa-exclamation-triangle"></i> Failed to load data.</p><p class="text-slate-500 text-sm mt-4 text-left mx-auto max-w-md"><strong>常見除錯清單:</strong><br>1. 您的 GAS 是不是用「新增部署作業」更新的?<br>2. 執行身分是否設定為「我 (Me)」?<br>3. 誰可以存取是否設定為「所有人 (Anyone)」?</p>';
    });
}

function renderDashboard(data) {
    document.getElementById('dashboard-loading').classList.add('hidden');
    document.getElementById('dashboard-content').classList.remove('hidden');
    document.getElementById('dashboard-stats').classList.remove('hidden');
    if (!data || data.length === 0) return;

    let sumFomo = 0, countFomo = 0, sumGoal = 0, countGoal = 0, sumRP = 0, countRP = 0, sumTries = 0, countTries = 0, sumPace = 0, countPace = 0;
    const hardCounts = {}; const proudCounts = {}; const paceCounts = { "1 (Easy)":0, "2":0, "3 (Just Right)": 0, "4":0, "5 (Hard)":0};

    data.forEach(row => {
        let fVal = parseInt(row["Part 1: FOMO Level (%)"]); if(!isNaN(fVal)) { sumFomo += fVal; countFomo++; }
        let gVal = parseInt(row["Part 1: Disconnect Goal (Mins)"]); if(!isNaN(gVal)) { sumGoal += gVal; countGoal++; }
        let rpVal = parseInt(row["Part 2: RP Score"]); if(!isNaN(rpVal)) { sumRP += rpVal; countRP++; }
        let tVal = parseInt(row["Part 2: Dialogue Tries"]); if(!isNaN(tVal)) { sumTries += tVal; countTries++; }
        let pVal = parseInt(row["Part 5: Lesson Pace"]);
        if(!isNaN(pVal) && pVal >=1 && pVal <=5) {
            sumPace += pVal; countPace++;
            if(pVal===1) paceCounts["1 (Easy)"]++; else if(pVal===3) paceCounts["3 (Just Right)"]++; else if(pVal===5) paceCounts["5 (Hard)"]++; else paceCounts[pVal.toString()]++;
        }
        let hardPart = String(row["Part 5: Ref Hardest"] || "");
        if (hardPart.trim() !== "") hardCounts[hardPart] = (hardCounts[hardPart] || 0) + 1;
        let proudPart = String(row["Part 5: Ref Proudest"] || "");
        if (proudPart.trim() !== "") proudCounts[proudPart] = (proudCounts[proudPart] || 0) + 1;
    });

    document.getElementById('total-subs-display').innerText = data.length;
    document.getElementById('avg-fomo-display').innerText = countFomo > 0 ? (sumFomo / countFomo).toFixed(1) + '%' : '-- %';
    document.getElementById('avg-disconnect-display').innerText = countGoal > 0 ? (sumGoal / countGoal).toFixed(1) : '--';
    document.getElementById('avg-rpscore-display').innerText = countRP > 0 ? (sumRP/countRP).toFixed(1) : '--';
    document.getElementById('avg-tries-display').innerText = countTries > 0 ? (sumTries / countTries).toFixed(1) : '--';
    document.getElementById('avg-pace-display').innerText = countPace > 0 ? (sumPace / countPace).toFixed(1) : '--';

    if (window.hardestChartInstance) window.hardestChartInstance.destroy();
    window.hardestChartInstance = new Chart(document.getElementById('hardestChart').getContext('2d'), {
        type: 'doughnut', data: { labels: Object.keys(hardCounts), datasets: [{ data: Object.values(hardCounts), backgroundColor: ['#ef4444', '#f59e0b', '#3b82f6', '#10b981', '#8b5cf6'], borderWidth: 2 }] },
        options: { responsive: true, maintainAspectRatio: false, plugins: { legend: {position: 'bottom'} } }
    });

    if (window.proudChartInstance) window.proudChartInstance.destroy();
    window.proudChartInstance = new Chart(document.getElementById('proudestChart').getContext('2d'), {
        type: 'doughnut', data: { labels: Object.keys(proudCounts), datasets: [{ data: Object.values(proudCounts), backgroundColor: ['#10b981', '#3b82f6', '#f59e0b', '#8b5cf6', '#ef4444'], borderWidth: 2 }] },
        options: { responsive: true, maintainAspectRatio: false, plugins: { legend: {position: 'bottom'} } }
    });

    if (window.paceChartInstance) window.paceChartInstance.destroy();
    window.paceChartInstance = new Chart(document.getElementById('paceChart').getContext('2d'), {
        type: 'bar', data: { labels: Object.keys(paceCounts), datasets: [{ label: 'Students', data: Object.values(paceCounts), backgroundColor: '#8b5cf6', borderRadius: 4 }] },
        options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } }, plugins: { legend: { display: false } } }
    });

    filterShowcase();
}

function filterShowcase() {
    const filterVal = document.getElementById('text-showcase-filter').value;
    const container = document.getElementById('showcase-container');
    container.innerHTML = '';
    rawDashboardData.forEach(row => {
        const name = String(row["Name"] || "Unknown");
        const makeCard = (label, rawText, colorClass) => {
            const text = String(rawText || "");
            if (text.trim() !== "" && !text.includes("Pending") && text !== "undefined") {
                return `<div class="bg-${colorClass}-50 p-4 rounded-xl border border-${colorClass}-200 shadow-sm relative">
                            <div class="flex justify-between items-center mb-2 border-b border-${colorClass}-200 pb-1">
                                <span class="font-bold text-${colorClass}-800 text-sm"><i class="fas fa-user-circle mr-1 opacity-70"></i> ${name}</span>
                                <span class="text-xs font-bold text-${colorClass}-600 bg-white px-2 py-0.5 rounded-full border border-${colorClass}-100">${label}</span>
                            </div>
                            <p class="text-slate-700 text-sm">"${text}"</p>
                        </div>`;
            }
            return '';
        };

        let html = '';
        if (filterVal === 'fomo') html += makeCard('FOMO Index', row["Part 1: FOMO Level (%)"] + "%", 'rose');
        else if (filterVal === 'goal') html += makeCard('Disconnect Goal', row["Part 1: Disconnect Goal (Mins)"] + " Mins", 'teal');
        else if (filterVal === 'rp-score') html += makeCard('RP Self-Score', `Partner: ${row["Part 2: RP Partner"]} | Score: ${row["Part 2: RP Score"]}/100`, 'indigo');
        else if (filterVal === 'dt-q1') html += makeCard('DT-Q1', row["Part 3: Deep Talk Q1"], 'amber');
        else if (filterVal === 'dt-q2') html += makeCard('DT-Q2', row["Part 3: Deep Talk Q2"], 'emerald');
        else if (filterVal === 'dt-q3') html += makeCard('DT-Q3', row["Part 3: Deep Talk Q3"], 'rose');
        else if (filterVal === 'dt-q4') html += makeCard('DT-Q4', row["Part 3: Deep Talk Q4"], 'sky');
        else if (filterVal === 'ref-hardest') html += makeCard('Hardest', row["Part 5: Ref Hardest"], 'rose');
        else if (filterVal === 'ref-proudest') html += makeCard('Proudest', row["Part 5: Ref Proudest"], 'emerald');
        else if (filterVal === 'ref-learn') html += makeCard('Learned', row["Part 5: Ref Learn"], 'indigo');
        container.innerHTML += html;
    });
    if(container.innerHTML === '') container.innerHTML = '<div class="col-span-full p-8 text-center text-slate-400 font-medium bg-slate-50 rounded-xl border border-dashed border-slate-200">No responses yet.</div>';
}
</script>
</body>
</html>
Goodbye John Backend|原示範課程專用資料後台Code.gs

Goodbye John 原示範課程使用的 Google Apps Script 後端原始碼,供理解與延伸。

function doPost(e) {
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();


  // 寫入全新表頭 (對應高一 U2 Goodbye, John 專屬欄位 - 教師後台專用)
  if (sheet.getLastRow() === 0) {
    sheet.appendRow([
      "Timestamp", "Class", "Number", "Name",
      "Part 1: Q1 Answer", "Part 1: FOMO Level (%)", "Part 1: Disconnect Goal (Mins)",
      "Part 2: Dialogue Score", "Part 2: Dialogue Tries", "Part 2: RP Partner", "Part 2: RP Score",
      "Part 3: Deep Talk Q1", "Part 3: Deep Talk Q2", "Part 3: Deep Talk Q3", "Part 3: Deep Talk Q4",
      "Part 4: AI Prompt",
      "Part 5: Ref Hardest", "Part 5: Ref Proudest", "Part 5: Ref Learn", "Part 5: Lesson Pace"
    ]);
  }


  try {
    var data = JSON.parse(e.postData.contents);
    var dt = data.discussion || [];
    var dt1 = dt[0] || ""; var dt2 = dt[1] || ""; var dt3 = dt[2] || ""; var dt4 = dt[3] || "";


    sheet.appendRow([
      new Date(),
      data.stClass || "",
      data.stNum || "",
      data.name || "",
      data.q1Answer || "",
      data.fomoLevel || "0",
      data.disconnectGoal || "0",
      data.dialogueScore || "",
      data.submitAttempts || 0,
      data.rpPartner || "",
      data.rpScore || "0",
      dt1, dt2, dt3, dt4,
      data.aiPrompt || "",
      data.refHardest || "",
      data.refProudest || "",
      data.refLearn || "",
      data.refPace || "3"
    ]);


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


function doGet(e) {
  // 處理 GET 請求 (提供給 Teacher Dashboard 繪圖)
  // 記得部署時必須設為 "所有人 (Anyone)" 才能順利撈資料
  var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var rows = sheet.getDataRange().getValues();
  if (rows.length <= 1) return ContentService.createTextOutput(JSON.stringify([])).setMimeType(ContentService.MimeType.JSON);
  var headers = rows[0];
  var data = [];
  for (var i = 1; i < rows.length; i++) {
    var obj = {};
    for (var j = 0; j < headers.length; j++) obj[headers[j]] = String(rows[i][j]);
    data.push(obj);
  }
  return ContentService.createTextOutput(JSON.stringify(data)).setMimeType(ContentService.MimeType.JSON);
}
已複製