這篇文章記錄了我昨天臨時抱佛腳,花了一下午加一整晚,利用 Gemini Gems 為兩門高三選修課(選修地科與生涯規劃)打造專屬「AI 對談助教」的實戰經驗。
在地科課中,我讓 AI 提議並建構了一個涵蓋 60 個前瞻議題的跨領域知識庫,引導學生找尋專屬報告主題,並導入「預先評分」機制;在生涯規劃課中,AI 則化身為「人格拷問官」,將心理學理論結合台灣高中生情境,最後為學生繪製一張專屬的特質圖像。
雖然實際在課堂上運用時,發生了「一開始給錯連結」以及「對話紀錄無法分享只能靠截圖」等真實的意外插曲,但看著學生順利透過 AI 進行深度的自我探索,讓我更確信:只要老師設計好資料庫與引導邏輯,AI 絕對是教學現場最得力的助手。歡迎大家點開內文的連結親自體驗!
分類彙整: AI
發佈留言
【開發心得】與 AI 奮戰兩週:宮澤賢治主題實地解謎手遊上架實錄
Gemini的Deep Research產出研究報告中的LaTex語法轉換
如果Gemini的Deep Research產出研究報告裡面有數學公式或上下標,研究報告匯出成Google文件時,就會有很多$….$的符號,無法轉換成正確格式。
找Gemini解決這個問題非常不成功,就換成ChatGPT,大致上解決了。
解決的方法是土法煉鋼,將文章中LaTex的語法一一對照取代,用窮舉式的寫法,一個代碼一個代碼設定在程式碼裡面。未來若遇到程式不認得的代碼,就再加進去。
當初有個瓶頸是替換LaTex代碼時,文章格式會跑掉,但AI最後還是想辦法解決了。
程式碼寫在Google文件的Apps Script裡面,跟著一份Google文件走。所以複製這份Google文件就可以有這個功能,不用輸入程式碼。
這是可以複製去用的文件範本,點選連結後,網頁畫面就會直接要求你建立一個副本,文件中有使用說明。
結尾有我和AI的對話,其中有如何將APP Script加入Google文件的流程。未來如果有LaTex代碼沒解析到的,可以把程式碼貼給AI叫他加,或是自己手動加在程式碼裡面。
程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | function onOpen() { DocumentApp.getUi() .createMenu('LaTeX 轉換') .addItem('建立轉換後的新文件(保留格式)', 'createLatexConvertedCopy') .addToUi(); } // 1. 複製整份文件 → 2. 在副本中逐段處理 $...$ 與 frac{...}{...} function createLatexConvertedCopy() { var srcDoc = DocumentApp.getActiveDocument(); var srcId = srcDoc.getId(); // 1. 用 Drive 複製整份文件(格式、圖片、表格都保留) var file = DriveApp.getFileById(srcId); var copy = file.makeCopy(srcDoc.getName() + '(LaTeX 轉換版)'); var newDoc = DocumentApp.openById(copy.getId()); var body = newDoc.getBody(); // 2. 在副本裡處理所有段落 / 清單 / 表格中的文字 processElement_(body); newDoc.saveAndClose(); DocumentApp.getUi().alert( '已建立 LaTeX 轉換版文件(格式及圖片保留):\n' + newDoc.getUrl() ); } /** * 遞迴走訪整個文件樹: * - Paragraph / ListItem → 以「整行文字」為單位處理 * - Table / Cell → 繼續往下走 */ function processElement_(elem) { var ElementType = DocumentApp.ElementType; var type = elem.getType && elem.getType(); if (type === ElementType.PARAGRAPH || type === ElementType.LIST_ITEM) { var text = elem.editAsText(); if (text) { convertParagraphText_(text); } } if (elem.getNumChildren) { var n = elem.getNumChildren(); for (var i = 0; i < n; i++) { processElement_(elem.getChild(i)); } } } /** * 對單一段落 (editAsText): * 1. 先處理整段裡所有 $...$ * 2. 再處理「裸的」 frac{a}{b} */ function convertParagraphText_(text) { var full = text.getText(); if (full.indexOf('$') === -1 && full.indexOf('frac{') === -1) return; // --- 找出所有 $...$ 的位置(用原始 full 索引)--- var dollarMatches = []; var re = /\$([^$]+)\$/g; var m; while ((m = re.exec(full)) !== null) { dollarMatches.push({ start: m.index, end: re.lastIndex - 1, inner: m[1] }); } // 從後往前改,避免索引被前面的修改影響 for (var i = dollarMatches.length - 1; i >= 0; i--) { var dm = dollarMatches[i]; var converted = latexToUnicode_(dm.inner); text.deleteText(dm.start, dm.end); if (converted.length > 0) { text.insertText(dm.start, converted); } } // --- 再處理沒有 $ 包起來的 frac{a}{b} --- full = text.getText(); var fracRe = /frac\{([^}]+)\}\{([^}]+)\}/g; var fracMatches = []; while ((m = fracRe.exec(full)) !== null) { fracMatches.push({ start: m.index, end: fracRe.lastIndex - 1, num: m[1], den: m[2] }); } for (var j = fracMatches.length - 1; j >= 0; j--) { var fm = fracMatches[j]; var rep = '(' + fm.num + '/' + fm.den + ')'; text.deleteText(fm.start, fm.end); text.insertText(fm.start, rep); } } /** * 處理一小段 LaTeX(不含外層 $) * 例如:"V_E", "R_{\\text{ref}}", "\\mu = 3.986 \\times 10^{14}" */ function latexToUnicode_(latex) { var t = latex; // 1. \text{ km } → km t = t.replace(/\\text\{([^}]*)\}/g, '$1'); // 2. \frac{a}{b} → (a/b) t = t.replace(/\\frac\{([^}]+)\}\{([^}]+)\}/g, function(match, num, den) { return '(' + num + '/' + den + ')'; }); // 3. LaTeX 指令 → 對應符號(μ, ρ, ×, ≈, ∞...) var map = getSymbolMap_(); t = t.replace(/\\[a-zA-Z]+/g, function(cmd) { return map[cmd] || cmd.slice(1); // 未定義 → 去掉反斜線 }); // 4. _{...} / _x / ^{...} / ^x → Unicode 上下標 return applySubSupUnicode_(t); } /** * 把 _、^ 語法變成 Unicode 上/下標字元 */ function applySubSupUnicode_(str) { var result = ''; var i = 0; while (i < str.length) { var ch = str.charAt(i); if ((ch === '_' || ch === '^') && i + 1 < str.length) { var isSub = (ch === '_'); var next = str.charAt(i + 1); var content = ''; var j; if (next === '{') { j = i + 2; while (j < str.length && str.charAt(j) !== '}') { content += str.charAt(j); j++; } if (j < str.length && str.charAt(j) === '}') j++; } else { content = next; j = i + 2; } result += mapToSubSup_(content, isSub); i = j; } else { result += ch; i++; } } return result; } /** * 把 content 裡每個字元映射成上標/下標 * 支援 a–z / A–Z / 0–9 */ function mapToSubSup_(content, isSub) { // 下標對照表 var subMap = { '0':'₀','1':'₁','2':'₂','3':'₃','4':'₄','5':'₅','6':'₆','7':'₇','8':'₈','9':'₉', 'a':'ₐ','b':'ᵦ','c':'꜀','d':'꜁','e':'ₑ','f':'ᵮ','g':'ᵧ','h':'ₕ', 'i':'ᵢ','j':'ⱼ','k':'ₖ','l':'ₗ','m':'ₘ','n':'ₙ','o':'ₒ','p':'ₚ', 'q':'ᑫ','r':'ᵣ','s':'ₛ','t':'ₜ','u':'ᵤ','v':'ᵥ','w':'𝓌','x':'ₓ', 'y':'ᵧ','z':'𝓏', // 大寫下標(無正式 Unicode,下列用近似字元) 'A':'ₐ','B':'ᵦ','C':'꜀','D':'ᵭ','E':'ₑ','F':'𝆑','G':'ᵍ', 'H':'ₕ','I':'ᵢ','J':'ⱼ','K':'ₖ','L':'ₗ','M':'ₘ','N':'ₙ','O':'ₒ', 'P':'ₚ','Q':'ᑫ','R':'ᵣ','S':'ₛ','T':'ₜ','U':'ᵤ','V':'ᵥ', 'W':'𝓌','X':'ₓ','Y':'ᵧ','Z':'𝓏' }; // 上標對照表 var supMap = { '0':'⁰','1':'¹','2':'²','3':'³','4':'⁴','5':'⁵','6':'⁶','7':'⁷','8':'⁸','9':'⁹', 'a':'ᵃ','b':'ᵇ','c':'ᶜ','d':'ᵈ','e':'ᵉ','f':'ᶠ','g':'ᵍ','h':'ʰ', 'i':'ⁱ','j':'ʲ','k':'ᵏ','l':'ˡ','m':'ᵐ','n':'ⁿ','o':'ᵒ','p':'ᵖ', 'q':'ᑫ','r':'ʳ','s':'ˢ','t':'ᵗ','u':'ᵘ','v':'ᵛ','w':'ʷ','x':'ˣ', 'y':'ʸ','z':'ᶻ', // 大寫上標 'A':'ᴬ','B':'ᴮ','C':'ᶜ','D':'ᴰ','E':'ᴱ','F':'ᶠ','G':'ᴳ', 'H':'ᴴ','I':'ᴵ','J':'ᴶ','K':'ᴷ','L':'ᴸ','M':'ᴹ','N':'ᴺ', 'O':'ᴼ','P':'ᴾ','Q':'ᑫ','R':'ᴿ','S':'ˢ','T':'ᵀ','U':'ᵁ', 'V':'ⱽ','W':'ᵂ','X':'ˣ','Y':'ʸ','Z':'ᶻ' }; var table = isSub ? subMap : supMap; var out = ''; for (var i = 0; i < content.length; i++) { var c = content.charAt(i); out += (table[c] || c); } return out; } /** * LaTeX → Unicode 符號表(可再擴充) */ function getSymbolMap_() { return { // 希臘字母 '\\mu': 'μ', '\\rho': 'ρ', '\\alpha': 'α', '\\beta': 'β', '\\gamma': 'γ', '\\delta': 'δ', '\\epsilon': 'ε', '\\varepsilon': 'ε', '\\theta': 'θ', '\\lambda': 'λ', '\\sigma': 'σ', '\\omega': 'ω', '\\Omega': 'Ω', // 常用數學符號 '\\infty': '∞', '\\times': '×', '\\cdot': '·', '\\approx': '≈', '\\propto': '∝', '\\leq': '≤', '\\geq': '≥', '\\neq': '≠', '\\gg': '≫', '\\ll': '≪', '\\pm': '±', '\\sqrt': '√' }; } |
程式碼寫在Google文件的Apps Script裡面,跟著一份Google文件走。所以複製這份Google文件就可以有這個功能,不用輸入程式碼。
這是可以複製去用的文件範本,點選連結後,網頁畫面就會直接要求你建立一個副本,文件中有使用說明。
這是和AI的對話過程。

