0. 作品介紹

作者 陳乃誠 竹光國中

半夜還在回覆家長「明天段考考哪幾課?」、「手機可不可以帶去學校?」、「請假要怎麼辦?」嗎? 將班級日常資訊存入 Google 試算表,隨改隨生效;整合在線請假留言簿與群組防洗版機制,10 分鐘讓導師擁有 24 小時智慧分身!


264 0 0
班級經營
封面圖 封面

2. 資料表規則

請照下面規則建立分頁與欄位。

第1分頁: 班級經營與知識庫 A 欄: 領域分類 B 欄: 標題 / 主題 C 欄: 詳細說明內容與規範指引 D 欄: 啟用狀態 E 欄: 最後更新時間

📄 解析結果(自動表格)

分頁:班級經營與知識庫
A 欄 B 欄 C 欄 D 欄 E 欄
領域分類 標題 / 主題 詳細說明內容與規範指引 啟用狀態 最後更新時間

3. 開啟 Apps Script

從試算表開 Apps Script,才能直接用 SpreadsheetApp 讀寫資料。

  1. 試算表上方選單:擴充功能 → Apps Script
  2. 刪掉預設的程式碼(或全部選取覆蓋)
  3. 保留/建立檔案:
    • Code.gs
    • index.html

4. 貼上後端(Code.gs)

把後端程式貼到 Apps Script 的 Code.gs。

Code.gs(後端)
/**
 * ============================================================================
 * 🏫 全國班級導師 LINE Bot AI 智慧小幫手・通用開源範本 (GAS + Gemini RAG)
 * ============================================================================
 * 專為全台中小學、高中職各班班級導師設計的 24 小時智慧親師小助理!
 * 
 * 聚焦四大核心領域:
 * 1. 📈 學習情勢與學習歷程:作業繳交、課堂進度、平板自主學習、補救教學
 * 2. 📝 段考範圍與評量時程:各科考程表、命題範圍、命題重點、平時成績比例
 * 3. 🛡️ 班級經營與生活常規:班規、榮譽積點制度、手機保管規範、幹部職責
 * 4. 🎪 學校活動與親師溝通:學校日親師座談會、運動會、校外教學、請假留言
 * 
 * 核心功能:
 * • ⚡ Google 試算表即班級大腦:段考範圍、作業進度隨改隨生效,免工程師維護!
 * • 💌 在線請假與提問留言簿:支援「#請假」、「#提問」、「#留言」,自動生成編號入表!
 * • 🧠 未解問題自進化機制:家長特殊提問自動收集,導師批改作業空檔補上即可!
 * • 📱 班級群組防洗版過濾:私訊即問即答;在班級家長群需 @小幫手 始發言,不干擾日常!
 * • ⏳ LINE 原生輸入中載入動畫:防止家長因等待而重複點擊。
 * • 🚀 一鍵初始化函式:initKnowledgeBaseSheet() 自動建立 12 組班級常見標準問答。
 * ============================================================================
 */

const CONFIG = {
  // --------------------------------------------------------------------------
  // 1. 必填金鑰 (請至 Google AI Studio 與 LINE Developers 後台取得)
  // --------------------------------------------------------------------------
  GEMINI_API_KEY: '填入您的_GEMINI_API_KEY',
  LINE_ACCESS_TOKEN: '填入您的_LINE_CHANNEL_ACCESS_TOKEN',

  // --------------------------------------------------------------------------
  // 2. 學校與班級基本設定 (自動組裝溫暖專業的導師 AI 小分身)
  // --------------------------------------------------------------------------
  SCHOOL_NAME: '光明國民中學',                       // 學校名稱(例:光明國中、竹光國中)
  CLASS_NAME: '八年一班',                           // 班級名稱(例:七年三班、高一甲班)
  TEACHER_NAME: '陳乃誠老師',                        // 班級導師姓名
  BOT_NAME: '八一班級小幫手',                        // 機器人稱呼
  CONTACT_PHONE: '導師辦公室分機 201',               // 學校總機與分機
  CONTACT_TIME: '平日 08:00~17:00 (課間或放學後)',    // 方便通話時段
  CLASS_WEBSITE_URL: '',                           // 班級網站或雲端聯絡簿 (可選)

  // --------------------------------------------------------------------------
  // 3. 可選擴充模組 (若無需求請維持預設空字串 '',系統將自動略過不報錯)
  // --------------------------------------------------------------------------
  CALENDAR_ID: '',                                 // 班級公開 Google 行事曆 ID (段考、活動、班會)
  CALENDAR_WEB_URL: '',                            // 線上行事曆瀏覽網址

  // --------------------------------------------------------------------------
  // 4. 管理後台與工作表設定
  // --------------------------------------------------------------------------
  ADMIN_PASSWORD: '',                              // 手機 LINE 寫入密碼(留空即免密碼模式)
  SHEET_NAME: '班級經營與知識庫',                    // 班級知識庫主要儲存表
  LOG_SHEET_NAME: '親師對話紀錄',                    // 提問對話紀錄表
  LEAVE_SHEET_NAME: '學生請假留言簿',                // 家長請假與提問留言表 (#請假 #提問)
  UNANSWERED_SHEET_NAME: '待補充問題庫'             // AI 未命中待補問題表
};

// ============================================================================
// 🤖 動態組裝導師 AI 小幫手人設 Prompt (System Instruction)
// ============================================================================
function getSystemInstruction() {
  return `你是一位充滿愛心、專業、耐心且溝通溫暖的「${CONFIG.SCHOOL_NAME} ${CONFIG.CLASS_NAME} 班級 AI 小幫手」(班導師「${CONFIG.TEACHER_NAME}」的官方 AI 秘書)。
你的任務是協助 ${CONFIG.CLASS_NAME} 的家長與學生,清楚解答學習進度、作業與課業輔導、定期評量(段考)範圍與考程、班級經營規定、榮譽獎勵辦法、學生請假以及學校重大活動日程。

【基本資訊與導師聯絡管道】:
- 學校與班級:${CONFIG.SCHOOL_NAME} ${CONFIG.CLASS_NAME}
- 班級導師:${CONFIG.TEACHER_NAME}
- 導師分機:${CONFIG.CONTACT_PHONE}
- 適合通話時段:${CONFIG.CONTACT_TIME}
${CONFIG.CLASS_WEBSITE_URL ? '- 班級專區/聯絡簿網址:' + CONFIG.CLASS_WEBSITE_URL : ''}

【回答原則與排版規範(極重要)】:
1. 一律使用台灣繁體中文,口吻親切溫和、誠懇同理,像老師對家長面對面聊天般自然。
2. 常用台灣中小學校園生活術語:段考、定期評量、聯絡簿、第八節課輔、因材網、平板、班規、請假、病假、事假、晨讀、午休、打掃、運動會、親師座談會。
3. 【嚴格 LINE 排版規範】:
   - 嚴禁使用 Markdown 粗體星號(絕對不要輸出 **粗體** 或 *斜體* 或 ### 標題 或 - 破折號清單)!
   - 強調重點請使用【...】或「...」。
   - 清單一律使用 Emoji 圖示或數字(如 📚、📝、⏰、🎒、🟢、💡、👉、📅、🏢、•、1. 2. 3.)並適度空行,方便家長在手機 LINE 上舒適閱覽。
4. 【請假重要提示】:
   - 若家長要在 LINE 登記請假,請主動提醒格式:輸入「#請假 [座號/姓名] [假別與事由]」,系統會自動建立紀錄並通知老師!
5. 【個別學生隱私與重大事件】:
   - 涉及個別學生個人的家庭隱私、特殊身心身分或特定個案輔導狀況,請委婉告知家長:「因涉及孩子個人學習隱私,建議請於 ${CONFIG.CONTACT_TIME} 致電 ${CONFIG.CONTACT_PHONE} 與 ${CONFIG.TEACHER_NAME} 親自通話討論,親師共同守護孩子的成長!」
6. 若知識庫查無特定資訊,請誠懇告知家長,並主動引導家長輸入「#提問 [問題內容]」或於聯絡簿留言。`;
}

// ============================================================================
// 🌐 Web 介面與 LINE Webhook 入口
// ============================================================================

function doGet(e) {
  return HtmlService.createHtmlOutputFromFile('index')
    .setTitle(`${CONFIG.SCHOOL_NAME} ${CONFIG.CLASS_NAME} 班級小幫手 - 管理後台`)
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL)
    .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

function doPost(e) {
  try {
    if (!e || !e.postData || !e.postData.contents) {
      return ContentService.createTextOutput('OK').setMimeType(ContentService.MimeType.TEXT);
    }
    const data = JSON.parse(e.postData.contents);
    if (!data.events || data.events.length === 0) {
      return ContentService.createTextOutput('OK').setMimeType(ContentService.MimeType.TEXT);
    }

    const event = data.events[0];
    if (event.type === 'message' && event.message.type === 'text') {
      const userMessage = event.message.text.trim();
      const replyToken = event.replyToken;
      const isGroup = event.source.type === 'group' || event.source.type === 'room';
      const userId = event.source.userId || 'unknown';

      // 1. 啟動 LINE「正在輸入中...」載入動畫
      if (event.source.userId) {
        showLineLoading(event.source.userId);
      }

      // 2. 群組過濾:在班級家長群中,需 Tag @小幫手 或提及關鍵字才回應,避免干擾親師群
      if (isGroup) {
        const escapedBot = escapeRegExp(CONFIG.BOT_NAME);
        const escapedTeacher = escapeRegExp(CONFIG.TEACHER_NAME);
        const triggerPattern = new RegExp(`(@小幫手|班級小幫手|小幫手|導師小幫手|@${escapedBot}|${escapedBot}|${escapedTeacher})`, 'i');
        if (!triggerPattern.test(userMessage) && !userMessage.startsWith('#')) {
          return ContentService.createTextOutput('OK');
        }
      }

      // 3. 檢查是否為學生請假指令 (#請假 ...)
      if (/^(#請假|請假)\s*/.test(userMessage)) {
        const leaveReply = handleLeaveCommand(userMessage, userId);
        sendLineReply(replyToken, leaveReply);
        logConversation(userMessage, leaveReply, userId);
        return ContentService.createTextOutput('OK');
      }

      // 4. 檢查是否為家長在線提問/留言指令 (#提問, #留言, #聯絡 ...)
      if (/^(#提問|提問|#留言|留言|#聯絡|聯絡)\s*/.test(userMessage)) {
        const inquiryReply = handleInquiryCommand(userMessage, userId);
        sendLineReply(replyToken, inquiryReply);
        logConversation(userMessage, inquiryReply, userId);
        return ContentService.createTextOutput('OK');
      }

      // 5. 檢查是否為導師/管理員指令 (#查詢請假, #查詢提問, #記住, #查詢)
      if (userMessage.startsWith('#')) {
        const adminReply = handleAdminCommand(userMessage);
        sendLineReply(replyToken, adminReply);
        return ContentService.createTextOutput('OK');
      }

      // 6. 一般家長提問:執行 RAG 知識庫比對與 Gemini 智慧回覆
      const cleanPattern = new RegExp(`(@小幫手|班級小幫手|導師小幫手|小幫手|@${escapeRegExp(CONFIG.BOT_NAME)}|${escapeRegExp(CONFIG.BOT_NAME)}|${escapeRegExp(CONFIG.TEACHER_NAME)})`, 'gi');
      const cleanQuery = userMessage.replace(cleanPattern, '').trim();
      
      const aiReply = generateAiResponse(cleanQuery, userId);
      sendLineReply(replyToken, aiReply);
      logConversation(cleanQuery, aiReply, userId);
    }

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

// ============================================================================
// 📝 家長請假與在線留言處理 (#請假 / #提問 / #留言)
// ============================================================================

function handleLeaveCommand(text, userId) {
  const content = text.replace(/^(#請假|請假)\s*/, '').trim();
  if (!content) {
    return `📝 【${CONFIG.CLASS_NAME}・家長線上請假格式說明】\n\n` +
      '請依下列格式輸入:\n' +
      '👉 #請假 [學生座號或姓名] [請假日期] [假別與事由]\n\n' +
      '💡 範例:\n' +
      '• #請假 05號李小明 9/15(一) 因發燒就醫請病假一天\n' +
      '• #請假 王小華 9/20上午因家庭事由請事假半天\n\n' +
      `系統將自動建立請假登記單,並即時通知 ${CONFIG.TEACHER_NAME}!`;
  }

  const now = new Date();
  const twTimeStr = Utilities.formatDate(now, 'GMT+8', 'yyyy/MM/dd HH:mm');
  const leaveId = 'L' + Utilities.formatDate(now, 'GMT+8', 'MMddHHmm');

  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    let sheet = ss.getSheetByName(CONFIG.LEAVE_SHEET_NAME);
    if (!sheet) {
      sheet = ss.insertSheet(CONFIG.LEAVE_SHEET_NAME);
      sheet.appendRow(['登記單號', '登記時間', '類別', '家長ID', '學生與請假事由內容', '處理狀態', '導師回覆備註']);
      sheet.getRange('A1:G1').setBackground('#0f766e').setFontColor('#ffffff').setFontWeight('bold');
    }

    sheet.appendRow([leaveId, twTimeStr, '學生請假', userId, content, '⏳ 待導師確認', '']);
    return `📝 【${CONFIG.CLASS_NAME}・請假通報已受理】\n\n` +
      `• 請假單號:${leaveId}\n` +
      `• 通報時間:${twTimeStr}\n` +
      `• 請假內容:${content}\n\n` +
      `已同步登記至「${CONFIG.LEAVE_SHEET_NAME}」,並通知 ${CONFIG.TEACHER_NAME}!\n` +
      `溫馨提醒:若為連續病假 3 天以上,回校時請記得檢附就醫收據或診斷證明書。祝孩子早日康復!`;
  } catch (err) {
    console.error('handleLeaveCommand error:', err);
    return `⚠️ 抱歉,線上請假登記失敗,請直接致電學校轉分機:${CONFIG.CONTACT_PHONE} 與導師聯絡。`;
  }
}

function handleInquiryCommand(text, userId) {
  const content = text.replace(/^(#提問|提問|#留言|留言|#聯絡|聯絡)\s*/, '').trim();
  if (!content) {
    return `💌 【${CONFIG.CLASS_NAME}・家長在線留話登記】\n\n` +
      '若有特定事項需請導師了解,請依下列格式輸入:\n' +
      '👉 #提問 [家長稱謂與具體事項]\n\n' +
      '💡 範例:\n' +
      '• #提問 05號小明媽媽:想了解孩子近期上課分組表現\n' +
      '• #提問 12號大華爸爸:想請教英文科單字自主學習推薦資源\n\n' +
      `系統將自動轉交給 ${CONFIG.TEACHER_NAME},老師將在空堂或放學後回覆!`;
  }

  const now = new Date();
  const twTimeStr = Utilities.formatDate(now, 'GMT+8', 'yyyy/MM/dd HH:mm');
  const inquiryId = 'Q' + Utilities.formatDate(now, 'GMT+8', 'MMddHHmm');

  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    let sheet = ss.getSheetByName(CONFIG.LEAVE_SHEET_NAME);
    if (!sheet) {
      sheet = ss.insertSheet(CONFIG.LEAVE_SHEET_NAME);
      sheet.appendRow(['登記單號', '登記時間', '類別', '家長ID', '學生與請假事由內容', '處理狀態', '導師回覆備註']);
      sheet.getRange('A1:G1').setBackground('#0f766e').setFontColor('#ffffff').setFontWeight('bold');
    }

    sheet.appendRow([inquiryId, twTimeStr, '家長提問留言', userId, content, '⏳ 待導師回覆', '']);
    return `💌 【${CONFIG.CLASS_NAME}・提問留言已收到】\n\n` +
      `• 留言單號:${inquiryId}\n` +
      `• 登記時間:${twTimeStr}\n` +
      `• 留言內容:${content}\n\n` +
      `感謝家長的關心!${CONFIG.TEACHER_NAME} 將於空堂或課後查覆。若有急事亦可於 ${CONFIG.CONTACT_TIME} 致電分機 ${CONFIG.CONTACT_PHONE}。`;
  } catch (err) {
    console.error('handleInquiryCommand error:', err);
    return `⚠️ 抱歉,留言登記暫時發生異常,請直接透過聯絡簿留言或致電分機 ${CONFIG.CONTACT_PHONE}。`;
  }
}

// ============================================================================
// 📅 班級公開行事曆查詢 (可選模組)
// ============================================================================

function getClassCalendarSummary(query) {
  if (!CONFIG.CALENDAR_ID || CONFIG.CALENDAR_ID.trim() === '') {
    return '';
  }

  try {
    const calendar = CalendarApp.getCalendarById(CONFIG.CALENDAR_ID.trim());
    if (!calendar) return '';

    const now = new Date();
    const twNow = new Date(now.getTime() + (8 * 3600 * 1000) + (now.getTimezoneOffset() * 60000));
    
    let daysAhead = 14;
    let daysPast = 1;

    if (query.indexOf('整學期') !== -1 || query.indexOf('月曆') !== -1 || query.indexOf('下個月') !== -1) {
      daysAhead = 45;
    } else if (query.indexOf('段考') !== -1 || query.indexOf('考試') !== -1 || query.indexOf('學校日') !== -1) {
      daysAhead = 21;
    }

    const startTw = new Date(twNow.getTime() - (daysPast * 24 * 3600 * 1000));
    const endTw = new Date(twNow.getTime() + (daysAhead * 24 * 3600 * 1000));

    const events = calendar.getEvents(startTw, endTw);
    if (!events || events.length === 0) return '';

    const startStr = formatDateStr(startTw);
    const endStr = formatDateStr(endTw);
    const eventList = [];

    for (let i = 0; i < events.length; i++) {
      const e = events[i];
      const sum = e.getTitle().trim();
      if (!sum) continue;

      if (e.isAllDayEvent()) {
        const sTime = new Date(e.getStartTime().getTime() + (8 * 3600 * 1000) + (e.getStartTime().getTimezoneOffset() * 60000));
        const dateFmt = formatDateStr(sTime);
        const wStr = ['日', '一', '二', '三', '四', '五', '六'][sTime.getDay()];
        eventList.push({
          dateStr: dateFmt,
          display: `• 【${String(sTime.getMonth()+1).padStart(2, '0')}/${String(sTime.getDate()).padStart(2, '0')} (${wStr})】:${sum}`
        });
      } else {
        const s = new Date(e.getStartTime().getTime() + (8 * 3600 * 1000) + (e.getStartTime().getTimezoneOffset() * 60000));
        const dateFmt = formatDateStr(s);
        const wStr = ['日', '一', '二', '三', '四', '五', '六'][s.getDay()];
        const timeFmt = `${String(s.getHours()).padStart(2, '0')}:${String(s.getMinutes()).padStart(2, '0')}`;
        eventList.push({
          dateStr: dateFmt,
          display: `• 【${String(s.getMonth()+1).padStart(2, '0')}/${String(s.getDate()).padStart(2, '0')} (${wStr}) ${timeFmt}】:${sum}`
        });
      }
    }

    eventList.sort((a, b) => a.dateStr.localeCompare(b.dateStr));

    if (eventList.length === 0) {
      return `【班級重要日程】:在 ${startStr} 至 ${endStr} 期間查無特定登記活動${CONFIG.CALENDAR_WEB_URL ? '(可至線上行事曆 ' + CONFIG.CALENDAR_WEB_URL + ' 查看)' : ''}。`;
    }

    return `📅 【${CONFIG.CLASS_NAME}班級與學校重要行事明細 (${startStr} ~ ${endStr},共 ${eventList.length} 項)】:\n` +
      eventList.map(e => e.display).join('\n') +
      (CONFIG.CALENDAR_WEB_URL ? `\n\n【完整線上行事曆】:${CONFIG.CALENDAR_WEB_URL}` : '');
  } catch(e) {
    console.error('getClassCalendarSummary error:', e);
    return '';
  }
}

// ============================================================================
// 📚 班級知識庫管理核心函式
// ============================================================================

function getKnowledgeBaseData() {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    let sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
    if (!sheet) {
      initKnowledgeBaseSheet();
      sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
    }
    const values = sheet.getDataRange().getValues();
    if (values.length <= 1) return [];

    const list = [];
    for (let i = 1; i < values.length; i++) {
      const row = values[i];
      const cat = (row[0] || '').toString().trim();
      const title = (row[1] || '').toString().trim();
      const content = (row[2] || '').toString().trim();
      const status = (row[3] || '').toString().trim();

      if ((cat || title) && content && status !== '停用' && status !== 'false') {
        list.push({
          category: cat || '班級日常事項',
          title: title || cat,
          content: content
        });
      }
    }
    return list;
  } catch (e) {
    console.warn('getKnowledgeBaseData error:', e);
    return [];
  }
}

function handleAdminCommand(text) {
  const parts = text.split(' ');
  const cmd = parts[0];

  if (cmd === '#查詢請假' || cmd === '#查詢提問' || cmd === '#查詢留言') {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName(CONFIG.LEAVE_SHEET_NAME);
    if (!sheet) return '📋 目前尚無任何家長請假或提問紀錄!';
    const values = sheet.getDataRange().getValues();
    if (values.length <= 1) return '📋 目前尚無任何家長請假或提問紀錄!';

    let pending = [];
    for (let i = 1; i < values.length; i++) {
      if (values[i][5] && values[i][5].toString().includes('待')) {
        pending.push(`• [${values[i][0]}] ${values[i][2]} (${values[i][1]}):${values[i][4]}`);
      }
    }

    if (pending.length === 0) return '🎉 太棒了!目前所有家長請假與留言皆已處理完畢,無待辦事項!';
    return `📝 【待處理請假與留言清單 (${pending.length} 筆)】:\n` + pending.slice(0, 8).join('\n');
  }

  if (cmd === '#查詢') {
    const kw = parts.slice(1).join(' ');
    if (!kw) return '請輸入要查詢的關鍵字,例如:#查詢 段考';
    const list = getKnowledgeBaseData().filter(k => k.title.includes(kw) || k.content.includes(kw) || k.category.includes(kw));
    if (list.length === 0) return `🔍 查無包含「${kw}」的班級資訊。`;
    return `🔍 找到 ${list.length} 筆資料:\n` +
      list.map(x => `• [${x.category}] ${x.title}:${x.content.slice(0, 40)}...`).join('\n');
  }

  if (cmd === '#記住') {
    let content = parts.slice(1).join(' ');
    if (CONFIG.ADMIN_PASSWORD && parts[1] === CONFIG.ADMIN_PASSWORD) {
      content = parts.slice(2).join(' ');
    } else if (CONFIG.ADMIN_PASSWORD) {
      return '❌ 密碼錯誤,無法寫入知識庫。';
    }

    if (!content) return '❌ 請提供要記住的內容,例如:#記住 明天數學小考改至週四早自習';

    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
    const title = content.slice(0, 15);
    sheet.appendRow(['臨時班級快訊', title, content, '啟用', new Date()]);
    return `✅ 已成功寫入班級知識庫!\n【分類】臨時班級快訊\n【項目】${title}\n【內容】${content}`;
  }

  return '💡 導師常用指令:\n• #請假 [座號/事由]\n• #提問 [內容]\n• #查詢請假\n• #查詢 [關鍵字]\n• #記住 [快訊內容]';
}

// ============================================================================
// 🧠 RAG 知識庫比對與 Gemini AI 生成
// ============================================================================

function generateAiResponse(userQuery, userId) {
  if (!CONFIG.GEMINI_API_KEY || CONFIG.GEMINI_API_KEY.includes('填入您的')) {
    return '⚠️ 系統提示:尚未在 Code.gs 中設定 GEMINI_API_KEY,請先前往 Google AI Studio 取得免費金鑰!';
  }

  const kbData = getKnowledgeBaseData();
  const contextText = kbData.map(function(item) {
    return '【主題:' + item.category + '】項目:' + item.title + '\n內容:' + item.content;
  }).join('\n\n');

  // 1. 判斷是否需要擴充模組資訊(行事曆)
  let liveCalendarContext = '';
  if (CONFIG.CALENDAR_ID && /(行事曆|段考|考試|學校日|運動會|校慶|放假|露營|畢業旅行|時程|下週|這週|明天|今天)/i.test(userQuery)) {
    liveCalendarContext = getClassCalendarSummary(userQuery);
  }

  const prompt = getSystemInstruction() + '\n\n' +
    '以下是「' + CONFIG.SCHOOL_NAME + ' ' + CONFIG.CLASS_NAME + ' 班級經營與課業指引知識庫」:\n' +
    '--------------------------------\n' +
    contextText + '\n' +
    '--------------------------------\n' +
    (liveCalendarContext ? ('\n【📅 即時連線班級重要日程】:\n' + liveCalendarContext + '\n--------------------------------\n') : '') +
    '\n家長或同學的提問是:「' + userQuery + '」\n\n' +
    '請依據上述知識庫、導師人設與回答規範,給出溫暖體貼、條理清晰、易於閱讀的解答:';

  const payload = {
    contents: [{ parts: [{ text: prompt }] }],
    generationConfig: { temperature: 0.2 }
  };

  const models = getAuthorizedGoogleModels(CONFIG.GEMINI_API_KEY);
  if (models.length === 0) {
    return '⚠️ Google API 金鑰異常:無法從 Google 取得可用模型,請確認 API Key 是否在 Google AI Studio 正常啟用!';
  }

  let lastError = '';
  for (let i = 0; i < models.length; i++) {
    const model = models[i];
    try {
      const url = 'https://generativelanguage.googleapis.com/v1beta/models/' + model + ':generateContent?key=' + CONFIG.GEMINI_API_KEY;
      const response = UrlFetchApp.fetch(url, {
        method: 'post',
        contentType: 'application/json',
        payload: JSON.stringify(payload),
        muteHttpExceptions: true
      });

      const resJson = JSON.parse(response.getContentText());
      if (resJson.candidates && resJson.candidates[0] && resJson.candidates[0].content && resJson.candidates[0].content.parts && resJson.candidates[0].content.parts[0].text) {
        const finalReply = sanitizeLineFormat(resJson.candidates[0].content.parts[0].text);
        
        // 🧠 若 AI 查無答案,自動收集進待補充清單
        if (finalReply.includes('目前沒有這項特定資料') || (finalReply.includes('致電') && finalReply.includes(CONFIG.TEACHER_NAME))) {
          logUnansweredQuestion(userQuery, finalReply, userId || 'anonymous');
        }
        return finalReply;
      } else if (resJson.error) {
        lastError = '[' + model + '] ' + (resJson.error.message || JSON.stringify(resJson.error));
      }
    } catch (e) {
      lastError = '[' + model + '] 連線失敗: ' + e.toString();
    }
  }

  if (lastError.indexOf('RESOURCE_EXHAUSTED') !== -1 || lastError.indexOf('429') !== -1) {
    return '⚠️ 系統提示:目前連線較為踴躍,請稍候 3 秒後再次提問!';
  }
  return '⚠️ 抱歉,AI 服務連線稍有延遲,請稍候片刻再試一次,或直接致電 ' + CONFIG.TEACHER_NAME + '(' + CONFIG.CONTACT_PHONE + ')。';
}

function getAuthorizedGoogleModels(apiKey) {
  const cache = CacheService.getScriptCache();
  const cached = cache.get('CLASS_GEMINI_MODELS_V1');
  if (cached) {
    try { return JSON.parse(cached); } catch(e) {}
  }

  try {
    const url = 'https://generativelanguage.googleapis.com/v1beta/models?key=' + apiKey;
    const resp = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
    const data = JSON.parse(resp.getContentText());
    if (data.models && data.models.length > 0) {
      const valid = [];
      for (let i = 0; i < data.models.length; i++) {
        const m = data.models[i];
        if (m.supportedGenerationMethods && m.supportedGenerationMethods.indexOf('generateContent') !== -1) {
          valid.push(m.name.replace('models/', ''));
        }
      }
      valid.sort(function(a, b) {
        if (a.indexOf('flash') !== -1 && b.indexOf('flash') === -1) return -1;
        if (a.indexOf('flash') === -1 && b.indexOf('flash') !== -1) return 1;
        return 0;
      });
      if (valid.length > 0) {
        cache.put('CLASS_GEMINI_MODELS_V1', JSON.stringify(valid), 86400);
        return valid;
      }
    }
  } catch (e) {
    console.warn('getAuthorizedGoogleModels error:', e);
  }
  return ['gemini-2.5-flash', 'gemini-1.5-flash', 'gemini-1.5-flash-latest'];
}

// ============================================================================
// 🚀 工具函式庫 (LINE 訊息傳送、排版美化、日期格式化)
// ============================================================================

function sendLineReply(replyToken, messageText) {
  if (!CONFIG.LINE_ACCESS_TOKEN || CONFIG.LINE_ACCESS_TOKEN.includes('填入您的')) {
    console.warn('LINE_ACCESS_TOKEN 未設定');
    return;
  }
  const url = 'https://api.line.me/v2/bot/message/reply';
  UrlFetchApp.fetch(url, {
    method: 'post',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + CONFIG.LINE_ACCESS_TOKEN
    },
    payload: JSON.stringify({
      replyToken: replyToken,
      messages: [{ type: 'text', text: messageText }]
    }),
    muteHttpExceptions: true
  });
}

function showLineLoading(chatId) {
  if (!chatId || !CONFIG.LINE_ACCESS_TOKEN || CONFIG.LINE_ACCESS_TOKEN.includes('填入您的')) return;
  try {
    UrlFetchApp.fetch('https://api.line.me/v2/bot/chat/loading/start', {
      method: 'post',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + CONFIG.LINE_ACCESS_TOKEN
      },
      payload: JSON.stringify({
        chatId: chatId,
        loadingSeconds: 15
      }),
      muteHttpExceptions: true
    });
  } catch (e) {
    console.warn('showLineLoading failed:', e);
  }
}

function sanitizeLineFormat(text) {
  if (!text) return '';
  return text
    .replace(/\*\*(.*?)\*\*/g, '【$1】')
    .replace(/\*(.*?)\*/g, '$1')
    .replace(/^#+\s+/gm, '')
    .replace(/^-\s+/gm, '• ')
    .trim();
}

function escapeRegExp(string) {
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function formatDateStr(date) {
  const y = date.getFullYear();
  const m = String(date.getMonth() + 1).padStart(2, '0');
  const d = String(date.getDate()).padStart(2, '0');
  return `${y}-${m}-${d}`;
}

function logConversation(query, reply, userId) {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    let sheet = ss.getSheetByName(CONFIG.LOG_SHEET_NAME);
    if (!sheet) {
      sheet = ss.insertSheet(CONFIG.LOG_SHEET_NAME);
      sheet.appendRow(['提問時間', '家長/學生ID', '提問內容', '小幫手回覆摘要']);
      sheet.getRange('A1:D1').setBackground('#0f766e').setFontColor('#ffffff').setFontWeight('bold');
    }
    sheet.appendRow([new Date(), userId, query, reply.slice(0, 150)]);
  } catch (e) {
    console.warn('logConversation error:', e);
  }
}

function logUnansweredQuestion(query, aiReply, userId) {
  try {
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    let sheet = ss.getSheetByName(CONFIG.UNANSWERED_SHEET_NAME);
    if (!sheet) {
      sheet = ss.insertSheet(CONFIG.UNANSWERED_SHEET_NAME);
      sheet.appendRow(['提問時間', '提問者ID', '家長詢問問題', 'AI 當下回覆', '導師備忘補充', '處理狀態']);
      sheet.getRange('A1:F1').setBackground('#b91c1c').setFontColor('#ffffff').setFontWeight('bold');
    }
    sheet.appendRow([new Date(), userId, query, aiReply.slice(0, 80), '', '⏳ 待導師補充']);
  } catch (e) {
    console.warn('logUnansweredQuestion error:', e);
  }
}

// ============================================================================
// 🚀 一鍵初始化函式:建立 12 組全台中小學班級常見標準題庫
// ============================================================================

function initKnowledgeBaseSheet() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName(CONFIG.SHEET_NAME);
  if (!sheet) {
    sheet = ss.insertSheet(CONFIG.SHEET_NAME);
  } else {
    sheet.clear();
  }

  sheet.appendRow(['領域分類', '標題 / 主題', '詳細說明內容與規範指引', '啟用狀態', '最後更新時間']);
  sheet.getRange('A1:E1').setBackground('#0f766e').setFontColor('#ffffff').setFontWeight('bold');

  const defaultKnowledge = [
    // ------------------------------------------------------------------------
    // A. 學習情勢與學習歷程 (3 題)
    // ------------------------------------------------------------------------
    [
      "學習情勢與歷程",
      "每日作業繳交、家庭聯絡簿簽閱與缺交補交機制",
      `1. 【聯絡簿簽閱時間】:請家長每日晚間主動查閱孩子的家庭聯絡簿,確認各科作業清單、小考成績並於「家長簽名欄」簽章。
2. 【每日作業繳交時限】:翌日早自習 07:45 前由各科小老師收齊送交各任課老師;若遇缺交,幹部將登記於「每日作業檢核表」。
3. 【補繳作業規範】:當日午休前完成補繳得予銷單;累計缺繳達 3 次未繳者,將於聯絡簿紅字提醒並由導師利用午休或放學後留校個別輔導補齊。
💡 家長若想了解孩子特定學科繳交狀況,可直接於聯絡簿留言或在 LINE 輸入「#提問 [學生姓名] 查詢作業」。`,
      "啟用",
      new Date()
    ],
    [
      "學習情勢與歷程",
      "課堂學習進度規劃與生生用平板自主學習指引",
      `1. 【各科教學進度】:學校依教育部 108 課綱與學期排課進度授課,各科任課教師每週會將當週教學進度及評量預告更新於班級聯絡簿。
2. 【生生用平板數位學習】:
   • 本班配合教育部「推動中小學數位學習精進方案」,課堂導入因材網、Cool English、均一教育平台與學習吧。
   • 學生皆有專屬教育雲端帳號(OpenID),放學返家亦可使用個人電腦或平板登入進行課後非同步複習。
3. 【自主學習追蹤】:導師會定期檢視學生因材網學習任務完成率,針對概念弱點進行個別化診斷提醒。`,
      "啟用",
      new Date()
    ],
    [
      "學習情勢與歷程",
      "第八節課後輔導(課藝活動)與教育部學習扶助(補救教學)實施辦法",
      `1. 【第八節課後輔導】:
   • 實施時段:週一至週四 16:20 ~ 17:05(17:10 放學)。
   • 實施原則:秉持「自由參加、學生自願、家長同意」原則,於學期初發放家長同意書辦理。
   • 教學重點:著重國英數社自等主科加深加廣、會考題型解題思維與統整演練。
2. 【教育部學習扶助班(補救教學)】:
   • 針對前次段考或學力檢測特定科目未達基礎標準之學生,由學校免費開設 6~10 人小班制精緻補救課程。
   • 經家長同意後,由專任師資針對單元迷思概念重點補強,學費全額由教育部補助。`,
      "啟用",
      new Date()
    ],

    // ------------------------------------------------------------------------
    // B. 段考範圍與評量時程 (3 題)
    // ------------------------------------------------------------------------
    [
      "段考範圍與日程",
      "第一次定期評量(段考)時程、考程表與放學作息調整",
      `1. 【考試日期】:學期第 8~9 週(依學校教務處行事曆正式公告為準,通常為週二至週三)。
2. 【考試作息調整】:
   • 每日 07:30 ~ 07:50 晨間到校安靜自修,08:15 第一節開始考。
   • 段考期間全校暫停第八節課輔,每日下午 15:55 準時放學(未留校溫書者此時離校)。
3. 【應考必備文具】:
   • 黑色 2B 鉛筆、橡皮擦(畫卡用,禁止使用非 2B 筆劃卡)。
   • 黑色或藍色原子筆、修正帶(國文手寫、數學計算過程必備)。
   • 嚴禁隨身攜帶手機或具聯網通訊功能之智慧手錶進入試場。`,
      "啟用",
      new Date()
    ],
    [
      "段考範圍與日程",
      "各科定期評量(段考)命題範圍與命題重點一覽",
      `【本學期第一次定期評量範圍】:
• 【國文】:第 1 課至第 4 課(含語文常識一、課本注釋、成語選粹與論語選讀)。
• 【英語】:Unit 1 ~ Unit 3(含 Review 1、單字片語、課文文法重點與英聽測驗)。
• 【數學】:第 1 章 乘法公式與多項式 ~ 第 2 章 平方根與畢氏定理。
• 【自然/理化】:第 1 章 基本測量 ~ 第 2 章 物質的世界(含實驗操作題型)。
• 【社會(歷/地/公)】:
   - 歷史:第 1 ~ 2 章
   - 地理:第 1 ~ 2 章(地形與氣候)
   - 公民:第 1 ~ 2 章(個人與家庭生活常規)
💡 各科任課老師已發放段考複習卷,請督促孩子提早擬定溫書計畫!`,
      "啟用",
      new Date()
    ],
    [
      "段考範圍與日程",
      "學生學期成績評量計算標準與平時多元評量計分方式",
      `1. 【學期成績配分比例】:
   • 定期評量(三次段考平均):佔 40%
   • 平時成績(多元評量總和):佔 60%
2. 【平時多元評量包含項目】:
   • 各單元紙筆測驗與隨堂小考:佔 30%
   • 課堂作業、習作與學習單繳交品質:佔 30%
   • 課堂學習態度、分組合作專案、口頭發表與上課參與度:佔 40%
💡 導師特別提醒:平時作業認真書寫、準時繳交,以及課堂積極參與,佔比高達六成,請同學切勿只在段考前臨時抱佛腳!`,
      "啟用",
      new Date()
    ],

    // ------------------------------------------------------------------------
    // C. 班級經營與生活常規 (3 題)
    // ------------------------------------------------------------------------
    [
      "班級經營與常規",
      "班級公約、每日晨間作息與生活常規守則",
      `1. 【晨間作息規範】:
   • 07:30 ~ 07:50:到校時間,進教室後立即交齊作業至小老師桌面。
   • 07:50 ~ 08:25:晨讀靜息時間或導師時間,教室保持安靜,嚴禁吃早餐、聊天或走動(請於 07:45 前用畢早餐)。
2. 【核心班級公約】:
   • 【自律尊重】:尊重師長、友愛同學,禁止使用言語嘲諷、肢體碰撞或網路霸凌。
   • 【環境維護】:個人座位周遭落實「垃圾不落地」,每日放學徹底清空抽屜,桌椅對齊地磚線。
   • 【課堂專注】:鐘聲響畢 1 分鐘內就座,擺妥該節課本與文具,非當節課物品一律收入抽屜。`,
      "啟用",
      new Date()
    ],
    [
      "班級經營與常規",
      "班級榮譽點數(集點制度)與正向管教獎懲辦法",
      `1. 【個人與小組積點獎勵】:
   • 個人加分:段考/小考進步、作業特優、主動熱心公德、擔任值日幹部盡責、代表班級參賽。
   • 小組競賽:每週依課堂回答、作業全齊率、整潔評比累計「組別榮譽榜」。
2. 【榮譽點數兌換機制】:
   • 累積滿 20 點:可兌換「座位優先自選權一週」或「小文具點心獎勵」。
   • 累積滿 50 點:於學期末親師座談會獲頒「班級品學優良榮譽狀」並記嘉獎肯定。
3. 【違規叮嚀】:遲到、作業缺交、課堂喧嘩將予以提醒扣點或執行班級公共服務勞動。`,
      "啟用",
      new Date()
    ],
    [
      "班級經營與常規",
      "學生在校手機與智慧手錶保管規範、防潮鎖櫃管理辦法",
      `1. 【手機攜帶申請】:學生攜帶手機入校須由家長填具「學生在校攜帶行動電話申請書」備查。
2. 【班級養機場管理原則】:
   • 每日早自習 07:45 前,副班長統一督促全班將手機關機,收齊鎖入「班級防潮手機鎖櫃」。
   • 手機鑰匙由導師親自保管,放學前打掃完畢後統一發還。
3. 【緊急聯絡方式】:在校期間若有突發緊急事情需聯繫家長,可經師長同意後借用導師辦公室電話或至教務處公用電話聯絡。`,
      "啟用",
      new Date()
    ],

    // ------------------------------------------------------------------------
    // D. 學校活動與親師溝通 (3 題)
    // ------------------------------------------------------------------------
    [
      "學校活動與溝通",
      "學生請假手續(病假/事假)、差勤回報與當日通報管道",
      `1. 【當日臨時病假】:
   • 請家長於早上 07:45 前,直接在 LINE 輸入:「#請假 [座號姓名] [病假事由]」,系統會自動登記並發送給導師。
   • 或可於 07:30 ~ 08:00 致電學校總機轉 ${CONFIG.CONTACT_PHONE} 告知導師。
2. 【事前事假手續】:請於 3 天前由家長於家庭聯絡簿填寫事由,或由學生至學務處領取正式請假單簽核。
3. 【銷假與就醫證明】:病假連續 3 天以上,依學務處規定需檢附醫療收據或就醫診斷證明以完成銷假程序。`,
      "啟用",
      new Date()
    ],
    [
      "學校活動與溝通",
      "學校日(全校親師座談會)活動時程、座談主題與預約個別晤談",
      `1. 【學校日辦理時間】:每學期第 3 週之週五晚間 18:30 ~ 21:00(請見學校紙本邀請函及行事曆)。
2. 【親師座談流程】:
   • 18:30 ~ 19:15:全校校務與各處室願景報告(禮堂/線上直播)。
   • 19:20 ~ 20:30:班級導師時間(導師班級經營理念、會考學力展望、各科教學進度介紹)。
   • 20:30 ~ 21:00:班級家長代表(家長委員)推選與自由親師交流。
3. 【個別深度晤談預約】:若家長希望針對孩子的個別學習或情緒狀態進行深度對談,歡迎於聯絡簿留言預約空堂時段。`,
      "啟用",
      new Date()
    ],
    [
      "學校活動與溝通",
      "本學期校園重大活動(校慶運動會、戶外教育/隔宿露營、家長志工招募)",
      `1. 【全校運動會】:預計於 11 月中旬舉行,當日全天出缺席列入出勤考查,歡迎家長蒞校參觀為孩子加油打氣!
2. 【戶外教育/隔宿露營/畢業旅行】:
   • 行前一至兩個月將發放完整家長同意書、攜帶物品清單、服裝規定與退費規範。
   • 學生如有特殊體質(如氣喘、心臟病、特定藥物過敏),請務必於健康調查表中如實填報。
3. 【班級家長志工招募】:誠摯邀請熱心家長擔任班級家長代表、圖書志工或活動交通導護志工,共同為孩子的班級成長護航!`,
      "啟用",
      new Date()
    ]
  ];

  defaultKnowledge.forEach(function(row) {
    sheet.appendRow(row);
  });

  sheet.autoResizeColumns(1, 5);
  console.log('🎉 班級導師小幫手標準知識庫初始化完成!已填入 ' + defaultKnowledge.length + ' 筆班級核心題庫。');
}
貼到 Apps Script 的 Code.gs 檔案

5. 貼上前端(index.html)

把前端程式貼到 Apps Script 的 index.html。

index.html(前端)
<!DOCTYPE html>
<html lang="zh-TW">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>光明國民中學 八年一班 八一班級小幫手 - 班級管理後台</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
  <style>
    body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans TC", sans-serif; }
    .code-scroll::-webkit-scrollbar { height: 6px; width: 6px; }
    .code-scroll::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
  </style>
</head>
<body class="bg-slate-100 text-slate-800 min-h-screen">

  <!-- 頂部導航 -->
  <header class="bg-indigo-950 text-white shadow-md">
    <div class="max-w-6xl mx-auto px-4 py-4 flex items-center justify-between">
      <div class="flex items-center space-x-3">
        <div class="w-10 h-10 rounded-xl bg-indigo-500 flex items-center justify-center text-xl font-bold shadow-md">
          <i class="fa-solid fa-chalkboard-user"></i>
        </div>
        <div>
          <h1 class="text-lg font-bold">光明國民中學 八年一班 八一班級小幫手</h1>
          <p class="text-xs text-indigo-200">八年一班・親師溝通與段考範圍管理後台</p>
        </div>
      </div>
      <div class="flex items-center space-x-2">
        <span class="px-3 py-1 bg-indigo-900 border border-indigo-700 rounded-full text-xs text-indigo-200">
          <i class="fa-solid fa-circle text-emerald-400 text-[10px] mr-1"></i>Gemini RAG 運行中
        </span>
      </div>
    </div>
  </header>

  <!-- 主內容區 -->
  <main class="max-w-6xl mx-auto px-4 py-8 space-y-6">

    <!-- 快捷分類標籤與搜尋 -->
    <div class="bg-white p-5 rounded-2xl border border-slate-200 shadow-sm space-y-4">
      <div class="flex flex-col sm:flex-row items-center justify-between gap-3">
        <div class="relative w-full sm:w-80">
          <i class="fa-solid fa-magnifying-glass absolute left-3 top-3 text-slate-400 text-sm"></i>
          <input type="text" id="searchInput" placeholder="搜尋作業、段考、範圍、班規、請假、活動..." 
            class="w-full pl-9 pr-4 py-2 text-sm border border-slate-300 rounded-xl focus:ring-2 focus:ring-indigo-500 focus:outline-none"
            oninput="filterCards()">
        </div>
        <div class="flex flex-wrap gap-1.5 text-xs">
          <button onclick="filterCategory('ALL')" class="cat-btn px-3 py-1.5 rounded-lg bg-indigo-700 text-white font-medium">全部</button>
          <button onclick="filterCategory('學習情勢與歷程')" class="cat-btn px-3 py-1.5 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200">學習情勢與歷程</button>
          <button onclick="filterCategory('段考範圍與日程')" class="cat-btn px-3 py-1.5 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200">段考範圍與日程</button>
          <button onclick="filterCategory('班級經營與常規')" class="cat-btn px-3 py-1.5 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200">班級經營與常規</button>
          <button onclick="filterCategory('學校活動與溝通')" class="cat-btn px-3 py-1.5 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200">學校活動與溝通</button>
        </div>
      </div>
    </div>

    <!-- 知識庫卡片展示區 -->
    <div id="loading" class="text-center py-12 text-slate-400">
      <i class="fa-solid fa-spinner fa-spin text-3xl mb-3 text-indigo-600"></i>
      <p class="text-sm">正在載入試算表班級資料庫...</p>
    </div>

    <div id="cardGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 hidden"></div>

    <div id="emptyState" class="text-center py-16 bg-white rounded-2xl border border-slate-200 shadow-sm hidden">
      <i class="fa-solid fa-folder-open text-4xl text-slate-300 mb-2"></i>
      <p class="text-slate-500 text-sm">找不到符合搜尋條件的班級項目</p>
    </div>

  </main>

  <script>
    let rawData = [];
    let currentCategory = 'ALL';

    window.onload = function() {
      google.script.run
        .withSuccessHandler(renderData)
        .withFailureHandler(function(err) {
          document.getElementById('loading').innerHTML = 
            '<p class="text-rose-500 font-medium">載入失敗:' + err + '</p>';
        })
        .getKnowledgeBaseData();
    };

    function renderData(data) {
      rawData = data || [];
      document.getElementById('loading').classList.add('hidden');
      document.getElementById('cardGrid').classList.remove('hidden');
      filterCards();
    }

    function filterCategory(cat) {
      currentCategory = cat;
      document.querySelectorAll('.cat-btn').forEach(btn => {
        if (btn.innerText.includes(cat) || (cat === 'ALL' && btn.innerText === '全部') || (btn.getAttribute('onclick') && btn.getAttribute('onclick').includes(cat))) {
          btn.className = 'cat-btn px-3 py-1.5 rounded-lg bg-indigo-700 text-white font-medium';
        } else {
          btn.className = 'cat-btn px-3 py-1.5 rounded-lg bg-slate-100 text-slate-600 hover:bg-slate-200 font-medium';
        }
      });
      filterCards();
    }

    function filterCards() {
      const kw = document.getElementById('searchInput').value.trim().toLowerCase();
      const grid = document.getElementById('cardGrid');
      const empty = document.getElementById('emptyState');
      grid.innerHTML = '';

      const filtered = rawData.filter(item => {
        const matchCat = currentCategory === 'ALL' || item.category.includes(currentCategory);
        const matchKw = !kw || item.title.toLowerCase().includes(kw) || item.content.toLowerCase().includes(kw);
        return matchCat && matchKw;
      });

      if (filtered.length === 0) {
        empty.classList.remove('hidden');
        return;
      }
      empty.classList.add('hidden');

      filtered.forEach(item => {
        const card = document.createElement('div');
        card.className = "bg-white p-5 rounded-2xl border border-slate-200 shadow-sm flex flex-col justify-between hover:border-indigo-400 transition-all";
        card.innerHTML = `
          <div class="space-y-3">
            <span class="inline-block px-2.5 py-1 rounded-md text-xs font-semibold bg-indigo-50 text-indigo-700 border border-indigo-200">
              ${escapeHtml(item.category)}
            </span>
            <h3 class="font-bold text-slate-900 text-base leading-snug">${escapeHtml(item.title)}</h3>
            <div class="text-xs text-slate-600 whitespace-pre-line leading-relaxed code-scroll max-h-48 overflow-y-auto bg-slate-50 p-3 rounded-xl border border-slate-100">
              ${escapeHtml(item.content)}
            </div>
          </div>
          <div class="pt-4 mt-3 border-t border-slate-100 flex items-center justify-between text-[11px] text-slate-400">
            <span><i class="fa-solid fa-check text-emerald-500 mr-1"></i>試算表即時連動</span>
            <span class="text-indigo-600 font-medium">Gemini RAG</span>
          </div>
        `;
        grid.appendChild(card);
      });
    }

    function escapeHtml(str) {
      if (!str) return '';
      return String(str)
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;');
    }
  </script>
</body>
</html>
貼到 Apps Script 的 index.html 檔案

6. 部署

部署成 Web App 後,就會拿到可分享網址。

  1. Apps Script 右上:部署 → 新部署
  2. 類型選:Web app
  3. 執行身分:你
  4. 存取權限:任何人
  5. 完成後複製網址

💬 評論區

老師們的交流與提問

載入評論中...