---
# System prepended metadata

title: 英文刷題程式建立  2 __本地端加入測驗程式和Gemini API

---

# 在練習程式上加入輔助工具

---
待改進清單
04/03 生成過的可以在更新一次生成內容(已解決)
04/04 例句範圍限定成多益常考邏輯(已解決)
04/17 API設計需要一次大量
04/19 語音模擬多益題目(Gemini Multi-modal Live)

---


這是屬於第二章
這邊的基礎是建立於上一章節的內容
要記得

---
## BUG大集合：
- 1. 當出現這Bug 代表你的vs code資料出現錯誤，先嘗試把舊的***vocab_app.db*** 資料庫檔案刪除
![image](https://hackmd.io/_uploads/SkxvhjAcZx.png)
![螢幕擷取畫面 2026-03-23 200700](https://hackmd.io/_uploads/SJYZTsCqWe.png)
一樣關閉原始cmd再重新開一個cmd
輸入啟動指令
==streamlit run app.py==
終端機中可以按 ctrl+c來重新跳出輸入行

---
- 2. 出現停用指令碼執行的問題，基本上是因為Windows 為了保護系統，預設禁止執行任何腳本（.ps1 檔案），所以虛擬環境（venv）啟動腳本被擋下了。
![image](https://hackmd.io/_uploads/r1VIDsU3Wg.png)
```
PS D:\pye> & d:\pye\.venv\Scripts\Activate.ps1
& : 因為這個系統上已停用指令碼執行，所以無法載入 D:\pye\.venv\Scripts\Activate.ps1 檔案。如需詳細資訊，請參閱 about_Execution_Policies，網址為 https:/go.microsoft.com/fwlink/?LinkID=135170。
位於 線路:1 字元:3
+ & d:\pye\.venv\Scripts\Activate.ps1
+   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    + FullyQualifiedErrorId : UnauthorizedAccess
PS D:\pye> 
```
但這是因為我們的命令提示字元切換到PoewrShell，切換回Command Promt可以先不用管這問題。
![image](https://hackmd.io/_uploads/HkFzYoIhbe.png)



---


重點：
==自己的api晶鑰請小心，絕對不可以公開，避免惡意盜用去做高耗運算讓你帳單起飛==


## h3**學習資源參考：**
開始使用 Google AI Studio
https://steam.oxxostudio.tw/category/aigc/2025/google-ai-studio-1.html
官方資料
https://ai.google.dev/gemini-api/docs?hl=zh-tw
筆記軟體 MarkDown語法
https://hackmd.io/@eMP9zQQ0Qt6I8Uqp2Vqy6w/SyiOheL5N/%2FBVqowKshRH246Q7UDyodFA
費率限制
https://ai.google.dev/gemini-api/docs/rate-limits?hl=zh-tw

## 前置作業 申請Gemini的API晶鑰
進入官方網站申請，理論不需要使用付費方案就能使用
這邊會有新的專有名詞需要注意，之後都會使用到
* `RPM` 每分鐘請求數（requests per minute）
* `TPM` 每分鐘 Token 數（tokens per minute）
* `RPD` 每日請求數（requests per day）
---
進入官網，點選 ==Get API Key==來申請(或是他創辦帳號完就已自動升成一個能使用的API晶鑰)
    
    https://aistudio.google.com/
![image](https://hackmd.io/_uploads/ryEkAV6sbe.png)


---
==Usage==這頁面負責後台管理，選定的ai版本用了多少資源跟次數，在偵錯時候能直觀看到到底用了哪一個版本跟次數，也就是前文說到的
這在管理跟開發上會需要特別注意，
![image](https://hackmd.io/_uploads/H1TnfBps-x.png)
![image](https://hackmd.io/_uploads/B1fVErasWe.png)



常見錯誤表：
強烈推薦到官方文件中搜尋 **Gemini API 後端服務錯誤代碼** 
我目前有遇過在串接API會出現404錯誤，後來發現是搜尋不到所設定的Gemini AI工具版本，所以語法上更改成使用詢問方式解決，程式解說部份再詳解。
    
    https://ai.google.dev/gemini-api/docs/troubleshooting?hl=zh-tw#error-codes





# 1  先練習加入Gemini API



在vs code的cmd中輸入 ``` pip install google-generativeai```
更新```pip install --upgrade google-generativeai```

## 一、 系統架構流程圖
1. 系統初始化與 Excel 匯入流程 (去重複邏輯)
這部分描述了 init_db 的欄位補齊功能，以及 save_to_db_upsert 如何處理重複單字。
![Untitled diagram-2026-04-02-181608](https://hackmd.io/_uploads/rkPFGNniZl.png =70%x)
2. AI 教學內容生成邏輯 (快取與 API 調度)
這部分描述了程式如何自動選擇模型，並利用資料庫作為 Cache (快取) 來節省 API 額度。
![Untitled diagram-2026-04-02-182031](https://hackmd.io/_uploads/SyZt743oZx.png =70%x)
3. 循環測驗與單字池管理 (防止重複抽題)
這部分解釋了 q_pool 如何運作，確保在單元測完前不重複，測完後自動重啟。
![Untitled diagram-2026-04-02-182145](https://hackmd.io/_uploads/SJJJ4N3sZl.png =70%x)



## 二、 Imports
`streamlit`建立 Web 介面、按鈕、側邊欄及分頁。全域：負責所有畫面的呈現。
`pandas`處理 Excel 匯入及資料表的篩選與操作。檔案處理：上傳 Excel 並轉為 DataFrame。
`gtts`,`Google Text-to-Speech`將文字轉為語音檔。發音功能：點擊喇叭按鈕時生成音訊。
`sqlite3`輕量級資料庫，存放單字、錯誤次數與 AI 內容。資料持久化：確保關閉程式後資料不遺失。
`random`,隨機挑選單字與生成測驗選項。測驗模式：洗牌選項與隨機抽題。
`base64`,將音訊二進位資料轉為編碼字串。自動播放：讓語音不需下載即可在瀏覽器播放。
`json`處理 AI 生成的多個例句 (List/Dict) 轉換。AI 儲存：將複雜資料轉為字串存入資料庫。
`google.generativeai`串接 Google Gemini 1.5 大模型。AI 教學：生成字根分析、記憶法與例句。

## 三、 核心函式 (Functions) 的功能解說
- `init_db():`
用途：資料庫初始化與升級。
環節：程式一啟動即執行，確保資料表結構正確。
  

- `save_to_db_upsert():`
用途：存入單字並「去重複」。若單字已存在則更新 Unit 而非新增。
環節：側邊欄上傳 Excel 後執行。

- `load_full_db():`
用途：從 SQLite 讀取全部單字回 Pandas DataFrame。
環節：主畫面渲染前，確保抓到最新數據。

- `get_ai_learning_content():`
用途：核心 AI 請求邏輯。包含自動選擇模型（Flash/Pro）與清理 JSON 字串。
環節：點擊生成 AI 教學內容按鈕時觸發。

- `save_ai_content():`
用途：將 AI 跑完的內容回填至資料庫。
環節：AI 生成成功後立即執行。

- `autoplay_audio():`
用途：將文字轉為語音並利用 `HTML5 <audio>` 標籤自動播放。
環節：點擊任何喇叭按鈕時。

- `update_error_count():`
用途：當使用者測驗答錯時，SQL 指令將該單字錯誤次數 +1。
環節：測驗模式點選錯誤選項時。


## 四、 程式執行生命週期 (Step-by-Step)

- Step 1： 環境檢測與資料載入
當 app.p 執行，系統第一時間呼叫 `init_db()`。隨後執行 `load_full_db()`，將硬碟資料載入 `st.session_state` 作為全局緩存。

- Step 2： 側邊欄檔案監視
監控 file_uploader。一旦有檔案上傳，立即補齊 AI 欄位的空值，寫入 SQL，並觸發 `st.rerun()`。這會讓上傳器清空，並讓新檔名出現在「已上傳清單」。

- Step 3： 單元切換與狀態初始化
當使用者切換 selectbox：
系統觸發「狀態守衛」。
重置 `active_unit` 標籤。
清除舊單元的題目快取，重新複製一份該單元所有單字的 Index 形成新的 Pool。

- Step 4： 教學模式與「懶加載」內容
當進入 Tab 1：
系統根據 SQL 中的 AI 欄位狀態決定 UI。
若欄位為空，顯示「生成」按鈕；若已有資料，直接顯示字根與例句。
點擊生成時，啟動 `st.spinner` 動態提示，直到 AI 回傳並更新資料庫。

- Step 5: 循環測驗流程
當進入 Tab 2跟3：
偵測 Pool 是否為空。若空則從 DB 重新填充（Circular logic）。
隨機抽取題目，並從 Pool 移除該 ID 以防重複。
利用 `st.columns` 動態佈局生成選項按鈕，並執行答錯次數的 SQL 更新。

## 五、 系統架構流程圖



## 六、 把Gemini API加入本地部屬的程式(單純單字教學頁面) 








```python=
import streamlit as st
import pandas as pd
from gtts import gTTS
import io
import sqlite3
import os
import random
import base64
import json
import google.generativeai as genai

# --- 1. 資料庫層 (DAO) -------------------------------------------------------
DB_NAME = "vocab_app.db"

def init_db():
    conn = sqlite3.connect(DB_NAME)
    c = conn.cursor()
    # 建立完整表格 (含 AI 欄位)
    c.execute('''CREATE TABLE IF NOT EXISTS vocab 
                 (unit INTEGER, english TEXT UNIQUE, chinese TEXT, source_file TEXT, 
                  error_count INTEGER DEFAULT 0,
                  root_analysis TEXT, mnemonic TEXT, sentences_json TEXT)''')
    
    # 欄位自動檢查 (Schema Migration)
    c.execute("PRAGMA table_info(vocab)")
    existing_cols = [col[1] for col in c.fetchall()]
    for col in ["root_analysis", "mnemonic", "sentences_json"]:
        if col not in existing_cols:
            c.execute(f"ALTER TABLE vocab ADD COLUMN {col} TEXT")
    conn.commit()
    conn.close()

def save_ai_content(word, root, mnem, sents):
    conn = sqlite3.connect(DB_NAME)
    c = conn.cursor()
    c.execute('UPDATE vocab SET root_analysis=?, mnemonic=?, sentences_json=? WHERE english=?',
              (root, mnem, json.dumps(sents), word))
    conn.commit()
    conn.close()

# --- 2. AI 核心邏輯 (Gemini API) ----------------------------------------------
def get_ai_learning_content(word, chinese):
    try:
        # 1. API 配置
        API_KEY = "api整串直接貼過來 需要注意安全性" 
        genai.configure(api_key=API_KEY)
        
        # 2. 自動選擇可用模型 (解決 404 的核心邏輯)
        available_models = [m.name for m in genai.list_models() if 'generateContent' in m.supported_generation_methods]
        
        # 優先順序：1.5-flash -> 1.0-pro -> 第一個可用的模型
        target_model = ""
        if 'models/gemini-1.5-flash' in available_models:
            target_model = 'models/gemini-1.5-flash'
        elif 'models/gemini-pro' in available_models:
            target_model = 'models/gemini-pro'
        else:
            target_model = available_models[0] if available_models else ""

        if not target_model:
            st.error("找不到任何可用的 Gemini 模型，請檢查 API Key 權限。")
            return None

        model = genai.GenerativeModel(target_model)
        
        # 3. 設定 Prompt
        prompt = f"""
        你是一位專業英語老師。請針對單字 "{word}" (中文意思: {chinese}) 提供教學。
        請直接回傳 JSON 字串，不要包含任何 Markdown 標籤(如 ```json )。
        
        格式必須精確如下：
        {{
            "root": "字根字首拆解",
            "mnemonic": "記憶法(包含諧音)",
            "sentences": [
                {{"en": "TOEIC sentence 1", "zh": "翻譯1"}},
                {{"en": "TOEIC sentence 2", "zh": "翻譯2"}},
                {{"en": "TOEIC sentence 3", "zh": "翻譯3"}}
            ]
        }}
        """
        
        # 4. 呼叫與強制清理 (防止 AI 碎碎念)
        response = model.generate_content(prompt)
        raw_text = response.text.strip()
        
        # 強力清理 Markdown 殘留
        if "```" in raw_text:
            # 抓取第一對括號之間的內容
            import re
            json_match = re.search(r'\{.*\}', raw_text, re.DOTALL)
            if json_match:
                raw_text = json_match.group()

        return json.loads(raw_text)

    except Exception as e:
        st.error(f"AI 請求失敗: {e}")
        # 如果發生 404，印出所有可用模型供 debug
        try:
            models = [m.name for m in genai.list_models()]
            st.info(f"當前可用模型列表: {models}")
        except: pass
        return None
# --- 3. 工具與發音 -----------------------------------------------------------
def autoplay_audio(text):
    if not text: return
    try:
        tts = gTTS(text=text, lang='en')
        fp = io.BytesIO()
        tts.write_to_fp(fp)
        b64 = base64.b64encode(fp.getvalue()).decode()
        md = f'<audio autoplay="true"><source src="data:audio/mp3;base64,{b64}" type="audio/mp3"></audio>'
        st.markdown(md, unsafe_allow_html=True)
    except: pass

# --- 4. 初始化 -------------------------------------------------------------
st.set_page_config(page_title="本地 AI 單字系統", layout="wide")
init_db()

# --- 5. 側邊欄 -------------------------------------------------------------
with st.sidebar:
    st.header("⚙️ 本地管理")
    if "up_key" not in st.session_state: st.session_state.up_key = 0
    up_file = st.file_uploader("匯入 Excel", type=["xlsx"], key=f"up_{st.session_state.up_key}")
    
    if up_file:
        df = pd.read_excel(up_file, header=None)
        df.columns = ['unit', 'english', 'chinese']
        conn = sqlite3.connect(DB_NAME); c = conn.cursor()
        for _, r in df.iterrows():
            c.execute('INSERT INTO vocab (unit,english,chinese,source_file) VALUES (?,?,?,?) ON CONFLICT(english) DO UPDATE SET unit=excluded.unit, chinese=excluded.chinese', 
                      (int(r['unit']), str(r['english']), str(r['chinese']), up_file.name))
        conn.commit(); conn.close()
        st.session_state.up_key += 1
        st.rerun()

    st.divider()
    st.subheader("📄 已上傳清單")
    conn = sqlite3.connect(DB_NAME); c = conn.cursor()
    c.execute('SELECT DISTINCT source_file FROM vocab'); files = [r[0] for r in c.fetchall() if r[0]]; conn.close()
    for i, f in enumerate(files):
        c1, c2 = st.columns([4, 1])
        with c1: st.write(f"· {f}")
        with c2:
            if st.button("🗑️", key=f"del_{i}"):
                conn = sqlite3.connect(DB_NAME); conn.execute('DELETE FROM vocab WHERE source_file=?', (f,)); conn.commit(); conn.close()
                st.rerun()

# --- 6. 主畫面展現 -----------------------------------------------------------
conn = sqlite3.connect(DB_NAME); db = pd.read_sql('SELECT * FROM vocab', conn); conn.close()

if not db.empty:
    unit_list = sorted(db['unit'].unique())
    selected_unit = st.selectbox("選擇單元：", unit_list)

    # Unit 切換初始化 Pool
    if "active_unit" not in st.session_state or st.session_state.active_unit != selected_unit:
        st.session_state.active_unit = selected_unit
        u_idx = db[db['unit'] == selected_unit].index.tolist()
        st.session_state.q1_pool, st.session_state.q2_pool = u_idx.copy(), u_idx.copy()
        st.rerun()

    curr_df = db[db['unit'] == selected_unit].reset_index(drop=True)
    tab1, tab2, tab3 = st.tabs(["📖 AI 教學模式", "📝 測驗一", "📝 測驗二"])

    with tab1:
        for i, row in curr_df.iterrows():
            word, ch = str(row['english']), str(row['chinese'])
            with st.expander(f"#{i+1} - {word} ({ch})", expanded=False):
                cL, cR = st.columns([1, 1])
                with cL:
                    st.header(word)
                    st.write(f"❌ 錯誤次數：{row['error_count']}")
                    if st.button(f"🔊 播放單字", key=f"t1_s_{i}"): autoplay_audio(word)
                    
                    if not row['root_analysis']:
                        if st.button("✨ 生成 AI 教學內容", key=f"gen_{i}"):
                            with st.spinner(f'AI 正在分析 {word}...'):
                                ai_res = get_ai_learning_content(word, ch)
                                if ai_res:
                                    save_ai_content(word, ai_res['root'], ai_res['mnemonic'], ai_res['sentences'])
                                    st.rerun()
                    else:
                        st.subheader("🔍 字根字首拆解")
                        st.info(row['root_analysis'])
                        st.subheader("💡 記憶法")
                        st.warning(row['mnemonic'])
                
                with cR:
                    st.subheader("📝 多益實戰例句")
                    if row['sentences_json']:
                        s_list = json.loads(row['sentences_json'])
                        for s_i, s in enumerate(s_list):
                            st.write(f"**{s_i+1}.** {s['en']}")
                            st.caption(f"翻譯：{s['zh']}")
                            if st.button(f"🔊 播放例句 {s_i+1}", key=f"spk_s_{i}_{s_i}"): autoplay_audio(s['en'])
                st.divider()

    # --- 測驗功能 (維持 v1.5.2 穩定邏輯) ---
    # ... (此處可填入之前完成的 get_next_q 與測驗 Tab 2/3 代碼)
else:
    st.info("請先匯入單字 Excel 檔案。")
```
![螢幕擷取畫面 2026-04-03 013244](https://hackmd.io/_uploads/BksO2_Aj-x.png)



# 2 加入測驗(中選英 英選中)
經典題目練習 算是初步測試你對這回合單字有多少了解(成為無情刷題機器)
一樣要在程式第50行的  API_KEY =輸入你的api 晶鑰 
==拜託必須記得請好好保管晶鑰==
==拜託必須記得請好好保管晶鑰==
==拜託必須記得請好好保管晶鑰==

```python=
import streamlit as st
import pandas as pd
from gtts import gTTS
import io
import sqlite3
import os
import random
import base64
import json
import google.generativeai as genai

# --- 1. 資料庫層 (DAO) -------------------------------------------------------
DB_NAME = "vocab_app.db"

def init_db():
    conn = sqlite3.connect(DB_NAME)
    c = conn.cursor()
    c.execute('''CREATE TABLE IF NOT EXISTS vocab 
                 (unit INTEGER, english TEXT UNIQUE, chinese TEXT, source_file TEXT, 
                  error_count INTEGER DEFAULT 0,
                  root_analysis TEXT, mnemonic TEXT, sentences_json TEXT)''')
    
    c.execute("PRAGMA table_info(vocab)")
    existing_cols = [col[1] for col in c.fetchall()]
    for col in ["root_analysis", "mnemonic", "sentences_json"]:
        if col not in existing_cols:
            c.execute(f"ALTER TABLE vocab ADD COLUMN {col} TEXT")
    conn.commit()
    conn.close()

def save_ai_content(word, root, mnem, sents):
    conn = sqlite3.connect(DB_NAME)
    c = conn.cursor()
    c.execute('UPDATE vocab SET root_analysis=?, mnemonic=?, sentences_json=? WHERE english=?',
              (root, mnem, json.dumps(sents), word))
    conn.commit()
    conn.close()

def update_error_count(english_word):
    conn = sqlite3.connect(DB_NAME)
    c = conn.cursor()
    c.execute('UPDATE vocab SET error_count = error_count + 1 WHERE english = ?', (english_word,))
    conn.commit()
    conn.close()

# --- 2. AI 核心邏輯 ----------------------------------------------------------
def get_ai_learning_content(word, chinese):
    try:
        # 請確保此 API KEY 正確且無前後空格
        API_KEY = "API KEY!!!!!!!!!!!!!!!!!!!".strip()
        genai.configure(api_key=API_KEY)
        
        available_models = [m.name for m in genai.list_models() if 'generateContent' in m.supported_generation_methods]
        
        target_model = ""
        if 'models/gemini-1.5-flash' in available_models:
            target_model = 'models/gemini-1.5-flash'
        elif 'models/gemini-pro' in available_models:
            target_model = 'models/gemini-pro'
        else:
            target_model = available_models[0] if available_models else ""

        if not target_model:
            st.error("找不到可用模型")
            return None

        model = genai.GenerativeModel(target_model)
        
        prompt = f"""
        你是一位專業多益TOEIC英語老師。請針對單字 "{word}" (中文意思: {chinese}) 提供多益TOEIC考試題目常出的教學。
        請直接回傳 JSON 字串，不要包含任何 Markdown 標籤。
        格式必須精確如下：
        {{
            "root": "字根字首拆解",
            "mnemonic": "記憶法，近似詞，常用短句",
            "sentences": [
                {{"en": "TOEIC sentence 1", "zh": "翻譯1"}},
                {{"en": "TOEIC sentence 2", "zh": "翻譯2"}},
                {{"en": "TOEIC sentence 3", "zh": "翻譯3"}}
            ]
        }}
        """
        response = model.generate_content(prompt)
        raw_text = response.text.strip()
        
        if "```" in raw_text:
            import re
            json_match = re.search(r'\{.*\}', raw_text, re.DOTALL)
            if json_match:
                raw_text = json_match.group()

        return json.loads(raw_text)
    except Exception as e:
        st.error(f"AI 請求失敗: {e}")
        return None

# --- 3. 工具與發音 -----------------------------------------------------------
def autoplay_audio(text):
    if not text: return
    try:
        tts = gTTS(text=text, lang='en')
        fp = io.BytesIO()
        tts.write_to_fp(fp)
        b64 = base64.b64encode(fp.getvalue()).decode()
        md = f'<audio autoplay="true"><source src="data:audio/mp3;base64,{b64}" type="audio/mp3"></audio>'
        st.markdown(md, unsafe_allow_html=True)
    except: pass

# --- 4. 初始化 -------------------------------------------------------------
st.set_page_config(page_title="本地 AI 多益學習系統", layout="wide")
init_db()

# --- 5. 側邊欄 -------------------------------------------------------------
with st.sidebar:
    st.header("⚙️ 本地管理")
    if "up_key" not in st.session_state: st.session_state.up_key = 0
    up_file = st.file_uploader("匯入 Excel", type=["xlsx"], key=f"up_{st.session_state.up_key}")
    
    if up_file:
        df = pd.read_excel(up_file, header=None)
        df.columns = ['unit', 'english', 'chinese']
        conn = sqlite3.connect(DB_NAME); c = conn.cursor()
        for _, r in df.iterrows():
            c.execute('''INSERT INTO vocab (unit,english,chinese,source_file) VALUES (?,?,?,?) 
                         ON CONFLICT(english) DO UPDATE SET unit=excluded.unit, chinese=excluded.chinese''', 
                      (int(r['unit']), str(r['english']), str(r['chinese']), up_file.name))
        conn.commit(); conn.close()
        st.session_state.up_key += 1
        st.rerun()

    st.divider()
    st.subheader("📄 已上傳清單")
    conn = sqlite3.connect(DB_NAME); c = conn.cursor()
    c.execute('SELECT DISTINCT source_file FROM vocab'); files = [r[0] for r in c.fetchall() if r[0]]; conn.close()
    for i, f in enumerate(files):
        c1, c2 = st.columns([4, 1])
        with c1: st.write(f"· {f}")
        with c2:
            if st.button("🗑️", key=f"del_{i}"):
                conn = sqlite3.connect(DB_NAME); conn.execute('DELETE FROM vocab WHERE source_file=?', (f,)); conn.commit(); conn.close()
                st.rerun()

# --- 6. 主畫面展現 -----------------------------------------------------------
conn = sqlite3.connect(DB_NAME); db = pd.read_sql('SELECT * FROM vocab', conn); conn.close()

if not db.empty:
    unit_list = sorted(db['unit'].unique())
    selected_unit = st.selectbox("選擇單元：", unit_list)

    if "active_unit" not in st.session_state or st.session_state.active_unit != selected_unit:
        st.session_state.active_unit = selected_unit
        u_idx = db[db['unit'] == selected_unit].index.tolist()
        st.session_state.q1_pool = u_idx.copy()
        st.session_state.q2_pool = u_idx.copy()
        if 'q1_data' in st.session_state: del st.session_state.q1_data
        if 'q2_data' in st.session_state: del st.session_state.q2_data
        st.rerun()

    curr_df = db[db['unit'] == selected_unit].reset_index(drop=True)
    t_unit_len = len(curr_df)
    tab1, tab2, tab3 = st.tabs(["📖 AI 教學模式", "📝 測驗一 (中選英)", "📝 測驗二 (英選中)"])

    # --- Tab 1: 教學模式 ---
    with tab1:
        for i, row in curr_df.iterrows():
            word, ch = str(row['english']), str(row['chinese'])
            with st.expander(f"#{i+1} - {word} ({ch})"):
                cL, cR = st.columns([1, 1])
                with cL:
                    st.header(word)
                    st.write(f"❌ 錯誤次數：{row['error_count']}")
                    b1, b2 = st.columns(2)
                    with b1:
                        if st.button(f"🔊 播放單字", key=f"t1_s_{i}"): autoplay_audio(word)
                    with b2:
                        label = "✨ 生成 AI 教學" if not row['root_analysis'] else "🔄 重新生成內容"
                        if st.button(label, key=f"gen_{i}"):
                            with st.spinner(f'AI 分析中...'):
                                res = get_ai_learning_content(word, ch)
                                if res:
                                    save_ai_content(word, res['root'], res['mnemonic'], res['sentences'])
                                    st.rerun()
                    if row['root_analysis']:
                        st.subheader("🔍 字根字首")
                        st.info(row['root_analysis'])
                        st.subheader("💡 記憶法")
                        st.warning(row['mnemonic'])
                with cR:
                    st.subheader("📝 多益實戰例句")
                    if row['sentences_json']:
                        s_list = json.loads(row['sentences_json'])
                        for s_i, s in enumerate(s_list):
                            st.write(f"**{s_i+1}.** {s['en']}")
                            st.caption(f"翻譯：{s['zh']}")
                            if st.button(f"🔊 播放例句 {s_i+1}", key=f"spk_s_{i}_{s_i}"): autoplay_audio(s['en'])
                st.divider()

    # --- 測驗抽取核心函式 ---
    def get_next_q(pool_key, q_type):
        if not st.session_state[pool_key]:
            st.session_state[pool_key] = db[db['unit'] == selected_unit].index.tolist()
            st.toast("🔄 測驗已自動循環！")
        
        pool = st.session_state[pool_key]
        idx = random.choice(pool)
        pool.remove(idx)
        st.session_state[pool_key] = pool
        
        row = db.loc[idx]
        q_text = row['chinese'] if q_type == 'c2e' else row['english']
        ans = row['english'] if q_type == 'c2e' else row['chinese']
        
        # 抓取干擾項
        all_opts = db[db['unit'] == selected_unit]['english' if q_type == 'c2e' else 'chinese'].unique().tolist()
        others = [o for o in all_opts if str(o) != str(ans)]
        distractors = random.sample(others, min(len(others), 3))
        opts = distractors + [ans]
        random.shuffle(opts)
        return {"q": q_text, "ans": ans, "opts": opts, "raw": row}

# --- Tab 2 & 3: 測驗循環展示 (加入語音功能) ---
    quiz_tabs = [(tab2, 'q1_pool', 'q1_data', 'c2e', "中選英"), 
                 (tab3, 'q2_pool', 'q2_data', 'e2c', "英選中")]

    for t, p_key, q_key, q_type, t_name in quiz_tabs:
        with t:
            rem = len(st.session_state.get(p_key, []))
            st.write(f"📊 {t_name} 進度：**{t_unit_len - rem}** / {t_unit_len}")
            st.progress((t_unit_len - rem) / t_unit_len if t_unit_len > 0 else 0)

            if st.button("下一題", key=f"btn_next_{q_key}") or q_key not in st.session_state:
                st.session_state[q_key] = get_next_q(p_key, q_type)
                st.rerun()

            curr_q = st.session_state[q_key]
            
            # 題目顯示區
            st.subheader(f"題目：{curr_q['q']}")
            
            # --- 語音功能邏輯 ---
            if q_type == 'e2c':
                # 英選中：題目本身就是英文，提供「聽題」按鈕
                if st.button("🔊 聽題目發音", key=f"aud_q_{q_key}"):
                    autoplay_audio(curr_q['q'])
            else:
                # 中選英：題目是中文，提供「聽答案（單字）發音」按鈕
                # 這樣可以幫助使用者在選出英文前，先透過聲音輔助記憶
                if st.button("🔊 聽答案(英文單字)發音", key=f"aud_ans_{q_key}"):
                    autoplay_audio(curr_q['raw']['english'])

            st.divider()

            # 選項區
            cols = st.columns(2)
            for idx, opt in enumerate(curr_q['opts']):
                with cols[idx % 2]:
                    if st.button(opt, key=f"opt_{q_key}_{idx}", use_container_width=True):
                        if opt == curr_q['ans']:
                            st.success("✅ 正確！")
                            # 答對時自動播放該英文單字發音 (強化記憶)
                            # 不論中選英還是英選中，都唸出英文
                            autoplay_audio(curr_q['raw']['english'])
                        else:
                            update_error_count(curr_q['raw']['english'])
                            st.error(f"❌ 錯誤！正解：{curr_q['ans']}")
else:
    st.info("👋 歡迎！請先在左側上傳 Excel 檔案。")
```


# 3


# 4 


# 5


# 6


# 7












