import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { cancelPMFlowLaunchDraftJob, createPMFlowLaunchDraftJob, confirmPMFlowLaunchDraft, getPMFlowLaunchDraftJob, revisePMFlowLaunchDraft, type PMFlowLaunchDraftJob, type PMFlowLaunchDraft, type PMFlowLaunchDraftItem, } from "@/api/pmflowLaunch"; import { spaceApi } from "@/api/space"; import { getAuthToken } from "@/lib/api"; import { normalizeApiError } from "@/lib/apiError"; import { consumePendingIntent, savePendingIntent, type ProjectLaunchDraftPendingPayload, type ProjectLaunchPendingPayload, } from "@/lib/pendingIntent"; import { aiOperationItemsMock, aiWorkStepsMock, launchInputMock, projectDraftMock, systemStatusMock, teamHintMock, } from "@/features/project-launch/mockData"; import type { AIOperationItem, AIWorkStep, AIWorkStepStatus, LaunchInputMode, LaunchStage, LaunchStateSnapshot, ProjectDraft } from "@/features/project-launch/types"; const terminalStepIndex = aiWorkStepsMock.length - 1; const PROJECT_LAUNCH_PREFILL_KEY = "pmflow_project_launch_prefill"; function compactText(text: string, maxLength = 88): string { const normalized = text.replace(/\s+/g, " ").trim(); if (!normalized) return "暂无输入摘要"; return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized; } function countText(count: number, unit = "项"): string { return count > 0 ? `${count} ${unit}` : "待补充"; } function formatOperationClock(timestamp: number | null | undefined): string { if (!timestamp) return "待返回"; const date = new Date(timestamp); if (Number.isNaN(date.getTime())) return "待返回"; return date.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false }); } const emptyProjectDraft: ProjectDraft = { name: "", oneLineGoal: "", targetUsers: [], mvpScope: { included: [], excluded: [], }, modelDecisionSuggestion: "", risks: [], confirmations: [], firstTasks: [], }; function cloneDraft(draft: ProjectDraft): ProjectDraft { return { ...draft, targetUsers: [...draft.targetUsers], mvpScope: { included: [...draft.mvpScope.included], excluded: [...draft.mvpScope.excluded], }, risks: draft.risks.map((risk) => ({ ...risk })), confirmations: draft.confirmations.map((item) => ({ ...item })), firstTasks: draft.firstTasks.map((task) => ({ ...task })), }; } function getInitialStage(): LaunchStage { const params = new URLSearchParams(window.location.search); const stage = params.get("state"); if ( stage === "intent_input" || stage === "ai_working" || stage === "human_review" || stage === "draft_edit" || stage === "generation_failed" || stage === "creation_success" ) { return stage; } return "intent_input"; } function getDataSourceMode(): "api" | "demo" { const params = new URLSearchParams(window.location.search); return params.get("demo") === "1" || params.get("dataSource") === "demo" ? "demo" : "api"; } type InitialLaunchInput = { mode: LaunchInputMode; text: string; autoStart: boolean; launchSeed?: Record }; function consumeProjectLaunchPrefill(): InitialLaunchInput | null { try { const raw = window.sessionStorage.getItem(PROJECT_LAUNCH_PREFILL_KEY) || window.localStorage.getItem(PROJECT_LAUNCH_PREFILL_KEY); if (!raw) return null; window.sessionStorage.removeItem(PROJECT_LAUNCH_PREFILL_KEY); window.localStorage.removeItem(PROJECT_LAUNCH_PREFILL_KEY); const payload = JSON.parse(raw) as { mode?: string; text?: string; autoStart?: boolean; launchSeed?: unknown; launch_seed?: unknown } | null; const text = typeof payload?.text === "string" ? payload.text.trim() : ""; if (!text) return null; const mode = payload?.mode === "import" || payload?.mode === "template" ? payload.mode : "idea"; const launchSeed = normalizeLaunchSeed(payload?.launchSeed || payload?.launch_seed); return { mode, text: text.slice(0, 2000), autoStart: Boolean(payload?.autoStart), ...(launchSeed ? { launchSeed } : {}), }; } catch { return null; } } function getInitialLaunchInput(dataSourceMode: "api" | "demo", pendingDraft: ProjectLaunchDraftPendingPayload | null): InitialLaunchInput { if (dataSourceMode === "demo") return { ...launchInputMock, autoStart: false }; if (pendingDraft?.text) { return { mode: pendingDraft.mode || "idea", text: pendingDraft.text, autoStart: false, }; } const params = new URLSearchParams(window.location.search); const actionPrefill = consumeProjectLaunchPrefill(); if (actionPrefill) return actionPrefill; const prefill = params.get("prefill")?.trim(); if (prefill) { return { mode: "idea" as const, text: prefill.slice(0, 2000), autoStart: params.get("autostart") === "1", }; } const pending = consumePendingIntent("project_launch_input"); if (pending?.payload?.text) { return { mode: pending.payload.mode || "idea", text: pending.payload.text, autoStart: false, ...(normalizeLaunchSeed(pending.payload.launchSeed) ? { launchSeed: normalizeLaunchSeed(pending.payload.launchSeed) } : {}), }; } return { mode: "idea" as const, text: "", autoStart: false }; } function saveProjectLaunchInput(input: LaunchStateSnapshot["input"]): void { savePendingIntent({ intentType: "project_launch_input", route: "/project-launch", payload: { text: input.text, mode: input.mode, ...(input.launchSeed ? { launchSeed: input.launchSeed } : {}), }, }); } function normalizeLaunchSeed(value: unknown): Record | undefined { if (!value || typeof value !== "object" || Array.isArray(value)) return undefined; return value as Record; } function buildLaunchDraftAttachments(input: LaunchStateSnapshot["input"]): Array> { const attachments: Array> = []; if (input.launchSeed) { attachments.push({ type: "pmflow_launch_seed", name: "Space AI 本轮产品方案种子", summary: "来自 PMFlow AI 本轮回答的产品定位、MVP 边界、风险、验证路径和下一步动作,用于保持项目草案与回答口径一致。", payload: input.launchSeed, }); } if (input.importedMaterial) attachments.push({ ...input.importedMaterial }); return attachments; } function draftGroupLabel(group: PMFlowLaunchDraftItem["group"]): ProjectDraft["firstTasks"][number]["group"] { if (group === "engineering") return "开发任务"; if (group === "qa") return "测试任务"; return "产品任务"; } function riskLevelLabel(level: PMFlowLaunchDraftItem["level"]): ProjectDraft["risks"][number]["level"] { if (level === "high") return "高"; if (level === "low") return "低"; return "中"; } function mapLaunchDraftToProjectDraft(payload: PMFlowLaunchDraft): ProjectDraft { return { name: payload.title, oneLineGoal: payload.one_liner, targetUsers: payload.target_user .split(/[、,/,\s]+/) .map((item) => item.trim()) .filter(Boolean), mvpScope: { included: payload.requirements.map((item) => item.title), excluded: [], }, modelDecisionSuggestion: payload.solution, risks: payload.risks.map((risk, index) => ({ id: `risk-${index}`, title: risk.title, level: riskLevelLabel(risk.level), summary: risk.summary || risk.mitigation || "", statusLabel: "待你确认", })), confirmations: payload.decisions.map((decision, index) => ({ id: `decision-${index}`, title: decision.title, reason: decision.reason || decision.summary || "", statusLabel: "待你确认", })), firstTasks: payload.tasks.map((task, index) => ({ id: `task-${index}`, group: draftGroupLabel(task.group), title: task.title, summary: task.summary || "", owner: "未分配", due: "待定", statusLabel: "AI 已生成", })), }; } function mapProjectDraftToEditedPayload(draft: ProjectDraft, rawDraft: Record | null) { const raw = { ...(rawDraft || {}) }; return { ...raw, title: draft.name, one_liner: draft.oneLineGoal, target_user: draft.targetUsers.join("、"), solution: draft.modelDecisionSuggestion || String(raw.solution || ""), requirements: draft.mvpScope.included.map((title) => ({ title, summary: title, acceptance_criteria: [] })), risks: draft.risks.map((risk) => ({ title: risk.title, summary: risk.summary, level: risk.level === "高" ? "high" : risk.level === "低" ? "low" : "medium", mitigation: "", })), decisions: draft.confirmations.map((item) => ({ title: item.title, reason: item.reason, owner: "" })), tasks: draft.firstTasks.map((task, index) => ({ title: task.title, summary: task.summary, group: task.group === "开发任务" ? "engineering" : task.group === "测试任务" ? "qa" : "product", priority: "medium", acceptance_criteria: [], requirement_index: index % Math.max(1, draft.mvpScope.included.length), })), }; } function buildApiStepTemplates(snapshot: LaunchStateSnapshot): AIWorkStep[] { const draft = snapshot.draft; const targetUserCount = draft.targetUsers.length; const requirementCount = draft.mvpScope.included.length; const riskCount = draft.risks.length; const taskCount = draft.firstTasks.length; const steps: AIWorkStep[] = [ { id: "goal", title: "定位变现机会", description: snapshot.stage === "ai_working" ? "正在从原始输入整理用户场景与付费假设..." : compactText(draft.oneLineGoal || snapshot.input.text, 64), estimate: snapshot.stage === "ai_working" ? "处理中" : "已完成", generatedCount: draft.oneLineGoal ? 1 : 0, status: "pending", }, { id: "users", title: "识别目标用户", description: targetUserCount > 0 ? `已整理 ${targetUserCount} 类目标用户` : "目标用户信息不足,等待补充或人工确认", estimate: snapshot.stage === "ai_working" ? "处理中" : "已完成", generatedCount: targetUserCount, status: "pending", }, { id: "scope", title: "提取 MVP 范围", description: requirementCount > 0 ? `已生成 ${requirementCount} 条首版需求范围` : "MVP 范围不足,等待补充", estimate: snapshot.stage === "ai_working" ? "处理中" : "已完成", generatedCount: requirementCount, status: "pending", }, { id: "risks", title: "识别变现风险", description: riskCount > 0 ? `已生成 ${riskCount} 条风险与待确认项` : "风险证据不足,等待补充", estimate: snapshot.stage === "ai_working" ? "处理中" : "已完成", generatedCount: riskCount, status: "pending", }, { id: "tasks", title: "生成首批任务", description: taskCount > 0 ? `已拆解 ${taskCount} 条首批任务建议` : "任务拆解不足,等待补充", estimate: snapshot.stage === "ai_working" ? "处理中" : "已完成", generatedCount: taskCount, status: "pending", }, { id: "confirmations", title: "准备待确认草案", description: snapshot.warnings.length > 0 ? `仍有 ${snapshot.warnings.length} 个缺口需要确认` : "已准备待确认草案", estimate: snapshot.stage === "ai_working" ? "处理中" : "已完成", generatedCount: snapshot.warnings.length, status: "pending", }, ]; return steps; } function buildApiOperations(snapshot: LaunchStateSnapshot): AIOperationItem[] { if (snapshot.stage === "ai_working") { if (snapshot.paused) { return [ { id: "generation-paused", type: "identified", title: "已暂停生成", summary: "已停止等待当前草案生成结果。点击继续生成会基于当前输入重新发起生成任务。", statusLabel: "待你确认", timestamp: "已暂停", items: [compactText(snapshot.input.text, 72)], }, ]; } const operations: AIOperationItem[] = [ { id: "input-analysis", type: "identified", title: "正在分析你的输入", summary: "正在调用项目草案生成,等待结果返回。", statusLabel: "AI 正在处理", timestamp: `开始 ${formatOperationClock(snapshot.generationStartedAt || snapshot.inputUpdatedAt)}`, items: [compactText(snapshot.input.text, 72)], }, ]; const foregroundWaitSeconds = snapshot.generationForegroundWaitSeconds ?? 10; if (snapshot.elapsedSeconds >= foregroundWaitSeconds) { operations.push({ id: "foreground-wait-exceeded", type: "identified", title: "已超过前台等待窗口", summary: `当前生成任务仍在运行,上限 ${snapshot.generationJobTimeoutSeconds ?? 180} 秒,结果返回后会自动进入草案确认。`, statusLabel: "AI 正在处理", timestamp: `已等待 ${snapshot.elapsedSeconds}s`, items: snapshot.generationLargeInputPolicy === "preprocess_then_generate" ? ["大输入策略:预处理后生成"] : [], }); } return operations; } const draft = snapshot.draft; if (!snapshot.draftId && !snapshot.rawDraft) return []; const targetUsers = draft.targetUsers.length > 0 ? draft.targetUsers : ["目标用户待补充"]; const requirements = draft.mvpScope.included.length > 0 ? draft.mvpScope.included : ["MVP 范围待补充"]; const risks = draft.risks.length > 0 ? draft.risks.map((risk) => risk.title) : ["风险信息待补充"]; const tasks = draft.firstTasks.length > 0 ? draft.firstTasks.map((task) => task.title) : ["首批任务待补充"]; const completedAt = `完成 ${formatOperationClock(snapshot.generationFinishedAt || snapshot.inputUpdatedAt)}`; const operations: AIOperationItem[] = [ { id: "target-users", type: "identified", title: `已整理目标用户(${countText(draft.targetUsers.length, "类")})`, summary: "基于真实草案返回结果整理,未命中的字段会保留为待补充。", statusLabel: "AI 已生成", timestamp: completedAt, items: targetUsers.slice(0, 4), }, { id: "requirements", type: "generated", title: `已生成核心需求(${countText(draft.mvpScope.included.length, "条")})`, summary: draft.modelDecisionSuggestion || "需求范围来自真实草案结果,仍需你确认边界。", statusLabel: "AI 已生成", timestamp: completedAt, items: requirements.slice(0, 4), }, { id: "risks", type: "risk", title: `已识别变现风险(${countText(draft.risks.length, "条")})`, summary: snapshot.warnings.length > 0 ? "草案仍有信息缺口,建议先补充再创建项目。" : "风险来自真实草案结果,正式创建前仍需人工确认。", statusLabel: "待你确认", timestamp: completedAt, items: risks.slice(0, 4), }, { id: "tasks", type: "confirmation", title: `已准备首批任务(${countText(draft.firstTasks.length, "条")})`, summary: "任务只是待确认建议,确认创建项目后才会写入真实任务对象。", statusLabel: "待你确认", timestamp: completedAt, items: tasks.slice(0, 4), }, ]; return operations.slice(0, Math.max(1, snapshot.visibleOperationCount)); } export function useProjectLaunchState() { const [snapshot, setSnapshot] = useState(() => { const now = Date.now(); const dataSourceMode = getDataSourceMode(); const pendingDraftIntent = dataSourceMode === "api" ? consumePendingIntent("project_launch_draft") : null; const pendingDraft = pendingDraftIntent?.payload ?? null; const initialInput = getInitialLaunchInput(dataSourceMode, pendingDraft); const autoStartGeneration = dataSourceMode === "api" && !pendingDraft && initialInput.autoStart && Boolean(initialInput.text.trim()); const stage = dataSourceMode === "demo" ? getInitialStage() : pendingDraft ? "human_review" : "intent_input"; const activeStepIndex = stage === "ai_working" ? 2 : stage === "intent_input" || stage === "generation_failed" ? 0 : terminalStepIndex; const initialDraft = pendingDraft ? mapLaunchDraftToProjectDraft(pendingDraft.rawDraft as unknown as PMFlowLaunchDraft) : dataSourceMode === "demo" ? cloneDraft(projectDraftMock) : cloneDraft(emptyProjectDraft); return { dataSourceMode, stage, input: { mode: initialInput.mode, text: initialInput.text, ...(initialInput.launchSeed ? { launchSeed: initialInput.launchSeed } : {}) }, draft: initialDraft, editingDraft: cloneDraft(initialDraft), systemStatus: systemStatusMock, teamHint: teamHintMock, activeStepIndex, visibleOperationCount: stage === "ai_working" ? 2 : aiOperationItemsMock.length, paused: false, elapsedSeconds: stage === "ai_working" ? 102 : 0, generationStartedAt: stage === "ai_working" ? now - 102000 : null, generationFinishedAt: pendingDraft ? now : null, inputUpdatedAt: initialInput.text ? now : null, savedIdea: false, savingIdea: false, creatingProject: false, savedIdeaId: null, savedIdeaHref: null, savedIdeaError: null, generationJobId: null, generationRevisionId: null, generationJobStatus: null, generationForegroundWaitSeconds: null, generationJobTimeoutSeconds: null, generationLargeInputPolicy: null, generationChangedFields: [], draftId: pendingDraft?.draftId || null, redirectUrl: null, errorMessage: null, failureKind: null, warnings: [], rawDraft: pendingDraft?.rawDraft || null, autoStartGeneration, }; }); const latestSnapshotRef = useRef(snapshot); useEffect(() => { latestSnapshotRef.current = snapshot; }, [snapshot]); useEffect(() => { if (snapshot.dataSourceMode !== "demo" || snapshot.stage !== "ai_working" || snapshot.paused) return undefined; const interval = window.setInterval(() => { setSnapshot((current) => { if (current.stage !== "ai_working" || current.paused) return current; const nextElapsed = current.elapsedSeconds + 7; const nextVisibleCount = Math.min(aiOperationItemsMock.length, current.visibleOperationCount + 1); const nextStepIndex = Math.min(terminalStepIndex, current.activeStepIndex + 1); if (current.activeStepIndex >= terminalStepIndex) { return { ...current, stage: "human_review", activeStepIndex: terminalStepIndex, visibleOperationCount: aiOperationItemsMock.length, elapsedSeconds: nextElapsed, }; } return { ...current, activeStepIndex: nextStepIndex, visibleOperationCount: nextVisibleCount, elapsedSeconds: nextElapsed, }; }); }, 850); return () => window.clearInterval(interval); }, [snapshot.dataSourceMode, snapshot.stage, snapshot.paused]); useEffect(() => { if (snapshot.dataSourceMode !== "api" || snapshot.stage !== "ai_working" || snapshot.paused || !snapshot.generationStartedAt) return undefined; const tick = () => { setSnapshot((current) => { if (current.dataSourceMode !== "api" || current.stage !== "ai_working" || !current.generationStartedAt) return current; const elapsedSeconds = Math.max(0, Math.floor((Date.now() - current.generationStartedAt) / 1000)); const foregroundWaitSeconds = current.generationForegroundWaitSeconds ?? 10; const isPastForegroundWait = elapsedSeconds >= foregroundWaitSeconds; return { ...current, elapsedSeconds, activeStepIndex: isPastForegroundWait ? Math.max(current.activeStepIndex, 1) : current.activeStepIndex, visibleOperationCount: isPastForegroundWait ? Math.max(current.visibleOperationCount, 2) : current.visibleOperationCount, systemStatus: isPastForegroundWait ? { ...current.systemStatus, message: "AI 仍在生成草案,系统会继续轮询结果", updatedAt: "刚刚" } : current.systemStatus, }; }); }; tick(); const interval = window.setInterval(tick, 1000); return () => window.clearInterval(interval); }, [snapshot.dataSourceMode, snapshot.stage, snapshot.paused, snapshot.generationStartedAt]); const applyJobResult = useCallback((job: PMFlowLaunchDraftJob) => { if (job.status === "completed" && job.draft) { const draft = mapLaunchDraftToProjectDraft(job.draft); setSnapshot((current) => { if (current.generationJobId && current.generationJobId !== job.job_id) return current; return { ...current, stage: "human_review", activeStepIndex: terminalStepIndex, visibleOperationCount: aiOperationItemsMock.length, elapsedSeconds: current.generationStartedAt ? Math.max(0, Math.floor((Date.now() - current.generationStartedAt) / 1000)) : current.elapsedSeconds, generationStartedAt: null, generationFinishedAt: Date.now(), generationJobId: job.job_id, generationRevisionId: job.revision_id, generationJobStatus: job.status, generationForegroundWaitSeconds: job.runtime_policy?.foreground_wait_seconds ?? current.generationForegroundWaitSeconds, generationJobTimeoutSeconds: job.runtime_policy?.job_timeout_seconds ?? current.generationJobTimeoutSeconds, generationLargeInputPolicy: job.runtime_policy?.large_input_policy ?? current.generationLargeInputPolicy, generationChangedFields: job.changed_fields || [], draft, editingDraft: cloneDraft(draft), draftId: job.draft.draft_id, rawDraft: job.draft as unknown as Record, warnings: job.draft.warnings || [], failureKind: null, errorMessage: null, systemStatus: { ...current.systemStatus, message: "真实项目草案已生成,等待确认", updatedAt: "刚刚" }, }; }); return; } if (job.status === "failed" || job.status === "cancelled") { setSnapshot((current) => { if (current.generationJobId && current.generationJobId !== job.job_id) return current; return { ...current, stage: "generation_failed", activeStepIndex: Math.min(current.activeStepIndex, 2), visibleOperationCount: Math.max(current.visibleOperationCount, 1), elapsedSeconds: current.generationStartedAt ? Math.max(0, Math.floor((Date.now() - current.generationStartedAt) / 1000)) : current.elapsedSeconds, generationStartedAt: null, generationJobStatus: job.status, generationForegroundWaitSeconds: job.runtime_policy?.foreground_wait_seconds ?? current.generationForegroundWaitSeconds, generationJobTimeoutSeconds: job.runtime_policy?.job_timeout_seconds ?? current.generationJobTimeoutSeconds, generationLargeInputPolicy: job.runtime_policy?.large_input_policy ?? current.generationLargeInputPolicy, errorMessage: job.error_message || "AI 暂时没有完成项目草案生成。你的输入已保留,可以稍后重试。", failureKind: job.status === "cancelled" ? "system_failed" : "ai_generation_failed", }; }); return; } setSnapshot((current) => { if (current.generationJobId && current.generationJobId !== job.job_id) return current; return { ...current, generationJobId: job.job_id, generationRevisionId: job.revision_id, generationJobStatus: job.status, generationForegroundWaitSeconds: job.runtime_policy?.foreground_wait_seconds ?? current.generationForegroundWaitSeconds, generationJobTimeoutSeconds: job.runtime_policy?.job_timeout_seconds ?? current.generationJobTimeoutSeconds, generationLargeInputPolicy: job.runtime_policy?.large_input_policy ?? current.generationLargeInputPolicy, }; }); }, []); useEffect(() => { if (snapshot.dataSourceMode !== "api" || snapshot.stage !== "ai_working" || snapshot.paused || !snapshot.generationJobId) return undefined; const jobId = snapshot.generationJobId; let stopped = false; const poll = () => { void getPMFlowLaunchDraftJob(jobId) .then((job) => { if (!stopped) applyJobResult(job); }) .catch((error) => { if (stopped) return; const normalized = normalizeApiError(error, "ai_generation_failed"); setSnapshot((current) => { if (current.generationJobId !== jobId) return current; return { ...current, stage: "generation_failed", generationStartedAt: null, errorMessage: normalized.message, failureKind: normalized.kind, }; }); }); }; poll(); const interval = window.setInterval(poll, 1500); return () => { stopped = true; window.clearInterval(interval); }; }, [applyJobResult, snapshot.dataSourceMode, snapshot.generationJobId, snapshot.paused, snapshot.stage]); const steps = useMemo( () => { const templates = snapshot.dataSourceMode === "demo" ? aiWorkStepsMock : buildApiStepTemplates(snapshot); return templates.map((step, index) => ({ ...step, status: (snapshot.stage === "generation_failed" && index === snapshot.activeStepIndex ? "failed" : index < snapshot.activeStepIndex || (snapshot.stage !== "ai_working" && index <= snapshot.activeStepIndex) ? "done" : index === snapshot.activeStepIndex ? "active" : "pending") as AIWorkStepStatus, })); }, [snapshot] ); const operations = useMemo( () => snapshot.dataSourceMode === "demo" && !snapshot.paused ? aiOperationItemsMock.slice(0, snapshot.visibleOperationCount) : buildApiOperations(snapshot), [snapshot] ); const updateInput = useCallback((text: string) => { setSnapshot((current) => ({ ...current, input: { ...current.input, text }, inputUpdatedAt: Date.now(), systemStatus: { ...current.systemStatus, message: "系统已保存输入", updatedAt: "刚刚" }, })); }, []); const setInputMode = useCallback((mode: LaunchInputMode) => { setSnapshot((current) => ({ ...current, input: { ...current.input, mode } })); }, []); const startGeneration = useCallback(() => { if (snapshot.dataSourceMode === "api") { const input = snapshot.input.text.trim(); if (!input) return; const generationStartedAt = Date.now(); setSnapshot((current) => ({ ...current, stage: "ai_working", activeStepIndex: 0, visibleOperationCount: 1, paused: false, elapsedSeconds: 0, generationStartedAt, generationFinishedAt: null, inputUpdatedAt: current.inputUpdatedAt || generationStartedAt, savedIdea: false, savedIdeaError: null, generationJobId: null, generationRevisionId: null, generationJobStatus: "queued", generationForegroundWaitSeconds: null, generationJobTimeoutSeconds: null, generationLargeInputPolicy: null, generationChangedFields: [], draftId: null, rawDraft: null, errorMessage: null, failureKind: null, autoStartGeneration: false, warnings: [], })); if (!getAuthToken()) { saveProjectLaunchInput(snapshot.input); setSnapshot((current) => ({ ...current, stage: "generation_failed", activeStepIndex: 0, visibleOperationCount: Math.max(current.visibleOperationCount, 1), generationStartedAt: null, errorMessage: "需要登录后继续。你的输入已保留,登录后可以继续生成执行草案。", failureKind: "auth_required", })); return; } void createPMFlowLaunchDraftJob({ input, input_type: "text", attachments: buildLaunchDraftAttachments(snapshot.input), source: "web", mode: "create", }) .then((job) => { const shouldCancelCreatedJob = latestSnapshotRef.current.paused; setSnapshot((current) => ({ ...current, generationJobId: job.job_id, generationRevisionId: job.revision_id, generationJobStatus: job.status, generationForegroundWaitSeconds: job.runtime_policy?.foreground_wait_seconds ?? current.generationForegroundWaitSeconds, generationJobTimeoutSeconds: job.runtime_policy?.job_timeout_seconds ?? current.generationJobTimeoutSeconds, generationLargeInputPolicy: job.runtime_policy?.large_input_policy ?? current.generationLargeInputPolicy, systemStatus: { ...current.systemStatus, message: current.paused ? "生成已暂停,当前任务会被取消" : "草案生成任务已创建,正在等待 AI 返回", updatedAt: "刚刚", }, })); if (shouldCancelCreatedJob) { void cancelPMFlowLaunchDraftJob(job.job_id).catch(() => undefined); return; } applyJobResult(job); }) .catch((error) => { const normalized = normalizeApiError(error, "ai_generation_failed"); if (normalized.kind === "auth_required" || normalized.kind === "session_expired") { saveProjectLaunchInput(snapshot.input); } setSnapshot((current) => ({ ...current, stage: "generation_failed", activeStepIndex: Math.min(current.activeStepIndex, 2), visibleOperationCount: Math.max(current.visibleOperationCount, 1), elapsedSeconds: current.generationStartedAt ? Math.max(0, Math.floor((Date.now() - current.generationStartedAt) / 1000)) : current.elapsedSeconds, generationStartedAt: null, errorMessage: normalized.message, failureKind: normalized.kind, })); }); return; } setSnapshot((current) => ({ ...current, stage: "ai_working", activeStepIndex: 0, visibleOperationCount: 1, paused: false, elapsedSeconds: 0, generationStartedAt: Date.now(), generationFinishedAt: null, inputUpdatedAt: current.inputUpdatedAt || Date.now(), savedIdea: false, savedIdeaError: null, errorMessage: null, failureKind: null, })); }, [applyJobResult, snapshot.dataSourceMode, snapshot.input]); useEffect(() => { if (!snapshot.autoStartGeneration || snapshot.dataSourceMode !== "api" || snapshot.stage !== "intent_input") return; if (!snapshot.input.text.trim()) { setSnapshot((current) => ({ ...current, autoStartGeneration: false })); return; } startGeneration(); }, [snapshot.autoStartGeneration, snapshot.dataSourceMode, snapshot.input.text, snapshot.stage, startGeneration]); const togglePause = useCallback(() => { if (snapshot.stage !== "ai_working") return; if (snapshot.paused) { startGeneration(); return; } const jobId = snapshot.generationJobId; setSnapshot((current) => { if (current.stage !== "ai_working") return current; return { ...current, paused: true, generationStartedAt: null, generationJobStatus: current.dataSourceMode === "api" && current.generationJobId ? "cancelled" : current.generationJobStatus, systemStatus: { ...current.systemStatus, message: current.dataSourceMode === "api" ? "已暂停生成,正在取消当前草案任务" : "已暂停生成", updatedAt: "刚刚", }, }; }); if (snapshot.dataSourceMode !== "api" || !jobId || !getAuthToken()) return; void cancelPMFlowLaunchDraftJob(jobId).catch((error) => { const normalized = normalizeApiError(error, "system_failed"); setSnapshot((current) => { if (current.generationJobId !== jobId || !current.paused) return current; return { ...current, systemStatus: { ...current.systemStatus, message: `暂停请求未完成:${normalized.message}`, updatedAt: "刚刚", }, }; }); }); }, [snapshot.dataSourceMode, snapshot.generationJobId, snapshot.paused, snapshot.stage, startGeneration]); const supplementInput = useCallback((note?: string) => { const supplement = String(note || "").trim(); if (!supplement) return; const currentInput = snapshot.input.text.trim(); const nextInput = currentInput ? `${currentInput}\n补充:${supplement}` : supplement; const generationStartedAt = Date.now(); setSnapshot((current) => ({ ...current, stage: "ai_working", input: { ...current.input, text: nextInput }, activeStepIndex: 0, visibleOperationCount: Math.min(aiOperationItemsMock.length, Math.max(1, current.visibleOperationCount + 1)), paused: false, elapsedSeconds: 0, generationStartedAt, generationFinishedAt: null, inputUpdatedAt: generationStartedAt, generationJobId: null, generationRevisionId: null, generationJobStatus: "queued", generationForegroundWaitSeconds: null, generationJobTimeoutSeconds: null, generationLargeInputPolicy: null, generationChangedFields: [], errorMessage: null, failureKind: null, systemStatus: { ...current.systemStatus, message: "已收到补充,正在生成新版草案", updatedAt: "刚刚" }, })); if (!getAuthToken()) { saveProjectLaunchInput({ ...snapshot.input, text: nextInput }); setSnapshot((current) => ({ ...current, stage: "generation_failed", generationStartedAt: null, errorMessage: "需要登录后继续。你的输入已保留,登录后可以继续生成执行草案。", failureKind: "auth_required", })); return; } const payload = { input: snapshot.draftId ? supplement : nextInput, input_type: "text" as const, attachments: buildLaunchDraftAttachments(snapshot.input), source: "web", mode: "merge_supplement" as const, revision_note: supplement, parent_draft_id: snapshot.draftId, }; const request = snapshot.draftId ? revisePMFlowLaunchDraft(snapshot.draftId, payload) : createPMFlowLaunchDraftJob(payload); void request .then((job) => { const shouldCancelCreatedJob = latestSnapshotRef.current.paused; setSnapshot((current) => ({ ...current, generationJobId: job.job_id, generationRevisionId: job.revision_id, generationJobStatus: job.status, generationForegroundWaitSeconds: job.runtime_policy?.foreground_wait_seconds ?? current.generationForegroundWaitSeconds, generationJobTimeoutSeconds: job.runtime_policy?.job_timeout_seconds ?? current.generationJobTimeoutSeconds, generationLargeInputPolicy: job.runtime_policy?.large_input_policy ?? current.generationLargeInputPolicy, systemStatus: { ...current.systemStatus, message: current.paused ? "生成已暂停,当前修订任务会被取消" : "补充已进入 AI 草案修订任务", updatedAt: "刚刚", }, })); if (shouldCancelCreatedJob) { void cancelPMFlowLaunchDraftJob(job.job_id).catch(() => undefined); return; } applyJobResult(job); }) .catch((error) => { const normalized = normalizeApiError(error, "ai_generation_failed"); if (normalized.kind === "auth_required" || normalized.kind === "session_expired") { saveProjectLaunchInput({ ...snapshot.input, text: nextInput }); } setSnapshot((current) => ({ ...current, stage: "generation_failed", generationStartedAt: null, errorMessage: normalized.message, failureKind: normalized.kind, })); }); }, [applyJobResult, snapshot.draftId, snapshot.input]); const regenerateDraft = useCallback(() => { if (snapshot.dataSourceMode !== "api" || !snapshot.draftId) { startGeneration(); return; } const input = snapshot.input.text.trim(); const generationStartedAt = Date.now(); setSnapshot((current) => ({ ...current, stage: "ai_working", activeStepIndex: 0, visibleOperationCount: 1, paused: false, elapsedSeconds: 0, generationStartedAt, generationFinishedAt: null, inputUpdatedAt: current.inputUpdatedAt || generationStartedAt, generationJobId: null, generationRevisionId: null, generationJobStatus: "queued", generationForegroundWaitSeconds: null, generationJobTimeoutSeconds: null, generationLargeInputPolicy: null, generationChangedFields: [], errorMessage: null, failureKind: null, systemStatus: { ...current.systemStatus, message: "正在重新生成一版可比较的项目草案", updatedAt: "刚刚" }, })); if (!getAuthToken()) { saveProjectLaunchInput(snapshot.input); setSnapshot((current) => ({ ...current, stage: "generation_failed", generationStartedAt: null, errorMessage: "需要登录后继续。你的输入已保留,登录后可以继续重新生成执行草案。", failureKind: "auth_required", })); return; } const payload = { input: input || "请基于当前创意重新生成一版项目草案", input_type: "text" as const, attachments: buildLaunchDraftAttachments(snapshot.input), source: "web", mode: "regenerate" as const, revision_note: "请基于同一创意重新生成一版可比较的替代草案,避免复刻上一版的标题、需求、任务和风险排序。", parent_draft_id: snapshot.draftId, }; void revisePMFlowLaunchDraft(snapshot.draftId, payload) .then((job) => { const shouldCancelCreatedJob = latestSnapshotRef.current.paused; setSnapshot((current) => ({ ...current, generationJobId: job.job_id, generationRevisionId: job.revision_id, generationJobStatus: job.status, generationForegroundWaitSeconds: job.runtime_policy?.foreground_wait_seconds ?? current.generationForegroundWaitSeconds, generationJobTimeoutSeconds: job.runtime_policy?.job_timeout_seconds ?? current.generationJobTimeoutSeconds, generationLargeInputPolicy: job.runtime_policy?.large_input_policy ?? current.generationLargeInputPolicy, systemStatus: { ...current.systemStatus, message: current.paused ? "重新生成已暂停,当前任务会被取消" : "重新生成任务已创建,正在等待 AI 返回", updatedAt: "刚刚", }, })); if (shouldCancelCreatedJob) { void cancelPMFlowLaunchDraftJob(job.job_id).catch(() => undefined); return; } applyJobResult(job); }) .catch((error) => { const normalized = normalizeApiError(error, "ai_generation_failed"); if (normalized.kind === "auth_required" || normalized.kind === "session_expired") { saveProjectLaunchInput(snapshot.input); } setSnapshot((current) => ({ ...current, stage: "generation_failed", generationStartedAt: null, errorMessage: normalized.message, failureKind: normalized.kind, })); }); }, [applyJobResult, snapshot.dataSourceMode, snapshot.draftId, snapshot.input, startGeneration]); const returnToInput = useCallback(() => { setSnapshot((current) => ({ ...current, stage: "intent_input", paused: false, activeStepIndex: 0, visibleOperationCount: 0, generationStartedAt: null, errorMessage: null, failureKind: null, systemStatus: { ...current.systemStatus, message: "系统已保存输入", updatedAt: "刚刚" }, })); }, []); const openDraftEdit = useCallback(() => { setSnapshot((current) => ({ ...current, stage: "draft_edit", editingDraft: cloneDraft(current.draft), })); }, []); const updateEditingDraft = useCallback((draft: ProjectDraft) => { setSnapshot((current) => ({ ...current, editingDraft: draft })); }, []); const saveDraft = useCallback(() => { setSnapshot((current) => ({ ...current, stage: "human_review", draft: cloneDraft(current.editingDraft), systemStatus: { ...current.systemStatus, message: "系统已自动保存草稿", updatedAt: "刚刚" }, })); }, []); const cancelDraftEdit = useCallback(() => { setSnapshot((current) => ({ ...current, stage: "human_review", editingDraft: cloneDraft(current.draft) })); }, []); const saveIdea = useCallback(() => { const input = snapshot.input.text.trim(); if (!input || snapshot.savingIdea) return; if (snapshot.dataSourceMode === "demo") { setSnapshot((current) => ({ ...current, stage: "intent_input", savedIdea: true, generationStartedAt: null, savedIdeaError: null, systemStatus: { ...current.systemStatus, message: "已暂存为想法", updatedAt: "刚刚" }, })); return; } if (!getAuthToken()) { saveProjectLaunchInput(snapshot.input); setSnapshot((current) => ({ ...current, stage: "generation_failed", generationStartedAt: null, errorMessage: "需要登录后继续。你的输入已保留,登录后可以继续保存为暂存想法。", failureKind: "auth_required", savedIdeaError: null, })); return; } setSnapshot((current) => ({ ...current, savingIdea: true, savedIdeaError: null, systemStatus: { ...current.systemStatus, message: "正在保存暂存想法", updatedAt: "刚刚" }, })); void spaceApi .createSavedIdea({ raw_text: input, source_type: "project_launch", input_mode: snapshot.input.mode, attachments: buildLaunchDraftAttachments(snapshot.input), }) .then((idea) => { setSnapshot((current) => ({ ...current, stage: "intent_input", savingIdea: false, savedIdea: true, generationStartedAt: null, savedIdeaId: idea.id, savedIdeaHref: "/space#saved-ideas", savedIdeaError: null, systemStatus: { ...current.systemStatus, message: "已保存到暂存想法", updatedAt: "刚刚" }, })); }) .catch((error) => { const normalized = normalizeApiError(error, "system_failed"); setSnapshot((current) => ({ ...current, savingIdea: false, savedIdea: false, savedIdeaError: normalized.message, systemStatus: { ...current.systemStatus, message: "暂存失败,输入已保留", updatedAt: "刚刚" }, })); }); }, [snapshot.dataSourceMode, snapshot.input, snapshot.savingIdea]); const createProject = useCallback(() => { if (snapshot.creatingProject) return; if (snapshot.dataSourceMode === "api") { if (!snapshot.draftId) { setSnapshot((current) => ({ ...current, stage: "generation_failed", generationStartedAt: null, errorMessage: "缺少可确认的项目草案,请重新生成。", failureKind: "input_invalid", })); return; } if (!getAuthToken()) { saveProjectLaunchInput(snapshot.input); setSnapshot((current) => ({ ...current, stage: "generation_failed", generationStartedAt: null, errorMessage: "需要登录后继续。你的输入已保留,登录后可以继续创建项目。", failureKind: "auth_required", })); return; } setSnapshot((current) => ({ ...current, creatingProject: true, systemStatus: { ...current.systemStatus, message: "正在创建真实项目", updatedAt: "刚刚" }, errorMessage: null, failureKind: null, })); void confirmPMFlowLaunchDraft(snapshot.draftId, { edited_draft: mapProjectDraftToEditedPayload(snapshot.draft, snapshot.rawDraft), confirm: true, }) .then((payload) => { setSnapshot((current) => ({ ...current, stage: "creation_success", activeStepIndex: terminalStepIndex, visibleOperationCount: aiOperationItemsMock.length, paused: false, creatingProject: false, generationStartedAt: null, redirectUrl: payload.redirect_url, warnings: payload.warnings || [], systemStatus: { ...current.systemStatus, message: "真实项目已创建", updatedAt: "刚刚" }, })); }) .catch((error) => { const normalized = normalizeApiError(error, "system_failed"); if (normalized.kind === "auth_required" || normalized.kind === "session_expired") { saveProjectLaunchInput(snapshot.input); } setSnapshot((current) => ({ ...current, stage: "generation_failed", creatingProject: false, generationStartedAt: null, errorMessage: normalized.message, failureKind: normalized.kind, })); }); return; } setSnapshot((current) => ({ ...current, stage: "creation_success", activeStepIndex: terminalStepIndex, visibleOperationCount: aiOperationItemsMock.length, paused: false, creatingProject: false, generationStartedAt: null, redirectUrl: "/command-center", })); }, [snapshot.creatingProject, snapshot.dataSourceMode, snapshot.draft, snapshot.draftId, snapshot.input, snapshot.rawDraft]); return { snapshot, steps, operations, updateInput, setInputMode, startGeneration, togglePause, supplementInput, regenerateDraft, returnToInput, openDraftEdit, updateEditingDraft, saveDraft, cancelDraftEdit, saveIdea, createProject, }; }