PMFlow/frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx

885 lines
44 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { LucideIcon } from "lucide-react";
import {
ArrowRight,
AlertTriangle,
Bot,
CheckCircle2,
ChevronDown,
CircleCheck,
Cloud,
FlaskConical,
GitBranch,
GitCommit,
GitPullRequest,
Loader2,
MessageSquareText,
RefreshCcw,
ShieldAlert,
TestTube2,
X,
} from "lucide-react";
import type { ReactNode } from "react";
import { Button } from "@/components/ui/button";
import type {
PMFlowReleaseGateEvidence,
PMFlowReleaseValidationDecision,
PMFlowReleaseValidationRun,
} from "@/api/pmflowOs";
import { usePMFlowOsStore } from "@/features/pmflow-os/store";
import { cn } from "@/lib/utils";
type StatusTone = "success" | "warning" | "danger" | "neutral" | "info";
const gateItems = [
{ label: "功能完整性", status: "通过", detail: "核心流程与功能验证完成", tone: "success" as const },
{ label: "性能基线", status: "通过", detail: "P95 响应时间 < 800ms", tone: "success" as const },
{ label: "安全扫描", status: "通过", detail: "无高危漏洞", tone: "success" as const },
{ label: "数据迁移", status: "通过", detail: "迁移脚本执行成功", tone: "success" as const },
{ label: "兼容性测试", status: "通过", detail: "主流浏览器验证通过", tone: "success" as const },
{ label: "可观测性", status: "通过", detail: "监控与告警已就绪", tone: "success" as const },
{ label: "回滚预案", status: "通过", detail: "回滚版本已准备", tone: "success" as const },
{ label: "文档与支持", status: "待完善", detail: "帮助文档需补充 2 篇", tone: "warning" as const },
];
const readinessMetrics = [
{ label: "质量", value: "94%", width: "w-[94%]" },
{ label: "可观测性", value: "92%", width: "w-[92%]" },
{ label: "稳定性", value: "90%", width: "w-[90%]" },
{ label: "缺陷度", value: "92%", width: "w-[92%]" },
];
const feedbackStats = [
{ label: "总反馈", value: "1,243", delta: "较上周 +18%" },
{ label: "正向反馈", value: "862", delta: "▲ 69%" },
{ label: "待处理", value: "214", delta: "▲ 17%" },
{ label: "负向反馈", value: "167", delta: "● 13%" },
];
const feedbackItems = [
{ name: "雁仪", source: "付费用户 · 5 分钟前", text: "希望支持上传更多类型的文档进行问答,目前仅支持 PDF 和 Word。", tag: "已处理", tone: "info" as const },
{ name: "阿聪", source: "免费用户 · 17 分钟前", text: "在长文本问答时,偶尔会出现答案截断的情况。", tag: "待处理", tone: "warning" as const },
{ name: "海峰", source: "企业用户 · 1 小时前", text: "新版的引用来源展示很有帮助,但任感提升 👍", tag: "已处理", tone: "info" as const },
];
const experiments = [
{ status: "进行中", title: "新手引导流程优化", goal: "提升新用户次日留存", metric: "次日留存率", version: "B", lift: "+12.4%", progress: "w-[62%]", remain: "3 天", tone: "success" as const },
{ status: "进行中", title: "定价页文案优化", goal: "提升付费转化率", metric: "付费转化率", version: "A", lift: "+6.1%", progress: "w-[48%]", remain: "5 天", tone: "success" as const },
{ status: "已完成", title: "推荐入口位置调整", goal: "提升功能使用率", metric: "功能点击率", version: "B vs A", lift: "+8.7%", progress: "w-full", remain: "采用版本 B", tone: "info" as const },
{ status: "草案", title: "周报订阅引导文案", goal: "提升周报订阅转化", metric: "订阅转化率", version: "-", lift: "预计开始", progress: "w-[0%]", remain: "2024-05-20", tone: "neutral" as const },
];
const riskItems = [
{
level: "高",
title: "第三方模型服务波动",
tag: "外部依赖",
owner: "程晨",
risk: "季度主线存在 5xx 波动",
impact: "登录、问答生成",
action: "已配置降级策略与超时重试",
rollback: "切回 v1.1.8(保留)",
},
{
level: "中",
title: "向量检索召回率下降",
tag: "模型效果",
owner: "召回年长风",
risk: "新版展开下滑明显",
impact: "答案相关性",
action: "已上线召回扩展实验",
rollback: "切回旧召回策略",
},
];
const funnelRows = [
{ label: "访问首页", value: "128,420", rate: "100%" },
{ label: "注册账号", value: "36,842", rate: "28.7%" },
{ label: "创建项目", value: "18,562", rate: "14.5%" },
{ label: "核心功能使用", value: "9,124", rate: "7.1%" },
{ label: "付费转化", value: "4,118", rate: "3.2%" },
];
const keyMetrics = [
{ label: "新增用户", value: "12,842", delta: "▲ 18%", path: "M2 16 L12 10 L22 12 L32 6 L42 9 L52 5 L62 8 L72 6" },
{ label: "付费转化率", value: "3.21%", delta: "▲ 0.42pp", path: "M2 14 L12 13 L22 16 L32 8 L42 11 L52 5 L62 14 L72 7" },
{ label: "ARPU", value: "¥98.6", delta: "▲ 6%", path: "M2 15 L12 11 L22 14 L32 12 L42 8 L52 10 L62 6 L72 5" },
{ label: "留存率(次日)", value: "48.7%", delta: "▲ 3.6pp", path: "M2 12 L12 14 L22 9 L32 10 L42 7 L52 12 L62 5 L72 9" },
{ label: "NPS", value: "62", delta: "▲ 5", path: "M2 16 L12 12 L22 13 L32 9 L42 11 L52 7 L62 10 L72 4" },
];
const aiActions = [
{ title: "优先处理", focus: "长文本截断", suffix: "问题" },
{ title: "扩展", focus: "新手引导", suffix: "实验至企业用户" },
{ title: "监控", focus: "第三方模型服务", suffix: "稳定性" },
{ title: "发布下一版迭代计划", focus: "", suffix: "预计 5 月 20 日" },
];
const nextSuggestions = [
{ title: "迭代优先级建议", body: "基于负向反馈,建议优先优化长文本截断问题,预计影响 13% 的活跃用户。", action: "查看建议详情" },
{ title: "实验推荐", body: "建议对「上传更多文档类型」进行需求验证实验,预估提升付费转化 +4~7%。", action: "创建实验" },
{ title: "增长机会", body: "新手引导流程优化效果良好,建议扩展至企业用户引导,目标提升次留 +10%。", action: "查看方案" },
{ title: "风险关注", body: "模型服务波动风险仍在,建议持续监控并保留回滚策略,保障稳定性。", action: "查看风险方案" },
];
function Card({ children, className }: { children: ReactNode; className?: string }) {
return <section className={cn("rounded-[14px] border border-[#e5edf8] bg-white shadow-[0_10px_24px_rgba(15,23,42,0.035)]", className)}>{children}</section>;
}
function StatusChip({ label, tone = "neutral" }: { label: string; tone?: StatusTone }) {
const toneClass: Record<StatusTone, string> = {
success: "border-emerald-100 bg-emerald-50 text-emerald-600",
warning: "border-orange-100 bg-orange-50 text-orange-500",
danger: "border-red-100 bg-red-50 text-red-500",
neutral: "border-slate-100 bg-slate-50 text-slate-500",
info: "border-blue-100 bg-blue-50 text-[#2463EB]",
};
return <span className={cn("rounded-md border px-2 py-0.5 text-[11px] font-semibold", toneClass[tone])}>{label}</span>;
}
function SectionTitle({ icon: Icon, title, meta, action }: { icon?: LucideIcon; title: string; meta?: string; action?: string }) {
return (
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
{Icon ? <Icon className="h-4 w-4 text-[#2463EB]" /> : null}
<h2 className="text-[16px] font-semibold text-slate-950">{title}</h2>
{meta ? <span className="text-xs text-slate-400">{meta}</span> : null}
</div>
{action ? <button className="text-xs font-semibold text-[#2463EB]">{action}</button> : null}
</div>
);
}
function ProgressLine({ label, value, width }: { label: string; value: string; width: string }) {
return (
<div>
<div className="flex items-center justify-between text-xs text-slate-500">
<span>{label}</span>
<span className="font-semibold">{value}</span>
</div>
<div className="mt-2 h-1.5 rounded-full bg-[#eef3fb]">
<div className={cn("h-1.5 rounded-full bg-[#2463EB]", width)} />
</div>
</div>
);
}
function MiniSpark({ path }: { path: string }) {
return (
<svg viewBox="0 0 74 20" className="h-6 w-20 text-[#2463EB]" aria-hidden="true">
<path d={path} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
function ReadinessRing() {
return (
<div className="relative flex h-40 w-40 items-center justify-center">
<svg viewBox="0 0 120 120" className="h-40 w-40 -rotate-90" aria-hidden="true">
<circle cx="60" cy="60" r="45" fill="none" stroke="#edf3fb" strokeWidth="10" />
<circle cx="60" cy="60" r="45" fill="none" stroke="#2463EB" strokeWidth="10" strokeLinecap="round" strokeDasharray="92 100" pathLength="100" />
</svg>
<div className="absolute text-center">
<div className="text-4xl font-semibold tracking-[-0.06em] text-slate-950">92%</div>
<div className="mt-1 text-sm font-medium text-slate-500">准备就绪</div>
</div>
</div>
);
}
function FunnelChart() {
return (
<div className="flex items-center gap-5">
<svg viewBox="0 0 220 150" className="h-36 flex-1 text-[#2463EB]" aria-hidden="true">
<polygon points="12,10 208,10 176,38 44,38" fill="#2463EB" />
<polygon points="44,42 176,42 154,68 66,68" fill="#3a8bff" />
<polygon points="66,72 154,72 135,98 85,98" fill="#6ab6ff" />
<polygon points="85,102 135,102 119,128 101,128" fill="#a4d7ff" />
</svg>
<div className="w-44 space-y-2">
{funnelRows.map((item) => (
<div key={item.label} className="grid grid-cols-[1fr_auto_auto] gap-3 text-xs">
<span className="text-slate-500">{item.label}</span>
<span className="font-medium text-slate-600">{item.value}</span>
<span className="text-slate-400">{item.rate}</span>
</div>
))}
</div>
</div>
);
}
function RevenueChart() {
return (
<svg viewBox="0 0 430 140" className="mt-3 h-36 w-full" aria-hidden="true">
<g stroke="#eef3fb" strokeWidth="1">
<line x1="32" y1="20" x2="408" y2="20" />
<line x1="32" y1="60" x2="408" y2="60" />
<line x1="32" y1="100" x2="408" y2="100" />
</g>
<g fill="#bfdbfe">
<rect x="58" y="88" width="18" height="28" rx="3" />
<rect x="104" y="78" width="18" height="38" rx="3" />
<rect x="150" y="68" width="18" height="48" rx="3" />
<rect x="196" y="58" width="18" height="58" rx="3" />
<rect x="242" y="64" width="18" height="52" rx="3" />
<rect x="288" y="49" width="18" height="67" rx="3" />
<rect x="334" y="32" width="18" height="84" rx="3" />
</g>
<path d="M48 95 C72 88 88 81 112 76 S158 63 186 58 S232 67 260 58 S302 40 326 34 S370 28 402 18" fill="none" stroke="#2463EB" strokeWidth="4" strokeLinecap="round" />
<g fill="#fff" stroke="#2463EB" strokeWidth="3">
<circle cx="48" cy="95" r="4" />
<circle cx="112" cy="76" r="4" />
<circle cx="186" cy="58" r="4" />
<circle cx="260" cy="58" r="4" />
<circle cx="326" cy="34" r="4" />
<circle cx="402" cy="18" r="4" />
</g>
</svg>
);
}
function AiCopilot() {
return (
<aside className="rounded-[18px] border border-[#e5edf8] bg-white p-5 shadow-[0_10px_24px_rgba(15,23,42,0.035)]">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-slate-950">AI 副驾</h2>
<button className="rounded-full p-1 text-slate-400 hover:bg-slate-50">
<X className="h-4 w-4" />
</button>
</div>
<div className="mt-6 grid grid-cols-3 border-b border-[#e8eef8] text-center text-sm font-semibold text-slate-500">
{["问", "做", "审"].map((tab) => (
<button key={tab} className={cn("pb-3", tab === "问" && "border-b-2 border-[#2463EB] text-[#2463EB]")}>
{tab}
</button>
))}
</div>
<div className="mt-5 space-y-5">
<div>
<div className="mb-2 text-sm font-semibold text-slate-500">当前上下文</div>
<div className="rounded-xl border border-[#e8eef8] bg-[#fbfdff] p-4 text-sm">
{[
["项目", "AI 智能问答产品"],
["阶段", "发布"],
["版本", "v1.2.0"],
["时间", "2024-05-12 ~ 近 7 天"],
].map(([label, value]) => (
<div key={label} className="grid grid-cols-[48px_1fr] py-1.5">
<span className="text-slate-400">{label}</span>
<span className="font-medium text-slate-600">{value}</span>
</div>
))}
</div>
</div>
<div>
<div className="mb-2 text-sm font-semibold text-slate-500">你可以问我</div>
<div className="space-y-2">
{["本次发布的整体表现如何?", "负向反馈的主要原因是什么?", "哪些实验效果最好?下一步做什么?", "收入趋势是否健康?", "有哪些潜在风险需要关注?"].map((item) => (
<button key={item} className="flex w-full items-center gap-3 rounded-xl border border-[#e8eef8] bg-white px-3 py-3 text-left text-sm text-slate-600 hover:border-[#bfd2ff] hover:text-[#2463EB]">
<MessageSquareText className="h-4 w-4 text-[#2463EB]" />
<span>{item}</span>
</button>
))}
</div>
</div>
<div>
<div className="mb-2 text-sm font-semibold text-slate-500">洞察与解释</div>
<div className="rounded-xl border border-[#e8eef8] bg-[#fbfdff] p-4 text-sm leading-7 text-slate-600">
本次发布整体表现良好,付费转化率环比提升 0.42pp,新手引导实验带来显著的次日留存提升。主要风险来自第三方模型服务波动,建议持续监控并保留降级策略。
</div>
</div>
<div>
<div className="mb-2 text-sm font-semibold text-slate-500">推荐动作</div>
<div className="rounded-xl border border-[#e8eef8]">
{aiActions.map((item) => (
<button key={item.title} className="flex w-full items-center gap-2 border-b border-[#edf2f9] px-3 py-3 text-left text-sm last:border-b-0">
<CircleCheck className="h-4 w-4 shrink-0 text-[#2463EB]" />
<span className="text-slate-600">{item.title}</span>
{item.focus ? <span className="font-semibold text-[#2463EB]">{item.focus}</span> : null}
<span className="text-slate-600">{item.suffix}</span>
</button>
))}
<div className="p-3">
<Button variant="outline" className="h-10 w-full rounded-xl border-[#dbe6ff] text-[#2463EB]">
创建迭代计划
</Button>
</div>
</div>
</div>
<div className="rounded-xl border border-[#e8eef8] bg-white p-3">
<div className="flex items-center gap-2">
<input className="h-10 min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-slate-400" placeholder="问任何关于发布与增长的问题..." />
<button className="flex h-10 w-10 items-center justify-center rounded-full bg-[#2463EB] text-white">
<ArrowRight className="h-5 w-5" />
</button>
</div>
</div>
<button className="w-full text-sm font-semibold text-[#2463EB]">查看全部对话历史</button>
</div>
</aside>
);
}
type PMFlowOsStoreSnapshot = ReturnType<typeof usePMFlowOsStore>;
function gateTone(status?: string): StatusTone {
if (status === "ready") return "success";
if (status === "blocked") return "danger";
if (status === "needs_confirmation") return "warning";
return "neutral";
}
function gateLabel(status?: string) {
if (status === "ready") return "可发布";
if (status === "blocked") return "阻塞";
if (status === "needs_confirmation") return "需要人工确认";
return "缺少执行证据";
}
function validationRunLabel(status?: string) {
if (status === "ready") return "发布候选";
if (status === "blocked") return "保持阻塞";
if (status === "needs_confirmation") return "需要确认";
return "未记录";
}
function validationSummaryText(run?: PMFlowReleaseValidationRun | null) {
if (!run) return "尚未创建 release validation run。";
const summary = run.validationSummary || {};
const sourceStatus = typeof summary.releaseGateStatus === "string" ? summary.releaseGateStatus : run.status;
return `基于 ${sourceStatus} release gate 固化,部署执行:${summary.deploymentExecuted ? "已执行" : "未执行"}`;
}
function checkTone(status?: string): StatusTone {
if (status === "passed") return "success";
if (status === "failed") return "danger";
return "warning";
}
function evidenceIcon(type: string) {
if (type.includes("branch")) return GitBranch;
if (type.includes("commit")) return GitCommit;
if (type.includes("pr") || type.includes("merged")) return GitPullRequest;
if (type.includes("test") || type.includes("check")) return TestTube2;
if (type.includes("fail") || type.includes("block")) return AlertTriangle;
return CheckCircle2;
}
function EvidenceRow({ item }: { item: PMFlowReleaseGateEvidence }) {
const Icon = evidenceIcon(item.type);
return (
<div className="grid gap-3 rounded-lg border border-[#e6edf7] bg-white p-4 md:grid-cols-[32px_minmax(0,1fr)_auto]">
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-[#eef4ff] text-[#2463EB]">
<Icon className="h-4 w-4" />
</span>
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<StatusChip label={item.type} tone={item.type.includes("fail") || item.type.includes("block") ? "danger" : "info"} />
{item.taskTitle ? <span className="text-sm font-semibold text-slate-800">{item.taskTitle}</span> : null}
</div>
<p className="mt-2 text-sm leading-6 text-slate-600">{item.summary}</p>
<div className="mt-2 flex flex-wrap gap-2 text-xs text-slate-400">
{item.repo ? <span>{item.repo}</span> : null}
{item.branchName ? <span>branch {item.branchName}</span> : null}
{item.commitSha ? <span>commit {item.commitSha.slice(0, 12)}</span> : null}
{item.prNumber ? <span>PR #{item.prNumber}</span> : null}
</div>
</div>
<div className="text-xs text-slate-400">{item.occurredAt ? new Date(item.occurredAt).toLocaleString("zh-CN") : "未记录时间"}</div>
</div>
);
}
function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
const {
state,
dataSourceMode,
dataSourceStatus,
dataSourceError,
executionSummary,
executionSummaryStatus,
executionSummaryError,
projectionWarnings,
refetchExecutionSummary,
createReleaseValidationRun,
} = store;
const gate = executionSummary?.releaseGate;
const latestValidationRun = gate?.latestValidationRun;
const isLoading = dataSourceStatus === "loading" || executionSummaryStatus === "loading";
const warnings = [...projectionWarnings, ...(executionSummary?.warnings || [])];
const emptyMode = dataSourceMode === "empty" || gate?.status === "empty" || !gate;
const canCreateValidationRun = Boolean(!emptyMode && gate?.evidence?.length);
const handleCreateValidationRun = (decision: PMFlowReleaseValidationDecision) => {
const decisionLabel = decision === "enter_release_candidate" ? "进入发布候选" : "保持阻塞";
createReleaseValidationRun({
decision,
note: `ReleaseGrowthPage 人工确认:${decisionLabel};PMFlow 仅记录发布前验证,不执行部署。`,
});
};
return (
<div className="min-h-full bg-[#f7faff] text-slate-950">
<div className="mx-auto flex w-full max-w-[1180px] flex-col gap-5 px-4 py-5 md:px-6 lg:px-8">
<Card className="p-5">
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-[#2463EB]">Release Gate</div>
<h1 className="mt-2 text-2xl font-semibold text-slate-950">发布 / 增长判断</h1>
<p className="mt-2 max-w-3xl text-sm leading-6 text-slate-500">
本页只读取 Execution Bridge 回写的 branch、commit、PR、test、failed 证据生成发布判断;PMFlow 不在这里执行发布命令。
</p>
</div>
<Button variant="outline" className="h-10 rounded-lg border-[#dbe6ff] text-[#2463EB]" onClick={refetchExecutionSummary}>
{isLoading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <RefreshCcw className="mr-2 h-4 w-4" />}
刷新执行证据
</Button>
</div>
</Card>
{dataSourceError || executionSummaryError ? (
<Card className="border-rose-100 bg-rose-50 p-4">
<div className="flex gap-3 text-sm text-rose-700">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
<span>{dataSourceError || executionSummaryError}</span>
</div>
</Card>
) : null}
<div className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_360px]">
<main className="space-y-5">
<Card className={cn("p-5", gate?.status === "blocked" && "border-rose-200 bg-rose-50", gate?.status === "ready" && "border-emerald-200 bg-emerald-50")}>
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div>
<StatusChip label={gateLabel(gate?.status)} tone={gateTone(gate?.status)} />
<h2 className="mt-4 text-2xl font-semibold text-slate-950">{gate?.title || "缺少执行证据"}</h2>
<p className="mt-2 text-sm leading-6 text-slate-600">
{gate?.summary || "URL 缺少 projectId 或后端尚未返回 execution-summary,无法生成发布判断。"}
</p>
</div>
<div className="rounded-lg border border-white/70 bg-white/80 px-4 py-3 text-sm">
<div className="text-xs text-slate-400">当前项目</div>
<div className="mt-1 font-semibold text-slate-900">{state.project.name}</div>
<div className="mt-3 text-xs text-slate-400">执行状态</div>
<div className="mt-1 font-semibold text-slate-700">{executionSummary?.execution.status || state.execution.status}</div>
</div>
</div>
</Card>
{emptyMode ? (
<Card className="border-orange-100 bg-orange-50 p-5">
<div className="flex gap-3">
<ShieldAlert className="mt-1 h-5 w-5 shrink-0 text-orange-500" />
<div>
<h2 className="text-lg font-semibold text-slate-950">没有可用于发布判断的执行证据</h2>
<p className="mt-2 text-sm leading-6 text-slate-600">
需要先从执行桥回写 branch、commit、PR、test 或 failed 事件。当前页面不会用 mock 发布结果补位。
</p>
</div>
</div>
</Card>
) : null}
<Card className="p-5">
<SectionTitle title="门禁检查" meta={gate?.updatedAt ? `更新于 ${new Date(gate.updatedAt).toLocaleString("zh-CN")}` : undefined} />
<div className="mt-4 grid gap-3 md:grid-cols-2">
{(gate?.checks || []).map((item) => (
<div key={item.key} className="rounded-lg border border-[#e6edf7] bg-white p-4">
<div className="flex items-center justify-between gap-3">
<div className="font-semibold text-slate-800">{item.label}</div>
<StatusChip label={item.status === "passed" ? "通过" : item.status === "failed" ? "失败" : "缺失"} tone={checkTone(item.status)} />
</div>
<div className="mt-2 text-sm leading-6 text-slate-500">{item.detail || "暂无对应证据"}</div>
</div>
))}
{!gate?.checks?.length ? <div className="text-sm text-slate-500">暂无门禁检查项。</div> : null}
</div>
</Card>
<Card className="p-5">
<SectionTitle title="执行证据" meta={`${gate?.evidence?.length || 0} 条`} />
<div className="mt-4 space-y-3">
{(gate?.evidence || []).map((item) => (
<EvidenceRow key={`${item.id}-${item.type}`} item={item} />
))}
{!gate?.evidence?.length ? <div className="rounded-lg border border-dashed border-[#d6e2f2] p-6 text-sm text-slate-500">暂无真实 execution evidence。</div> : null}
</div>
</Card>
</main>
<aside className="space-y-5">
<Card className="p-5">
<SectionTitle title="阻塞原因" icon={AlertTriangle} />
<div className="mt-4 space-y-2">
{(gate?.blockingReasons || []).map((item) => (
<div key={item} className="rounded-lg border border-rose-100 bg-rose-50 px-3 py-2 text-sm leading-6 text-rose-700">
{item}
</div>
))}
{!gate?.blockingReasons?.length ? <div className="text-sm leading-6 text-slate-500">当前没有阻塞原因。</div> : null}
</div>
</Card>
<Card className="p-5">
<SectionTitle title="下一步动作" icon={CircleCheck} />
<div className="mt-4 space-y-2">
{(gate?.nextActions || ["先补充执行回写证据。"]).map((item) => (
<div key={item} className="rounded-lg border border-[#e6edf7] bg-[#fbfdff] px-3 py-2 text-sm leading-6 text-slate-600">
{item}
</div>
))}
</div>
</Card>
<Card className="p-5">
<SectionTitle title="发布验证记录" icon={ShieldAlert} />
<div className="mt-4 rounded-lg border border-[#e6edf7] bg-[#fbfdff] p-4">
<div className="flex items-center justify-between gap-3">
<div>
<StatusChip label={validationRunLabel(latestValidationRun?.status)} tone={gateTone(latestValidationRun?.status)} />
<p className="mt-3 text-sm leading-6 text-slate-600">{validationSummaryText(latestValidationRun)}</p>
</div>
{latestValidationRun?.createdAt ? (
<div className="shrink-0 text-xs text-slate-400">
{new Date(latestValidationRun.createdAt).toLocaleString("zh-CN")}
</div>
) : null}
</div>
<div className="mt-4 text-xs leading-5 text-slate-500">
记录会固化当前 evidence、阻塞原因、下一步动作和人工确认结果;这里不会触发真实部署。
</div>
</div>
<div className="mt-4 grid gap-2">
{gate?.status === "blocked" ? (
<Button
className="h-10 rounded-lg bg-[#2463EB] text-white hover:bg-[#1f57d1]"
disabled={!canCreateValidationRun || isLoading}
onClick={() => handleCreateValidationRun("keep_blocked")}
>
记录保持阻塞
</Button>
) : (
<>
<Button
className="h-10 rounded-lg bg-[#2463EB] text-white hover:bg-[#1f57d1]"
disabled={!canCreateValidationRun || isLoading}
onClick={() => handleCreateValidationRun("enter_release_candidate")}
>
生成发布验证记录
</Button>
<Button
variant="outline"
className="h-10 rounded-lg border-[#dbe6ff] text-[#2463EB]"
disabled={!canCreateValidationRun || isLoading}
onClick={() => handleCreateValidationRun("keep_blocked")}
>
人工保持阻塞
</Button>
</>
)}
{!canCreateValidationRun ? (
<div className="rounded-lg border border-orange-100 bg-orange-50 px-3 py-2 text-xs leading-5 text-orange-700">
缺少真实执行证据,不能创建成功的发布验证记录。
</div>
) : null}
</div>
</Card>
<Card className="p-5">
<SectionTitle title="运行警告" />
<div className="mt-4 space-y-2">
{warnings.length ? (
warnings.map((item) => (
<div key={item} className="rounded-lg border border-orange-100 bg-orange-50 px-3 py-2 text-xs leading-5 text-orange-700">
{item}
</div>
))
) : (
<div className="text-sm leading-6 text-slate-500">暂无 projection warning。</div>
)}
</div>
</Card>
</aside>
</div>
</div>
</div>
);
}
export default function ReleaseGrowthPage() {
const store = usePMFlowOsStore();
if (store.dataSourceMode !== "demo") {
return <RealReleaseGrowthPage store={store} />;
}
return (
<div className="min-h-full bg-[#f7faff] text-slate-950">
<div className="grid gap-5 min-[1440px]:grid-cols-[minmax(0,1fr)_360px]">
<main className="min-w-0 space-y-4">
<Card className="overflow-hidden p-3">
<div className="flex min-w-0 items-center gap-3">
<div className="w-32 shrink-0 border-r border-[#e5edf8] pr-3">
<div className="text-xs font-medium text-slate-400">当前项目</div>
<div className="mt-2 whitespace-nowrap text-base font-semibold text-slate-950">AI 智能问答产品</div>
</div>
<div className="w-16 shrink-0 border-r border-[#e5edf8] pr-3">
<div className="text-xs font-medium text-slate-400">当前阶段</div>
<div className="mt-2">
<StatusChip label="发布" tone="success" />
</div>
</div>
<div className="w-28 shrink-0 border-r border-[#e5edf8] pr-3">
<div className="text-xs font-medium text-slate-400">发布状态</div>
<div className="mt-2 inline-flex whitespace-nowrap rounded-full border border-emerald-100 bg-emerald-50 px-3 py-1 text-sm font-semibold text-emerald-600">已发布 v1.2.0</div>
</div>
<div className="w-32 shrink-0 border-r border-[#e5edf8] pr-3">
<div className="text-xs font-medium text-slate-400">发布时间</div>
<div className="mt-2 whitespace-nowrap text-base font-semibold text-slate-700">2024-05-12 10:30</div>
</div>
<div className="w-16 shrink-0 border-r border-[#e5edf8] pr-3">
<div className="text-xs font-medium text-slate-400">当前风险</div>
<div className="mt-2 flex items-center gap-2 text-base font-semibold text-slate-700">
<span className="h-2.5 w-2.5 rounded-full bg-orange-400" />
中等
</div>
</div>
<div className="grid w-64 shrink-0 grid-cols-3 gap-3 border-r border-[#e5edf8] pr-3">
<div>
<div className="text-xs text-slate-400">新增用户</div>
<div className="mt-1 text-base font-semibold text-slate-700">12,842</div>
</div>
<div>
<div className="text-xs text-slate-400">付费转化率</div>
<div className="mt-1 text-base font-semibold text-slate-700">3.21%</div>
</div>
<div>
<div className="text-xs text-slate-400">月度经常性收入</div>
<div className="mt-1 text-base font-semibold text-slate-700">¥126,800</div>
</div>
</div>
<div className="ml-auto flex shrink-0 items-center justify-end gap-2">
<Button variant="outline" className="h-9 rounded-xl border-[#dbe6ff] bg-white px-3 text-sm">
<Cloud className="mr-2 h-4 w-4" />
发布记录
</Button>
<Button variant="outline" className="h-9 rounded-xl border-[#dbe6ff] bg-white px-3 text-sm">
<FlaskConical className="mr-2 h-4 w-4" />
添加实验
<ChevronDown className="ml-2 h-4 w-4" />
</Button>
<Button className="h-9 rounded-xl bg-[#2463EB] px-4 text-sm text-white hover:bg-[#1f57d1]">新建迭代计划</Button>
</div>
</div>
</Card>
<div className="grid gap-4 xl:grid-cols-[1fr_0.9fr_1fr]">
<Card className="p-4">
<SectionTitle title="发布门禁 / 发布检查清单" meta="● 通过 7/8" />
<div className="mt-4 divide-y divide-[#edf2f9] rounded-xl border border-[#edf2f9]">
{gateItems.map((item) => (
<div key={item.label} className="grid grid-cols-[1fr_66px_1.4fr] items-center gap-4 px-3 py-2.5 text-sm">
<div className="flex items-center gap-2 font-medium text-slate-700">
<CheckCircle2 className={cn("h-4 w-4", item.tone === "success" ? "text-emerald-500" : "text-orange-500")} />
{item.label}
</div>
<StatusChip label={item.status} tone={item.tone} />
<div className="truncate text-slate-500">{item.detail}</div>
</div>
))}
</div>
<button className="mt-4 w-full text-sm font-semibold text-[#2463EB]">查看详情</button>
</Card>
<Card className="p-5">
<SectionTitle title="发布准备度" />
<div className="mt-6 grid grid-cols-[180px_1fr] items-center gap-5">
<ReadinessRing />
<div className="space-y-4">
{readinessMetrics.map((item) => (
<ProgressLine key={item.label} {...item} />
))}
</div>
</div>
<button className="mt-6 w-full text-sm font-semibold text-[#2463EB]">准备度详情</button>
</Card>
<Card className="p-5">
<SectionTitle title="已知风险与回滚提醒" meta="● 2 个高风险" />
<div className="mt-4 space-y-3">
{riskItems.map((item) => (
<div key={item.title} className="rounded-xl border border-[#edf2f9] bg-[#fbfdff] p-4">
<div className="flex items-center gap-2">
<span className={cn("h-2.5 w-2.5 rounded-full", item.level === "高" ? "bg-red-500" : "bg-orange-400")} />
<span className={cn("text-sm font-semibold", item.level === "高" ? "text-red-500" : "text-orange-500")}>{item.level}</span>
<span className="font-semibold text-slate-800">{item.title}</span>
<StatusChip label={item.tag} tone={item.level === "高" ? "danger" : "warning"} />
</div>
<div className="mt-3 grid gap-1 text-sm leading-6 text-slate-500">
<span>依赖的模型服务在高峰期存在 {item.risk}</span>
<span>影响范围:{item.impact}</span>
<span>应对方案:{item.action}</span>
<span>回滚方案:{item.rollback}</span>
</div>
<button className="mt-2 text-xs font-semibold text-[#2463EB]">{item.level === "高" ? "查看监控" : "查看详情"}</button>
</div>
))}
</div>
<button className="mt-3 w-full text-sm font-semibold text-[#2463EB]">风险中心</button>
</Card>
</div>
<div className="grid gap-4 xl:grid-cols-[0.9fr_1.7fr]">
<Card className="p-4">
<SectionTitle title="用户反馈摘要" meta="近 7 天" />
<div className="mt-4 grid grid-cols-4 gap-3 border-b border-[#edf2f9] pb-4">
{feedbackStats.map((item) => (
<div key={item.label}>
<div className="text-xs text-slate-400">{item.label}</div>
<div className="mt-1 text-lg font-semibold text-slate-700">{item.value}</div>
<div className="mt-1 text-xs text-emerald-600">{item.delta}</div>
</div>
))}
</div>
<div className="mt-3 space-y-2">
{feedbackItems.map((item, index) => (
<div key={item.text} className="grid grid-cols-[32px_1fr_auto] items-center gap-3 rounded-xl border border-[#edf2f9] bg-white p-3">
<div className={cn("flex h-8 w-8 items-center justify-center rounded-full text-xs font-semibold text-white", index === 0 ? "bg-orange-300" : index === 1 ? "bg-blue-300" : "bg-emerald-300")}>{item.name.slice(0, 1)}</div>
<div className="min-w-0">
<div className="truncate text-sm text-slate-600">{item.text}</div>
<div className="mt-1 text-xs text-slate-400">来自 {item.source}</div>
</div>
<StatusChip label={item.tag} tone={item.tone} />
</div>
))}
</div>
<button className="mt-4 w-full text-sm font-semibold text-[#2463EB]">查看全部反馈</button>
</Card>
<Card className="p-4">
<SectionTitle title="增长实验" action="全部实验 →" />
<div className="mt-4 grid gap-3 xl:grid-cols-4">
{experiments.map((item) => (
<div key={item.title} className="rounded-xl border border-[#e8eef8] bg-[#fbfdff] p-4">
<StatusChip label={item.status} tone={item.tone} />
<div className="mt-3 text-sm font-semibold text-slate-800">{item.title}</div>
<div className="mt-3 space-y-2 text-xs leading-5 text-slate-500">
<div>实验目标</div>
<div className="font-medium text-slate-600">{item.goal}</div>
<div>关键指标:{item.metric}</div>
<div>当前版本:{item.version}</div>
<div className="text-emerald-600">提升效果 {item.lift}</div>
</div>
<div className="mt-4">
<div className="mb-1 flex items-center justify-between text-xs text-slate-400">
<span>运行进度</span>
<span>{item.remain}</span>
</div>
<div className="h-1.5 rounded-full bg-[#edf2f9]">
<div className={cn("h-1.5 rounded-full bg-[#2463EB]", item.progress)} />
</div>
</div>
{item.status === "已完成" ? (
<Button variant="outline" className="mt-4 h-9 w-full rounded-xl border-[#dbe6ff] text-[#2463EB]">
查看报告
</Button>
) : null}
{item.status === "草案" ? <Button className="mt-4 h-9 w-full rounded-xl bg-[#2463EB] text-white hover:bg-[#1f57d1]">启动实验</Button> : null}
</div>
))}
</div>
</Card>
</div>
<div className="grid gap-4 xl:grid-cols-[0.9fr_1fr_0.75fr]">
<Card className="p-4">
<SectionTitle title="转化漏斗" meta="近 7 天" />
<div className="mt-4">
<FunnelChart />
</div>
<div className="mt-4 flex items-center justify-between rounded-lg border border-[#e8eef8] px-4 py-2 text-sm">
<span className="font-medium text-slate-600">整体转化率</span>
<span className="font-semibold text-[#2463EB]">3.2%</span>
</div>
<button className="mt-4 w-full text-sm font-semibold text-[#2463EB]">查看漏斗分析</button>
</Card>
<Card className="p-4">
<div className="flex items-center justify-between">
<SectionTitle title="收入信号趋势" />
<button className="rounded-lg border border-[#e8eef8] px-3 py-1 text-xs text-slate-500">
近 7 天
<ChevronDown className="ml-1 inline h-3 w-3" />
</button>
</div>
<div className="mt-3 flex items-center gap-4 text-xs text-slate-500">
<span className="flex items-center gap-1">
<span className="h-2 w-2 rounded-full bg-[#2463EB]" />
收入(元)
</span>
<span className="flex items-center gap-1">
<span className="h-2 w-2 rounded-full bg-[#9cc7ff]" />
新增付费用户(人)
</span>
</div>
<RevenueChart />
<button className="w-full text-sm font-semibold text-[#2463EB]">查看收入分析</button>
</Card>
<Card className="p-4">
<SectionTitle title="关键指标概览" meta="近 7 天" />
<div className="mt-3 divide-y divide-[#edf2f9]">
{keyMetrics.map((item) => (
<div key={item.label} className="grid grid-cols-[1fr_auto_auto] items-center gap-4 py-2.5 text-sm">
<span className="text-slate-500">{item.label}</span>
<span className="font-semibold text-slate-700">{item.value}</span>
<div className="flex items-center gap-3">
<span className="text-xs font-medium text-emerald-600">{item.delta}</span>
<MiniSpark path={item.path} />
</div>
</div>
))}
</div>
<button className="mt-2 w-full text-sm font-semibold text-[#2463EB]">进入指标中心</button>
</Card>
</div>
<Card className="border-[#9dbbff] p-4 shadow-[0_0_0_1px_rgba(36,99,235,0.12)]">
<div className="mb-3 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-[#2463EB] text-white">
<Bot className="h-5 w-5" />
</div>
<h2 className="text-xl font-semibold text-slate-950">AI 下一步建议</h2>
</div>
</div>
<div className="grid gap-3 xl:grid-cols-4">
{nextSuggestions.map((item) => (
<div key={item.title} className="rounded-xl border border-[#e8eef8] bg-white p-4">
<div className="text-sm font-semibold text-slate-800">{item.title}</div>
<p className="mt-2 min-h-16 text-sm leading-6 text-slate-500">{item.body}</p>
<Button variant="outline" className="mt-3 h-9 w-full rounded-xl border-[#dbe6ff] text-[#2463EB]">
{item.action}
</Button>
</div>
))}
</div>
<div className="mt-3 flex items-center justify-end gap-2 text-xs text-slate-400">
<span>以上建议由 AI 基于当前数据生成</span>
<RefreshCcw className="h-3.5 w-3.5 text-[#2463EB]" />
<button className="font-semibold text-[#2463EB]">刷新建议</button>
</div>
</Card>
</main>
<div className="hidden min-[1440px]:block">
<AiCopilot />
</div>
</div>
</div>
);
}