8 Commits

Author SHA1 Message Date
ZhuJW
679d6947e7 提交task 2026-03-13 21:11:24 +08:00
zhongcheng
250192f699 Merge pull request 'update ui color' (#4) from cheng-0313 into main
Reviewed-on: #4
2026-03-13 17:56:50 +08:00
zhongcheng
cef1ae0414 update ui color 2026-03-13 17:55:57 +08:00
zhongcheng
eeb5e728ff Merge pull request 'update ui' (#3) from cheng-0313 into main
Reviewed-on: #3
2026-03-13 16:42:51 +08:00
zhongcheng
98f916bd37 update ui 2026-03-13 16:41:31 +08:00
Dang Zerong
80430c674b 修改 质量门控 2026-03-13 12:39:15 +08:00
99a3281d58 Merge pull request 'Enhance PlanningAgent with Markdown rendering and Tailwind Typography' (#2) from feature/ding into main
Reviewed-on: #2
2026-03-13 11:20:43 +08:00
Ding Shuo
cca1b8d046 Enhance PlanningAgent with Markdown rendering and Tailwind Typography 2026-03-13 10:52:06 +08:00
20 changed files with 4512 additions and 297 deletions

View File

@@ -6,6 +6,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<title>SAFe OS — Multi-Agent 敏捷协同指挥中心</title>
</head>
<body>

1754
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,9 +9,13 @@
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/typography": "^0.5.19",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.28.0"
"react-markdown": "^10.1.0",
"react-router-dom": "^6.28.0",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.1"
},
"devDependencies": {
"@types/node": "^25.4.0",

View File

@@ -3,17 +3,22 @@ import Layout from "./Layout";
import PlanningAgent from "./pages/PlanningAgent";
import DevOpsAgent from "./pages/DevOpsAgent";
import QualityGate from "./pages/QualityGate";
import TaskExecutor from "./pages/TaskExecutor";
import Home from "./pages/Home";
export default function App() {
return (
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="/home" element={<Home />} />
<Route path="/planning" element={<PlanningAgent />} />
<Route path="/devops" element={<DevOpsAgent />} />
<Route path="/quality" element={<Navigate to="/quality/dashboard" replace />} />
<Route path="/quality/dashboard" element={<QualityGate view="dashboard" />} />
<Route path="/quality/pr-list" element={<QualityGate view="pr-list" />} />
<Route path="/quality/settings" element={<QualityGate view="settings" />} />
<Route path="/task" element={<TaskExecutor />} />
<Route path="*" element={<Navigate to="/planning" replace />} />
</Route>
</Routes>

View File

@@ -1,158 +1,144 @@
import { useEffect, useMemo, useState } from "react";
import { useMemo } from "react";
import { NavLink, Outlet, useLocation } from "react-router-dom";
const QUALITY_SUB_ITEMS = [
{ to: "/quality/dashboard", title: "Overview" },
{ to: "/quality/pr-list", title: "PR List" },
{ to: "/quality/settings", title: "Settings" },
] as const;
const PAGE_TITLES: Record<string, string> = {
"/planning": "Strategic Planning Workspace",
"/devops": "Delivery Execution Workspace",
"/quality/dashboard": "Quality Gate Overview",
"/quality/pr-list": "Quality Gate · PR List",
"/quality/settings": "Quality Gate · Settings",
"/": "总览控制台",
"/planning": "战略规划 (Planning)",
"/devops": "开发运维 (DevOps)",
"/quality": "质量门控 (Quality Gate Dashboard)",
"/quality/dashboard": "质量门控 (Quality Gate Dashboard)",
"/quality/pr-list": "合并请求审查 (PR List)",
"/quality/settings": "质量设置",
"/task": "任务执行器 (Task Executor)",
};
export default function Layout() {
const { pathname } = useLocation();
const inQuality = pathname.startsWith("/quality");
const [qualityExpanded, setQualityExpanded] = useState<boolean>(inQuality);
useEffect(() => {
if (inQuality) {
setQualityExpanded(true);
}
}, [inQuality]);
const pageTitle = useMemo(() => {
if (PAGE_TITLES[pathname]) {
return PAGE_TITLES[pathname];
}
if (inQuality) {
return "Quality Gate";
}
return "SAFe OS";
}, [pathname, inQuality]);
return PAGE_TITLES[pathname] || "SAFe OS";
}, [pathname]);
return (
<div className="flex h-screen w-screen bg-[#fafafa]">
{/* ─── Sidebar ─── */}
<nav className="w-[280px] shrink-0 bg-surface-dark text-txt-inverse flex flex-col py-8 border-r border-white/10">
<div className="px-8 mb-9 text-2xl font-extrabold tracking-tight select-none">
SAFe <span className="text-magenta">OS</span>
</div>
<div className="px-8 text-[11px] text-gray-500 uppercase font-bold tracking-widest mb-4">
Agent Pipeline
<div className="bg-[#f7f7f9] flex h-screen w-screen overflow-hidden text-txt font-sans">
{/* 侧边栏导航 */}
<aside className="w-64 bg-white border-r border-border flex flex-col z-10 shrink-0">
<div className="h-16 flex items-center px-6 border-b border-border">
<i className="fa-solid fa-layer-group text-magenta text-xl mr-3"></i>
<span className="text-lg font-extrabold text-txt">SAFe OS</span>
</div>
<div className="p-4 flex-1">
<div className="text-[10px] font-bold text-txt-muted uppercase tracking-widest mb-3 px-1"></div>
<nav className="space-y-1">
<NavLink
to="/"
className={({ isActive }) =>
`flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-semibold transition-all duration-150 ${
isActive && pathname === '/'
? "bg-magenta/10 text-magenta"
: "text-txt hover:bg-surface-muted hover:text-magenta"
}`
}
>
<i className="fa-solid fa-house w-4 text-center shrink-0"></i>
<span></span>
</NavLink>
<NavLink
to="/planning"
className={({ isActive }) =>
`mx-4 rounded-xl flex items-center gap-3 px-4 py-3 border border-transparent transition-all cursor-pointer ${
`flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-semibold transition-all duration-150 ${
isActive
? "border-magenta/40 bg-white/10"
: "hover:bg-white/5"
? "bg-[#0EA5E9]/10 text-[#0EA5E9]"
: "text-txt hover:bg-surface-muted hover:text-[#0EA5E9]"
}`
}
>
{({ isActive }) => (
<>
<span
className={`w-2 h-2 rounded-full shrink-0 ${
isActive ? "bg-magenta" : "bg-gray-600"
}`}
/>
<div>
<div className="font-bold text-[0.95rem]">1. Planning Council Agent</div>
<div className="text-xs text-gray-400">Business and architecture planning</div>
</div>
</>
)}
<i className="fa-solid fa-chess-knight w-4 text-center shrink-0"></i>
<span></span>
</NavLink>
<NavLink
to="/devops"
className={({ isActive }) =>
`mx-4 rounded-xl flex items-center gap-3 px-4 py-3 border border-transparent transition-all cursor-pointer ${
`flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-semibold transition-all duration-150 ${
isActive
? "border-magenta/40 bg-white/10"
: "hover:bg-white/5"
? "bg-[#10B981]/10 text-[#10B981]"
: "text-txt hover:bg-surface-muted hover:text-[#10B981]"
}`
}
>
{({ isActive }) => (
<>
<span
className={`w-2 h-2 rounded-full shrink-0 ${
isActive ? "bg-magenta" : "bg-gray-600"
}`}
/>
<div>
<div className="font-bold text-[0.95rem]">2. DevOps Agent</div>
<div className="text-xs text-gray-400">Story breakdown and implementation</div>
</div>
</>
)}
<i className="fa-solid fa-code-branch w-4 text-center shrink-0"></i>
<span></span>
</NavLink>
<div className="mx-4 rounded-xl border border-transparent">
<button
type="button"
onClick={() => setQualityExpanded((v) => !v)}
className={`w-full rounded-xl flex items-center gap-3 px-4 py-3 transition-all text-left ${
inQuality ? "bg-white/10 border border-magenta/40" : "hover:bg-white/5"
}`}
>
<span
className={`w-2 h-2 rounded-full shrink-0 ${
inQuality ? "bg-magenta" : "bg-gray-600"
}`}
/>
<div className="min-w-0 flex-1">
<div className="font-bold text-[0.95rem]">3. Quality Gate Agent</div>
<div className="text-xs text-gray-400">Automated validation and review</div>
</div>
<span className={`text-xs text-gray-400 transition-transform ${qualityExpanded ? "rotate-90" : ""}`}>
</span>
</button>
{qualityExpanded && (
<div className="pl-10 pr-3 pb-3 pt-1 flex flex-col gap-1">
{QUALITY_SUB_ITEMS.map((sub) => (
<NavLink
key={sub.to}
to={sub.to}
className={({ isActive }) =>
`text-xs font-bold px-3 py-2 rounded-lg transition-colors ${
isActive
? "text-magenta bg-white/10"
: "text-gray-400 hover:text-white hover:bg-white/5"
to="/quality/dashboard"
className={() =>
`flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-semibold transition-all duration-150 ${
pathname.startsWith('/quality')
? "bg-[#8B5CF6]/10 text-[#8B5CF6]"
: "text-txt hover:bg-surface-muted hover:text-[#8B5CF6]"
}`
}
>
{sub.title}
<i className="fa-solid fa-shield-halved w-4 text-center shrink-0"></i>
<span></span>
</NavLink>
<NavLink
to="/task"
className={({ isActive }) =>
`flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-semibold transition-all duration-150 ${
isActive
? "bg-orange-500/10 text-orange-500"
: "text-txt hover:bg-surface-muted hover:text-orange-500"
}`
}
>
<i className="fa-solid fa-list-check w-4 text-center shrink-0"></i>
<span></span>
</NavLink>
))}
</div>
)}
</div>
<div className="mt-auto px-8 text-[0.7rem] text-gray-600">
T-Systems · SAFe Multi-Agent Demo UI
</div>
</nav>
{/* ─── Main workspace ─── */}
<main className="flex-1 flex flex-col bg-white min-w-0">
<header className="h-20 px-10 flex items-center justify-between border-b border-border/80 bg-white/90 backdrop-blur shrink-0">
<h1 className="text-2xl font-extrabold tracking-tight">{pageTitle}</h1>
<div className="text-[10px] font-bold text-txt-muted uppercase tracking-widest mt-8 mb-3 px-1"></div>
<nav className="space-y-1">
<a href="#" className="flex items-center gap-3 px-4 py-2.5 text-txt text-sm font-semibold hover:bg-surface-muted rounded-xl transition-all duration-150">
<i className="fa-solid fa-gear w-4 text-center shrink-0"></i>
<span></span>
</a>
</nav>
</div>
<div className="p-4 border-t border-border flex items-center gap-3">
<div className="w-9 h-9 rounded-full bg-magenta/10 text-magenta flex items-center justify-center text-sm font-bold shrink-0">A</div>
<div>
<p className="text-sm font-semibold text-txt"></p>
<p className="text-xs text-txt-muted flex items-center gap-1">
<span className="w-1.5 h-1.5 bg-green-400 rounded-full inline-block"></span>线
</p>
</div>
</div>
</aside>
{/* 主内容区 */}
<main className="flex-1 flex flex-col h-screen overflow-y-auto relative min-w-0">
{/* 顶部导航 */}
<header className="h-14 bg-white border-b border-border flex items-center justify-between px-8 z-0 shrink-0 shadow-[0_1px_4px_rgba(0,0,0,0.04)]">
<h1 className="text-base font-bold text-txt">{pageTitle}</h1>
{pathname === '/' && (
<div className="flex items-center gap-3">
<div className="relative">
<i className="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-txt-muted text-xs"></i>
<input type="text" placeholder="搜索需求、代码或 PR…" className="pl-9 pr-4 py-2 border border-border rounded-xl text-sm focus:outline-none focus:border-magenta focus:ring-2 focus:ring-magenta/10 w-60 transition-all bg-[#f7f7f9]" />
</div>
<button className="relative p-2 text-txt-muted hover:text-magenta transition-colors">
<i className="fa-regular fa-bell text-base"></i>
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full border border-white"></span>
</button>
</div>
)}
</header>
<div className="flex-1 overflow-hidden">
{/* 内容画布 */}
<div className="flex-1 w-full bg-[#f7f7f9] overflow-y-auto">
<Outlet />
</div>
</main>

View File

@@ -12,4 +12,7 @@ export const API = {
/** 质量门禁 Agent — PR scanning & code review */
quality: "/quality-api",
/** SDLC Task Executor — multi-agent software delivery */
sdlc: "/api/v1/sdlc",
} as const;

View File

@@ -23,7 +23,7 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background:
radial-gradient(circle at 20% -10%, #fff0f7 0%, transparent 35%),
radial-gradient(circle at 20% -10%, #fff1f2 0%, transparent 35%),
radial-gradient(circle at 90% 0%, #f5f5f5 0%, transparent 28%),
#fff;
color: #1a1a1a;

View File

@@ -489,7 +489,7 @@ export default function DevOpsAgent() {
<div className="flex flex-col h-full overflow-hidden bg-[#f7f7f9]">
{/* ── 步骤栏 ── */}
<div className="shrink-0 border-b border-border bg-white shadow-[0_1px_8px_rgba(0,0,0,0.04)]">
<div className="flex items-center gap-0 px-8 py-0">
<div className="flex items-center gap-0 px-[60px] py-0">
{STEPS.map((s, i) => (
<div key={i} className="flex items-center">
<button
@@ -532,15 +532,15 @@ export default function DevOpsAgent() {
{/* Error */}
{error && (
<div className="flex items-center justify-between px-4 py-2.5 mx-8 mt-4 text-sm text-red-700 bg-red-50 border border-red-200 rounded-xl">
<div className="flex items-center justify-between px-4 py-2.5 mx-10 mt-4 text-sm text-red-700 bg-red-50 border border-red-200 rounded-xl">
<span className="flex items-center gap-2"><span></span>{error}</span>
<button className="ml-4 font-bold text-red-300 hover:text-red-500" onClick={() => setError("")}></button>
</div>
)}
{/* ── 内容区 ── */}
<div className="flex-1 px-8 py-6 overflow-y-auto">
<div className="max-w-5xl mx-auto space-y-0">
<div className="flex-1 px-[60px] py-6 overflow-y-auto">
<div className="space-y-0">
{/* ═══ Step 0: Requirement Input ═══ */}
{step === 0 && !sessionId && (

173
src/pages/Home.tsx Normal file
View File

@@ -0,0 +1,173 @@
import React from 'react';
import { Link } from 'react-router-dom';
export default function Home() {
return (
<div className="p-[60px] w-full min-h-[calc(100vh-3.5rem)] flex flex-col">
{/* 欢迎横幅 */}
<div className="mb-8 flex items-center justify-between overflow-hidden relative rounded-2xl border border-magenta/15 bg-gradient-to-br from-magenta/[0.06] via-white to-white shadow-[0_4px_16px_rgba(244,63,94,0.06)] p-6">
<div className="absolute top-0 left-0 right-0 h-0.5 bg-gradient-to-r from-magenta via-magenta/60 to-transparent"></div>
<div className="pt-1">
<h2 className="text-2xl font-extrabold text-txt mb-1.5">SAFe OS 👋</h2>
<p className="text-txt-muted text-sm mb-4">AI Agent </p>
<div className="flex items-center gap-2 text-sm bg-surface-muted px-4 py-2 rounded-xl w-fit">
<span className="w-2 h-2 bg-green-400 rounded-full shrink-0"></span>
<span className="text-txt-muted">AI </span>
<span className="text-green-600 font-semibold"></span>
</div>
</div>
<div className="hidden md:block shrink-0 ml-8 text-6xl font-extrabold text-txt opacity-[0.04] select-none">
<i className="fa-brands fa-hubspot"></i>
</div>
</div>
{/* 数据概览 */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 mb-8">
<div className="bg-white rounded-2xl border border-border shadow-[0_4px_16px_rgba(0,0,0,0.04)] overflow-hidden flex">
<div className="w-1 shrink-0 bg-[#0EA5E9]"></div>
<div className="flex items-center gap-4 px-5 py-5 flex-1">
<div className="w-10 h-10 rounded-xl bg-[#0EA5E9]/10 flex items-center justify-center shrink-0">
<i className="fa-solid fa-list-check text-[#0EA5E9] text-sm"></i>
</div>
<div>
<p className="text-[11px] font-bold text-txt-muted uppercase tracking-wide mb-0.5"></p>
<div className="flex items-baseline gap-1.5">
<span className="text-3xl font-extrabold text-txt leading-none">24</span>
<span className="text-xs font-semibold text-green-500"> 3</span>
</div>
</div>
</div>
</div>
<div className="bg-white rounded-2xl border border-border shadow-[0_4px_16px_rgba(0,0,0,0.04)] overflow-hidden flex">
<div className="w-1 shrink-0 bg-[#10B981]"></div>
<div className="flex items-center gap-4 px-5 py-5 flex-1">
<div className="w-10 h-10 rounded-xl bg-[#10B981]/10 flex items-center justify-center shrink-0">
<i className="fa-solid fa-code-pull-request text-[#10B981] text-sm"></i>
</div>
<div>
<p className="text-[11px] font-bold text-txt-muted uppercase tracking-wide mb-0.5"> PRs</p>
<div className="flex items-baseline gap-1.5">
<span className="text-3xl font-extrabold text-txt leading-none">12</span>
<span className="text-xs font-semibold text-red-500"> 2</span>
</div>
</div>
</div>
</div>
<div className="bg-white rounded-2xl border border-border shadow-[0_4px_16px_rgba(0,0,0,0.04)] overflow-hidden flex">
<div className="w-1 shrink-0 bg-[#8B5CF6]"></div>
<div className="flex items-center gap-4 px-5 py-5 flex-1">
<div className="w-10 h-10 rounded-xl bg-[#8B5CF6]/10 flex items-center justify-center shrink-0">
<i className="fa-solid fa-heart-pulse text-[#8B5CF6] text-sm"></i>
</div>
<div>
<p className="text-[11px] font-bold text-txt-muted uppercase tracking-wide mb-0.5"></p>
<div className="flex items-baseline gap-1.5">
<span className="text-3xl font-extrabold text-txt leading-none">92%</span>
<span className="text-xs font-semibold text-green-500"></span>
</div>
</div>
</div>
</div>
</div>
{/* 核心模块入口 */}
<div className="flex items-center gap-3 mb-5">
<h3 className="text-base font-extrabold text-txt"></h3>
<div className="flex-1 h-px bg-border"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 flex-1">
{/* 模块 1: 战略规划 */}
<div className="group flex flex-col bg-white rounded-2xl overflow-hidden border border-border shadow-[0_4px_16px_rgba(0,0,0,0.04)] hover:-translate-y-1 hover:shadow-[0_12px_32px_rgba(0,0,0,0.08)] transition-all duration-300">
<div className="h-1 bg-[#0EA5E9]"></div>
<div className="p-6 flex-1">
<div className="w-11 h-11 bg-[#0EA5E9]/10 text-[#0EA5E9] rounded-xl flex items-center justify-center text-xl mb-4 group-hover:bg-[#0EA5E9] group-hover:text-white transition-colors duration-200">
<i className="fa-solid fa-chess-knight"></i>
</div>
<h4 className="text-base font-extrabold text-txt mb-2"></h4>
<p className="text-txt-muted text-sm mb-5 leading-relaxed">AI </p>
<div className="space-y-2">
<div className="flex items-center gap-2.5 text-sm text-txt bg-surface-muted px-3 py-2 rounded-lg">
<i className="fa-solid fa-check text-[#0EA5E9] text-xs shrink-0"></i>
<span></span>
</div>
<div className="flex items-center gap-2.5 text-sm text-txt bg-surface-muted px-3 py-2 rounded-lg">
<i className="fa-solid fa-check text-[#0EA5E9] text-xs shrink-0"></i>
<span></span>
</div>
</div>
</div>
<div className="px-6 py-4 bg-surface-muted border-t border-border">
<Link to="/planning" className="block text-center w-full py-2 bg-white border border-border text-[#0EA5E9] text-sm font-semibold rounded-xl hover:border-[#0EA5E9] hover:bg-[#0EA5E9]/5 transition-colors">
</Link>
</div>
</div>
{/* 模块 2: 开发运维 */}
<div className="group flex flex-col bg-white rounded-2xl overflow-hidden border border-border shadow-[0_4px_16px_rgba(0,0,0,0.04)] hover:-translate-y-1 hover:shadow-[0_12px_32px_rgba(0,0,0,0.08)] transition-all duration-300">
<div className="h-1 bg-[#10B981]"></div>
<div className="p-6 flex-1">
<div className="w-11 h-11 bg-[#10B981]/10 text-[#10B981] rounded-xl flex items-center justify-center text-xl mb-4 group-hover:bg-[#10B981] group-hover:text-white transition-colors duration-200">
<i className="fa-solid fa-code-branch"></i>
</div>
<h4 className="text-base font-extrabold text-txt mb-2"></h4>
<p className="text-txt-muted text-sm mb-5 leading-relaxed">线</p>
<div className="space-y-2">
<div className="flex items-center gap-2.5 text-sm text-txt bg-surface-muted px-3 py-2 rounded-lg">
<i className="fa-solid fa-check text-[#10B981] text-xs shrink-0"></i>
<span>AI </span>
</div>
<div className="flex items-center gap-2.5 text-sm text-txt bg-surface-muted px-3 py-2 rounded-lg">
<i className="fa-solid fa-check text-[#10B981] text-xs shrink-0"></i>
<span></span>
</div>
</div>
</div>
<div className="px-6 py-4 bg-surface-muted border-t border-border">
<Link to="/devops" className="block text-center w-full py-2 bg-white border border-border text-[#10B981] text-sm font-semibold rounded-xl hover:border-[#10B981] hover:bg-[#10B981]/5 transition-colors">
线
</Link>
</div>
</div>
{/* 模块 3: 质量门控 */}
<div className="group flex flex-col bg-white rounded-2xl overflow-hidden border border-border shadow-[0_4px_16px_rgba(0,0,0,0.04)] hover:-translate-y-1 hover:shadow-[0_12px_32px_rgba(0,0,0,0.08)] transition-all duration-300">
<div className="h-1 bg-[#8B5CF6]"></div>
<div className="p-6 flex-1">
<div className="w-11 h-11 bg-[#8B5CF6]/10 text-[#8B5CF6] rounded-xl flex items-center justify-center text-xl mb-4 group-hover:bg-[#8B5CF6] group-hover:text-white transition-colors duration-200">
<i className="fa-solid fa-shield-halved"></i>
</div>
<h4 className="text-base font-extrabold text-txt mb-2"></h4>
<p className="text-txt-muted text-sm mb-5 leading-relaxed"> Dashboard PR</p>
<div className="space-y-2">
<div className="flex items-center gap-2.5 text-sm text-txt bg-surface-muted px-3 py-2 rounded-lg">
<i className="fa-solid fa-check text-[#8B5CF6] text-xs shrink-0"></i>
<span>PR </span>
</div>
<div className="flex items-center gap-2.5 text-sm text-txt bg-surface-muted px-3 py-2 rounded-lg">
<i className="fa-solid fa-check text-[#8B5CF6] text-xs shrink-0"></i>
<span></span>
</div>
</div>
</div>
<div className="px-6 py-4 bg-surface-muted border-t border-border">
<Link to="/quality" className="block text-center w-full py-2 bg-white border border-border text-[#8B5CF6] text-sm font-semibold rounded-xl hover:border-[#8B5CF6] hover:bg-[#8B5CF6]/5 transition-colors">
</Link>
</div>
</div>
</div>
{/* 悬浮 AI 问答按钮 */}
<button
className="fixed bottom-8 right-8 w-14 h-14 bg-magenta text-white rounded-2xl shadow-[0_8px_24px_rgba(244,63,94,0.35)] hover:shadow-[0_12px_32px_rgba(244,63,94,0.45)] hover:-translate-y-1 active:translate-y-0 transition-all duration-200 flex items-center justify-center text-xl z-50"
title="AI 助手"
>
<i className="fa-solid fa-robot"></i>
</button>
</div>
);
}

View File

@@ -1,4 +1,7 @@
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import { API } from "../config";
/* ─── Types ─── */
@@ -271,8 +274,8 @@ export default function PlanningAgent() {
<div className="h-full">
{/* ─── Main Chat ─── */}
<div className="h-full flex flex-col min-w-0">
<div className="flex-1 overflow-y-auto px-10 py-8">
<div className="flex flex-col gap-6 max-w-5xl w-full mx-auto">
<div className="flex-1 overflow-y-auto px-[72px] py-8">
<div className="flex flex-col gap-6 w-full">
{state.messages.length === 0 && (
<div className="text-center py-20 card">
<h2 className="text-2xl font-extrabold mb-2">Planning Council Agent</h2>
@@ -294,7 +297,18 @@ export default function PlanningAgent() {
{msg.role === "assistant" && (
<span className="badge mb-3 block w-fit">SYSTEM / RE-ACT LOOP</span>
)}
{msg.role === "assistant" ? (
<div className="prose prose-sm max-w-none prose-p:my-1 prose-pre:bg-gray-800 prose-pre:text-gray-100 prose-code:text-magenta prose-code:bg-magenta/10 prose-code:px-1 prose-code:rounded prose-li:my-0">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
>
{msg.content || "Thinking..."}
</ReactMarkdown>
</div>
) : (
msg.content || "Thinking..."
)}
{msg.status === "failed" && (
<span className="text-red-500 text-xs block mt-2">Failed to send</span>
)}
@@ -306,7 +320,7 @@ export default function PlanningAgent() {
{/* Error */}
{state.error && (
<div className="mx-10 mb-2 px-4 py-2 bg-red-50 text-red-700 text-sm flex justify-between items-center">
<div className="mx-[72px] mb-2 px-4 py-2 bg-red-50 text-red-700 text-sm flex justify-between items-center">
{state.error}
<button
className="text-red-400 hover:text-red-600 font-bold"

View File

@@ -31,22 +31,216 @@ type ChangedFile = {
deletions: number;
};
type FileContent = {
content: string;
issues: Array<{
type FileContentIssue = {
line: number;
severity: string;
message: string;
rule?: string;
}>;
scanner?: string;
};
type FileContent = {
content: string;
issues: FileContentIssue[];
};
function normalizeFileContent(data: {
content?: string;
issues?: Array<{ line?: number; severity?: string; message?: string; description?: string; rule?: string; scanner?: string }>;
scan_issues?: Array<{ line?: number; severity?: string; message?: string; description?: string; rule?: string; scanner?: string }>;
}): FileContent {
const raw = data.scan_issues ?? data.issues ?? [];
const issues: FileContentIssue[] = raw.map((iss) => {
const line = typeof iss.line === "number" ? iss.line : typeof iss.line === "string" ? parseInt(iss.line, 10) : 1;
return {
line: Number.isFinite(line) ? line : 1,
severity: String(iss.severity ?? "info").toLowerCase(),
message: String(iss.message ?? iss.description ?? ""),
rule: iss.rule,
scanner: iss.scanner,
};
});
return {
content: data.content ?? "",
issues,
};
}
type View = "dashboard" | "pr-list" | "settings";
type PRHistoryItem = {
pr_number: number;
error_count?: number;
warning_count?: number;
total_issues?: number;
};
type QualityGateProps = {
view: View;
};
const TREND_SLOTS = 15;
const TREND_PX_PER_PR = 80;
const TREND_CHART_HEIGHT = 220;
function ProblemTrendChart({
history,
loading,
}: {
history: PRHistoryItem[];
loading: boolean;
}) {
if (loading) {
return (
<div className="rounded-2xl border border-border bg-white shadow-[0_6px_22px_rgba(0,0,0,0.04)] p-4">
<h3 className="text-base font-extrabold mb-3"></h3>
<div className="text-center py-8 text-txt-muted text-sm">...</div>
</div>
);
}
if (!history || history.length === 0) {
return (
<div className="rounded-2xl border border-border bg-white shadow-[0_6px_22px_rgba(0,0,0,0.04)] p-4">
<h3 className="text-base font-extrabold mb-3"></h3>
<div className="text-center py-8 text-txt-muted text-sm"></div>
</div>
);
}
const n = history.length;
const pad = Math.max(0, TREND_SLOTS - n);
const labels = history.map((p) => `#${p.pr_number}`).concat(Array(pad).fill(""));
const errorData: (number | null)[] = history.map((p) => p.error_count ?? 0).concat(Array(pad).fill(null));
const warningData: (number | null)[] = history.map((p) => p.warning_count ?? 0).concat(Array(pad).fill(null));
const width = TREND_SLOTS * TREND_PX_PER_PR;
const height = TREND_CHART_HEIGHT;
const padding = { top: 28, right: 24, bottom: 32, left: 40 };
const chartW = width - padding.left - padding.right;
const chartH = height - padding.top - padding.bottom;
const maxVal = Math.max(
1,
...errorData.filter((v): v is number => v != null),
...warningData.filter((v): v is number => v != null)
);
const yMax = Math.ceil(maxVal / 2) * 2 || 2;
const yTicks = Array.from({ length: yMax + 1 }, (_, i) => i);
const xScale = (i: number) => padding.left + (i / (labels.length - 1 || 1)) * chartW;
const yScale = (v: number) => padding.top + chartH - (v / yMax) * chartH;
const toPath = (data: (number | null)[]) => {
const parts: string[] = [];
for (let i = 0; i < data.length; i++) {
const v = data[i];
if (v == null) continue;
const x = xScale(i);
const y = yScale(v);
parts.push(parts.length === 0 ? `M ${x} ${y}` : `L ${x} ${y}`);
}
return parts.join(" ");
};
const toAreaPath = (data: (number | null)[]) => {
const points: { i: number; v: number }[] = [];
data.forEach((v, i) => {
if (v != null) points.push({ i, v });
});
if (points.length === 0) return "";
const linePath = points.map(({ i, v }) => `${i === 0 ? "M" : "L"} ${xScale(i)} ${yScale(v)}`).join(" ");
const lastX = xScale(points[points.length - 1].i);
const baseY = padding.top + chartH;
const startX = xScale(points[0].i);
return `${linePath} L ${lastX} ${baseY} L ${startX} ${baseY} Z`;
};
const errorPath = toPath(errorData);
const warningPath = toPath(warningData);
const errorAreaPath = toAreaPath(errorData);
const warningAreaPath = toAreaPath(warningData);
return (
<div className="rounded-2xl border border-border bg-white shadow-[0_6px_22px_rgba(0,0,0,0.04)] p-4">
<h3 className="text-base font-extrabold mb-3"></h3>
<div className="overflow-x-auto overflow-y-hidden" style={{ maxWidth: "100%" }}>
<div style={{ width, minWidth: width, height }}>
<svg width={width} height={height} className="overflow-visible">
{/* 网格线 */}
{yTicks.map((tick, i) => (
<line
key={i}
x1={padding.left}
y1={yScale(tick)}
x2={padding.left + chartW}
y2={yScale(tick)}
stroke="rgba(0,0,0,0.12)"
strokeWidth={1}
strokeDasharray="4 4"
/>
))}
{labels.map((_, i) => (
<line
key={i}
x1={xScale(i)}
y1={padding.top}
x2={xScale(i)}
y2={padding.top + chartH}
stroke="rgba(0,0,0,0.12)"
strokeWidth={1}
strokeDasharray="4 4"
/>
))}
{/* 图例 */}
<g transform={`translate(${padding.left}, 8)`}>
<rect x={0} y={2} width={14} height={10} fill="#dc3545" rx={1} />
<text x={18} y={11} fontSize={11} fill="currentColor" className="text-txt">
</text>
<rect x={70} y={2} width={14} height={10} fill="#ffc107" rx={1} />
<text x={88} y={11} fontSize={11} fill="currentColor" className="text-txt">
</text>
</g>
{/* 面积填充 */}
<path d={errorAreaPath} fill="rgba(220, 53, 69, 0.1)" />
<path d={warningAreaPath} fill="rgba(255, 193, 7, 0.1)" />
{/* 折线 */}
<path d={errorPath} fill="none" stroke="#dc3545" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
<path d={warningPath} fill="none" stroke="#ffc107" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
{/* Y 轴刻度 */}
{yTicks.map((tick) => (
<text
key={tick}
x={padding.left - 8}
y={yScale(tick) + 4}
textAnchor="end"
fontSize={10}
fill="var(--color-txt-muted, #666)"
>
{tick}
</text>
))}
{/* X 轴刻度 */}
{labels.map((label, i) => (
<text
key={i}
x={xScale(i)}
y={height - 8}
textAnchor="middle"
fontSize={10}
fill={label ? "var(--color-txt-muted, #666)" : "transparent"}
>
{label || "#"}
</text>
))}
</svg>
</div>
</div>
</div>
);
}
const BASE = API.quality;
async function apiGet<T>(path: string): Promise<T> {
@@ -73,6 +267,9 @@ export default function QualityGate({ view }: QualityGateProps) {
const [fileContent, setFileContent] = useState<FileContent | null>(null);
const [modalOpen, setModalOpen] = useState(false);
const [trendHistory, setTrendHistory] = useState<PRHistoryItem[]>([]);
const [trendLoading, setTrendLoading] = useState(false);
const fetchPRs = useCallback(async () => {
setLoading(true);
setError("");
@@ -91,6 +288,23 @@ export default function QualityGate({ view }: QualityGateProps) {
fetchPRs();
}, [fetchPRs]);
const fetchTrendHistory = useCallback(async () => {
setTrendLoading(true);
try {
const data = await apiGet<PRHistoryItem[] | { history?: PRHistoryItem[] }>("/prs/history?limit=15");
const list = Array.isArray(data) ? data : data.history ?? [];
setTrendHistory(list);
} catch {
setTrendHistory([]);
} finally {
setTrendLoading(false);
}
}, []);
useEffect(() => {
if (view === "dashboard") fetchTrendHistory();
}, [view, fetchTrendHistory]);
const openPRDetail = useCallback(async (prId: number) => {
setLoading(true);
setError("");
@@ -116,8 +330,8 @@ export default function QualityGate({ view }: QualityGateProps) {
useEffect(() => {
if (!modalOpen || !selectedPR || !selectedFile) return;
setFileContent(null);
apiGet<FileContent>(`/prs/${selectedPR.id}/file?path=${encodeURIComponent(selectedFile)}`)
.then(setFileContent)
apiGet<Parameters<typeof normalizeFileContent>[0]>(`/prs/${selectedPR.id}/file?path=${encodeURIComponent(selectedFile)}`)
.then((data) => setFileContent(normalizeFileContent(data)))
.catch(() => setFileContent({ content: "// Failed to load file", issues: [] }));
}, [modalOpen, selectedPR, selectedFile]);
@@ -183,6 +397,10 @@ export default function QualityGate({ view }: QualityGateProps) {
))}
</div>
<div className="mt-8 mb-8">
<ProblemTrendChart history={trendHistory} loading={trendLoading} />
</div>
<h3 className="text-lg font-extrabold mb-4">Recent Pull Requests</h3>
<PRTable prs={prs.slice(0, 8)} loading={loading} onView={(pr) => openPRDetail(pr.id)} />
</>
@@ -281,53 +499,84 @@ export default function QualityGate({ view }: QualityGateProps) {
))}
</div>
<div className="flex-1 overflow-auto bg-white">
{/* 代码区:完整代码 + 每行右侧缺陷标注,一行一个滚动条(参考 index copy.html */}
<div className="flex-1 min-h-0 min-w-0 flex flex-col overflow-hidden">
<div className="flex-1 min-h-0 overflow-auto bg-[#1e1e1e] font-mono text-[13px] leading-[1.5]">
{fileContent ? (
<pre className="text-sm font-mono leading-6 p-4">
{fileContent.content.split("\n").map((line, i) => {
<div className="min-w-min">
{(fileContent.content ?? "").split("\n").map((line, i) => {
const lineNum = i + 1;
const issues = fileContent.issues.filter((iss) => iss.line === lineNum);
const lineIssues = (fileContent.issues ?? []).filter((iss) => iss.line === lineNum);
const hasIssue = lineIssues.length > 0;
const reasonText = hasIssue
? lineIssues.map((iss) => (iss.scanner ? `[${iss.scanner}] ` : "") + (iss.message || "")).join("")
: "";
const displayText = line === "" ? "\u00A0" : line;
return (
<div key={i} className={`flex ${issues.length > 0 ? "bg-red-50/80" : ""}`}>
<span className="w-12 shrink-0 text-right pr-4 text-txt-muted select-none text-xs leading-6">
{lineNum}
</span>
<span className="flex-1 whitespace-pre-wrap">{line}</span>
{issues.length > 0 && (
<span className="shrink-0 px-2 text-xs text-red-600 max-w-xs truncate" title={issues[0].message}>
{issues[0].message}
<div
key={i}
className={`flex min-h-[1.5em] items-stretch ${hasIssue ? "code-line-has-issue" : ""}`}
>
{/* 行号区 */}
<div className="w-12 min-w-[48px] shrink-0 flex items-center justify-end pr-2 text-[#6c757d] bg-[#252526] select-none">
<span className="mr-1">{lineNum}</span>
{hasIssue && (
<span
className={
lineIssues[0]?.severity === "error" || lineIssues[0]?.severity === "high"
? "text-[#f14c4c]"
: "text-[#cca700]"
}
title={reasonText}
>
</span>
)}
</div>
{/* 代码内容 */}
<div
className={`flex-1 min-w-0 px-3 py-0 whitespace-pre-wrap break-all text-[#d4d4d4] ${
hasIssue ? "bg-red-900/20 border-l-2 border-l-red-500 pl-2" : ""
}`}
>
{displayText}
</div>
{/* 虚线连接区 */}
<div className="w-5 min-w-[20px] shrink-0 bg-[#1e1e1e] relative">
{hasIssue && (
<span
className="absolute left-0 right-0 top-1/2 -mt-px block border-b border-dashed border-red-500/80"
aria-hidden
/>
)}
</div>
{/* 缺陷标注(与该行对齐,在右侧) */}
<div
className={`w-[180px] min-w-[180px] shrink-0 py-1.5 px-2 text-[11px] border-l flex items-center ${
hasIssue
? "bg-red-50/90 border-l-2 border-l-red-500 text-red-800"
: "bg-[#252526] border-[#3c3c3c] text-[#9d9d9d]"
}`}
>
{hasIssue && reasonText ? (
<>
<span className="text-red-500 mr-1.5 shrink-0"></span>
<span className="break-words leading-snug">{reasonText}</span>
</>
) : (
<span className="invisible">-</span>
)}
</div>
</div>
);
})}
</pre>
</div>
) : (
<div className="flex items-center justify-center h-full text-txt-muted text-sm">
<div className="flex items-center justify-center h-full min-h-[200px] text-[#6c757d] text-sm">
{selectedFile ? "Loading file..." : "Select a file to inspect"}
</div>
)}
</div>
<div className="w-[220px] shrink-0 border-l border-border overflow-y-auto p-4 bg-surface-muted/60">
<h3 className="text-xs font-bold text-txt-muted uppercase mb-3">Issue Panel</h3>
{fileContent?.issues.length === 0 && (
<p className="text-xs text-txt-muted">No issues</p>
)}
{fileContent?.issues.map((iss, i) => (
<div key={i} className="mb-3 p-2 bg-white border border-border text-xs rounded-lg">
<div className="flex items-center gap-1 mb-1">
<span className={`font-bold ${
iss.severity === "error" ? "text-red-600" : iss.severity === "warning" ? "text-yellow-600" : "text-blue-600"
}`}>
{iss.severity === "error" ? "❌" : iss.severity === "warning" ? "⚠️" : ""}
</span>
<span className="text-txt-muted">L{iss.line}</span>
</div>
<p className="text-txt">{iss.message}</p>
{iss.rule && <p className="text-txt-muted mt-0.5">{iss.rule}</p>}
</div>
))}
</div>
</div>

740
src/pages/TaskExecutor.tsx Normal file
View File

@@ -0,0 +1,740 @@
import { useCallback, useEffect, useReducer, useRef } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
/* ─── Types ─── */
type Stage = {
id: string;
name: string;
agent: string;
status: "pending" | "processing" | "completed";
};
type LogEntry = {
timestamp: string;
event: string;
source: string;
message: string;
};
type Result = {
title: string;
content: string;
timestamp: string;
expanded: boolean;
};
type PollResponse = {
events: Array<{
event: string;
data: any;
}>;
has_more: boolean;
status: "processing" | "completed" | "failed";
};
type State = {
requirement: string;
taskId: string | null;
isProcessing: boolean;
connectionStatus: "connecting" | "connected" | "disconnected";
stages: Stage[];
logs: LogEntry[];
results: Result[];
showCopyToast: boolean;
};
type Action =
| { type: "set-requirement"; value: string }
| { type: "set-task-id"; value: string }
| { type: "set-processing"; value: boolean }
| { type: "set-connection"; status: "connecting" | "connected" | "disconnected" }
| { type: "init-stages" }
| { type: "update-stage"; stageId: string; status: "pending" | "processing" | "completed" }
| { type: "add-log"; log: LogEntry }
| { type: "add-result"; result: Result }
| { type: "toggle-result"; index: number }
| { type: "clear-logs" }
| { type: "show-copy-toast"; show: boolean }
| { type: "reset" };
/* ─── Helpers ─── */
function formatTime(ts: number) {
return new Date(ts).toLocaleTimeString("zh-CN");
}
function formatDate(timestamp: string | number) {
if (!timestamp) return "";
try {
return new Date(timestamp).toLocaleString("zh-CN");
} catch {
return String(timestamp);
}
}
function reducer(state: State, action: Action): State {
switch (action.type) {
case "set-requirement":
return { ...state, requirement: action.value };
case "set-task-id":
return { ...state, taskId: action.value };
case "set-processing":
return { ...state, isProcessing: action.value };
case "set-connection":
return { ...state, connectionStatus: action.status };
case "init-stages":
return {
...state,
stages: [
{ id: "pm", name: "需求分析", agent: "PM Agent", status: "pending" },
{ id: "qa", name: "测试设计", agent: "QA Agent", status: "pending" },
{ id: "dev", name: "代码实现", agent: "Dev Agent", status: "pending" },
{ id: "final", name: "交付完成", agent: "Orchestrator", status: "pending" },
],
};
case "update-stage":
return {
...state,
stages: state.stages.map((s) =>
s.id === action.stageId ? { ...s, status: action.status } : s
),
};
case "add-log":
return {
...state,
logs: [...state.logs, action.log].slice(-100),
};
case "add-result":
return {
...state,
results: [...state.results, action.result],
};
case "toggle-result":
return {
...state,
results: state.results.map((r, i) =>
i === action.index ? { ...r, expanded: !r.expanded } : r
),
};
case "clear-logs":
return { ...state, logs: [] };
case "show-copy-toast":
return { ...state, showCopyToast: action.show };
case "reset":
return {
...state,
taskId: null,
isProcessing: false,
connectionStatus: "disconnected",
stages: [],
logs: [],
results: [],
showCopyToast: false,
};
default:
return state;
}
}
const initialState: State = {
requirement: "",
taskId: null,
isProcessing: false,
connectionStatus: "disconnected",
stages: [],
logs: [],
results: [],
showCopyToast: false,
};
/* ─── API Functions ─── */
async function startSDLC(requirement: string) {
const res = await fetch("/task-api/v1/sdlc/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ requirement }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
return (await res.json()) as { task_id: string };
}
async function pollEvents(taskId: string, lastIndex: number): Promise<PollResponse> {
const res = await fetch(`/task-api/v1/sdlc/poll/${taskId}?last_index=${lastIndex}`);
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
return res.json();
}
/* ─── Copy to Clipboard ─── */
async function copyToClipboard(text: string) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
return false;
}
}
/* ━━━━━━━━━━━━━━━━━━━━━━━ Component ━━━━━━━━━━━━━━━━━━━━━━━ */
export default function TaskExecutor() {
const [state, dispatch] = useReducer(reducer, initialState);
const pollTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const pollCountRef = useRef(0);
const lastIndexRef = useRef(0);
const maxPolls = 600; // 最多轮询 600 次 (10 分钟)
const canStart = state.requirement.trim().length >= 10 && !state.isProcessing;
const connectionStatusClass = {
connecting: "bg-yellow-500 animate-pulse",
connected: "bg-green-500",
disconnected: "bg-red-500",
}[state.connectionStatus];
const connectionStatusText = {
connecting: "连接中...",
connected: "已连接",
disconnected: "未连接",
}[state.connectionStatus];
const getStageIconClass = (status: Stage["status"]) => {
const map = {
pending: "bg-gray-400",
processing: "bg-blue-500",
completed: "bg-green-500",
};
return map[status] || map.pending;
};
const getStageBadgeClass = (status: Stage["status"]) => {
const map = {
pending: "bg-gray-100 text-gray-800",
processing: "bg-blue-100 text-blue-800",
completed: "bg-green-100 text-green-800",
};
return map[status] || map.pending;
};
const getStageStatusText = (status: Stage["status"]) => {
const map = {
pending: "等待中",
processing: "进行中",
completed: "已完成",
};
return map[status] || status;
};
const getLogLevelClass = (event: string) => {
const map: Record<string, string> = {
pm_start: "text-blue-400",
pm_complete: "text-green-400",
qa_start: "text-blue-400",
qa_complete: "text-green-400",
dev_start: "text-blue-400",
dev_complete: "text-green-400",
final_result: "text-purple-400",
error: "text-red-400",
system: "text-yellow-400",
task_started: "text-white",
};
return map[event] || "text-gray-400";
};
/**
* 处理单个事件
*/
const handleEvent = useCallback(
(eventType: string, data: any) => {
switch (eventType) {
case "task_started":
dispatch({
type: "add-log",
log: {
timestamp: formatTime(Date.now()),
event: "task_started",
source: "System",
message: data.message || "任务已启动",
},
});
break;
case "pm_start":
dispatch({ type: "update-stage", stageId: "pm", status: "processing" });
dispatch({
type: "add-log",
log: {
timestamp: formatTime(Date.now()),
event: "pm_start",
source: "PM Agent",
message: "开始需求分析...",
},
});
break;
case "pm_complete":
dispatch({ type: "update-stage", stageId: "pm", status: "completed" });
dispatch({
type: "add-log",
log: {
timestamp: formatTime(Date.now()),
event: "pm_complete",
source: "PM Agent",
message: "需求分析完成",
},
});
if (data.content && data.timestamp) {
dispatch({
type: "add-result",
result: {
title: "📋 软件需求规格说明书 (SRS)",
content: data.content,
timestamp: data.timestamp,
expanded: true,
},
});
}
break;
case "qa_start":
dispatch({ type: "update-stage", stageId: "qa", status: "processing" });
dispatch({
type: "add-log",
log: {
timestamp: formatTime(Date.now()),
event: "qa_start",
source: "QA Agent",
message: "开始测试用例设计...",
},
});
break;
case "qa_complete":
dispatch({ type: "update-stage", stageId: "qa", status: "completed" });
dispatch({
type: "add-log",
log: {
timestamp: formatTime(Date.now()),
event: "qa_complete",
source: "QA Agent",
message: "测试用例设计完成",
},
});
if (data.content && data.timestamp) {
dispatch({
type: "add-result",
result: {
title: "🧪 测试方案与用例",
content: data.content,
timestamp: data.timestamp,
expanded: true,
},
});
}
break;
case "dev_start":
dispatch({ type: "update-stage", stageId: "dev", status: "processing" });
dispatch({
type: "add-log",
log: {
timestamp: formatTime(Date.now()),
event: "dev_start",
source: "Dev Agent",
message: "开始代码实现...",
},
});
break;
case "dev_complete":
dispatch({ type: "update-stage", stageId: "dev", status: "completed" });
dispatch({
type: "add-log",
log: {
timestamp: formatTime(Date.now()),
event: "dev_complete",
source: "Dev Agent",
message: "代码实现完成",
},
});
if (data.content && data.timestamp) {
dispatch({
type: "add-result",
result: {
title: "💻 代码实现",
content: data.content,
timestamp: data.timestamp,
expanded: true,
},
});
}
break;
case "final_result":
dispatch({ type: "update-stage", stageId: "final", status: "completed" });
dispatch({
type: "add-log",
log: {
timestamp: formatTime(Date.now()),
event: "final_result",
source: "System",
message: "SDLC 流程完成",
},
});
break;
case "error":
dispatch({
type: "add-log",
log: {
timestamp: formatTime(Date.now()),
event: "error",
source: "Error",
message: data.error || "未知错误",
},
});
alert(`执行错误:${data.error}`);
break;
}
},
[],
);
/**
* 轮询函数
*/
const poll = useCallback(
(taskId: string) => {
if (pollCountRef.current >= maxPolls) {
dispatch({
type: "add-log",
log: {
timestamp: formatTime(Date.now()),
event: "system",
source: "POLL",
message: "轮询超时",
},
});
dispatch({ type: "set-processing", value: false });
dispatch({ type: "set-connection", status: "disconnected" });
return;
}
pollEvents(taskId, lastIndexRef.current)
.then(({ events, has_more, status }) => {
// 处理新事件
events.forEach((event) => {
lastIndexRef.current++;
handleEvent(event.event, event.data);
});
// 检查是否继续轮询
if (status === "completed" || status === "failed") {
dispatch({ type: "set-processing", value: false });
dispatch({ type: "set-connection", status: "disconnected" });
dispatch({
type: "add-log",
log: {
timestamp: formatTime(Date.now()),
event: "system",
source: "POLL",
message: `任务完成,状态:${status}`,
},
});
return;
}
if (has_more || events.length > 0) {
pollCountRef.current++;
pollTimeoutRef.current = setTimeout(() => poll(taskId), 500);
} else if (status === "processing") {
pollCountRef.current++;
pollTimeoutRef.current = setTimeout(() => poll(taskId), 1000);
}
})
.catch((err) => {
console.error("轮询失败:", err);
dispatch({
type: "add-log",
log: {
timestamp: formatTime(Date.now()),
event: "error",
source: "POLL",
message: err.message,
},
});
pollCountRef.current++;
pollTimeoutRef.current = setTimeout(() => poll(taskId), 2000);
});
},
[handleEvent],
);
const handleStart = useCallback(async () => {
if (!canStart) return;
dispatch({ type: "reset" });
dispatch({ type: "set-processing", value: true });
try {
const data = await startSDLC(state.requirement);
dispatch({ type: "set-task-id", value: data.task_id });
dispatch({
type: "add-log",
log: {
timestamp: formatTime(Date.now()),
event: "system",
source: "System",
message: `Task ID: ${data.task_id}`,
},
});
dispatch({ type: "init-stages" });
// 开始轮询
dispatch({ type: "set-connection", status: "connecting" });
dispatch({
type: "add-log",
log: {
timestamp: formatTime(Date.now()),
event: "system",
source: "POLL",
message: `开始轮询任务:${data.task_id}`,
},
});
lastIndexRef.current = 0;
pollCountRef.current = 0;
setTimeout(() => poll(data.task_id), 500);
} catch (err) {
dispatch({
type: "add-log",
log: {
timestamp: formatTime(Date.now()),
event: "error",
source: "Error",
message: (err as Error).message,
},
});
dispatch({ type: "set-processing", value: false });
alert(`启动失败:${(err as Error).message}`);
}
}, [canStart, state.requirement, poll]);
const handleCopy = useCallback(async (content: string) => {
const success = await copyToClipboard(content);
if (success) {
dispatch({ type: "show-copy-toast", show: true });
setTimeout(() => {
dispatch({ type: "show-copy-toast", show: false });
}, 2000);
} else {
alert("复制失败,请手动复制");
}
}, []);
// 清理轮询定时器
useEffect(() => {
return () => {
if (pollTimeoutRef.current) {
clearTimeout(pollTimeoutRef.current);
}
};
}, []);
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<header className="bg-white shadow-sm border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 py-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between">
<div className="flex items-center">
<div className="flex-shrink-0">
<svg className="h-8 w-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<div className="ml-4">
<h1 className="text-xl font-bold text-gray-900">SDLC Agent Demo</h1>
<p className="text-sm text-gray-500"></p>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center text-sm">
<span className={`inline-block w-2 h-2 rounded-full mr-2 ${connectionStatusClass}`}></span>
<span className="text-gray-600">{connectionStatusText}</span>
</div>
</div>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 py-8 sm:px-6 lg:px-8">
{/* Requirement Input */}
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">1. </h2>
<div className="space-y-4">
<textarea
value={state.requirement}
onChange={(e) => dispatch({ type: "set-requirement", value: e.target.value })}
rows={5}
placeholder={`请输入您的软件需求描述,例如:\n开发一个用户管理系统支持用户的增删改查功能需要包含以下特性\n- 用户注册和登录\n- 用户信息管理\n- 角色权限控制\n- 操作日志记录`}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
disabled={state.isProcessing}
/>
<div className="flex items-center justify-between">
<p className="text-sm text-gray-500">
ID: {state.taskId || "无"}
</p>
<button
onClick={handleStart}
disabled={!canStart}
className={`px-6 py-2.5 rounded-lg font-medium text-white transition-all duration-200 ${
canStart
? "bg-blue-600 hover:bg-blue-700 shadow-md hover:shadow-lg"
: "bg-gray-400 cursor-not-allowed"
}`}
>
{state.isProcessing ? "执行中..." : "开始执行"}
</button>
</div>
</div>
</div>
{/* Progress Stages */}
{state.stages.length > 0 && (
<div className="mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">2. </h2>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{state.stages.map((stage, index) => (
<div
key={stage.id}
className={`rounded-lg border-2 p-4 bg-white transition-all duration-300 ${
stage.status === "processing"
? "border-blue-500 transform scale-[1.02] shadow-[0_10px_25px_rgba(59,130,246,0.3)] animate-pulse"
: stage.status === "completed"
? "border-green-500 bg-gradient-to-br from-green-50 to-white"
: "border-gray-200"
}`}
>
<div className="flex items-center mb-2">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center mr-3 ${getStageIconClass(stage.status)}`}
>
<span className="text-white font-bold text-sm">{index + 1}</span>
</div>
<div>
<h3 className="font-medium text-gray-900">{stage.name}</h3>
<p className="text-xs text-gray-500">{stage.agent}</p>
</div>
</div>
<div className="mt-3">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStageBadgeClass(stage.status)}`}
>
{getStageStatusText(stage.status)}
</span>
</div>
</div>
))}
</div>
</div>
)}
{/* Real-time Logs */}
{state.logs.length > 0 && (
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900">3. </h2>
<button
onClick={() => dispatch({ type: "clear-logs" })}
className="text-sm text-gray-500 hover:text-gray-700"
>
</button>
</div>
<div className="bg-gray-900 rounded-lg p-4 h-64 overflow-y-auto font-mono text-sm">
{state.logs.map((log, index) => (
<div key={index} className="mb-1">
<span className="text-gray-500">{log.timestamp}</span>
<span className={`ml-2 ${getLogLevelClass(log.event)}`}>[{log.event}]</span>
<span className="text-gray-300 ml-2">{log.message}</span>
</div>
))}
</div>
</div>
)}
{/* Results */}
{state.results.length > 0 && (
<div className="space-y-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">4. </h2>
{state.results.map((result, index) => (
<div key={index} className="bg-white rounded-lg shadow-md overflow-hidden">
<div
onClick={() => dispatch({ type: "toggle-result", index })}
className="px-6 py-4 bg-gray-50 border-b border-gray-200 cursor-pointer hover:bg-gray-100 transition-colors"
>
<div className="flex items-center justify-between">
<div className="flex items-center">
<svg
className={`w-5 h-5 mr-2 text-gray-500 transform transition-transform ${
result.expanded ? "rotate-90" : ""
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
<h3 className="font-semibold text-gray-900">{result.title}</h3>
</div>
<div className="flex items-center space-x-2">
<span className="text-xs text-gray-500">{formatDate(result.timestamp)}</span>
<button
onClick={(e) => {
e.stopPropagation();
handleCopy(result.content);
}}
className="text-sm text-blue-600 hover:text-blue-700"
>
</button>
</div>
</div>
</div>
{result.expanded && (
<div className="p-6">
<div className="markdown-body prose prose-sm max-w-none">
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
{result.content}
</ReactMarkdown>
</div>
</div>
)}
</div>
))}
</div>
)}
</main>
{/* Footer */}
<footer className="bg-white border-t border-gray-200 mt-12">
<div className="max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8">
<p className="text-center text-sm text-gray-500">
CrewAI + Qwen3.5-flash + FastAPI(Polling) | Bosch Demo
</p>
</div>
</footer>
{/* Copy Toast */}
{state.showCopyToast && (
<div className="fixed bottom-4 right-4 bg-green-600 text-white px-6 py-3 rounded-lg shadow-lg transition-opacity duration-300">
</div>
)}
</div>
);
}

View File

@@ -5,11 +5,11 @@ export default {
extend: {
colors: {
magenta: {
DEFAULT: "#E20074",
50: "#FFF0F7",
100: "#FFE0EF",
600: "#E20074",
700: "#B8005E",
DEFAULT: "#F43F5E",
50: "#FFF1F2",
100: "#FFE4E6",
600: "#F43F5E",
700: "#BE123C",
},
surface: {
DEFAULT: "#FFFFFF",
@@ -30,5 +30,7 @@ export default {
},
},
},
plugins: [],
plugins: [
require('@tailwindcss/typography'),
],
};

144
template/devops.html Normal file
View File

@@ -0,0 +1,144 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SAFe OS - 开发运维 (DevOps)</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#2563EB',
planning: '#0EA5E9',
devops: '#10B981',
quality: '#8B5CF6'
}
}
}
}
</script>
</head>
<body class="bg-gray-50 flex h-screen overflow-hidden text-gray-800 font-sans">
<!-- 侧边栏导航 -->
<aside class="w-64 bg-white shadow-md flex flex-col z-10">
<div class="h-16 flex items-center px-6 border-b border-gray-100">
<i class="fa-solid fa-layer-group text-primary text-2xl mr-3"></i>
<span class="text-xl font-bold text-gray-800">SAFe OS</span>
</div>
<div class="p-4 flex-1">
<nav class="space-y-2">
<a href="index.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-primary rounded-lg font-medium transition-colors">
<i class="fa-solid fa-house w-6"></i> 总览控制台
</a>
<a href="planning.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-planning rounded-lg font-medium transition-colors">
<i class="fa-solid fa-chess-knight w-6"></i> 战略规划 (Planning)
</a>
<a href="devops.html" class="flex items-center px-4 py-3 bg-green-50 text-devops rounded-lg font-medium">
<i class="fa-solid fa-code-branch w-6"></i> 开发运维 (DevOps)
</a>
<a href="quality.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-quality rounded-lg font-medium transition-colors">
<i class="fa-solid fa-shield-halved w-6"></i> 质量门控 (Quality Gate)
</a>
</nav>
</div>
</aside>
<main class="flex-1 flex flex-col">
<header class="h-16 bg-white shadow-sm flex items-center px-8 border-b-4 border-devops">
<h1 class="text-xl font-semibold text-gray-800">开发运维 (DevOps)</h1>
</header>
<div class="p-8 flex-1 overflow-y-auto">
<!-- 进度条 -->
<div class="max-w-4xl mx-auto mb-10">
<div class="flex items-center justify-between">
<div class="flex flex-col items-center">
<div class="w-10 h-10 rounded-full bg-devops text-white flex items-center justify-center font-bold relative z-10 mb-2">1</div>
<span class="text-sm font-semibold text-devops">需求输入</span>
</div>
<div class="flex-1 border-t-2 border-devops relative -top-3"></div>
<div class="flex flex-col items-center">
<div class="w-10 h-10 rounded-full bg-devops text-white flex items-center justify-center font-bold relative z-10 mb-2">2</div>
<span class="text-sm font-semibold text-devops">代码与测试生成</span>
</div>
<div class="flex-1 border-t-2 border-gray-300 relative -top-3"></div>
<div class="flex flex-col items-center opacity-50">
<div class="w-10 h-10 rounded-full bg-gray-300 text-gray-600 flex items-center justify-center font-bold relative z-10 mb-2">3</div>
<span class="text-sm font-semibold">自动化测试执行</span>
</div>
</div>
</div>
<!-- 主内容区域:代码与测试生成结果 -->
<div class="max-w-6xl mx-auto bg-white rounded-lg shadow-sm border border-gray-200 flex flex-col h-[600px]">
<div class="p-4 border-b bg-gray-50 font-bold flex justify-between items-center">
<span>AI 生成结果</span>
<button class="bg-devops text-white px-4 py-2 rounded text-sm hover:bg-green-600 transition-colors">执行自动化测试 <i class="fa-solid fa-play ml-1"></i></button>
</div>
<div class="flex flex-1 overflow-hidden">
<!-- 左侧:对话 -->
<div class="w-1/3 border-r border-gray-200 bg-gray-50 flex flex-col">
<div class="flex-1 p-4 overflow-y-auto space-y-4 text-sm">
<div class="bg-white p-3 rounded-lg border shadow-sm">
<p class="text-gray-800 font-semibold mb-1"><i class="fa-solid fa-robot text-devops mr-1"></i> Developer Agent</p>
<p class="text-gray-600">我已经根据您的 "登录功能" 需求生成了用户控制器的代码框架和对应的单元测试Jest</p>
<p class="text-gray-600 mt-2">测试用例涵盖了 5 种边界情况,您可以查看右侧代码。</p>
</div>
</div>
<div class="p-4 border-t border-gray-200 bg-white">
<div class="relative">
<input type="text" placeholder="向 AI 提出修改建议..." class="w-full pl-4 pr-10 py-2 border rounded-md focus:outline-none focus:border-devops text-sm">
<button class="absolute right-2 top-1/2 transform -translate-y-1/2 text-devops p-1 hover:bg-green-50 rounded">
<i class="fa-solid fa-paper-plane"></i>
</button>
</div>
</div>
</div>
<!-- 右侧:代码编辑器视图 -->
<div class="w-2/3 bg-[#1e1e1e] text-gray-300 font-mono text-sm flex flex-col">
<div class="flex bg-[#2d2d2d] border-b border-[#404040]">
<button class="px-4 py-2 bg-[#1e1e1e] border-t-2 border-devops text-white">auth.controller.js</button>
<button class="px-4 py-2 hover:bg-[#1e1e1e] transition-colors">auth.controller.test.js</button>
</div>
<div class="flex-1 p-4 overflow-y-auto">
<pre><code><span class="text-purple-400">const</span> <span class="text-yellow-200">AuthService</span> = <span class="text-blue-400">require</span>(<span class="text-green-300">'../services/auth.service'</span>);
<span class="text-purple-400">class</span> <span class="text-yellow-200">AuthController</span> {
<span class="text-gray-500">// 处理用户登录请求</span>
<span class="text-purple-400">async</span> <span class="text-blue-300">login</span>(req, res) {
<span class="text-purple-400">try</span> {
<span class="text-purple-400">const</span> { username, password } = req.body;
<span class="text-purple-400">if</span> (!username || !password) {
<span class="text-purple-400">return</span> res.status(<span class="text-orange-300">400</span>).json({ error: <span class="text-green-300">"缺少必要的参数"</span> });
}
<span class="text-purple-400">const</span> token = <span class="text-purple-400">await</span> AuthService.<span class="text-blue-300">authenticate</span>(username, password);
<span class="text-purple-400">return</span> res.status(<span class="text-orange-300">200</span>).json({ token });
} <span class="text-purple-400">catch</span> (error) {
<span class="text-purple-400">if</span> (error.message === <span class="text-green-300">'AccountLocked'</span>) {
<span class="text-purple-400">return</span> res.status(<span class="text-orange-300">403</span>).json({ error: <span class="text-green-300">"账号已被锁定,请稍后再试"</span> });
}
<span class="text-purple-400">return</span> res.status(<span class="text-orange-300">401</span>).json({ error: <span class="text-green-300">"认证失败"</span> });
}
}
}
<span class="text-purple-400">module</span>.exports = <span class="text-purple-400">new</span> <span class="text-yellow-200">AuthController</span>();
</code></pre>
</div>
</div>
</div>
</div>
</div>
</main>
</body>
</html>

237
template/index.html Normal file
View File

@@ -0,0 +1,237 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SAFe OS - AI Agent 统一平台入口</title>
<!-- 引入 Tailwind CSS 进行快速样式构建 -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- 引入 FontAwesome 图标库 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#2563EB',
secondary: '#4F46E5',
planning: '#0EA5E9',
devops: '#10B981',
quality: '#8B5CF6'
}
}
}
}
</script>
<style>
.module-card {
transition: transform 0.2s, box-shadow 0.2s;
}
.module-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
}
.ai-pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(37, 99, 235, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(37, 99, 235, 0); }
100% { box-shadow: 0 0 0 0 rgba(37, 99, 235, 0); }
}
</style>
</head>
<body class="bg-gray-50 flex h-screen overflow-hidden text-gray-800 font-sans">
<!-- 侧边栏导航 -->
<aside class="w-64 bg-white shadow-md flex flex-col z-10">
<div class="h-16 flex items-center px-6 border-b border-gray-100">
<i class="fa-solid fa-layer-group text-primary text-2xl mr-3"></i>
<span class="text-xl font-bold text-gray-800">SAFe OS</span>
</div>
<div class="p-4 flex-1">
<div class="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-4">主导模块</div>
<nav class="space-y-2">
<a href="index.html" class="flex items-center px-4 py-3 bg-blue-50 text-primary rounded-lg font-medium">
<i class="fa-solid fa-house w-6"></i> 总览控制台
</a>
<a href="planning.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-planning rounded-lg font-medium transition-colors">
<i class="fa-solid fa-chess-knight w-6"></i> 战略规划 (Planning)
</a>
<a href="devops.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-devops rounded-lg font-medium transition-colors">
<i class="fa-solid fa-code-branch w-6"></i> 开发运维 (DevOps)
</a>
<a href="quality.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-quality rounded-lg font-medium transition-colors">
<i class="fa-solid fa-shield-halved w-6"></i> 质量门控 (Quality Gate)
</a>
</nav>
<div class="text-xs font-semibold text-gray-400 uppercase tracking-wider mt-8 mb-4">设置与支持</div>
<nav class="space-y-2">
<a href="#" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 rounded-lg transition-colors">
<i class="fa-solid fa-gear w-6"></i> 平台设置
</a>
</nav>
</div>
<div class="p-4 border-t border-gray-100 flex items-center">
<img src="https://ui-avatars.com/api/?name=Admin&background=F3F4F6&color=374151" alt="User" class="w-10 h-10 rounded-full mr-3">
<div>
<p class="text-sm font-semibold">项目管理员</p>
<p class="text-xs text-gray-500">在线</p>
</div>
</div>
</aside>
<!-- 主内容区 -->
<main class="flex-1 flex flex-col h-screen overflow-y-auto relative">
<!-- 顶部导航 -->
<header class="h-16 bg-white shadow-sm flex items-center justify-between px-8 z-0">
<h1 class="text-xl font-semibold text-gray-800">总览控制台</h1>
<div class="flex items-center space-x-4">
<div class="relative">
<i class="fa-solid fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
<input type="text" placeholder="全局搜索需求、代码或 PR..." class="pl-10 pr-4 py-2 border border-gray-200 rounded-full text-sm focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary w-64 transition-all">
</div>
<button class="relative p-2 text-gray-500 hover:text-primary transition-colors">
<i class="fa-regular fa-bell text-xl"></i>
<span class="absolute top-1 right-1 w-2.5 h-2.5 bg-red-500 rounded-full border-2 border-white"></span>
</button>
</div>
</header>
<!-- 内容画布 -->
<div class="p-8 max-w-7xl mx-auto w-full">
<!-- 欢迎与 AI 提示 -->
<div class="bg-gradient-to-r from-blue-600 to-indigo-700 rounded-2xl p-8 text-white mb-8 shadow-lg flex justify-between items-center">
<div>
<h2 class="text-3xl font-bold mb-2">欢迎回来, SAFe OS 团队 👋</h2>
<p class="text-blue-100 mb-4 text-lg">AI Agent 已经准备就绪,今天想从哪个环节开始推进项目?</p>
<div class="flex items-center text-sm bg-black/20 inline-block px-4 py-2 rounded-lg backdrop-blur-sm">
<i class="fa-solid fa-robot mr-2 text-blue-300"></i> AI 状态: <span class="text-green-300 ml-1 font-semibold">✓ 运行中 (随时可唤醒)</span>
</div>
</div>
<div class="hidden md:block text-right">
<div class="text-5xl font-bold opacity-20"><i class="fa-brands fa-hubspot"></i></div>
</div>
</div>
<!-- 数据概览精要 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="bg-white p-5 rounded-xl text-center border border-gray-100 shadow-sm">
<p class="text-sm text-gray-500 mb-1">规划中需求</p>
<p class="text-2xl font-bold text-gray-800">24 <span class="text-xs text-green-500 font-normal"><i class="fa-solid fa-arrow-up"></i> 3</span></p>
</div>
<div class="bg-white p-5 rounded-xl text-center border border-gray-100 shadow-sm">
<p class="text-sm text-gray-500 mb-1">待合入代码 (PRs)</p>
<p class="text-2xl font-bold text-gray-800">12 <span class="text-xs text-red-500 font-normal"><i class="fa-solid fa-arrow-down"></i> 2</span></p>
</div>
<div class="bg-white p-5 rounded-xl text-center border border-gray-100 shadow-sm">
<p class="text-sm text-gray-500 mb-1">质量健康度</p>
<p class="text-2xl font-bold text-gray-800 focus-text">92% <span class="text-xs text-green-500 font-normal">优秀</span></p>
</div>
</div>
<!-- 三大核心模块入口卡片 -->
<h3 class="text-lg font-bold text-gray-800 mb-4">核心工作流入口</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<!-- 模块 1: 战略规划 (Planning) -->
<div class="module-card bg-white rounded-2xl overflow-hidden border border-gray-100 shadow-sm relative group cursor-pointer flex flex-col">
<div class="h-2 bg-planning"></div>
<div class="p-6 flex-1">
<div class="w-12 h-12 bg-blue-50 text-planning rounded-xl flex items-center justify-center text-2xl mb-4 group-hover:bg-planning group-hover:text-white transition-colors">
<i class="fa-solid fa-chess-knight"></i>
</div>
<h4 class="text-xl font-bold text-gray-800 mb-2">战略规划</h4>
<p class="text-gray-500 mb-4 text-sm min-h-[60px]">一站式需求管理与分析。利用自然语言或文档AI帮助您生成验收标准、拆解任务和预测边界情况。</p>
<div class="space-y-3 mb-6">
<div class="flex items-center text-sm text-gray-600 bg-gray-50 p-2 rounded">
<i class="fa-solid fa-check text-planning mr-2 w-4"></i> 智能需求分析与拆解
</div>
<div class="flex items-center text-sm text-gray-600 bg-gray-50 p-2 rounded">
<i class="fa-solid fa-check text-planning mr-2 w-4"></i> 上下文动态问题对话
</div>
</div>
</div>
<div class="px-6 py-4 bg-gray-50 border-t border-gray-100">
<a href="planning.html" class="block text-center w-full py-2 bg-white border border-gray-200 text-planning font-semibold rounded-lg hover:border-planning hover:bg-blue-50 transition-colors">
进入规划看板 &rarr;
</a>
</div>
</div>
<!-- 模块 2: 开发运维 (DevOps) -->
<div class="module-card bg-white rounded-2xl overflow-hidden border border-gray-100 shadow-sm relative group cursor-pointer flex flex-col">
<div class="h-2 bg-devops"></div>
<div class="p-6 flex-1">
<div class="w-12 h-12 bg-green-50 text-devops rounded-xl flex items-center justify-center text-2xl mb-4 group-hover:bg-devops group-hover:text-white transition-colors">
<i class="fa-solid fa-code-branch"></i>
</div>
<h4 class="text-xl font-bold text-gray-800 mb-2">开发运维</h4>
<p class="text-gray-500 mb-4 text-sm min-h-[60px]">智能研发辅助流水线。输入功能点描述即可生成规范的代码框架和单元测试代码,一键直达测试验证。</p>
<div class="space-y-3 mb-6">
<div class="flex items-center text-sm text-gray-600 bg-gray-50 p-2 rounded">
<i class="fa-solid fa-check text-devops mr-2 w-4"></i> AI 代码框架生成
</div>
<div class="flex items-center text-sm text-gray-600 bg-gray-50 p-2 rounded">
<i class="fa-solid fa-check text-devops mr-2 w-4"></i> 自动化测试与建议
</div>
</div>
</div>
<div class="px-6 py-4 bg-gray-50 border-t border-gray-100">
<a href="devops.html" class="block text-center w-full py-2 bg-white border border-gray-200 text-devops font-semibold rounded-lg hover:border-devops hover:bg-green-50 transition-colors">
开启开发流水线 &rarr;
</a>
</div>
</div>
<!-- 模块 3: 质量门控 (Quality Gate) -->
<div class="module-card bg-white rounded-2xl overflow-hidden border border-gray-100 shadow-sm relative group cursor-pointer flex flex-col">
<div class="h-2 bg-quality"></div>
<div class="p-6 flex-1">
<div class="w-12 h-12 bg-purple-50 text-quality rounded-xl flex items-center justify-center text-2xl mb-4 group-hover:bg-quality group-hover:text-white transition-colors">
<i class="fa-solid fa-shield-halved"></i>
</div>
<h4 class="text-xl font-bold text-gray-800 mb-2">质量门控</h4>
<p class="text-gray-500 mb-4 text-sm min-h-[60px]">项目质量的守护神。提供可视化的Dashboard概览智能审查 PR 修改,拦截代码漏洞与规范问题。</p>
<div class="space-y-3 mb-6">
<div class="flex items-center text-sm text-gray-600 bg-gray-50 p-2 rounded">
<i class="fa-solid fa-check text-quality mr-2 w-4"></i> PR 级智能安全扫描
</div>
<div class="flex items-center text-sm text-gray-600 bg-gray-50 p-2 rounded">
<i class="fa-solid fa-check text-quality mr-2 w-4"></i> 对话式漏洞修复建议
</div>
</div>
</div>
<div class="px-6 py-4 bg-gray-50 border-t border-gray-100">
<a href="quality.html" class="block text-center w-full py-2 bg-white border border-gray-200 text-quality font-semibold rounded-lg hover:border-quality hover:bg-purple-50 transition-colors">
查看质量大盘 &rarr;
</a>
</div>
</div>
</div>
</div>
</main>
<!-- 全局悬浮 AI Agent 助手入口 -->
<div class="fixed bottom-10 right-10 flex flex-col items-end z-50">
<!-- 气泡提示 -->
<div class="bg-white px-4 py-3 rounded-2xl shadow-lg border border-gray-100 mb-4 relative animate-fade-in-up">
<p class="text-sm text-gray-600">需要我帮您新建一个“提测需求”还是“审查PR”呢</p>
<div class="absolute -bottom-2 right-6 w-4 h-4 bg-white border-b border-r border-gray-100 transform rotate-45"></div>
</div>
<!-- 悬浮按钮 -->
<button class="w-16 h-16 bg-primary text-white rounded-full shadow-2xl flex items-center justify-center text-3xl hover:bg-blue-700 transition-colors ai-pulse">
<i class="fa-brands fa-hubspot"></i>
</button>
</div>
</body>
</html>

134
template/planning.html Normal file
View File

@@ -0,0 +1,134 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SAFe OS - 战略规划 (Planning)</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#2563EB',
planning: '#0EA5E9',
devops: '#10B981',
quality: '#8B5CF6'
}
}
}
}
</script>
</head>
<body class="bg-gray-50 flex h-screen overflow-hidden text-gray-800 font-sans">
<!-- 侧边栏导航 -->
<aside class="w-64 bg-white shadow-md flex flex-col z-10">
<div class="h-16 flex items-center px-6 border-b border-gray-100">
<i class="fa-solid fa-layer-group text-primary text-2xl mr-3"></i>
<span class="text-xl font-bold text-gray-800">SAFe OS</span>
</div>
<div class="p-4 flex-1">
<nav class="space-y-2">
<a href="index.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-primary rounded-lg font-medium transition-colors">
<i class="fa-solid fa-house w-6"></i> 总览控制台
</a>
<a href="planning.html" class="flex items-center px-4 py-3 bg-blue-50 text-planning rounded-lg font-medium">
<i class="fa-solid fa-chess-knight w-6"></i> 战略规划 (Planning)
</a>
<a href="devops.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-devops rounded-lg font-medium transition-colors">
<i class="fa-solid fa-code-branch w-6"></i> 开发运维 (DevOps)
</a>
<a href="quality.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-quality rounded-lg font-medium transition-colors">
<i class="fa-solid fa-shield-halved w-6"></i> 质量门控 (Quality Gate)
</a>
</nav>
</div>
</aside>
<main class="flex-1 flex flex-col">
<header class="h-16 bg-white shadow-sm flex items-center px-8 border-b-4 border-planning">
<h1 class="text-xl font-semibold text-gray-800">战略规划 (Planning)</h1>
</header>
<div class="p-8 flex-1 overflow-y-auto flex gap-6">
<!-- 左侧AI 分析聊天/上传 -->
<div class="w-1/3 bg-white rounded-lg shadow-sm border border-gray-200 flex flex-col">
<div class="p-4 border-b bg-gray-50 font-bold">需求输入 & 分析</div>
<div class="flex-1 p-4 overflow-y-auto space-y-4">
<!-- 聊天记录示例 -->
<div class="bg-blue-50 text-blue-800 p-3 rounded-lg rounded-tl-none">
您好!我是需求分析助手。请描述您的需求或上传需求文档。
</div>
<div class="bg-gray-100 text-gray-800 p-3 rounded-lg rounded-tr-none ml-8">
我需要一个用户登录功能,支持密码和短信验证码,且多次错误锁定。
</div>
<div class="bg-blue-50 text-blue-800 p-3 rounded-lg rounded-tl-none">
<i class="fa-solid fa-spinner fa-spin mr-2"></i> 正在提取需求并分析边界条件...
</div>
</div>
<div class="p-4 border-t border-gray-200">
<div class="relative">
<i class="fa-solid fa-paperclip absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 cursor-pointer hover:text-primary"></i>
<input type="text" placeholder="输入需求描述..." class="w-full pl-10 pr-12 py-2 border rounded-full focus:outline-none focus:border-planning">
<button class="absolute right-2 top-1/2 transform -translate-y-1/2 text-planning p-2 hover:bg-blue-50 rounded-full">
<i class="fa-solid fa-paper-plane"></i>
</button>
</div>
</div>
</div>
<!-- 右侧:需求拆解结果展示 -->
<div class="flex-1 bg-white rounded-lg shadow-sm border border-gray-200 flex flex-col">
<div class="p-4 border-b bg-gray-50 font-bold flex justify-between items-center">
<span>需求拆解结果面板</span>
<button class="bg-planning text-white px-3 py-1 rounded text-sm hover:bg-blue-600">确认并生成任务</button>
</div>
<div class="p-6 overflow-y-auto pointer-events-none opacity-50 blur-[1px]">
<!-- Placeholder Data -->
<h3 class="font-bold text-lg mb-4">登录功能需求分析文档</h3>
<div class="mb-6">
<h4 class="font-semibold text-gray-700 border-l-4 border-planning pl-2 mb-2">功能需求 (Functional)</h4>
<ul class="list-disc pl-5 space-y-1 text-gray-600">
<li>支持用户名/手机号与密码登录</li>
<li>支持手机号与短信验证码登录</li>
</ul>
</div>
<div class="mb-6">
<h4 class="font-semibold text-gray-700 border-l-4 border-planning pl-2 mb-2">边界条件与非功能需求</h4>
<ul class="list-disc pl-5 space-y-1 text-gray-600">
<li>多次错误:连续错误 5 次锁定账号 15 分钟</li>
<li>验证码时效:短信验证码有效期 5 分钟</li>
</ul>
</div>
<div>
<h4 class="font-semibold text-gray-700 border-l-4 border-planning pl-2 mb-2">提取的任务清单</h4>
<div class="border rounded-md divide-y">
<div class="p-3 bg-gray-50 flex justify-between"><span class="font-mono text-sm">TASK-101</span> <span>UI登录界面设计</span></div>
<div class="p-3 bg-gray-50 flex justify-between"><span class="font-mono text-sm">TASK-102</span> <span>后端密码验证与Token派发</span></div>
<div class="p-3 bg-gray-50 flex justify-between"><span class="font-mono text-sm">TASK-103</span> <span>后端:短信验证码发送与校验</span></div>
</div>
</div>
</div>
<!-- 遮罩层提示等 -->
<div class="absolute inset-0 flex items-center justify-center p-8 ml-[33%] rounded text-center">
<div class="bg-white/80 p-8 rounded-lg shadow-lg backdrop-blur">
<i class="fa-solid fa-robot text-4xl text-planning mb-4"></i>
<p class="text-xl font-bold">等待通过左侧对话输入需求</p>
<p class="text-gray-500 mt-2">AI 分析完成后将在此展示结构化的需求文档和任务清单</p>
</div>
</div>
</div>
</div>
</main>
</body>
</html>

151
template/quality.html Normal file
View File

@@ -0,0 +1,151 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SAFe OS - 质量门控 (Quality Gate)</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#2563EB',
planning: '#0EA5E9',
devops: '#10B981',
quality: '#8B5CF6'
}
}
}
}
</script>
</head>
<body class="bg-gray-50 flex h-screen overflow-hidden text-gray-800 font-sans">
<!-- 侧边栏导航 -->
<aside class="w-64 bg-white shadow-md flex flex-col z-10">
<div class="h-16 flex items-center px-6 border-b border-gray-100">
<i class="fa-solid fa-layer-group text-primary text-2xl mr-3"></i>
<span class="text-xl font-bold text-gray-800">SAFe OS</span>
</div>
<div class="p-4 flex-1">
<nav class="space-y-2">
<a href="index.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-primary rounded-lg font-medium transition-colors">
<i class="fa-solid fa-house w-6"></i> 总览控制台
</a>
<a href="planning.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-planning rounded-lg font-medium transition-colors">
<i class="fa-solid fa-chess-knight w-6"></i> 战略规划 (Planning)
</a>
<a href="devops.html" class="flex items-center px-4 py-3 text-gray-600 hover:bg-gray-50 hover:text-devops rounded-lg font-medium transition-colors">
<i class="fa-solid fa-code-branch w-6"></i> 开发运维 (DevOps)
</a>
<a href="quality.html" class="flex items-center px-4 py-3 bg-purple-50 text-quality rounded-lg font-medium">
<i class="fa-solid fa-shield-halved w-6"></i> 质量门控 (Quality Gate)
</a>
</nav>
</div>
</aside>
<main class="flex-1 flex flex-col overflow-y-auto">
<header class="h-16 bg-white shadow-sm flex items-center px-8 border-b-4 border-quality sticky top-0 z-10">
<h1 class="text-xl font-semibold text-gray-800">质量门控 (Quality Gate)Dashboard</h1>
</header>
<div class="p-8 max-w-7xl mx-auto w-full">
<!-- 质量概览统计卡片 -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<div class="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
<div class="text-gray-500 mb-1 text-sm font-semibold">质量状态</div>
<div class="text-2xl font-bold text-green-500">通过 (Passed)</div>
</div>
<div class="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
<div class="text-gray-500 mb-1 text-sm font-semibold">代码覆盖率</div>
<div class="text-2xl font-bold text-gray-800">89.4%</div>
</div>
<div class="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
<div class="text-gray-500 mb-1 text-sm font-semibold">严重安全漏洞</div>
<div class="text-2xl font-bold text-red-500">2 <span class="text-sm font-normal text-gray-400">待修复</span></div>
</div>
<div class="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
<div class="text-gray-500 mb-1 text-sm font-semibold">遗留技术债</div>
<div class="text-2xl font-bold text-orange-500">48h</div>
</div>
</div>
<!-- PR 列表 -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 mb-8">
<div class="p-4 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
<h2 class="font-bold text-gray-800">合并请求审查 (PR List)</h2>
<span class="text-sm text-gray-500">共 3 个待处理</span>
</div>
<div class="divide-y divide-gray-200">
<!-- PR Item 1: 扫描失败被拦截 -->
<div class="p-4 hover:bg-gray-50 transition-colors flex items-center justify-between cursor-pointer">
<div class="flex items-center space-x-4">
<span class="w-2 h-2 rounded-full bg-red-500"></span>
<div>
<h3 class="font-semibold text-gray-800 hover:text-quality">PR #102: feat: 实现用户登录与注册接口</h3>
<p class="text-sm text-gray-500 mt-1">
<span class="mr-3"><i class="fa-regular fa-clock"></i> 2小时前</span>
<span class="mr-3"><i class="fa-regular fa-user"></i> 作者: zhangsan</span>
<span class="text-green-600 mr-2">+120行</span>
<span class="text-red-500">-20行</span>
</p>
</div>
</div>
<div class="flex items-center space-x-4 text-sm">
<span class="px-2 py-1 bg-red-50 text-red-600 rounded border border-red-100"><i class="fa-solid fa-xmark mr-1"></i> 扫描未通过</span>
<span class="text-red-500 font-bold">1个高危漏洞</span>
<button class="bg-quality text-white px-3 py-1.5 rounded hover:bg-purple-600">查看详情</button>
</div>
</div>
<!-- PR Item 2: 扫描通过 -->
<div class="p-4 hover:bg-gray-50 transition-colors flex items-center justify-between cursor-pointer opacity-70">
<div class="flex items-center space-x-4">
<span class="w-2 h-2 rounded-full bg-green-500"></span>
<div>
<h3 class="font-semibold text-gray-800">PR #101: fix: 首页在移动端布局错位问题</h3>
<p class="text-sm text-gray-500 mt-1">
<span class="mr-3"><i class="fa-regular fa-clock"></i> 5小时前</span>
<span class="mr-3"><i class="fa-regular fa-user"></i> 作者: lisi</span>
<span class="text-green-600 mr-2">+15行</span>
<span class="text-red-500">-10行</span>
</p>
</div>
</div>
<div class="flex items-center space-x-4 text-sm">
<span class="px-2 py-1 bg-green-50 text-green-600 rounded border border-green-100"><i class="fa-solid fa-check mr-1"></i> 扫描通过</span>
<span class="text-gray-500">可合并</span>
</div>
</div>
</div>
</div>
<!-- AI 扫描详情面板示例 (假装点击了 PR #102 后展开的详情) -->
<div class="bg-purple-50 rounded-lg shadow-inner border border-purple-100 p-6">
<div class="flex items-start justify-between mb-4">
<h3 class="font-bold text-gray-800 text-lg flex items-center">
<i class="fa-solid fa-robot text-quality mr-2"></i> PR #102 质量扫描报告
</h3>
<button class="text-gray-400 hover:text-gray-600"><i class="fa-solid fa-xmark text-xl"></i></button>
</div>
<div class="bg-white p-4 rounded border border-gray-200 mb-4">
<h4 class="text-red-600 font-semibold mb-2"><i class="fa-solid fa-triangle-exclamation mr-1"></i> [高危] SQL 注入风险</h4>
<p class="text-gray-600 text-sm mb-2">文件: <code class="bg-gray-100 px-1 rounded">auth.service.js</code>, 第 45 行</p>
<pre class="bg-gray-800 text-gray-300 p-3 rounded font-mono text-sm mb-4"><code><span class="text-red-400">- const query = `SELECT * FROM users WHERE username = '${req.body.username}'`;</span>
<span class="text-green-400">+ const query = 'SELECT * FROM users WHERE username = ?'; // 修复建议: 使用参数化查询</span></code></pre>
<button class="bg-gray-100 border border-gray-300 px-3 py-1.5 rounded text-sm hover:bg-gray-200 transition-colors">
<i class="fa-solid fa-comment-dots text-quality mr-1"></i> 让 AI 帮我自动修复这段代码
</button>
</div>
</div>
</div>
</main>
</body>
</html>

685
template/task.html Normal file
View File

@@ -0,0 +1,685 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SDLC Agent Demo - 多智能体软件交付协同系统</title>
<!-- TailwindCSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Vue 3 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- Highlight.js 代码高亮 - 使用浅色主题 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<!-- Marked Markdown 解析 -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
/* 自定义样式 */
.stage-card {
transition: all 0.3s ease;
}
.stage-card.active {
transform: scale(1.02);
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3);
}
.stage-card.completed {
border-color: #10B981;
background: linear-gradient(135deg, #ECFDF5 0%, #FFFFFF 100%);
}
.stage-card.processing {
border-color: #3B82F6;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4); }
50% { box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); }
}
.markdown-body {
font-size: 0.875rem;
line-height: 1.7;
}
.markdown-body pre {
background: #f6f8fa;
padding: 1rem;
border-radius: 0.5rem;
margin: 0.5rem 0;
overflow-x: auto;
border: 1px solid #e1e4e8;
}
.markdown-body code {
font-family: 'Consolas', 'Monaco', monospace;
color: #24292e;
}
.markdown-body h1, .markdown-body h2, .markdown-body h3 {
margin-top: 1rem;
margin-bottom: 0.5rem;
font-weight: 600;
}
.markdown-body ul, .markdown-body ol {
padding-left: 1.5rem;
margin: 0.5rem 0;
}
/* SSE 连接状态指示器 */
.connection-status {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 0.5rem;
}
.connection-connected {
background-color: #10B981;
}
.connection-disconnected {
background-color: #EF4444;
}
.connection-connecting {
background-color: #F59E0B;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div id="app" class="min-h-screen">
<!-- 顶部导航栏 -->
<header class="bg-white shadow-sm border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 py-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-8 w-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
</svg>
</div>
<div class="ml-4">
<h1 class="text-xl font-bold text-gray-900">SDLC Agent Demo</h1>
<p class="text-sm text-gray-500">多智能体端到端软件交付协同系统</p>
</div>
</div>
<div class="flex items-center space-x-4">
<div class="flex items-center text-sm">
<span :class="['connection-status', connectionStatusClass]"></span>
<span class="text-gray-600">{{ connectionStatusText }}</span>
</div>
</div>
</div>
</div>
</header>
<!-- 主内容区 -->
<main class="max-w-7xl mx-auto px-4 py-8 sm:px-6 lg:px-8">
<!-- 需求输入区 -->
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">1. 输入软件需求</h2>
<div class="space-y-4">
<textarea
v-model="requirement"
rows="5"
placeholder="请输入您的软件需求描述,例如:&#10;开发一个用户管理系统,支持用户的增删改查功能,需要包含以下特性:&#10;- 用户注册和登录&#10;- 用户信息管理&#10;- 角色权限控制&#10;- 操作日志记录"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
:disabled="isProcessing"
></textarea>
<div class="flex items-center justify-between">
<p class="text-sm text-gray-500">
当前任务 ID: {{ taskId || '无' }}
</p>
<button
@click="startSDLCProcess"
:disabled="!canStart || isProcessing"
:class="[
'px-6 py-2.5 rounded-lg font-medium text-white transition-all duration-200',
canStart && !isProcessing
? 'bg-blue-600 hover:bg-blue-700 shadow-md hover:shadow-lg'
: 'bg-gray-400 cursor-not-allowed'
]"
>
{{ isProcessing ? '执行中...' : '开始执行' }}
</button>
</div>
</div>
</div>
<!-- 进度展示区 -->
<div class="mb-6" v-show="stages.length > 0">
<h2 class="text-lg font-semibold text-gray-900 mb-4">2. 执行进度</h2>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div
v-for="(stage, index) in stages"
:key="stage.id"
:class="[
'stage-card rounded-lg border-2 p-4 bg-white',
{ 'active': stage.status === 'processing' },
{ 'completed': stage.status === 'completed' },
{ 'processing': stage.status === 'processing' }
]"
>
<div class="flex items-center mb-2">
<div
:class="[
'w-8 h-8 rounded-full flex items-center justify-center mr-3',
getStageIconClass(stage.status)
]"
>
<span class="text-white font-bold text-sm">{{ index + 1 }}</span>
</div>
<div>
<h3 class="font-medium text-gray-900">{{ stage.name }}</h3>
<p class="text-xs text-gray-500">{{ stage.agent }}</p>
</div>
</div>
<div class="mt-3">
<span :class="[
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
getStageBadgeClass(stage.status)
]">
{{ getStageStatusText(stage.status) }}
</span>
</div>
</div>
</div>
</div>
<!-- 实时日志区 -->
<div class="bg-white rounded-lg shadow-md p-6 mb-6" v-show="logs.length > 0">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-gray-900">3. 实时日志</h2>
<button
@click="clearLogs"
class="text-sm text-gray-500 hover:text-gray-700"
>
清空日志
</button>
</div>
<div class="bg-gray-900 rounded-lg p-4 h-64 overflow-y-auto font-mono text-sm">
<div v-for="(log, index) in logs" :key="index" class="mb-1">
<span class="text-gray-500">{{ log.timestamp }}</span>
<span :class="getLogLevelClass(log.event)" class="ml-2">[{{ log.event }}]</span>
<span class="text-gray-300 ml-2">{{ log.message }}</span>
</div>
</div>
</div>
<!-- 结果展示区 -->
<div class="space-y-6" v-show="results.length > 0">
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900">4. 输出结果</h2>
<button
@click="downloadResults"
:disabled="!isCompleted"
:class="[
'px-4 py-2 rounded-lg font-medium text-white transition-all duration-200 flex items-center',
isCompleted
? 'bg-green-600 hover:bg-green-700 shadow-md'
: 'bg-gray-400 cursor-not-allowed'
]"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
</svg>
{{ isCompleted ? '打包下载结果' : '执行完成后下载' }}
</button>
</div>
<div v-for="(result, index) in results" :key="index" class="bg-white rounded-lg shadow-md overflow-hidden">
<div
@click="result.expanded = !result.expanded"
class="px-6 py-4 bg-gray-50 border-b border-gray-200 cursor-pointer hover:bg-gray-100 transition-colors"
>
<div class="flex items-center justify-between">
<div class="flex items-center">
<svg
:class="['w-5 h-5 mr-2 text-gray-500 transform transition-transform', result.expanded ? 'rotate-90' : '']"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
</svg>
<h3 class="font-semibold text-gray-900">{{ result.title }}</h3>
</div>
<div class="flex items-center space-x-2">
<span class="text-xs text-gray-500">{{ formatDate(result.timestamp) }}</span>
<button
@click.stop="copyToClipboard(result.content)"
class="text-sm text-blue-600 hover:text-blue-700"
>
复制
</button>
</div>
</div>
</div>
<div v-show="result.expanded" class="p-6">
<div class="markdown-body" v-html="renderMarkdown(result.content)"></div>
</div>
</div>
</div>
</main>
<!-- 页脚 -->
<footer class="bg-white border-t border-gray-200 mt-12">
<div class="max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8">
<p class="text-center text-sm text-gray-500">
基于 CrewAI + Qwen3.5-flash + FastAPI(SSE) 构建 | Bosch Demo
</p>
</div>
</footer>
<!-- 复制成功提示 -->
<div v-if="showCopyToast" class="fixed bottom-4 right-4 bg-green-600 text-white px-6 py-3 rounded-lg shadow-lg transition-opacity duration-300">
✓ 已复制到剪贴板
</div>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
// 需求输入
requirement: '',
// 任务管理
taskId: null,
isProcessing: false,
// SSE 连接
eventSource: null,
connectionStatus: 'disconnected', // 'connecting', 'connected', 'disconnected'
// 阶段定义
stages: [],
// 实时日志
logs: [],
// 结果数据
results: [],
// UI 状态
showCopyToast: false
}
},
computed: {
canStart() {
return this.requirement.trim().length >= 10 && !this.isProcessing;
},
connectionStatusClass() {
const statusMap = {
'connecting': 'connection-connecting',
'connected': 'connection-connected',
'disconnected': 'connection-disconnected'
};
return statusMap[this.connectionStatus];
},
connectionStatusText() {
const textMap = {
'connecting': '连接中...',
'connected': '已连接',
'disconnected': '未连接'
};
return textMap[this.connectionStatus];
},
isCompleted() {
return this.stages.length > 0 &&
this.stages.every(s => s.status === 'completed');
}
},
methods: {
/**
* 下载打包结果
*/
downloadResults() {
if (!this.taskId || !this.isCompleted) return;
const url = `/api/v1/sdlc/download/${this.taskId}`;
window.open(url, '_blank');
},
/**
* 启动 SDLC 流程
*/
async startSDLCProcess() {
if (!this.canStart) return;
try {
this.isProcessing = true;
this.stages = [];
this.logs = [];
this.results = [];
// 调用 API 启动任务
const response = await fetch('/api/v1/sdlc/start', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
requirement: this.requirement
})
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
this.taskId = data.task_id;
this.addLog('system', '任务已启动', `Task ID: ${data.task_id}`);
// 初始化阶段
this.initStages();
// 开始轮询任务事件
this.connectPolling(data.task_id);
} catch (error) {
console.error('启动失败:', error);
this.addLog('error', '启动失败', error.message);
this.isProcessing = false;
alert(`启动失败:${error.message}`);
}
},
/**
* 初始化阶段
*/
initStages() {
this.stages = [
{ id: 'pm', name: '需求分析', agent: 'PM Agent', status: 'pending' },
{ id: 'qa', name: '测试设计', agent: 'QA Agent', status: 'pending' },
{ id: 'dev', name: '代码实现', agent: 'Dev Agent', status: 'pending' },
{ id: 'final', name: '交付完成', agent: 'Orchestrator', status: 'pending' }
];
},
/**
* 轮询任务事件(替代 SSE
*/
connectPolling(taskId) {
this.connectionStatus = 'connecting';
this.addLog('system', 'POLL', `开始轮询任务:${taskId}`);
let lastIndex = 0;
let pollCount = 0;
const maxPolls = 600; // 最多轮询 600 次 (10 分钟)
const poll = () => {
if (pollCount >= maxPolls) {
this.addLog('system', 'POLL', '轮询超时');
this.isProcessing = false;
this.connectionStatus = 'disconnected';
return;
}
fetch(`/api/v1/sdlc/poll/${taskId}?last_index=${lastIndex}`)
.then(res => res.json())
.then(data => {
const { events, has_more, status } = data;
// 处理新事件
events.forEach(event => {
lastIndex++;
this.handleEvent(event);
});
// 检查是否继续轮询
if (status === 'completed' || status === 'failed') {
this.isProcessing = false;
this.connectionStatus = 'disconnected';
this.addLog('system', 'POLL', `任务完成,状态:${status}`);
return;
}
if (has_more || events.length > 0) {
pollCount++;
setTimeout(poll, 500); // 每 500ms 轮询一次
} else if (status === 'processing') {
pollCount++;
setTimeout(poll, 1000); // 无新事件时 1 秒后再试
}
})
.catch(err => {
console.error('轮询失败:', err);
this.addLog('error', 'POLL', err.message);
pollCount++;
setTimeout(poll, 2000);
});
};
// 开始轮询
setTimeout(poll, 500);
},
/**
* 处理单个事件
*/
handleEvent(event) {
const eventType = event.event;
const data = event.data;
switch(eventType) {
case 'task_started':
this.addLog('task_started', 'System', data.message || '任务已启动');
break;
case 'pm_start':
this.updateStageStatus('pm', 'processing');
this.addLog('pm_start', 'PM Agent', '开始需求分析...');
break;
case 'pm_complete':
this.updateStageStatus('pm', 'completed');
this.addLog('pm_complete', 'PM Agent', '需求分析完成');
this.addResult('📋 软件需求规格说明书 (SRS)', data.content, data.timestamp);
break;
case 'qa_start':
this.updateStageStatus('qa', 'processing');
this.addLog('qa_start', 'QA Agent', '开始测试用例设计...');
break;
case 'qa_complete':
this.updateStageStatus('qa', 'completed');
this.addLog('qa_complete', 'QA Agent', '测试用例设计完成');
this.addResult('🧪 测试方案与用例', data.content, data.timestamp);
break;
case 'dev_start':
this.updateStageStatus('dev', 'processing');
this.addLog('dev_start', 'Dev Agent', '开始代码实现...');
break;
case 'dev_complete':
this.updateStageStatus('dev', 'completed');
this.addLog('dev_complete', 'Dev Agent', '代码实现完成');
this.addResult('💻 代码实现', data.content, data.timestamp);
break;
case 'final_result':
this.updateStageStatus('final', 'completed');
this.addLog('final_result', 'System', 'SDLC 流程完成');
break;
case 'error':
this.addLog('error', 'Error', data.error || '未知错误');
alert(`执行错误:${data.error}`);
break;
}
},
/**
* 更新阶段状态
*/
updateStageStatus(stageId, status) {
const stage = this.stages.find(s => s.id === stageId);
if (stage) {
stage.status = status;
}
},
/**
* 添加日志
*/
addLog(event, source, message) {
this.logs.push({
timestamp: new Date().toLocaleTimeString('zh-CN'),
event,
source,
message
});
// 保持最新 100 条日志
if (this.logs.length > 100) {
this.logs.shift();
}
},
/**
* 添加结果
*/
addResult(title, content, timestamp) {
this.results.push({
title,
content,
timestamp,
expanded: true
});
},
/**
* 清空日志
*/
clearLogs() {
this.logs = [];
},
/**
* 获取阶段图标样式
*/
getStageIconClass(status) {
const classMap = {
'pending': 'bg-gray-400',
'processing': 'bg-blue-500',
'completed': 'bg-green-500'
};
return classMap[status] || classMap['pending'];
},
/**
* 获取阶段徽章样式
*/
getStageBadgeClass(status) {
const classMap = {
'pending': 'bg-gray-100 text-gray-800',
'processing': 'bg-blue-100 text-blue-800',
'completed': 'bg-green-100 text-green-800'
};
return classMap[status] || classMap['pending'];
},
/**
* 获取阶段状态文本
*/
getStageStatusText(status) {
const textMap = {
'pending': '等待中',
'processing': '进行中',
'completed': '已完成'
};
return textMap[status] || status;
},
/**
* 获取日志级别样式
*/
getLogLevelClass(event) {
const classMap = {
'pm_start': 'text-blue-400',
'pm_complete': 'text-green-400',
'qa_start': 'text-blue-400',
'qa_complete': 'text-green-400',
'dev_start': 'text-blue-400',
'dev_complete': 'text-green-400',
'final_result': 'text-purple-400',
'error': 'text-red-400',
'system': 'text-yellow-400'
};
return classMap[event] || 'text-gray-400';
},
/**
* 渲染 Markdown
*/
renderMarkdown(content) {
if (!content) return '';
return marked.parse(content);
},
/**
* 格式化日期
*/
formatDate(timestamp) {
if (!timestamp) return '';
try {
return new Date(timestamp).toLocaleString('zh-CN');
} catch {
return timestamp;
}
},
/**
* 复制到剪贴板
*/
async copyToClipboard(content) {
try {
await navigator.clipboard.writeText(content);
this.showCopyToast = true;
setTimeout(() => {
this.showCopyToast = false;
}, 2000);
} catch (err) {
console.error('复制失败:', err);
alert('复制失败,请手动复制');
}
}
},
beforeUnmount() {
// 清理 SSE 连接
if (this.eventSource) {
this.eventSource.close();
}
}
}).mount('#app');
</script>
</body>
</html>

View File

@@ -0,0 +1,108 @@
# 工程产品功能描述
以下是当前工程产品的功能描述,从用户视角出发,帮助 UI 工程师重新设计页面和交互:
---
### **产品概览**
- **产品名称**: SAFe OS
- **目标用户**: 软件开发团队、项目经理、质量保证工程师。
- **核心目标**: 提供一个集成的工作平台,支持战略规划、开发运维管理、质量门控等功能,提升团队协作效率。
---
### **主要功能模块**
#### **1. 战略规划模块Planning**
- **用户目标**: 帮助项目经理进行需求分析、任务分解和优先级规划。
- **核心功能**:
- **需求管理**: 用户可以输入需求描述,系统自动生成功能需求、非功能需求、验收标准等。
- **任务分解**: 系统支持将需求分解为具体任务,生成任务清单。
- **边界情况分析**: 系统提示用户考虑潜在的边界情况,确保需求全面。
- **AI 交互特点**:
- 用户通过自然语言输入需求AI Agent 提供实时分析和建议。
- AI 会根据上下文动态生成问题,帮助用户完善需求。
- **数据交互说明**:
- 前端通过 API 将用户输入的需求发送到后端。
- 后端调用 AI 模型进行需求分析,返回结构化的需求数据(功能需求、非功能需求、验收标准等)。
- 前端实时更新界面,展示分析结果,并允许用户进一步编辑。
- **用户交互流程**:
- 用户可以通过文本框输入需求,也可以上传包含需求的文件(如 Word 或 PDF
- 系统解析文件内容并提取关键信息,展示在界面上供用户确认。
- 用户可以对提取的内容进行修改或补充,最终确认后进入任务分解阶段。
#### **2. 开发运维模块DevOps**
- **用户目标**: 帮助开发团队高效完成代码开发、测试和部署。
- **核心功能**:
- **代码生成**: 用户输入功能描述AI Agent 自动生成代码模板和单元测试。
- **测试执行**: 系统支持自动化测试执行,展示测试通过率、失败用例等信息。
- **实施建议**: AI 提供代码实现的优化建议,帮助开发者提升代码质量。
- **操作流程**:
- 用户通过一个多步骤的进度条界面完成操作,每一步都有明确的指引。
- 例如:需求输入 → 代码生成 → 测试生成 → 测试执行 → 结果分析。
- 每一步完成后,用户可以选择继续下一步或返回修改。
- **AI 交互特点**:
- 用户可以通过对话式界面与 AI 讨论代码实现细节。
- AI 提供实时反馈,例如代码片段、测试结果和优化建议。
- **数据交互说明**:
- 前端通过 API 将用户输入的功能描述发送到后端。
- 后端调用代码生成服务,返回生成的代码模板和单元测试。
- 测试执行时,前端发送测试请求到后端,后端运行测试并返回测试结果(通过率、失败用例、错误日志等)。
- 前端根据后端返回的数据动态更新进度条和结果展示。
- **用户交互流程**:
- 用户在界面上输入功能描述,或上传包含功能需求的文件。
- 系统解析需求后,生成代码模板并展示给用户。
- 用户可以对生成的代码进行修改或直接运行测试。
- 测试完成后,系统展示测试结果,并提供优化建议。
#### **3. 质量门控模块Quality Gate**
- **用户目标**: 帮助质量保证工程师监控代码质量,确保交付物符合标准。
- **核心功能**:
- **质量概览Dashboard**: 展示项目的整体质量状态,包括问题数量、严重性分布等。
- **PR 列表**: 展示所有待审核的 Pull Request包含扫描状态、问题详情等。
- **数据来源**:
- 后端提供 PR 的详细信息,包括:
- PR 标题、作者、状态(打开、合并、关闭)。
- 代码变更文件的数量、每个文件的修改行数(新增/删除)。
- 安全扫描结果,包括问题的行号、严重性、规则说明等。
- AI 生成的代码审查建议。
- **质量设置**: 用户可以配置质量门控的规则,例如代码覆盖率要求、安全扫描标准等。
- **操作特点**:
- 用户可以点击 PR 查看详细信息,包括代码变更和扫描报告。
- 系统支持通过图表展示问题分布,帮助用户快速定位高风险区域。
- **AI 交互特点**:
- AI 自动分析 PR 的代码质量,生成详细的扫描报告。
- 用户可以通过与 AI 对话,快速定位问题并获取修复建议。
- **数据交互说明**:
- 前端通过 API 请求后端获取 PR 列表和详细信息。
- 后端调用静态代码分析工具和安全扫描服务,生成扫描报告并返回给前端。
- 前端根据后端返回的数据动态渲染 PR 列表和图表。
- 用户提交质量设置时,前端将配置数据发送到后端,后端保存并应用新规则。
- **用户交互流程**:
- 用户在 PR 列表中选择一个 PR查看详细的代码变更和扫描结果。
- 系统展示问题的分布图表,用户可以点击具体问题查看代码上下文。
- 用户可以通过界面直接修改质量规则,或与 AI 讨论修复建议。
---
### **用户体验目标**
- **简洁直观**: 页面布局清晰,用户可以快速找到所需功能。
- **高效交互**: 通过对话式交互和实时反馈,减少用户操作步骤。
- **数据驱动**: 以数据图表为核心,帮助用户快速理解项目状态。
- **模块化设计**: 各模块功能独立,用户可以根据需求自由切换。
- **AI 驱动**: 每个模块都集成 AI Agent提供智能化的辅助功能。
---
### **设计建议**
- **导航优化**: 提供更清晰的导航结构,例如在侧边栏中分组显示主要模块和子功能。
- **视觉层次**: 通过颜色、字体大小等方式突出重要信息,例如关键数据和操作按钮。
- **用户引导**: 在每个模块中添加新手引导,帮助用户快速上手。
- **响应式设计**: 确保页面在不同设备上的良好显示效果。
- **AI 交互设计**:
- 在页面中突出 AI Agent 的入口,例如对话框或悬浮按钮。
- 提供 AI 的实时状态反馈,例如“正在分析...”、“已完成”等提示。
---
以上描述从用户功能和 AI Agent 的交互角度出发,帮助 UI 工程师理解产品目标和用户需求,从而设计出更符合用户期望的界面和交互。

View File

@@ -23,6 +23,11 @@ export default defineConfig(({ mode }) => {
changeOrigin: true,
rewrite: (path) => path.replace(/^\/quality-api/, "/api"),
},
"/task-api": {
target: env.VITE_QUALITY_API_BASE || "http://localhost:8080",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/task-api/, "/api"),
},
},
},
};