diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js index cb5aea0..8c66b0a 100644 --- a/web/storyforge-web-v4/assets/app.js +++ b/web/storyforge-web-v4/assets/app.js @@ -3138,11 +3138,53 @@ function openDashboardProjectSwitcher() { setScreen("intake"); return; } + const selectedProject = getSelectedProject(); + const projects = safeArray(appState.dashboard?.projects); + const projectCards = projects.map((project) => { + const stats = getProjectStats(project.id); + const reviewCount = getProjectReviews(project.id).length; + const isActive = project.id === selectedProject?.id; + return ` +
+

${escapeHtml(project.name || "未命名项目")}

+

${escapeHtml(project.description || "还没有补项目说明,适合先切过去继续完善。")}

+
+ ${escapeHtml(isActive ? "当前项目" : "可切换")} + 最近任务 ${escapeHtml(formatNumber(stats.jobs.length))} + Agent ${escapeHtml(formatNumber(stats.assistants.length))} + 复盘 ${escapeHtml(formatNumber(reviewCount))} +
+
+ `; + }).join(""); openActionModal({ title: "切换当前项目", - description: "首页上下文、今日动作和项目概览都会跟着当前项目一起切换。", + description: "首页上下文、今日动作和项目概览都会跟着当前项目一起切换。手机端会优先让你先扫一眼当前项目和最近任务。", submitLabel: "切换项目", fields: [ + { + name: "projectSummary", + label: "项目速览", + type: "html", + html: ` +
+

${escapeHtml(selectedProject?.name || "当前还没有项目")}

+

${escapeHtml(selectedProject?.description || "切换项目后,首页、OneLiner 和工作台会一起同步到对应上下文。")}

+
+ 当前项目 + 最近任务 ${escapeHtml(formatNumber(getProjectStats(selectedProject?.id || "").jobs.length))} + 切换到这个项目后会同步总台、Agent 和生产中心 +
+
+
+
+

最近任务

+

优先切到你现在真正要推进的项目,再从首页和主 Agent 继续往下走。

+
+ ${projectCards} +
+ ` + }, { name: "projectId", label: "当前项目", type: "select", value: getSelectedProject()?.id || "", options } ], onSubmit: async (payload) => { @@ -3161,6 +3203,7 @@ function openDashboardProjectSwitcher() { } finally { setBusy(false, ""); } + rememberAction("当前项目已切换", `已切换到「${getSelectedProject()?.name || "所选项目"}」,你现在看到的首页、Agent 和任务都会跟随更新。`, "green"); renderAll(); } }); @@ -7096,21 +7139,54 @@ async function createProject() { openAuthModal(); return; } - const name = window.prompt("输入项目名称"); - if (!name) return; - const description = window.prompt("输入项目说明(可选)") || ""; - setBusy(true, "正在创建项目..."); - try { - await storyforgeFetch("/v2/projects", { - method: "POST", - body: { name, description } - }); - await bootstrap(); - } catch (error) { - alert("创建项目失败: " + error.message); - } finally { - setBusy(false, ""); - } + const existingProjects = safeArray(appState.dashboard?.projects); + openActionModal({ + title: "新建项目", + description: "先把项目建起来,首页动作、主 Agent 和记忆都会跟着这个项目继续往下走。", + submitLabel: "创建并进入项目", + fields: [ + { + name: "projectGuide", + label: "创建提示", + type: "html", + html: ` +
+

先建一个可继续推进的项目

+

推荐直接按你现在的业务目标来命名,例如某个平台增长、某个账号矩阵或某个内容专题。

+
+ 当前已有 ${escapeHtml(formatNumber(existingProjects.length))} 个项目 + 创建后会自动切过去 + 主 Agent 会沿用这个项目上下文 +
+
+ ` + }, + { name: "name", label: "项目名称", placeholder: "例如:创业 IP 增长实验室" }, + { name: "description", label: "项目说明", type: "textarea", rows: 4, placeholder: "写一句这个项目主要解决什么问题、接下来准备推进什么" } + ], + onSubmit: async (values) => { + const name = String(values.name || "").trim(); + if (!name) throw new Error("请填写项目名称"); + setBusy(true, "正在创建项目..."); + try { + const project = await storyforgeFetch("/v2/projects", { + method: "POST", + body: { + name, + description: String(values.description || "").trim() + } + }); + appState.selectedProjectId = project.id || appState.selectedProjectId; + rememberAction("项目已创建", `已创建「${project.name || name}」,并切到这个项目继续推进。`, "green", project); + await bootstrap(); + } catch (error) { + presentActionFailure(error, "创建项目失败"); + throw error; + } finally { + setBusy(false, ""); + } + } + }); } function openPreferredModelAction() { diff --git a/web/storyforge-web-v4/tests/workbench-pages.test.mjs b/web/storyforge-web-v4/tests/workbench-pages.test.mjs index 0922380..4f92fe8 100644 --- a/web/storyforge-web-v4/tests/workbench-pages.test.mjs +++ b/web/storyforge-web-v4/tests/workbench-pages.test.mjs @@ -103,6 +103,17 @@ test("mobile action sheets and oneliner runtime behave like bottom sheets", () = assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.oneliner-composer\s*\{[\s\S]*position:\s*sticky/); }); +test("project creation and switching use in-app sheets instead of browser prompts", () => { + const createProject = extractBetween(APP, "async function createProject()", "function openPreferredModelAction()"); + const projectSwitcher = extractBetween(APP, "function openDashboardProjectSwitcher()", "function openDashboardActionReasonAction("); + assert.match(createProject, /openActionModal\(\{/); + assert.doesNotMatch(createProject, /window\.prompt/); + assert.doesNotMatch(createProject, /alert\(/); + assert.match(projectSwitcher, /type: "html"/); + assert.match(projectSwitcher, /最近任务/); + assert.match(projectSwitcher, /切换到这个项目/); +}); + test("mobile touch targets raise tappable buttons, tabs, and action tags closer to native sizes", () => { assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.btn,\s*[\s\S]*\.tab,\s*[\s\S]*\.tag\.clickable-tag\s*\{[\s\S]*min-height:\s*44px/); assert.match(CSS, /@media \(max-width: 760px\)[\s\S]*\.btn,\s*[\s\S]*\.tab,\s*[\s\S]*\.tag\.clickable-tag\s*\{[\s\S]*display:\s*inline-flex/);