From 1cb6c3e78fd247965af1baa6d2277ceaebd89376 Mon Sep 17 00:00:00 2001 From: kris Date: Mon, 30 Mar 2026 01:24:37 +0800 Subject: [PATCH] feat: add mobile-first production task deck --- web/storyforge-web-v4/assets/app.js | 132 ++++++++++++++++++ web/storyforge-web-v4/assets/styles.css | 6 + .../tests/workbench-pages.test.mjs | 4 + 3 files changed, 142 insertions(+) diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index 5651cf4..7e64cf6 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -6069,6 +6069,130 @@ function renderPlaybookScreen() { ); } +function renderProductionMobileTaskDeck({ activeTab, activeJobs, failedJobs, recoverableCount, works, recentDocs }) { + const taskCards = []; + if (activeTab === "recovery") { + const nextRecoverable = failedJobs.find((item) => item.recovery.recoverable) || null; + if (nextRecoverable) { + taskCards.push(` +
+

当前要先处理

+

${escapeHtml(`${nextRecoverable.job.title || nextRecoverable.job.id} 仍可恢复,建议优先重开。`)}

+
+ ${escapeHtml(nextRecoverable.recovery.label)} + ${actionTag(nextRecoverable.recovery.actionLabel || "立即恢复", "recover-job", `data-job-id="${escapeHtml(nextRecoverable.job.id)}"`)} +
+
+ `); + } + taskCards.push(` +
+

恢复面板

+

${escapeHtml(recoverableCount ? `当前有 ${recoverableCount} 条任务可以直接恢复。` : "当前没有可直接恢复的失败任务。")}

+
+ ${actionTag("批量恢复", "batch-recover-jobs")} + ${actionTag("恢复记录", "select-page-tab", `data-page-tab-key="productionDetailTab" data-page-tab-value="recovery"`)} +
+
+ `); + } else if (activeTab === "recorder") { + const status = appState.liveRecorderStatus || {}; + const activeCount = safeArray(status.active_recordings).length; + taskCards.push(` +
+

当前要先处理

+

${escapeHtml(status.running ? `Live Recorder 正在运行,当前有 ${activeCount} 路活动录制。` : "先确认 Live Recorder 是否在线,再检查录制源和文件。")}

+
+ ${escapeHtml(status.running ? "运行中" : "待检查")} + ${actionTag("录制维护", "select-page-tab", `data-page-tab-key="productionDetailTab" data-page-tab-value="recorder"`)} +
+
+ `); + taskCards.push(` +
+

录制源与文件

+

${escapeHtml(`录制源 ${formatNumber(safeArray(appState.liveRecorderSources).length)} 个 · 文件 ${formatNumber(safeArray(appState.liveRecorderFiles).length)} 个`)}

+
+ ${actionTag("交给主 Agent", "handoff-to-main-agent", buildMainAgentHandoffAttrs({ + sourceScreen: "production", + sourceActionKey: "production-mobile-recorder-handoff", + intentKey: "production_coordination", + title: "继续处理录制维护", + goal: "继续处理录制维护", + summary: "结合录制维护状态给出下一步动作。", + platform: getPreferredPlatform(), + platformScope: "single_platform", + planSteps: ["读取录制维护状态", "识别当前阻塞项", "生成下一步处理动作"] + }))} +
+
+ `); + } else if (activeTab === "outputs") { + const topWork = works[0] || null; + taskCards.push(` +
+

当前要先处理

+

${escapeHtml(topWork ? `先看 ${describeVideo(topWork)} 的结果,再决定是否回到复盘。` : "先看最近产物和学习素材,再决定是否继续复盘或返回生产。")}

+
+ ${actionTag("去复盘", "goto-review")} + ${actionTag("查看产物", "select-page-tab", `data-page-tab-key="productionDetailTab" data-page-tab-value="outputs"`)} +
+
+ `); + if (recentDocs.length) { + taskCards.push(` +
+

最近学习素材

+

${escapeHtml(brief(recentDocs[0].title || recentDocs[0].style_summary || "最近文档", 72))}

+
+ 学习素材 + ${escapeHtml(recentDocs[0].source_type || "document")} +
+
+ `); + } + } else { + const topJob = (activeJobs.length ? activeJobs : []).slice(0, 1)[0] || null; + taskCards.push(` +
+

当前要先处理

+

${escapeHtml(topJob ? `${topJob.title} 还在推进中,建议先看状态再决定是否做 AI 视频或实拍剪辑。` : "先看当前生产队列,再决定是否继续恢复或进入复盘。")}

+
+ ${actionTag("交给主 Agent", "handoff-to-main-agent", buildMainAgentHandoffAttrs({ + sourceScreen: "production", + sourceActionKey: "production-mobile-queue-handoff", + intentKey: "production_coordination", + title: "继续推进生产队列", + goal: "继续推进生产队列", + summary: "结合当前生产队列给出下一步动作。", + platform: getPreferredPlatform(), + platformScope: "single_platform", + planSteps: ["读取当前生产队列", "识别最该优先推进的项", "生成下一步处理动作"] + }))} + ${recoverableCount ? actionTag("看失败恢复", "select-page-tab", `data-page-tab-key="productionDetailTab" data-page-tab-value="recovery"`) : actionTag("去复盘", "goto-review")} +
+
+ `); + if (topJob) { + taskCards.push(` +
+

${escapeHtml(topJob.title)}

+

${escapeHtml(brief(topJob.style_summary || topJob.transcript_text || topJob.error || "暂无摘要", 84))}

+
+ ${escapeHtml(topJob.status)} + ${escapeHtml(topJob.line_type || "analysis")} +
+
+ `); + } + } + return ` +
+ ${taskCards.slice(0, 2).join("")} +
+ `; +} + function renderProductionScreen() { if (!appState.dashboard) { if (isAutoConnectionPending()) { @@ -6163,6 +6287,14 @@ function renderProductionScreen() { 可恢复 ${escapeHtml(formatNumber(recoverableCount))} 产物 ${escapeHtml(formatNumber(works.length))} + ${renderProductionMobileTaskDeck({ + activeTab, + activeJobs, + failedJobs, + recoverableCount, + works, + recentDocs + })} ${renderDetailTabs("productionDetailTab", tabs)} ${activeTab === "queue" ? `
diff --git a/web/storyforge-web-v4/assets/styles.css b/web/storyforge-web-v4/assets/styles.css index 5dda6f7..1184075 100644 --- a/web/storyforge-web-v4/assets/styles.css +++ b/web/storyforge-web-v4/assets/styles.css @@ -2359,6 +2359,12 @@ tbody tr:hover { text-align: center; } + .production-mobile-task-deck { + display: grid; + gap: 10px; + margin-bottom: 14px; + } + .entity-cell { align-items: flex-start; } diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index d97b979..c2ced6e 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -207,9 +207,13 @@ test("discovery and production screens expose mobile focus cards with next-step assert.match(discovery, /导入当前对标/); assert.match(discovery, /查相似/); assert.match(production, /mobile-only mobile-flow-focus-card/); + assert.match(APP, /function renderProductionMobileTaskDeck\(/); + assert.match(APP, /mobile-only production-mobile-task-deck/); + assert.match(APP, /当前要先处理/); assert.match(production, /当前工作流/); assert.match(production, /批量恢复/); assert.match(cssMobile, /\.mobile-flow-focus-card/); + assert.match(cssMobile, /\.production-mobile-task-deck/); }); test("mobile heavy screens mark redundant desktop metric blocks for compact hiding", () => {