diff --git a/docs/PRODUCT_READINESS.md b/docs/PRODUCT_READINESS.md index ea8d2ad..34bef6b 100644 --- a/docs/PRODUCT_READINESS.md +++ b/docs/PRODUCT_READINESS.md @@ -702,6 +702,18 @@ passed(3 個 QR/journal 字型像素為容器缺 Noto CJK 假象,CI 權威)。**前端 `LocalizeMenu` 傳 `project_id`** 屬後續前端 slice(route 欄位選填、不破壞現況)。**自動建議術語**(掃教材抽術語) 碰 Gemini 額度 = GATE,另寫 proposal 再做。F9-2 offline slice 至此到齊。 + - ✅ 2026-06-14 **F9-2j:前端 `LocalizeMenu` 傳 `project_id` 完成(offline,F9-2i 前端收尾)**。 + 把 F9-2i 落地的選填 `project_id` 接上前端在地化入口:`frontend/edustudio/app.jsx` 的 + `LocalizeMenu` 新增 `projectId` prop,呼叫 `POST /localization/translate` 時**有作用中課程才** + 帶 `project_id`(`...(projectId ? { project_id: projectId } : {})`),讓後端套該課 glossary 的 + 固定譯名、術語前後一致。兩個呼叫端各以最精準的課程來源接線:① **影片任務卡**(`TaskCard`)—— + `esJobToTask` 補帶 `rec.project_id`(F9-2g 已落 `JobRecord.project_id`),LocalizeMenu 收 + `task.project_id`=**該 job 自己所屬的課**;② **視覺成品卡**(`VisualCard`)——`VisualStation` + 把作用中課程 `projectId` 透傳下去。守紀律:route 欄位選填=**沒作用中課程沿用現行行為、零破壞** + (fail-soft 在後端,前端只是「給或不給」),**完全不碰 review gate / 狀態機**(只影響「術語怎麼 + 譯」)。純前端,未動 server/core/schemas/runner(route F9-2i 已落地且有測),故不需跑 pytest。 + `npm run build`(vite, node22)編譯通過;**視覺驗收待人工**(此環境無瀏覽器,依既定「前端 build + 為準、人後視覺驗收」)。F9-2 連同前端在地化入口至此端到端到齊(自動建議術語仍 = GATE 待額度)。 - [~] 🟡 **F9-3 本機可插拔模型後端**(GATE,= M 軸 Option B 的本機 provider)— 支援 **Ollama 等本機 LLM** 跑文字(大綱/旁白/翻譯),老師可零雲端成本跑(翻譯已用本機 translategemma 驗過路子)。**依賴 M-4 provider 介面就緒**後加 ollama adapter + 設定頁可選 diff --git a/frontend/edustudio/app.jsx b/frontend/edustudio/app.jsx index 6fccc3f..ec08c5c 100644 --- a/frontend/edustudio/app.jsx +++ b/frontend/edustudio/app.jsx @@ -377,7 +377,7 @@ function LangChip({ code, removable, onRemove }) { // 前端短碼 → 後端 canonical 連字號碼(/localization 邊界再轉底線)。 const ES_LANG_API = { "en": "en-US", "ja": "ja-JP", "ko": "ko-KR", "zh-CN": "zh-CN", "vi": "vi-VN", "zh-TW": "zh-TW" }; -function LocalizeMenu({ localized = [], onChange, size = "sm", label = "一鍵在地化", text = "" }) { +function LocalizeMenu({ localized = [], onChange, size = "sm", label = "一鍵在地化", text = "", projectId = "" }) { const [open, setOpen] = useState(false); const [picked, setPicked] = useState([]); const [phase, setPhase] = useState("idle"); // idle | running | done @@ -390,6 +390,8 @@ function LocalizeMenu({ localized = [], onChange, size = "sm", label = "一鍵 setPicked(p => p.includes(code) ? p.filter(c => c !== code) : [...p, code]); // 接 /localization/translate:對每個選的語言真的翻譯(傳成品標題作示範)。 + // F9-2j:有作用中課程(projectId)→ 帶 project_id,讓後端套該課 glossary 固定譯名 + //(route 欄位選填、fail-soft;沒給/查無沿用現行行為)。 const run = async () => { if (!picked.length) return; setPhase("running"); @@ -399,7 +401,10 @@ function LocalizeMenu({ localized = [], onChange, size = "sm", label = "一鍵 try { const r = await fetch("/localization/translate", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ text: src, target_lang: ES_LANG_API[code] || code, source_lang: "zh-TW" }), + body: JSON.stringify({ + text: src, target_lang: ES_LANG_API[code] || code, source_lang: "zh-TW", + ...(projectId ? { project_id: projectId } : {}), + }), }); const d = await r.json(); out[code] = d.translated_text || ""; @@ -1146,7 +1151,7 @@ function TaskCard({ task, onReview, onLocalize, onRetry, onCancel, onPublish, on