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
{isReview && } {isApproved && <> - onLocalize(task.id, l)} text={task.title} /> + onLocalize(task.id, l)} text={task.title} projectId={task.project_id} /> } {isFailed && } @@ -1263,6 +1268,7 @@ function esJobToTask(rec) { source: esBasename(src.path) || src.url || rec.source_type || "—", lang: "zh-TW", updated: rec.updated_at ? new Date(rec.updated_at).toLocaleString("zh-TW") : "", cost: 0, model: "Gemini", error: rec.error || "來源或生成發生錯誤", _job: true, + project_id: rec.project_id || "", // F9-2g:job 所屬課程,在地化套該課 glossary(F9-2j) _stype: rec.source_type, _src: rec.source || {}, // 重試用:保留原始來源 rawState: rec.state, progress: prog.percent, progLabel: prog.label, stage: prog.stage, _stages: rec.stages || [], @@ -2508,7 +2514,7 @@ function VisualComposer({ projectId }) { ); } -function VisualCard({ o, onLocalize }) { +function VisualCard({ o, onLocalize, projectId = "" }) { const m = VISUAL_MODES[o.mode]; return ( @@ -2521,7 +2527,7 @@ function VisualCard({ o, onLocalize }) {
{o.title}
{o.meta}{o.localized.length ? ` · ${o.localized.length} 種語言` : ""}
- onLocalize(o.id, l)} text={o.title} /> + onLocalize(o.id, l)} text={o.title} projectId={projectId} />
@@ -2543,7 +2549,7 @@ function VisualStation({ projectId }) {

視覺成品

- {outputs.map(o => )} + {outputs.map(o => )}
);