885 lines
44 KiB
TypeScript
885 lines
44 KiB
TypeScript
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>
|
||
);
|
||
}
|