作者彙整: Han-Chiang Chou

【教學分享】當 AI 成為專屬對談助教:利用 Gemini Gems 設計高三選修課

這篇文章記錄了我昨天臨時抱佛腳,花了一下午加一整晚,利用 Gemini Gems 為兩門高三選修課(選修地科與生涯規劃)打造專屬「AI 對談助教」的實戰經驗。

在地科課中,我讓 AI 提議並建構了一個涵蓋 60 個前瞻議題的跨領域知識庫,引導學生找尋專屬報告主題,並導入「預先評分」機制;在生涯規劃課中,AI 則化身為「人格拷問官」,將心理學理論結合台灣高中生情境,最後為學生繪製一張專屬的特質圖像。

雖然實際在課堂上運用時,發生了「一開始給錯連結」以及「對話紀錄無法分享只能靠截圖」等真實的意外插曲,但看著學生順利透過 AI 進行深度的自我探索,讓我更確信:只要老師設計好資料庫與引導邏輯,AI 絕對是教學現場最得力的助手。歡迎大家點開內文的連結親自體驗!

閱讀全文

【開發心得】與 AI 奮戰兩週:宮澤賢治主題實地解謎手遊上架實錄

這篇文章是我這兩週瘋狂旅程的總結。我把老婆熱愛的遊戲靈感,轉化成了帶領玩家走入《銀河鐵道之夜》世界的實體冒險。在 AI 的領航下,我用 Flutter 完成了跨平台手遊開發,並在岩手縣的地圖上撒下了宮澤賢治的記憶碎片。我想分享的不只是技術操作,更多的是在 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的對話過程

批次編輯商品屬性

WooCommerce 後台清單頁,上方可用分類/標籤篩選;提供母子式下拉選單,批次為勾選商品加入指定屬性與屬性項目(若無該屬性則先建立並排在最後)。列表移除SKU欄位;每個全域屬性各自成欄顯示;屬性類別以 slug 升冪排列;欄位標題可換行;外層視需要允許水平滾動。
閱讀全文

項目符號的形式

order list的項目:

  • none ( 沒有符號 )
  • cjk-ideographic ( 一、二、三 )
  • decimal ( 1、2、3 )
  • decimal-leading-zero ( 01、02、03 )
  • lower-alpha ( a、b、c )
  • upper-alpha ( A、B、C )
  • lower-roman ( i、ii、iii )
  • upper-roman ( I、II、III )
  • initial

語法:

1
<ol style="list-style-type:upper-alpha">

unorder list的項目:

  • disc
  • circle
  • square
  • none
1
<li style="list-style-type: circle;">

大量新增wordpress部落格文章的外掛 – WP Ultimate CSV Importer

大概很少人需要用到這種「一次在wordpress部落格新增很多文章」的工具,卻是我非常重要的工具!

因為我的加菲貓漫畫部落格(http://mygarfield.org/),常常一次張貼幾十篇甚至上百篇翻譯,其中最辛苦的步驟是建立「張貼日期」。因為每一篇漫畫都有原始的張貼日期,但是我都累積很多篇才一起張貼,所以要設定每一篇的張貼日期就會非常痛苦。

過去我是用mass-page-maker這個外掛,它可以設定每一篇文章的張貼時間間隔,例如設定24小時,那他大量產生的新文章就會是每天一則,然後我再編寫內容就可以。不過這個外掛9年前就停止更新,而且最近終於無法正常使用了。

網路上大量新增文章的外掛其實不少,但很少有可以設定張貼日期的功能。後來找到以CSV匯入文章的方式似乎可行,只要在EXCEL檔裡面把每篇文章的張貼日期先設好,匯出成CSV檔再上傳wordpress就好。

目前我找到最好用的是WP Ultimate CSV Importer這個外掛,他可以在CSV檔上傳之後,讓使用者選擇CSV檔案內的每個欄位,要對應到wordpress文章的哪些欄位,非常好用。

總而言之,加菲貓漫畫的翻譯工作又可以繼續更新了!

google文件無法使用自然輸入法?!其實現在chrome不用安裝輸入法了,有線上輸入法可以使用!

最近突然發現自然輸入法無法在Google文件上輸入文字(Chrome瀏覽器),下載新酷音就可以輸入文字,但選字時看不到游標。原來是Google文件現在已經提供線上輸入法,可能是程式上的衝突,導致電腦系統的輸入法反而出問題。所以雖然要熟悉一個新輸入法,但不用安裝輸入法實在太棒,特別是到了其他國家沒有中文輸入法,或是突然想輸入日文還得另外安裝輸入法,應該是「利大於弊」吧!

呼叫線上輸入法的方式:
Windows:Ctrl + Shift + K
Mac Os:Cmd + Shift + K

全形標點符號:
逗號(,):Shift + ,
句號(。):Shift + .
問號(?):Shift + /
冒號(:):Shift + ;
分號(;):切換為全型後(shift+空白鍵 )再按 ;
頓號(、):\
驚嘆號(!):Shift + 1
上引號(「):[
下引號(」):]
左括號(():Shift + 9
右括號()):Shift + 0
全形字元:Shift + 空白鍵

詳細介紹:
Google 輸入工具!支援繁體中文注音輸入、拼音、日文、韓文、手寫辨識..等 40 國語言輸入法(Google Chrome)by 重灌狂人