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/);