fix(frontend): 「開始推論」按鈕只在 camera 分頁顯示

「開始/停止推論」按鈕是 Camera 即時攝影機專用(打 /api/camera/start),
但原本渲染在所有分頁共用頂部。影片/圖片/批次是「上傳即自動推論」
(media-tab useInferenceStream=uploaded),不需此按鈕,顯示會誤導使用者
以為要先按開始推論。

用 activeTab === "camera" 條件渲染,非 camera 分頁隱藏。純顯示層改動,
handleStart/handleStop/isRunning/disabled 邏輯完全未動;與 handleTabChange
「切走即停」語意一致(切回 camera 時 isRunning 已 false、無殘留)。

reviewer 通過(0C/0M/0m)。tsc/eslint/build/14 test 綠 + 新增 1 測試驗
「按鈕只在 camera 顯示、切走隱藏、切回重現」。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jim800121chen 2026-08-02 03:38:09 +08:00
parent 145ed8e960
commit 44b877318d
2 changed files with 31 additions and 12 deletions

View File

@ -234,6 +234,20 @@ describe("WorkspaceClient — 切 tab 時 camera 串流 / WS 正確關閉S-1
expect(lastCameraWsEnabled()).toBe(false); expect(lastCameraWsEnabled()).toBe(false);
}); });
it("開始推論按鈕只在 camera tab 顯示,切到 image/video/batch 隱藏", async () => {
renderClient();
// camera tab預設按鈕在
expect(screen.getByText("開始推論")).toBeInTheDocument();
// 切到影片:按鈕隱藏(上傳即自動推論,不需此按鈕)
await switchTab("影片");
expect(screen.queryByText("開始推論")).not.toBeInTheDocument();
// 切回 camera按鈕重新出現
await switchTab("Camera");
expect(screen.getByText("開始推論")).toBeInTheDocument();
});
it("切到 image tab 後 camera WS 保持 false媒體 tab 有自己的 WS兩者互斥", async () => { it("切到 image tab 後 camera WS 保持 false媒體 tab 有自己的 WS兩者互斥", async () => {
post.mockResolvedValue({ streamUrl: "/api/camera/stream", sourceType: "camera" }); post.mockResolvedValue({ streamUrl: "/api/camera/stream", sourceType: "camera" });
renderClient(); renderClient();

View File

@ -276,18 +276,23 @@ export function WorkspaceClient({ deviceId }: WorkspaceClientProps) {
/> />
)} )}
</div> </div>
<div className="flex gap-2"> {/* / camera tab /api/camera/start
{isRunning ? ( image/video/batch camera tab
<Button variant="destructive" onClick={handleStop} disabled={busy}> UIcamera start/stop handleTabChange */}
{t("workspace.inference.stop")} {activeTab === "camera" && (
</Button> <div className="flex gap-2">
) : ( {isRunning ? (
// WP-Cserial 為空無法路由 → disable原因見下方 no-serial banner <Button variant="destructive" onClick={handleStop} disabled={busy}>
<Button onClick={handleStart} disabled={busy || !isOnline || !hasSerial}> {t("workspace.inference.stop")}
{t("workspace.inference.start")} </Button>
</Button> ) : (
)} // WP-Cserial 為空無法路由 → disable原因見下方 no-serial banner
</div> <Button onClick={handleStart} disabled={busy || !isOnline || !hasSerial}>
{t("workspace.inference.start")}
</Button>
)}
</div>
)}
</div> </div>
{/* WP-CADR-018serial 為空 → 無法路由到 local agent說明原因與解法 */} {/* WP-CADR-018serial 為空 → 無法路由到 local agent說明原因與解法 */}