Prep|開始前先準備好
- 強烈建議用桌機或筆電;若能準備雙載具,邊看邊操作會更方便。
- 選一篇你想拿來操作的英文課文/教材;若已經有 AI 教學網頁,先準備好 HTML 程式碼。
- Google 個人帳號通常最容易操作。學校帳號也可以,但每校管理設定不同;操作時盡量只登入一個帳號。
Warm-up|先看看我們現在在哪裡
開場調查
Sylvia’s Data Flow Wall|開場彈幕
開場彈幕 QR Code
開始之前
不是每堂課都需要網頁。如果紙筆能更快、更好,就用紙筆。
Presentation|Experience the why
先當一次學生
先體驗學生端,再看教師端。
Goodbye, John|示範課程
Goodbye, John 示範課程 QR Code
先問自己:好不好玩?如果到這裡就結束,老師其實知道了什麼?
三個教學轉變
| 轉變 | 真正的價值 |
|---|---|
| Every student responds. | 從少數幾位主動學生的聲音,變成每個人都留下思考痕跡。 |
| The teacher sees the whole class. | Dashboard 不是代替老師,而是先把 30–40 位學生的訊號整理給老師看。 |
| Data tells you what NOT to teach. | 數據不只是告訴你要補教什麼,也能告訴你哪些內容可以不用重教。 |
Behind the magic
學生操作
固定資料通道
留下資料
看見全班
你不需要自己會寫這些程式。今天真正要學的是:先決定什麼 Learning Evidence 值得留下,再讓 AI 幫我們把前端、後端與資料接起來。
Practice|Learn the CARE workflow
From Play to Proof is the goal; CARE is how we get there.
Technical Prep|先把固定後台準備好
為了降低現場除錯風險,我們先把全場共用、已驗證的資料通道準備完成。這一步只是先把技術底座準備好;真正的 R,會在 A 階段確認資料後才發生。
Prep.1|建立 Google Sheet
- 在 Google Drive 新增一份 Google 試算表。
- 不需要先設計表頭與分頁;Universal GAS 會依前端網頁標題自動建立專屬活動分頁,並自動建立/擴充欄位(無標題時自動 fallback 至 Responses 分頁)。
- 從試算表上方選單開啟:擴充功能 → Apps Script。
Prep.2|貼上 Universal GAS 萬用後端(終身免換試算表版)
- 刪除 Apps Script 內的預設程式碼。
- 複製下方完整 Universal GAS 萬用後端程式碼。
- 貼上後儲存。
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
- 點右上角「部署」→「新增部署作業」。
- 類型選「網頁應用程式」。
- 執行身分選「我」;誰可以存取務必選「所有人 (Anyone)」。
- 完成授權後,複製 Web App URL,先留在記事本備用。
完成後,今天甚至往後一輩子都不需要再回 Apps Script 改程式或換網址;接下來的所有課次教學活動主要都在 AI 與 HTML 中操作。
C — Create|先有一份能正常操作的 HTML
已經有自己的 HTML 就直接使用,不需要重做。沒有的話,可以用講師 Starter HTML、把紙本學習單交給 AI,或從 10 個 Starter Prompts 挑一個快速生成。
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>
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 對話:
- 自己的 HTML 網頁程式碼
- CARE Universal Upgrade Prompt(萬用升級助手)
- 剛才取得的自己的 GAS Web App URL
這份 Prompt 會幫你做什麼?
| 功能 | 白話版 |
|---|---|
| 保留原本活動 | 不會為了加新功能,就把你好不容易做好的網頁整個重做。 |
| 判斷哪些學習資料值得留下 | AI 先幫你想:哪些學生表現真的值得記錄,而不是什麼都收。 |
| 老師先確認 | AI 第一回合只提規劃;你說「可以,照這個做」之後才開始改。 |
| 產生學生個人學習報告 | 學生做完不只看到分數,也能回顧自己的作答與學習。 |
| 沒有後台也能完成 | 即使資料暫時送不出去,學生仍可完成活動、看到報告並下載成果。 |
| 有後台時自動收資料 | 學生作答可送進 Google Sheet,老師不用逐份整理。 |
| 整理全班學習狀況 | 建立 Teacher Dashboard,快速看出哪些題目或觀念需要處理。 |
| 自動檢查與防呆 | 盡量保護原本已成功的功能;出錯時只修必要部分。 |
點開後只要上傳您的 HTML 檔案 + 貼上您的 GAS URL,連 Prompt 都不用複製,AI 直接為您無腦升級!
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 真正接進資料流。後端已經準備好,這裡不再回頭部署;只驗證資料有沒有真的走通。
- 開啟完成版 HTML,確認原本活動、Learning Report、右上角 Teacher 入口都正常。
- 以學生身分只送 1 筆測試資料。
- 確認 Google Sheet 出現資料,而且欄位符合剛才確認的 Learning Evidence。
- 打開 Teacher Dashboard,確認 KPI、圖表與開放式回答都能讀回同一筆資料。
| Safety Checkpoint | 通過標準 |
|---|---|
| 1|HTML | 可以正常開啟與操作。 |
| 2|Google Sheet | 收得到 1 筆測試資料。 |
| 3|Teacher Dashboard | 讀得到同一筆資料。 |
學生完成 → Learning Report → Download PNG → Classroom/LMS → 課後再用 AI/Gemini Notebook 整理。
GAS 是即時資料回收的進階路徑,不是使用教學網頁的入場券。
E — Evaluate & Respond|看到資料後,下一步怎麼教?
打開 Dashboard,問自己:「看到這些資料後,我下一步會怎麼教?」
| 層次 | 用途 |
|---|---|
| 課堂當下 | 直接看 Teacher Dashboard,處理「現在我要怎麼教?」 |
| 課後分析 | 把 Sheet 資料交給 AI,處理「這批資料還告訴我什麼?」 |
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、表格或學生回答貼在這裡】
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
每張圖最後用一句話回答:「老師看到這張圖後,最值得採取什麼行動?」
若資料不足以產生某張圖,請直接說明,不要猜測。
Production|Use it for real
Production 的目標不是再寫一次程式,而是讓另一個人真的使用,而且老師真的收到 Learning Evidence。
Step 1|Publish
R 的三個 Safety Checkpoints 都通過後,再把完成版 HTML 放到 Google Sites。
- 開啟 Google Sites,進入要放學習單的頁面。
- 使用「嵌入程式碼」貼上 AI 產出的完整 HTML。
- 預覽正常後插入並發布。
Google Sites 是免費、穩定、低門檻的發布方式;未來也可以學習 Netlify、GitHub Pages 等其他方式。
Step 2|One real response
把網站交給一位夥伴老師實際作答,再確認:學生端 → Sheet → Dashboard 整條資料流在正式發布環境中也成功。
Step 3|Production Check
完成標準不是「我有一份程式碼」;而是另一個人真的可以使用這個網站,而且老師真的看得到 Learning Evidence。
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 Links|課後延伸
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);
}