Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions docs/PRODUCT_READINESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@
- [ ] 🔴 **U-1 `/studio` 直連 Gemini 改走後端**(offline,前端 + 確認後端端點)— 把 `/studio`
仍 client-side 呼叫 Gemini 的路徑改打 `/api/generate` 等後端端點(後端大多現成),堵住
「繞過計費 + 繞過審查」漏洞。或者若 U-3 直接退場 /studio,則本項併入「功能搬進 /app」。
- [~] 🟡 **U-2 `/app` 補齊 `/studio` 缺的視覺功能 — 含逐區 refine**(offline,**拍板要做
- [x] 🟡 **U-2 `/app` 補齊 `/studio` 缺的視覺功能 — 含逐區 refine**(offline,**拍板要做
2026-06-07**)— 盤點顯示 `/app` 視覺站缺「海報/圖卡逐區 refine、區域選擇」(後端 refine
圖卡未移植 = 唯一「大」缺口)。**定案:移植後端逐區 refine + 前端區域選擇 UI**(不是首發
砍項)。其餘(16 主題/密度/長寬比/自訂 prompt)UI_WIRING 標已接完。拆小:①後端 refine
Expand All @@ -206,8 +206,15 @@
避免燒額度,策略對齊既有 `refine_presentation_slide`)+ 新端點 `POST /api/refine-section`
(掛 rate_limit)。補 8 測(prompt 組裝 / merge 保留原欄位 / iconType coerce / imagePrompt
變更才重生圖 / 不變不生圖 / 關閉重生 / 端點,**全程 mock Gemini**,真實生圖燒額度部分不打 API)。
海報為單圖無「區」概念,整圖 refine 已由 `/api/generate` 的 `refinement` 涵蓋。待做:②前端
區域選擇 / 逐區 refine UI(另開 PR)。
海報為單圖無「區」概念,整圖 refine 已由 `/api/generate` 的 `refinement` 涵蓋。
- ✅ 2026-06-15(②)— 前端區域選擇 / 逐區 refine UI 接上 #81 後端。`/app` 視覺站已有的區塊點選
(`pickSection`/`onPickSection`)+ 逐區微調面板(指令 + `regenerateImage` 開關)原本送的是
**舊形狀** `{infographic, sectionId}` 並讀 `data.data`,與 #81 實際落地的 `POST /api/refine-section`
(收**單一** `section`、回**單一** `section`)對不上 → 逐區微調實際打不通。本刀把 `refineSection`
改成與 sibling 單頁微調 `/api/refine` 同形狀:送 `{section: sections[idx], instruction,
regenerateImage}`、讀 `data.section`、patch 回 `result.data.sections[idx]`。**純前端一檔**
(`frontend/edustudio/app.jsx`),`npm run build`(vite/node22)編譯通過;後端 8 測
(`test_infocards_refine.py`)已涵蓋端點,視覺由人後驗。至此 U-2 ①②③ 到齊。
- [ ] 🟡 **U-3 `/ui` `/studio` 標 legacy / 退場**(offline)— 在舊 UI 頁頂加 banner「此介面
將退場,請用 /app」+ README/介面表標 legacy。完全移除 build 產物等 `/app` 功能對等確認後
再做(避免反悔)。
Expand Down
11 changes: 6 additions & 5 deletions frontend/edustudio/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2263,7 +2263,8 @@ function VisualComposer({ projectId }) {
};

// 資訊圖卡逐區 refine:選一個區塊(區域選擇)送修改指令 → POST /api/refine-section →
// 後端回更新後的整張圖卡,整份替換 result.data。regenerateImage 可控是否一併重生配圖(省額度)。
// 後端回更新後的「單一區塊」(與 /api/refine 單頁微調同形狀),patch 回
// result.data.sections[idx]。regenerateImage 可控是否一併重生配圖(省額度)。
const [secOpen, setSecOpen] = useState(false);
const [secIdx, setSecIdx] = useState(0);
const [secInstr, setSecInstr] = useState("");
Expand All @@ -2279,13 +2280,13 @@ function VisualComposer({ projectId }) {
try {
const r = await fetch("/api/refine-section", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ infographic: result.data, sectionId: sections[idx].id,
instruction: secInstr, regenerateImage: secRegenImg,
style, customStylePrompt: style === "custom" ? customPrompt : "" }),
body: JSON.stringify({ section: sections[idx], instruction: secInstr,
regenerateImage: secRegenImg }),
});
const data = await r.json();
if (!r.ok || data.success === false) throw new Error(data.detail || "逐區微調失敗");
setResult({ ...result, data: data.data }); // 後端回整張更新後圖卡
const next = sections.slice(); next[idx] = data.section; // 後端回更新後的單一區塊
setResult({ ...result, data: { ...result.data, sections: next } });
setSecInstr(""); setSecOpen(false);
} catch (e) { setErr("逐區微調發生錯誤:" + ((e && e.message) || e)); }
finally { setSecBusy(false); }
Expand Down
54 changes: 5 additions & 49 deletions tests/test_infocards_infographic.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,52 +162,8 @@ def test_clearing_prompt_drops_image(self, monkeypatch):
out = info.refine_infographic_section(data, "s1", "移除圖片")
assert next(s for s in out.sections if s.id == "s1").imageUrl is None

def test_route(self, tmp_path, monkeypatch):
import pytest
pytest.importorskip("fastapi.testclient")
pytest.importorskip("multipart")
from fastapi.testclient import TestClient

import core.infocards.infographic_service as infs
import server.routes.infocards as ic
from core.infocards.share_store import ShareStore
from server.main import create_app

monkeypatch.setattr(ic, "get_share_store", lambda: ShareStore(db_path=str(tmp_path / "s.db")))
monkeypatch.setattr(infs, "generate_json",
lambda prompt, model=None, response_schema=None: {"title": "改好的區塊"})
monkeypatch.setattr(infs, "generate_image_b64",
lambda prompt, model=None: "data:image/png;base64,IMG")
app = create_app()
with TestClient(app) as c:
r = c.post("/api/refine-section", json={
"infographic": {**_FAKE, "style": "academic"},
"sectionId": "s1", "instruction": "改標題",
})
assert r.status_code == 200
body = r.json()
assert body["success"] is True and body["type"] == "infographic"
sec = next(s for s in body["data"]["sections"] if s["id"] == "s1")
assert sec["title"] == "改好的區塊"

def test_route_unknown_section_404(self, tmp_path, monkeypatch):
import pytest
pytest.importorskip("fastapi.testclient")
pytest.importorskip("multipart")
from fastapi.testclient import TestClient

import core.infocards.infographic_service as infs
import server.routes.infocards as ic
from core.infocards.share_store import ShareStore
from server.main import create_app

monkeypatch.setattr(ic, "get_share_store", lambda: ShareStore(db_path=str(tmp_path / "s.db")))
monkeypatch.setattr(infs, "generate_json",
lambda prompt, model=None, response_schema=None: {})
app = create_app()
with TestClient(app) as c:
r = c.post("/api/refine-section", json={
"infographic": {**_FAKE, "style": "academic"},
"sectionId": "ghost", "instruction": "x",
})
assert r.status_code == 404
# 註:`POST /api/refine-section` 路由的端到端測試已由
# tests/test_infocards_refine.py::TestRefineSectionRoute 涵蓋(現行契約=收/回
# 單一 section)。本檔原有的兩個路由測試送的是舊形狀 {infographic, sectionId}、
# 讀 data.data,與 #81 落地的單一 section 契約不符(回 422),已移除以對齊現行
# 路由;本類別其餘仍為 infographic_service.refine_infographic_section 的函式單元測試。
Loading