Compare commits
13 Commits
cheng-0312
...
replace-te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28b85e1422 | ||
|
|
ccfba711e7 | ||
|
|
43f7cca0c2 | ||
|
|
cdea59af92 | ||
|
|
48c6f84239 | ||
|
|
250192f699 | ||
|
|
cef1ae0414 | ||
|
|
eeb5e728ff | ||
|
|
98f916bd37 | ||
|
|
80430c674b | ||
| 99a3281d58 | |||
|
|
cca1b8d046 | ||
|
|
c8c687cf60 |
116
API_Gateway_Rules.md
Normal file
116
API_Gateway_Rules.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# Safe OS UI - API Gateway 路由转发规则文档
|
||||||
|
|
||||||
|
本系统包含多个独立的后端AI服务(规划系统、开发平台、质量门禁等)。为了使前端统一指向单一入口并简化跨域和请求分发配置,建议使用 API Gateway 进行统一路由和负载均衡。
|
||||||
|
|
||||||
|
以下是当前前端所有与后端服务交互的接口清单以及转发规则。Gateway 服务需要解析相应的路径前缀(Ingress Prefix),将请求 Rewrite 后转发至对应的内部后端服务。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 整体架构与入口规则
|
||||||
|
|
||||||
|
- **统一入口 URL**:`http://<api-gateway-host>:<port>` (前端将所有 API 请求指向此地址)
|
||||||
|
- **核心路由匹配策略**:基于 URL前缀 进行正则匹配或前缀匹配。
|
||||||
|
|
||||||
|
| 业务模块 | Ingress Prefix (外部路径) | Forward Target (内部服务地址) | Rewrite Rule (路径重写规则) |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| **Planning System** | `/planning-api/*` | `http://localhost:8090` | `/planning-api/(.*)` ➔ `/api/$1` |
|
||||||
|
| **DevOps System** | `/devops-api/*` | `http://localhost:8000` | `/devops-api/(.*)` ➔ `/$1` |
|
||||||
|
| **Quality Gate System** | `/quality-api/*` | `http://localhost:5000` | `/quality-api/(.*)` ➔ `/api/$1` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 规划子系统接口 (Planning System)
|
||||||
|
**底层服务**: `http://localhost:8090`
|
||||||
|
前缀重写规则:请求至统一入口 `.../planning-api/X` 会被转发至内部的 `.../api/X`。
|
||||||
|
|
||||||
|
| 方法 | 外部暴露路径 (Gateway URL) | 内部转发路径 (Target URL) | 接口描述 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `POST` | `/planning-api/chat/stream` | `/api/chat/stream` | 铁三角 Agent:发送大系统规划/史诗业务需求内容(SSE流式数据返回) |
|
||||||
|
| `POST` | `/planning-api/upload` | `/api/upload` | 文档上传:支持用户上传附件作为系统知识或输入来源 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 研发自动引擎接口 (DevOps System)
|
||||||
|
**底层服务**: `http://localhost:8000`
|
||||||
|
前缀重写规则:请求至统一入口 `.../devops-api/X` 会被去掉前缀,转为后端的 `.../X`。该系统重度依赖**SSE (Server-Sent Events) 流式连接**,请在Gateway配置中确保不会缓冲或阻断流数据。
|
||||||
|
|
||||||
|
| 方法 | 外部暴露路径 (Gateway URL) | 内部转发路径 (Target URL) | 接口描述 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `POST` | `/devops-api/session/start` | `/session/start` | 新建研发自动化任务会话 (Session) |
|
||||||
|
| `POST` | `/devops-api/session/{sessionId}/clarify` | `/session/{sessionId}/clarify` | 基于需求说明进行需求澄清和补充 |
|
||||||
|
| `GET` | `/devops-api/session/{sessionId}/pm/stream` | `/session/{sessionId}/pm/stream` | PM角色执行需求细化分析流程 (SSE流) |
|
||||||
|
| `GET` | `/devops-api/session/{sessionId}/pm/refine/stream` | `/session/{sessionId}/pm/refine/stream` | 追加PM反馈,继续流式提炼需求 (SSE流) |
|
||||||
|
| `GET` | `/devops-api/session/{sessionId}/qa/stream` | `/session/{sessionId}/qa/stream` | QA角色基于需求生成测试用例并执行评估 (SSE流) |
|
||||||
|
| `GET` | `/devops-api/session/{sessionId}/qa/refine/stream` | `/session/{sessionId}/qa/refine/stream` | 追加QA用例反馈并更新测试集 (SSE流) |
|
||||||
|
| `GET` | `/devops-api/session/{sessionId}/dev/stream` | `/session/{sessionId}/dev/stream` | 核心编码过程:依据需求、架构与测试生成最终业务/Java等工程代码 (SSE流) |
|
||||||
|
| `POST` | `/devops-api/session/{sessionId}/test/run` | `/session/{sessionId}/test/run` | 运行集成编译与单元/端到端测试并获取结果 |
|
||||||
|
| `GET` | `/devops-api/session/{sessionId}/test/fix/stream` | `/session/{sessionId}/test/fix/stream` | 针对测试错误进行的AI代码自动修复 (SSE流) |
|
||||||
|
|
||||||
|
*注意: `{sessionId}` 属于路径层级中的动态参数,具体网关转发时使用泛类型或通配符放行。*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 代码质量门禁接口 (Quality Gate)
|
||||||
|
**底层服务**: `http://localhost:5000`
|
||||||
|
前缀重写规则:请求至统一入口 `.../quality-api/X` 会被转发至后端内部的 `.../api/X`。
|
||||||
|
|
||||||
|
| 方法 | 外部暴露路径 (Gateway URL) | 内部转发路径 (Target URL) | 接口描述 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `GET` | `/quality-api/prs` | `/api/prs` | 获取 PR 扫描工单列表(支持多种查询参数) |
|
||||||
|
| `GET` | `/quality-api/prs/history` | `/api/prs/history` | 获取 PR 处理历史,用于趋势看板 (e.g., `?limit=15`) |
|
||||||
|
| `GET` | `/quality-api/prs/{prId}` | `/api/prs/{prId}` | 获取某一条指定 PR 扫描记录的详细执行状态及摘要 |
|
||||||
|
| `GET` | `/quality-api/prs/{prId}/files` | `/api/prs/{prId}/files` | 拉取这条 PR 的被影响/改动的文件目录结构 |
|
||||||
|
| `GET` | `/quality-api/prs/{prId}/file` | `/api/prs/{prId}/file` | 获取PR里指定文件的 Diff 对象(包含行内审查反馈),附带 `?path=xxx` 查询参数 |
|
||||||
|
| `POST`| `/quality-api/prs/{prId}/merge` | `/api/prs/{prId}/merge` | 在门禁界面确认问题修缮无误后,合并该次 PR |
|
||||||
|
| `POST`| `/quality-api/prs/{prId}/close` | `/api/prs/{prId}/close` | 门禁审查不通过,拒绝/关闭此条扫描和合并申请 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nginx Gateway 配置示例 (参考)
|
||||||
|
|
||||||
|
如果网关选用 Nginx,可快速参考如下配置来完成上述 Rewrite 与 Proxy:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name api-gateway.safe-os.local;
|
||||||
|
|
||||||
|
# 1. Planning API 转发
|
||||||
|
location /planning-api/ {
|
||||||
|
# 截取 /planning-api/ 后的内容,拼接到 /api/
|
||||||
|
rewrite ^/planning-api/(.*)$ /api/$1 break;
|
||||||
|
proxy_pass http://localhost:8090;
|
||||||
|
|
||||||
|
# 启用流连接所需的头信息
|
||||||
|
proxy_set_header Connection '';
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
chunked_transfer_encoding off;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_cache off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. DevOps API 转发
|
||||||
|
location /devops-api/ {
|
||||||
|
# 截取 /devops-api/ 后的内容,直接拼接
|
||||||
|
rewrite ^/devops-api/(.*)$ /$1 break;
|
||||||
|
proxy_pass http://localhost:8000;
|
||||||
|
|
||||||
|
# 支撑 SSE (Server-Sent Events) 的流配置
|
||||||
|
proxy_set_header Connection '';
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
chunked_transfer_encoding off;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_cache off;
|
||||||
|
proxy_read_timeout 3600s; # 考虑大模型生成代码时间较长
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. Quality Gate API 转发
|
||||||
|
location /quality-api/ {
|
||||||
|
# 截取 /quality-api/ 后的内容,拼接到 /api/
|
||||||
|
rewrite ^/quality-api/(.*)$ /api/$1 break;
|
||||||
|
proxy_pass http://localhost:5000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
以上文档即针对前端当前服务现状所整理的 API Gateway 路由规划规范,您可以直接根据此规范开发 API Gateway 或调整 Nginx 等负载均衡器的配置。
|
||||||
140
Artifact_SplitScreen_Design.md
Normal file
140
Artifact_SplitScreen_Design.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# Vibe Coding Design Docs: Workspace/Artifact Split-Screen Pattern
|
||||||
|
|
||||||
|
## 1. Context & Objective
|
||||||
|
The goal is to implement a UI/UX pattern similar to **Claude Artifacts** or **Gemini Deep Research**. When a specific complex task is triggered (e.g., "Project Planning Skill"), the single-column chat interface should smoothly transition into a split-screen layout:
|
||||||
|
- **Left Panel (35%)**: Conversational context, CoT (Chain of Thought) traces, tool calls, and user input.
|
||||||
|
- **Right Panel (65%)**: A dedicated "Workspace" or "Artifact" rendering area to display long-form content (Markdown, code, diagrams) generated by the Agent's skills.
|
||||||
|
|
||||||
|
Crucially, this system must support **Reflexion/Iterative generation**. The user can comment on the generated artifact in the left panel, and the agent should update the artifact in the right panel based on the feedback.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Frontend Implementation Guide (React + Vite + Tailwind)
|
||||||
|
|
||||||
|
### 2.1 State Management (State & Types)
|
||||||
|
Extend the existing frontend state to track the workspace status and content.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 1. Extend the StreamEvent type to support UI control and artifact streaming
|
||||||
|
type StreamEvent = {
|
||||||
|
type:
|
||||||
|
| "thought"
|
||||||
|
| "tool_call"
|
||||||
|
| "tool_result"
|
||||||
|
| "message" // Standard chat message
|
||||||
|
| "error"
|
||||||
|
| "workspace_start" // Trigger right panel open
|
||||||
|
| "workspace_delta" // Streaming text for the right panel
|
||||||
|
| "workspace_end"; // Streaming completed
|
||||||
|
content: string;
|
||||||
|
step?: number;
|
||||||
|
tool_name?: string;
|
||||||
|
workspace_title?: string; // Optional title for the artifact
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. Add Workspace State (Can be added to useReducer or a separate useState)
|
||||||
|
type WorkspaceState = {
|
||||||
|
isOpen: boolean;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
isGenerating: boolean;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 SSE Parsing Logic
|
||||||
|
Modify the `onEvent` handler inside `streamChat` to intercept `workspace_*` events.
|
||||||
|
- When `workspace_start` arrives: Set `workspace.isOpen = true`, clear previous content, set `isGenerating = true`.
|
||||||
|
- When `workspace_delta` arrives: Append text to `workspace.content`. Do **not** append this text to the left-panel chat history to avoid redundancy.
|
||||||
|
- When `workspace_end` arrives: Set `isGenerating = false`.
|
||||||
|
|
||||||
|
### 2.3 Layout & UI Re-architecture
|
||||||
|
Refactor the root `<div>` of `PlanningAgent.tsx` to handle dynamic flex layouts. Use Tailwind's transition utilities for smooth scaling.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<div className="flex h-full w-full overflow-hidden bg-surface">
|
||||||
|
|
||||||
|
{/* Left Panel: Chat & Controls */}
|
||||||
|
<div
|
||||||
|
className={`flex flex-col h-full transition-all duration-300 ease-in-out ${
|
||||||
|
workspace.isOpen ? 'w-[35%] border-r border-border min-w-[350px]' : 'w-full max-w-5xl mx-auto'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{/* Existing Message List & Input Area */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Panel: Workspace / Deep Research Output */}
|
||||||
|
{workspace.isOpen && (
|
||||||
|
<div className="w-[65%] h-full flex flex-col bg-surface-muted transition-opacity duration-300 animate-fade-in">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="h-14 border-b border-border flex items-center px-6 justify-between bg-white">
|
||||||
|
<h3 className="font-semibold text-txt flex items-center gap-2">
|
||||||
|
<IconDocument /> {workspace.title || 'Project Planning Document'}
|
||||||
|
</h3>
|
||||||
|
{workspace.isGenerating && (
|
||||||
|
<span className="text-sm text-magenta animate-pulse flex items-center gap-1">
|
||||||
|
<Spinner /> Generating...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Markdown Content Area */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-8 prose prose-slate max-w-none">
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
|
||||||
|
{workspace.content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Backend Implementation Guide (Agent / ReAct Loop)
|
||||||
|
|
||||||
|
The backend agent requires structural changes to understand the "Artifact" concept, emit correct SSE events, and maintain the artifact in its memory for iterative edits.
|
||||||
|
|
||||||
|
### 3.1 Tool / Skill Definition
|
||||||
|
When defining the `Project Planning Skill` for the LLM, clearly state its output behavior so the LLM knows *when* to use it.
|
||||||
|
- **Tool Description**: `use_planning_workspace`: "Invoke this tool to generate, structure, or update a major project planning document. The output will be rendered in a dedicated UI workspace."
|
||||||
|
|
||||||
|
### 3.2 Context Injection (Memory for Reflexion)
|
||||||
|
To allow the user to say *"extend the testing phase to 2 weeks"*, the LLM **must know what is currently in the right panel**.
|
||||||
|
- **Before sending the prompt to the LLM**, query the database/session for the current Artifact state.
|
||||||
|
- **Prompt Assembly**:
|
||||||
|
```text
|
||||||
|
[System Prompt / ReAct Instructions]
|
||||||
|
...
|
||||||
|
|
||||||
|
[Current Workspace Artifact (if exists)]
|
||||||
|
<workspace>
|
||||||
|
# Project Plan
|
||||||
|
1. Dev Phase: 1 week
|
||||||
|
2. Testing Phase: 1 week
|
||||||
|
</workspace>
|
||||||
|
|
||||||
|
[Chat History]
|
||||||
|
User: extend the testing phase to 2 weeks.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Streaming Control (Hijacking the Stream)
|
||||||
|
Within the ReAct execution loop, when the Agent decides to execute the `Project Planning Skill`:
|
||||||
|
1. The Backend normally streams `thought` or `tool_call` events.
|
||||||
|
2. Upon entering the specific Skill execution, the backend emits `{"type": "workspace_start", "workspace_title": "Update: Project Plan"}`.
|
||||||
|
3. As the LLM (or a sub-agent) generates the Markdown schema, the backend maps these tokens to `workspace_delta` events and flushes them to the frontend.
|
||||||
|
4. (CRITICAL) Do **not** send these tokens as `message` or `final` chat events. The chat bubble should only say something like: *"I have updated the project plan in the workspace area."*
|
||||||
|
5. Save the final generated Markdown text into the session memory as the `Current Artifact` for future context injection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Work Flow Summary (For LLM context generation)
|
||||||
|
|
||||||
|
1. `User` sends prompt: "Plan the new feature".
|
||||||
|
2. `Agent` thinks (`type: thought`), decides to use `Project Planning Skill` (`type: tool_call`).
|
||||||
|
3. `Agent` emits `{"type": "workspace_start"}`.
|
||||||
|
4. `Frontend` expands right panel (65% width).
|
||||||
|
5. `Agent` streams `{"type": "workspace_delta", "content": "..."}`.
|
||||||
|
6. `Frontend` live-renders Markdown in the right panel.
|
||||||
|
7. `Agent` finishes, saves artifact to backend session.
|
||||||
|
8. `User` reads right panel, types in left panel: "Change point 2".
|
||||||
|
9. `Agent` receives Left Panel history + Right Panel Artifact Content.
|
||||||
|
10. `Agent` updates document, streaming new `workspace_delta`. Frontend live-updates the right panel.
|
||||||
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Stage 1: Build
|
||||||
|
FROM node:20-slim AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Stage 2: Serve with Nginx
|
||||||
|
FROM nginx:alpine
|
||||||
|
# Copy build output to nginx folder
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
# Custom nginx config to handle SPA routing if needed
|
||||||
|
RUN echo 'server { \
|
||||||
|
listen 80; \
|
||||||
|
location / { \
|
||||||
|
root /usr/share/nginx/html; \
|
||||||
|
index index.html; \
|
||||||
|
try_files $uri $uri/ /index.html; \
|
||||||
|
} \
|
||||||
|
}' > /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
29
build_and_push.sh
Executable file
29
build_and_push.sh
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
REGISTRY="dcr-by1jwyxk44.71826370.xyz"
|
||||||
|
IMAGE_NAME="safe-os-ui"
|
||||||
|
TAG="latest"
|
||||||
|
FULL_IMAGE_NAME="$REGISTRY/$IMAGE_NAME:$TAG"
|
||||||
|
|
||||||
|
echo "Step 1: Building Docker image..."
|
||||||
|
docker build -t $IMAGE_NAME:$TAG .
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error: Docker build failed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Step 2: Tagging image for registry..."
|
||||||
|
docker tag $IMAGE_NAME:$TAG $FULL_IMAGE_NAME
|
||||||
|
|
||||||
|
echo "Step 3: Pushing image to $REGISTRY..."
|
||||||
|
# Note: You might need to run 'docker login $REGISTRY' beforehand
|
||||||
|
docker push $FULL_IMAGE_NAME
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "Success: Image has been pushed to $FULL_IMAGE_NAME"
|
||||||
|
else
|
||||||
|
echo "Error: Docker push failed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
45
deploy/docker-compose.yml
Normal file
45
deploy/docker-compose.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Frontend Service
|
||||||
|
safe-os-ui:
|
||||||
|
image: dcr-by1jwyxk44.71826370.xyz/safe-os-ui:latest
|
||||||
|
container_name: safe-os-ui
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- npm_network
|
||||||
|
|
||||||
|
# Backend: Planning Agent
|
||||||
|
planning-agent:
|
||||||
|
image: dcr-by1jwyxk44.71826370.xyz/planning-agent:latest
|
||||||
|
container_name: planning-agent
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- PORT=8090
|
||||||
|
networks:
|
||||||
|
- npm_network
|
||||||
|
|
||||||
|
# Backend: DevOps Agent
|
||||||
|
devops-agent:
|
||||||
|
image: dcr-by1jwyxk44.71826370.xyz/devops-agent:latest
|
||||||
|
container_name: devops-agent
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- PORT=8000
|
||||||
|
networks:
|
||||||
|
- npm_network
|
||||||
|
|
||||||
|
# Backend: Quality Gate
|
||||||
|
quality-gate:
|
||||||
|
image: dcr-by1jwyxk44.71826370.xyz/quality-gate:latest
|
||||||
|
container_name: quality-gate
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- PORT=5000
|
||||||
|
networks:
|
||||||
|
- npm_network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
npm_network:
|
||||||
|
external: true
|
||||||
|
name: proxy-net # 已更新为实际的 Docker 网络名称
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<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 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>
|
<title>SAFe OS — Multi-Agent 敏捷协同指挥中心</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
1754
package-lock.json
generated
1754
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,9 +9,13 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^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": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.4.0",
|
"@types/node": "^25.4.0",
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ import PlanningAgent from "./pages/PlanningAgent";
|
|||||||
import DevOpsAgent from "./pages/DevOpsAgent";
|
import DevOpsAgent from "./pages/DevOpsAgent";
|
||||||
import QualityGate from "./pages/QualityGate";
|
import QualityGate from "./pages/QualityGate";
|
||||||
|
|
||||||
|
import Home from "./pages/Home";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<Layout />}>
|
<Route element={<Layout />}>
|
||||||
|
<Route path="/" element={<Home />} />
|
||||||
|
<Route path="/home" element={<Home />} />
|
||||||
<Route path="/planning" element={<PlanningAgent />} />
|
<Route path="/planning" element={<PlanningAgent />} />
|
||||||
<Route path="/devops" element={<DevOpsAgent />} />
|
<Route path="/devops" element={<DevOpsAgent />} />
|
||||||
<Route path="/quality" element={<Navigate to="/quality/dashboard" replace />} />
|
<Route path="/quality" element={<Navigate to="/quality/dashboard" replace />} />
|
||||||
|
|||||||
256
src/Layout.tsx
256
src/Layout.tsx
@@ -1,158 +1,144 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import { NavLink, Outlet, useLocation } from "react-router-dom";
|
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> = {
|
const PAGE_TITLES: Record<string, string> = {
|
||||||
"/planning": "Strategic Planning Workspace",
|
"/": "总览控制台",
|
||||||
"/devops": "Delivery Execution Workspace",
|
"/planning": "战略规划 (Planning)",
|
||||||
"/quality/dashboard": "Quality Gate Overview",
|
"/devops": "开发运维 (DevOps)",
|
||||||
"/quality/pr-list": "Quality Gate · PR List",
|
"/quality": "质量门控 (Quality Gate Dashboard)",
|
||||||
"/quality/settings": "Quality Gate · Settings",
|
"/quality/dashboard": "质量门控 (Quality Gate Dashboard)",
|
||||||
|
"/quality/pr-list": "合并请求审查 (PR List)",
|
||||||
|
"/quality/settings": "质量设置",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const inQuality = pathname.startsWith("/quality");
|
|
||||||
const [qualityExpanded, setQualityExpanded] = useState<boolean>(inQuality);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (inQuality) {
|
|
||||||
setQualityExpanded(true);
|
|
||||||
}
|
|
||||||
}, [inQuality]);
|
|
||||||
|
|
||||||
const pageTitle = useMemo(() => {
|
const pageTitle = useMemo(() => {
|
||||||
if (PAGE_TITLES[pathname]) {
|
return PAGE_TITLES[pathname] || "SAFe OS";
|
||||||
return PAGE_TITLES[pathname];
|
}, [pathname]);
|
||||||
}
|
|
||||||
if (inQuality) {
|
|
||||||
return "Quality Gate";
|
|
||||||
}
|
|
||||||
return "SAFe OS";
|
|
||||||
}, [pathname, inQuality]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-screen bg-[#fafafa]">
|
<div className="bg-[#f7f7f9] flex h-screen w-screen overflow-hidden text-txt font-sans relative">
|
||||||
{/* ─── Sidebar ─── */}
|
{/* Demo Badge */}
|
||||||
<nav className="w-[280px] shrink-0 bg-surface-dark text-txt-inverse flex flex-col py-8 border-r border-white/10">
|
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none z-50 select-none opacity-[0.08] flex flex-col items-center">
|
||||||
<div className="px-8 mb-9 text-2xl font-extrabold tracking-tight select-none">
|
<div className="border-[8px] border-magenta px-10 py-4 transform -rotate-12 flex flex-col items-center">
|
||||||
SAFe <span className="text-magenta">OS</span>
|
<span className="text-8xl font-black text-magenta tracking-widest">DEMO ONLY</span>
|
||||||
|
<span className="text-3xl font-bold text-magenta mt-2">TECHNICAL SHARING 2026</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 侧边栏导航 */}
|
||||||
|
<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>
|
||||||
|
|
||||||
<div className="px-8 text-[11px] text-gray-500 uppercase font-bold tracking-widest mb-4">
|
<div className="p-4 flex-1">
|
||||||
Agent Pipeline
|
<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 }) =>
|
||||||
|
`flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-semibold transition-all duration-150 ${
|
||||||
|
isActive
|
||||||
|
? "bg-[#0EA5E9]/10 text-[#0EA5E9]"
|
||||||
|
: "text-txt hover:bg-surface-muted hover:text-[#0EA5E9]"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<i className="fa-solid fa-chess-knight w-4 text-center shrink-0"></i>
|
||||||
|
<span>战略规划</span>
|
||||||
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
to="/devops"
|
||||||
|
className={({ isActive }) =>
|
||||||
|
`flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-semibold transition-all duration-150 ${
|
||||||
|
isActive
|
||||||
|
? "bg-[#10B981]/10 text-[#10B981]"
|
||||||
|
: "text-txt hover:bg-surface-muted hover:text-[#10B981]"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<i className="fa-solid fa-code-branch w-4 text-center shrink-0"></i>
|
||||||
|
<span>开发运维</span>
|
||||||
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
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]"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<i className="fa-solid fa-shield-halved w-4 text-center shrink-0"></i>
|
||||||
|
<span>质量门控</span>
|
||||||
|
</NavLink>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
<NavLink
|
<div className="p-4 border-t border-border flex items-center gap-3">
|
||||||
to="/planning"
|
<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>
|
||||||
className={({ isActive }) =>
|
<div>
|
||||||
`mx-4 rounded-xl flex items-center gap-3 px-4 py-3 border border-transparent transition-all cursor-pointer ${
|
<p className="text-sm font-semibold text-txt">项目管理员</p>
|
||||||
isActive
|
<p className="text-xs text-txt-muted flex items-center gap-1">
|
||||||
? "border-magenta/40 bg-white/10"
|
<span className="w-1.5 h-1.5 bg-green-400 rounded-full inline-block"></span>在线
|
||||||
: "hover:bg-white/5"
|
</p>
|
||||||
}`
|
</div>
|
||||||
}
|
</div>
|
||||||
>
|
|
||||||
{({ isActive }) => (
|
{/* Sidebar Demo Badge */}
|
||||||
<>
|
<div className="mx-4 my-4 p-3 border-2 border-dashed border-magenta/20 rounded-xl flex flex-col items-center justify-center text-center bg-magenta/5 rotate-1">
|
||||||
<span
|
<p className="text-[10px] font-black text-magenta uppercase tracking-tighter opacity-70">Tech Sharing</p>
|
||||||
className={`w-2 h-2 rounded-full shrink-0 ${
|
<p className="text-[13px] font-black text-magenta -mt-1 uppercase tracking-[0.1em] italic">DEMO ONLY</p>
|
||||||
isActive ? "bg-magenta" : "bg-gray-600"
|
</div>
|
||||||
}`}
|
</aside>
|
||||||
/>
|
|
||||||
<div>
|
{/* 主内容区 */}
|
||||||
<div className="font-bold text-[0.95rem]">1. Planning Council Agent</div>
|
<main className="flex-1 flex flex-col h-screen overflow-y-auto relative min-w-0">
|
||||||
<div className="text-xs text-gray-400">Business and architecture planning</div>
|
{/* 顶部导航 */}
|
||||||
|
<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>
|
</div>
|
||||||
</>
|
<button className="relative p-2 text-txt-muted hover:text-magenta transition-colors">
|
||||||
)}
|
<i className="fa-regular fa-bell text-base"></i>
|
||||||
</NavLink>
|
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-red-500 rounded-full border border-white"></span>
|
||||||
|
</button>
|
||||||
<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 ${
|
|
||||||
isActive
|
|
||||||
? "border-magenta/40 bg-white/10"
|
|
||||||
: "hover:bg-white/5"
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{({ 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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</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"
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{sub.title}
|
|
||||||
</NavLink>
|
|
||||||
))}
|
|
||||||
</div>
|
</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>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="flex-1 overflow-hidden">
|
{/* 内容画布 */}
|
||||||
|
<div className="flex-1 w-full bg-[#f7f7f9] overflow-y-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
197
src/doc/WebUI_Stream_API_前端对接说明.md
Normal file
197
src/doc/WebUI_Stream_API_前端对接说明.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# WebUI `/api/chat/stream` 前端对接说明
|
||||||
|
|
||||||
|
本文档用于指导前端项目接入 LaodingBot 的流式聊天接口,并可直接作为提示词输入给 LLM,批量改造其他前端代码。
|
||||||
|
|
||||||
|
## 1. 接口总览
|
||||||
|
|
||||||
|
- 方法: `POST`
|
||||||
|
- 路径: `/api/chat/stream`
|
||||||
|
- 请求头: `Content-Type: application/json`
|
||||||
|
- 响应类型: `text/event-stream`
|
||||||
|
- 协议: SSE (Server-Sent Events)
|
||||||
|
|
||||||
|
说明: 该接口为单次请求、多次推送。后端会持续推送 `data: <json>\n\n` 格式的事件。
|
||||||
|
|
||||||
|
## 2. 请求体
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"text": "请帮我分析当前目录",
|
||||||
|
"session_id": "sess_abc",
|
||||||
|
"user_id": "user_001"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
字段说明:
|
||||||
|
|
||||||
|
- `text` (string, required): 用户输入文本,去除空白后不能为空。
|
||||||
|
- `session_id` (string, optional): 会话 ID,不传时后端自动生成。
|
||||||
|
- `user_id` (string, optional): 用户 ID,不传时后端自动生成。
|
||||||
|
|
||||||
|
兼容字段:
|
||||||
|
|
||||||
|
- `sessionId` 等价于 `session_id`
|
||||||
|
- `userId` 等价于 `user_id`
|
||||||
|
|
||||||
|
## 3. SSE 事件格式
|
||||||
|
|
||||||
|
每条 SSE 消息只包含 `data` 字段,内容是 JSON:
|
||||||
|
|
||||||
|
```text
|
||||||
|
data: {"type":"thought","content":"我先判断是否需要调用工具","step":1}
|
||||||
|
|
||||||
|
data: {"type":"tool_call","content":"{\"input\":\"pwd\"}","step":1,"tool_name":"shell"}
|
||||||
|
|
||||||
|
data: {"type":"tool_result","content":"C:/Project/MyProject","step":1,"tool_name":"shell"}
|
||||||
|
|
||||||
|
data: {"type":"final","content":"当前目录是 C:/Project/MyProject","step":2}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
事件字段:
|
||||||
|
|
||||||
|
- `type` (string): 事件类型
|
||||||
|
- `content` (string): 事件文本内容
|
||||||
|
- `step` (number, optional): ReAct 步骤编号
|
||||||
|
- `tool_name` (string, optional): 工具名
|
||||||
|
|
||||||
|
事件类型:
|
||||||
|
|
||||||
|
- `thought`: LLM 思考片段(可选透传)
|
||||||
|
- `tool_call`: 工具调用请求(可选透传)
|
||||||
|
- `tool_result`: 工具执行结果(可选透传)
|
||||||
|
- `final`: 最终回答
|
||||||
|
- `error`: 错误信息
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 默认情况下(`WEBUI_EXPOSE_REASONING=false`),WebUI 只向前端返回 `final` 和 `error`。
|
||||||
|
- 当设置 `WEBUI_EXPOSE_REASONING=true` 时,WebUI 会额外透传 `thought`、`tool_call`、`tool_result` 事件。
|
||||||
|
- 无论是否透传推理事件,`final` 都会分段累计推送,以便前端实现打字机效果。
|
||||||
|
|
||||||
|
## 4. 连接生命周期
|
||||||
|
|
||||||
|
- 正常结束: 收到 `type=final` 后结束渲染,连接可由浏览器自然关闭。
|
||||||
|
- 异常结束: 收到 `type=error`,前端应显示错误并结束当前轮次。
|
||||||
|
- 网络中断: 前端应允许用户重试,并保留已收到的事件记录。
|
||||||
|
|
||||||
|
## 5. 前端渲染建议
|
||||||
|
|
||||||
|
推荐前端仅处理两类结果:
|
||||||
|
|
||||||
|
- 答案区: 显示最后一个 `final`
|
||||||
|
- 错误区: 显示 `error`
|
||||||
|
|
||||||
|
如果开启了 `WEBUI_EXPOSE_REASONING=true`,建议额外提供“思考面板”:
|
||||||
|
|
||||||
|
- 思考区: 渲染 `thought`
|
||||||
|
- 工具区: 渲染 `tool_call` / `tool_result`
|
||||||
|
|
||||||
|
建议状态机:
|
||||||
|
|
||||||
|
- `idle`: 初始状态
|
||||||
|
- `streaming`: 请求中且持续接收事件
|
||||||
|
- `done`: 收到 `final`
|
||||||
|
- `failed`: 收到 `error` 或请求异常
|
||||||
|
|
||||||
|
## 6. TypeScript 对接示例 (fetch + ReadableStream)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type StreamEventType = 'thought' | 'tool_call' | 'tool_result' | 'final' | 'error';
|
||||||
|
|
||||||
|
interface StreamEvent {
|
||||||
|
type: StreamEventType;
|
||||||
|
content: string;
|
||||||
|
step?: number;
|
||||||
|
tool_name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function streamChat(
|
||||||
|
payload: { text: string; session_id?: string; user_id?: string },
|
||||||
|
onEvent: (event: StreamEvent) => void,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<void> {
|
||||||
|
const resp = await fetch('/api/chat/stream', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error(`HTTP ${resp.status}`);
|
||||||
|
}
|
||||||
|
if (!resp.body) {
|
||||||
|
throw new Error('ReadableStream is not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = resp.body.getReader();
|
||||||
|
const decoder = new TextDecoder('utf-8');
|
||||||
|
let buffer = '';
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true });
|
||||||
|
|
||||||
|
// SSE message delimiter: blank line
|
||||||
|
let idx = buffer.indexOf('\n\n');
|
||||||
|
while (idx >= 0) {
|
||||||
|
const chunk = buffer.slice(0, idx).trim();
|
||||||
|
buffer = buffer.slice(idx + 2);
|
||||||
|
|
||||||
|
for (const line of chunk.split('\n')) {
|
||||||
|
const text = line.trim();
|
||||||
|
if (!text.startsWith('data:')) continue;
|
||||||
|
const raw = text.slice(5).trim();
|
||||||
|
if (!raw) continue;
|
||||||
|
|
||||||
|
const event = JSON.parse(raw) as StreamEvent;
|
||||||
|
onEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
idx = buffer.indexOf('\n\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 给 LLM 的改造任务提示词模板
|
||||||
|
|
||||||
|
将下面模板发给 LLM,可用于自动改造其他前端项目:
|
||||||
|
|
||||||
|
```text
|
||||||
|
你要改造一个前端项目的聊天页面,把非流式接口 `/api/chat` 改为流式接口 `/api/chat/stream`。
|
||||||
|
|
||||||
|
后端协议约束:
|
||||||
|
1) 请求方法 POST,Content-Type=application/json
|
||||||
|
2) 请求体: { text, session_id?, user_id? }
|
||||||
|
3) 响应是 SSE 文本流,事件格式为 `data: <json>\n\n`
|
||||||
|
4) JSON 结构:
|
||||||
|
- type: thought | tool_call | tool_result | final | error
|
||||||
|
- content: string
|
||||||
|
- step?: number
|
||||||
|
- tool_name?: string
|
||||||
|
5) 收到 final 视为本轮完成;收到 error 视为失败
|
||||||
|
6) 默认不要假设前端一定会收到 thought/tool_call/tool_result;仅当 `WEBUI_EXPOSE_REASONING=true` 才会透传
|
||||||
|
|
||||||
|
你的改造要求:
|
||||||
|
1) 保留现有 UI 风格和组件结构,不做无关重构
|
||||||
|
2) 新增流式读取逻辑,支持中途取消 (AbortController)
|
||||||
|
3) 将事件按 step 渲染到消息区
|
||||||
|
4) 兼容旧会话字段命名 (session_id / sessionId, user_id / userId)
|
||||||
|
5) 增加错误态与重试按钮
|
||||||
|
6) 不破坏原有上传、历史消息和输入框行为
|
||||||
|
7) 输出改动文件列表 + 每个文件的关键变更说明
|
||||||
|
|
||||||
|
请直接给出可运行代码补丁。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 调试清单
|
||||||
|
|
||||||
|
- 检查响应头是否为 `text/event-stream`
|
||||||
|
- 检查每条事件是否以 `data:` 开头并以空行结尾
|
||||||
|
- 确认 `final` 和 `error` 都能正确结束当前轮次
|
||||||
|
- 验证弱网下不会丢失已收到的事件
|
||||||
|
- 验证用户快速连续提问时,旧流可被取消
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
background:
|
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%),
|
radial-gradient(circle at 90% 0%, #f5f5f5 0%, transparent 28%),
|
||||||
#fff;
|
#fff;
|
||||||
color: #1a1a1a;
|
color: #1a1a1a;
|
||||||
|
|||||||
@@ -1,6 +1,26 @@
|
|||||||
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
|
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
|
||||||
import { API } from "../config";
|
import { API } from "../config";
|
||||||
|
|
||||||
|
/* ─── Steps & Labels ─── */
|
||||||
|
const STEPS = [
|
||||||
|
{ title: "需求", icon: "💬" },
|
||||||
|
{ title: "产品分析", icon: "📋" },
|
||||||
|
{ title: "测试用例", icon: "🧪" },
|
||||||
|
{ title: "开发代码", icon: "💻" },
|
||||||
|
{ title: "测试执行", icon: "▶" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const STATUS_LABEL: Record<string, string> = {
|
||||||
|
clarifying: "澄清中",
|
||||||
|
pm_ready: "就绪",
|
||||||
|
pm_done: "产品分析完成",
|
||||||
|
qa_ready: "产品分析完成",
|
||||||
|
qa_done: "QA 完成",
|
||||||
|
dev_ready: "QA 完成",
|
||||||
|
dev_done: "代码就绪",
|
||||||
|
test_done: "测试完成",
|
||||||
|
};
|
||||||
|
|
||||||
/* ─── Types ─── */
|
/* ─── Types ─── */
|
||||||
type Step = 0 | 1 | 2 | 3 | 4;
|
type Step = 0 | 1 | 2 | 3 | 4;
|
||||||
type ClarifyMsg = { role: "user" | "assistant"; content: string };
|
type ClarifyMsg = { role: "user" | "assistant"; content: string };
|
||||||
@@ -200,13 +220,6 @@ function parseDevStream(text: string): DevStreamResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── 步骤标签 ── */
|
/* ── 步骤标签 ── */
|
||||||
const STEPS = [
|
|
||||||
{ title: "Requirements", icon: "💬" },
|
|
||||||
{ title: "PM Analysis", icon: "📋" },
|
|
||||||
{ title: "QA Cases", icon: "🧪" },
|
|
||||||
{ title: "Dev Code", icon: "💻" },
|
|
||||||
{ title: "Test Run", icon: "▶" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const STATUS_STEP: Record<string, number> = {
|
const STATUS_STEP: Record<string, number> = {
|
||||||
clarifying: 0, pm_ready: 0,
|
clarifying: 0, pm_ready: 0,
|
||||||
@@ -214,13 +227,7 @@ const STATUS_STEP: Record<string, number> = {
|
|||||||
qa_done: 2, dev_ready: 2,
|
qa_done: 2, dev_ready: 2,
|
||||||
dev_done: 3, test_done: 4,
|
dev_done: 3, test_done: 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_LABEL: Record<string, string> = {
|
|
||||||
clarifying: "Clarifying", pm_ready: "Ready",
|
|
||||||
pm_done: "PM Done", qa_ready: "PM Done",
|
|
||||||
qa_done: "QA Done", dev_ready: "QA Done",
|
|
||||||
dev_done: "Code Ready", test_done: "Tests Done",
|
|
||||||
};
|
|
||||||
|
|
||||||
const TYPE_EMOJI: Record<string, string> = {
|
const TYPE_EMOJI: Record<string, string> = {
|
||||||
"Functional": "🧩", "Performance": "⚡", "Security": "🔒",
|
"Functional": "🧩", "Performance": "⚡", "Security": "🔒",
|
||||||
@@ -228,9 +235,12 @@ const TYPE_EMOJI: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TYPE_LABEL: Record<string, string> = {
|
const TYPE_LABEL: Record<string, string> = {
|
||||||
"功能测试": "Functional", "性能测试": "Performance",
|
"Functional": "功能测试", "Performance": "性能测试",
|
||||||
"安全测试": "Security", "边界测试": "Boundary",
|
"Security": "安全测试", "Boundary": "边界测试",
|
||||||
"异常测试": "Exception", "集成测试": "Integration",
|
"Exception": "异常测试", "Integration": "集成测试",
|
||||||
|
"功能测试": "功能测试", "性能测试": "性能测试",
|
||||||
|
"安全测试": "安全测试", "边界测试": "边界测试",
|
||||||
|
"异常测试": "异常测试", "集成测试": "集成测试",
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ━━━━━━━━━━━━━━━━━━━━━ Component ━━━━━━━━━━━━━━━━━━━━━ */
|
/* ━━━━━━━━━━━━━━━━━━━━━ Component ━━━━━━━━━━━━━━━━━━━━━ */
|
||||||
@@ -489,7 +499,7 @@ export default function DevOpsAgent() {
|
|||||||
<div className="flex flex-col h-full overflow-hidden bg-[#f7f7f9]">
|
<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="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) => (
|
{STEPS.map((s, i) => (
|
||||||
<div key={i} className="flex items-center">
|
<div key={i} className="flex items-center">
|
||||||
<button
|
<button
|
||||||
@@ -532,15 +542,15 @@ export default function DevOpsAgent() {
|
|||||||
|
|
||||||
{/* Error */}
|
{/* Error */}
|
||||||
{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>
|
<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>
|
<button className="ml-4 font-bold text-red-300 hover:text-red-500" onClick={() => setError("")}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ── 内容区 ── */}
|
{/* ── 内容区 ── */}
|
||||||
<div className="flex-1 px-8 py-6 overflow-y-auto">
|
<div className="flex-1 px-[60px] py-6 overflow-y-auto">
|
||||||
<div className="max-w-5xl mx-auto space-y-0">
|
<div className="space-y-0">
|
||||||
|
|
||||||
{/* ═══ Step 0: Requirement Input ═══ */}
|
{/* ═══ Step 0: Requirement Input ═══ */}
|
||||||
{step === 0 && !sessionId && (
|
{step === 0 && !sessionId && (
|
||||||
@@ -548,22 +558,22 @@ export default function DevOpsAgent() {
|
|||||||
<div className="flex items-start gap-4 mb-6">
|
<div className="flex items-start gap-4 mb-6">
|
||||||
<div className="flex items-center justify-center w-10 h-10 text-xl rounded-2xl bg-magenta/10 shrink-0">📋</div>
|
<div className="flex items-center justify-center w-10 h-10 text-xl rounded-2xl bg-magenta/10 shrink-0">📋</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-base font-bold text-txt mb-0.5">Enter Requirements</h2>
|
<h2 className="text-base font-bold text-txt mb-0.5">输入需求</h2>
|
||||||
<p className="text-sm text-txt-muted">Describe your product requirements. AI will complete analysis → test cases → code generation automatically.</p>
|
<p className="text-sm text-txt-muted">描述你的产品需求,AI 会自动完成分析 → 用例 → 代码生成。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
className="input-field min-h-[160px] resize-y mb-5"
|
className="input-field min-h-[160px] resize-y mb-5"
|
||||||
value={requirement}
|
value={requirement}
|
||||||
onChange={(e) => setRequirement(e.target.value)}
|
onChange={(e) => setRequirement(e.target.value)}
|
||||||
placeholder="e.g. Implement a user login feature supporting username/password, log all login events, target 1000 QPS concurrency…"
|
placeholder="例如:实现用户登录,支持用户名/密码登录并记录登录事件,目标并发 1000 QPS…"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs text-txt-muted">{requirement.length > 0 ? `${requirement.length} chars` : ""}</span>
|
<span className="text-xs text-txt-muted">{requirement.length > 0 ? `${requirement.length} chars` : ""}</span>
|
||||||
<button className="btn-magenta" onClick={handleStart} disabled={loading || !requirement.trim()}>
|
<button className="btn-magenta" onClick={handleStart} disabled={loading || !requirement.trim()}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<span className="flex items-center gap-2"><span className="w-3.5 h-3.5 border-2 border-white/40 border-t-white rounded-full animate-spin inline-block"></span>Analyzing…</span>
|
<span className="flex items-center gap-2"><span className="w-3.5 h-3.5 border-2 border-white/40 border-t-white rounded-full animate-spin inline-block"></span>分析中…</span>
|
||||||
) : "Start AI Analysis →"}
|
) : "开始 AI 分析 →"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -578,7 +588,7 @@ export default function DevOpsAgent() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 mb-5 text-sm bg-[#f9f9f9] border border-border rounded-xl">
|
<div className="p-4 mb-5 text-sm bg-[#f9f9f9] border border-border rounded-xl">
|
||||||
<span className="block mb-1.5 text-[11px] font-bold text-txt-muted uppercase tracking-wide">Original Requirement</span>
|
<span className="block mb-1.5 text-[11px] font-bold text-txt-muted uppercase tracking-wide">原始需求</span>
|
||||||
<p className="leading-relaxed text-txt">{rawRequirement}</p>
|
<p className="leading-relaxed text-txt">{rawRequirement}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -604,17 +614,17 @@ export default function DevOpsAgent() {
|
|||||||
|
|
||||||
{status === "clarifying" && (
|
{status === "clarifying" && (
|
||||||
<div className="pt-4 mt-2 border-t border-border">
|
<div className="pt-4 mt-2 border-t border-border">
|
||||||
<p className="mb-2 text-xs font-semibold tracking-wide uppercase text-txt-muted">Reply to AI</p>
|
<p className="mb-2 text-xs font-semibold tracking-wide uppercase text-txt-muted">回复 AI</p>
|
||||||
<textarea
|
<textarea
|
||||||
className="input-field min-h-[80px] resize-y mb-3"
|
className="input-field min-h-[80px] resize-y mb-3"
|
||||||
value={clarifyInput}
|
value={clarifyInput}
|
||||||
onChange={(e) => setClarifyInput(e.target.value)}
|
onChange={(e) => setClarifyInput(e.target.value)}
|
||||||
placeholder="Enter your additional details…"
|
placeholder="输入补充说明…"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button className="btn-magenta" onClick={handleClarify} disabled={loading}>
|
<button className="btn-magenta" onClick={handleClarify} disabled={loading}>
|
||||||
{loading ? <span className="flex items-center gap-2"><span className="w-3.5 h-3.5 border-2 border-white/40 border-t-white rounded-full animate-spin inline-block"></span>Sending…</span> : "Send"}
|
{loading ? <span className="flex items-center gap-2"><span className="w-3.5 h-3.5 border-2 border-white/40 border-t-white rounded-full animate-spin inline-block"></span>发送中…</span> : "发送"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -623,11 +633,11 @@ export default function DevOpsAgent() {
|
|||||||
{status !== "clarifying" && status !== "" && (
|
{status !== "clarifying" && status !== "" && (
|
||||||
<div className="pt-4 mt-2 border-t border-border">
|
<div className="pt-4 mt-2 border-t border-border">
|
||||||
<div className="flex items-center gap-2 px-4 py-3 mb-4 text-sm text-green-700 border border-green-200 bg-green-50 rounded-xl">
|
<div className="flex items-center gap-2 px-4 py-3 mb-4 text-sm text-green-700 border border-green-200 bg-green-50 rounded-xl">
|
||||||
<span>✅</span><span>Requirements confirmed. Ready to start PM analysis.</span>
|
<span>✅</span><span>需求已确认,可开始产品分析。</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<button className="btn-magenta" onClick={handlePmRun} disabled={loading || streaming}>
|
<button className="btn-magenta" onClick={handlePmRun} disabled={loading || streaming}>
|
||||||
{loading ? "Analyzing…" : "Start PM Analysis →"}
|
{loading ? "分析中…" : "开始产品分析 →"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -641,12 +651,12 @@ export default function DevOpsAgent() {
|
|||||||
<div className="flex items-center justify-between mb-5">
|
<div className="flex items-center justify-between mb-5">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center justify-center w-8 h-8 text-base rounded-xl bg-orange-50">📋</div>
|
<div className="flex items-center justify-center w-8 h-8 text-base rounded-xl bg-orange-50">📋</div>
|
||||||
<h2 className="text-base font-bold">PM Analysis</h2>
|
<h2 className="text-base font-bold">产品分析</h2>
|
||||||
</div>
|
</div>
|
||||||
{streaming && (
|
{streaming && (
|
||||||
<span className="flex items-center gap-1.5 text-xs font-semibold text-magenta bg-magenta/5 px-3 py-1 rounded-full">
|
<span className="flex items-center gap-1.5 text-xs font-semibold text-magenta bg-magenta/5 px-3 py-1 rounded-full">
|
||||||
<span className="w-1.5 h-1.5 bg-magenta rounded-full animate-pulse inline-block"></span>
|
<span className="w-1.5 h-1.5 bg-magenta rounded-full animate-pulse inline-block"></span>
|
||||||
Analyzing… {streamText.length} chars
|
分析中… {streamText.length} 字符
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -655,10 +665,10 @@ export default function DevOpsAgent() {
|
|||||||
{streaming && pmStreamParsed && (
|
{streaming && pmStreamParsed && (
|
||||||
<div ref={pmStreamRef} className="rounded-xl border border-border bg-[#fafafa] p-4 mb-4 space-y-3 max-h-[55vh] overflow-y-auto">
|
<div ref={pmStreamRef} className="rounded-xl border border-border bg-[#fafafa] p-4 mb-4 space-y-3 max-h-[55vh] overflow-y-auto">
|
||||||
{([
|
{([
|
||||||
["functional_requirements", "🔧", "Functional Requirements", "bg-blue-50 text-blue-700"],
|
["functional_requirements", "🔧", "功能需求", "bg-blue-50 text-blue-700"],
|
||||||
["non_functional_requirements", "⚙️", "Non-Functional Requirements", "bg-purple-50 text-purple-700"],
|
["non_functional_requirements", "⚙️", "非功能需求", "bg-purple-50 text-purple-700"],
|
||||||
["acceptance_criteria", "✅", "Acceptance Criteria", "bg-green-50 text-green-700"],
|
["acceptance_criteria", "✅", "验收准则", "bg-green-50 text-green-700"],
|
||||||
["edge_cases", "🚧", "Edge Cases", "bg-amber-50 text-amber-700"],
|
["edge_cases", "🚧", "边界情况", "bg-amber-50 text-amber-700"],
|
||||||
] as const).map(([key, icon, label, color]) => {
|
] as const).map(([key, icon, label, color]) => {
|
||||||
const sec = pmStreamParsed[key];
|
const sec = pmStreamParsed[key];
|
||||||
if (!sec.items.length && pmStreamParsed.currentSection !== key) return null;
|
if (!sec.items.length && pmStreamParsed.currentSection !== key) return null;
|
||||||
@@ -677,8 +687,8 @@ export default function DevOpsAgent() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{(pmStreamParsed.summary.value || pmStreamParsed.currentSection === "summary") && (
|
{(pmStreamParsed.summary.value || pmStreamParsed.currentSection === "summary") && (
|
||||||
<div className={`transition-opacity ${ pmStreamParsed.currentSection === "summary" ? "opacity-100" : "opacity-50" }`}>
|
<div className={`transition-opacity ${ pmStreamParsed.currentSection === "summary" ? "opacity-100" : "opacity-50" }`}>
|
||||||
<div className="inline-flex items-center gap-1 text-[11px] font-bold px-2 py-0.5 rounded-md mb-2 bg-gray-100 text-gray-600">📌 Summary</div>
|
<div className="inline-flex items-center gap-1 text-[11px] font-bold px-2 py-0.5 rounded-md mb-2 bg-gray-100 text-gray-600">📌 摘要</div>
|
||||||
<p className="text-sm leading-relaxed">{pmStreamParsed.summary.value}{pmStreamParsed.summary.active && <span className="text-magenta animate-pulse">|</span>}</p>
|
<p className="text-sm leading-relaxed">{pmStreamParsed.summary.value}{pmStreamParsed.summary.active && <span className="text-magenta animate-pulse">|</span>}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
173
src/pages/Home.tsx
Normal file
173
src/pages/Home.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
|
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";
|
import { API } from "../config";
|
||||||
|
|
||||||
/* ─── Types ─── */
|
/* ─── Types ─── */
|
||||||
@@ -11,26 +14,58 @@ type ChatMessage = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type StreamEvent = {
|
type StreamEvent = {
|
||||||
type: "thought" | "tool_call" | "tool_result" | "final" | "error";
|
type: "thought" | "tool_call" | "tool_result" | "final" | "error" | "workspace_start" | "workspace_delta" | "workspace_end";
|
||||||
content: string;
|
content: string;
|
||||||
step?: number;
|
step?: number;
|
||||||
tool_name?: string;
|
tool_name?: string;
|
||||||
|
workspace_title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WorkspaceState = {
|
||||||
|
isOpen: boolean;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
isGenerating: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WORKSPACE_INIT: WorkspaceState = {
|
||||||
|
isOpen: false,
|
||||||
|
title: "",
|
||||||
|
content: "",
|
||||||
|
isGenerating: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
type HistorySession = {
|
||||||
|
id: string;
|
||||||
|
sessionId: string;
|
||||||
|
summary: string;
|
||||||
|
ts: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
|
reasoning: Record<string, string>; // Map message ID to its reasoning/traces
|
||||||
chatting: boolean;
|
chatting: boolean;
|
||||||
|
historySessions: HistorySession[];
|
||||||
|
workspace: WorkspaceState;
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Action =
|
type Action =
|
||||||
| { type: "add-msg"; msg: ChatMessage }
|
| { type: "add-msg"; msg: ChatMessage }
|
||||||
| { type: "update-msg"; id: string; patch: Partial<ChatMessage> }
|
| { type: "update-msg"; id: string; patch: Partial<ChatMessage> }
|
||||||
|
| { type: "update-reasoning"; id: string; content: string }
|
||||||
| { type: "set-chatting"; v: boolean }
|
| { type: "set-chatting"; v: boolean }
|
||||||
| { type: "set-error"; v?: string }
|
| { type: "set-error"; v?: string }
|
||||||
| { type: "reset"; sessionId: string; userId: string };
|
| { type: "set-history"; sessions: HistorySession[] }
|
||||||
|
| { type: "load-session"; sessionId: string; messages: ChatMessage[] }
|
||||||
|
| { type: "reset"; sessionId: string; userId: string }
|
||||||
|
| { type: "ws-start"; title: string }
|
||||||
|
| { type: "ws-delta"; content: string }
|
||||||
|
| { type: "ws-end" }
|
||||||
|
| { type: "ws-toggle"; open: boolean };
|
||||||
|
|
||||||
/* ─── Helpers ─── */
|
/* ─── Helpers ─── */
|
||||||
function uid() {
|
function uid() {
|
||||||
@@ -42,6 +77,19 @@ function makeId(prefix: string) {
|
|||||||
return `${prefix}_${uid()}`;
|
return `${prefix}_${uid()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getStoredHistory(): HistorySession[] {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem("safeos.planning.history");
|
||||||
|
return raw ? JSON.parse(raw) : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveStoredHistory(sessions: HistorySession[]) {
|
||||||
|
localStorage.setItem("safeos.planning.history", JSON.stringify(sessions));
|
||||||
|
}
|
||||||
|
|
||||||
function getSession() {
|
function getSession() {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem("safeos.planning.session");
|
const raw = localStorage.getItem("safeos.planning.session");
|
||||||
@@ -68,10 +116,27 @@ function reducer(state: State, action: Action): State {
|
|||||||
m.id === action.id ? { ...m, ...action.patch } : m,
|
m.id === action.id ? { ...m, ...action.patch } : m,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
case "update-reasoning":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
reasoning: { ...state.reasoning, [action.id]: action.content },
|
||||||
|
};
|
||||||
case "set-chatting":
|
case "set-chatting":
|
||||||
return { ...state, chatting: action.v };
|
return { ...state, chatting: action.v };
|
||||||
case "set-error":
|
case "set-error":
|
||||||
return { ...state, error: action.v };
|
return { ...state, error: action.v };
|
||||||
|
case "set-history":
|
||||||
|
saveStoredHistory(action.sessions);
|
||||||
|
return { ...state, historySessions: action.sessions };
|
||||||
|
case "load-session":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
sessionId: action.sessionId,
|
||||||
|
messages: action.messages,
|
||||||
|
reasoning: {},
|
||||||
|
chatting: false,
|
||||||
|
error: undefined,
|
||||||
|
};
|
||||||
case "reset": {
|
case "reset": {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"safeos.planning.session",
|
"safeos.planning.session",
|
||||||
@@ -81,10 +146,33 @@ function reducer(state: State, action: Action): State {
|
|||||||
sessionId: action.sessionId,
|
sessionId: action.sessionId,
|
||||||
userId: action.userId,
|
userId: action.userId,
|
||||||
messages: [],
|
messages: [],
|
||||||
|
reasoning: {},
|
||||||
chatting: false,
|
chatting: false,
|
||||||
|
historySessions: state.historySessions,
|
||||||
|
workspace: WORKSPACE_INIT,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "ws-start":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
workspace: { isOpen: true, title: action.title || "New Artifact", content: "", isGenerating: true },
|
||||||
|
};
|
||||||
|
case "ws-delta":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
workspace: { ...state.workspace, content: state.workspace.content + action.content },
|
||||||
|
};
|
||||||
|
case "ws-end":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
workspace: { ...state.workspace, isGenerating: false },
|
||||||
|
};
|
||||||
|
case "ws-toggle":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
workspace: { ...state.workspace, isOpen: action.open },
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -98,7 +186,7 @@ async function streamChat(
|
|||||||
) {
|
) {
|
||||||
const res = await fetch(`${API.planning}/chat/stream`, {
|
const res = await fetch(`${API.planning}/chat/stream`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json", "Accept": "text/event-stream" },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
@@ -114,11 +202,14 @@ async function streamChat(
|
|||||||
while (idx >= 0) {
|
while (idx >= 0) {
|
||||||
const pkt = buf.slice(0, idx);
|
const pkt = buf.slice(0, idx);
|
||||||
buf = buf.slice(idx + 2);
|
buf = buf.slice(idx + 2);
|
||||||
for (const line of pkt.split("\n")) {
|
// Split by literal newline, keeping in mind \r\n vs \n
|
||||||
|
const lines = pkt.split(/\r?\n/);
|
||||||
|
for (const line of lines) {
|
||||||
const t = line.trim();
|
const t = line.trim();
|
||||||
if (!t.startsWith("data:")) continue;
|
if (!t.startsWith("data:")) continue;
|
||||||
try {
|
try {
|
||||||
onEvent(JSON.parse(t.slice(5).trim()));
|
const data = JSON.parse(t.slice(5).trim());
|
||||||
|
onEvent(data);
|
||||||
} catch {
|
} catch {
|
||||||
/* skip */
|
/* skip */
|
||||||
}
|
}
|
||||||
@@ -159,12 +250,17 @@ export default function PlanningAgent() {
|
|||||||
sessionId: sess.current.sessionId,
|
sessionId: sess.current.sessionId,
|
||||||
userId: sess.current.userId,
|
userId: sess.current.userId,
|
||||||
messages: [],
|
messages: [],
|
||||||
|
reasoning: {},
|
||||||
chatting: false,
|
chatting: false,
|
||||||
|
historySessions: getStoredHistory(),
|
||||||
|
workspace: WORKSPACE_INIT,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const [files, setFiles] = useState<{ id: string; name: string }[]>([]);
|
const [files, setFiles] = useState<{ id: string; name: string }[]>([]);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
|
||||||
|
const [loadingHistory, setLoadingHistory] = useState(false);
|
||||||
const abortRef = useRef<AbortController | null>(null);
|
const abortRef = useRef<AbortController | null>(null);
|
||||||
const bottomRef = useRef<HTMLDivElement>(null);
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -176,6 +272,32 @@ export default function PlanningAgent() {
|
|||||||
return () => abortRef.current?.abort();
|
return () => abortRef.current?.abort();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/* Load session */
|
||||||
|
const loadHistorySession = async (sessionId: string) => {
|
||||||
|
if (loadingHistory) return;
|
||||||
|
setLoadingHistory(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API.planning}/history?session_id=${sessionId}&limit=100`);
|
||||||
|
if (!res.ok) throw new Error(`Load history failed: ${res.status}`);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
const messages: ChatMessage[] = data.map((item: any) => ({
|
||||||
|
id: item.ID.toString(),
|
||||||
|
role: item.Role.toLowerCase(),
|
||||||
|
content: item.Content,
|
||||||
|
ts: new Date(item.CreatedAt).getTime(),
|
||||||
|
status: "sent"
|
||||||
|
}));
|
||||||
|
|
||||||
|
dispatch({ type: "load-session", sessionId, messages });
|
||||||
|
setIsHistoryOpen(false);
|
||||||
|
} catch (err) {
|
||||||
|
dispatch({ type: "set-error", v: `加载历史失败: ${(err as Error).message}` });
|
||||||
|
} finally {
|
||||||
|
setLoadingHistory(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/* Send message */
|
/* Send message */
|
||||||
const send = useCallback(async () => {
|
const send = useCallback(async () => {
|
||||||
const text = input.trim();
|
const text = input.trim();
|
||||||
@@ -205,8 +327,19 @@ export default function PlanningAgent() {
|
|||||||
file_ids: files.map((f) => f.id),
|
file_ids: files.map((f) => f.id),
|
||||||
},
|
},
|
||||||
(evt) => {
|
(evt) => {
|
||||||
if (evt.type === "final") {
|
if (evt.type === "workspace_start") {
|
||||||
finalText = evt.content;
|
dispatch({ type: "ws-start", title: evt.workspace_title || "New Artifact" });
|
||||||
|
} else if (evt.type === "workspace_delta") {
|
||||||
|
dispatch({ type: "ws-delta", content: evt.content });
|
||||||
|
} else if (evt.type === "workspace_end") {
|
||||||
|
dispatch({ type: "ws-end" });
|
||||||
|
} else if (evt.type === "final") {
|
||||||
|
finalText += evt.content;
|
||||||
|
dispatch({
|
||||||
|
type: "update-msg",
|
||||||
|
id: assistMsg.id,
|
||||||
|
patch: { content: finalText },
|
||||||
|
});
|
||||||
} else if (evt.type === "error") {
|
} else if (evt.type === "error") {
|
||||||
traces.push(`[error] ${evt.content}`);
|
traces.push(`[error] ${evt.content}`);
|
||||||
} else {
|
} else {
|
||||||
@@ -215,15 +348,13 @@ export default function PlanningAgent() {
|
|||||||
traces.push(`${prefix}${evt.type}${tool}: ${evt.content}`);
|
traces.push(`${prefix}${evt.type}${tool}: ${evt.content}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts: string[] = [];
|
if (traces.length) {
|
||||||
if (traces.length) parts.push(traces.join("\n"));
|
dispatch({
|
||||||
if (finalText) parts.push(finalText);
|
type: "update-reasoning",
|
||||||
|
id: assistMsg.id,
|
||||||
dispatch({
|
content: traces.join("\n"),
|
||||||
type: "update-msg",
|
});
|
||||||
id: assistMsg.id,
|
}
|
||||||
patch: { content: parts.join("\n\n") || "思考中..." },
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
ctrl.signal,
|
ctrl.signal,
|
||||||
);
|
);
|
||||||
@@ -261,23 +392,121 @@ export default function PlanningAgent() {
|
|||||||
|
|
||||||
/* Reset */
|
/* Reset */
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
|
if (state.messages.length > 0) {
|
||||||
|
const lastMsg = state.messages[state.messages.length - 1];
|
||||||
|
const summary = lastMsg.role === "assistant"
|
||||||
|
? lastMsg.content.slice(0, 50) + (lastMsg.content.length > 50 ? "..." : "")
|
||||||
|
: (state.messages[state.messages.length - 2]?.content.slice(0, 50) || "Empty session") + "...";
|
||||||
|
|
||||||
|
const newHistorySession: HistorySession = {
|
||||||
|
id: uid(),
|
||||||
|
sessionId: state.sessionId,
|
||||||
|
summary: summary || "新会话",
|
||||||
|
ts: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedHistory = [newHistorySession, ...state.historySessions.filter(s => s.sessionId !== state.sessionId)];
|
||||||
|
dispatch({ type: "set-history", sessions: updatedHistory });
|
||||||
|
}
|
||||||
|
|
||||||
abortRef.current?.abort();
|
abortRef.current?.abort();
|
||||||
const fresh = { sessionId: makeId("sess"), userId: makeId("user") };
|
const fresh = { sessionId: makeId("sess"), userId: makeId("user") };
|
||||||
dispatch({ type: "reset", ...fresh });
|
dispatch({ type: "reset", ...fresh });
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const wsHasContent = !state.workspace.isOpen && state.workspace.content.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full">
|
<div className="h-full relative overflow-hidden flex">
|
||||||
|
{/* ─── Sidebar Overlay ─── */}
|
||||||
|
{isHistoryOpen && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black/20 z-40 transition-opacity duration-300"
|
||||||
|
onClick={() => setIsHistoryOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`absolute top-0 left-0 h-full w-1/4 bg-white border-r border-border z-50 transform transition-transform duration-300 ease-in-out shadow-xl ${
|
||||||
|
isHistoryOpen ? "translate-x-0" : "-translate-x-full"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="p-6 h-full flex flex-col">
|
||||||
|
<div className="flex items-center justify-between mb-8">
|
||||||
|
<h3 className="text-lg font-bold">历史对话</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsHistoryOpen(false)}
|
||||||
|
className="p-2 hover:bg-surface-muted rounded-full transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{state.historySessions.length === 0 ? (
|
||||||
|
<div className="text-center py-10 text-txt-muted text-sm italic">
|
||||||
|
暂无历史记录
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-3 py-2">
|
||||||
|
{state.historySessions.map((session) => (
|
||||||
|
<div
|
||||||
|
key={session.id}
|
||||||
|
onClick={() => loadHistorySession(session.sessionId)}
|
||||||
|
className={`p-4 rounded-xl border border-border cursor-pointer transition-all hover:bg-surface-muted hover:border-magenta group ${
|
||||||
|
state.sessionId === session.sessionId ? 'bg-magenta/5 border-magenta ring-1 ring-magenta' : 'bg-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<span className="text-[0.7rem] font-mono text-txt-muted truncate max-w-[120px]">
|
||||||
|
ID: {session.sessionId.slice(0, 8)}...
|
||||||
|
</span>
|
||||||
|
<span className="text-[0.65rem] text-txt-muted">
|
||||||
|
{new Date(session.ts).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-txt leading-snug line-clamp-2 group-hover:text-magenta transition-colors">
|
||||||
|
{session.summary}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{loadingHistory && (
|
||||||
|
<div className="absolute inset-0 bg-white/60 flex items-center justify-center z-[60]">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-magenta"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ─── Main Chat ─── */}
|
{/* ─── Main Chat ─── */}
|
||||||
<div className="h-full flex flex-col min-w-0">
|
<div className={`h-full flex flex-col min-w-0 transition-all duration-300 ease-in-out ${
|
||||||
<div className="flex-1 overflow-y-auto px-10 py-8">
|
state.workspace.isOpen ? 'w-[35%] border-r border-border' : 'w-full'
|
||||||
<div className="flex flex-col gap-6 max-w-5xl w-full mx-auto">
|
}`}>
|
||||||
|
<div className={`flex-1 overflow-y-auto py-8 ${
|
||||||
|
state.workspace.isOpen ? 'px-4' : 'px-[72px]'
|
||||||
|
}`}>
|
||||||
|
{/* History Toggle Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsHistoryOpen(true)}
|
||||||
|
className="absolute top-6 left-6 p-2 bg-white border border-border rounded-lg shadow-sm hover:bg-surface-muted transition-colors z-30 group"
|
||||||
|
title="查看历史记录"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 text-txt-muted group-hover:text-magenta" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-6 w-full">
|
||||||
{state.messages.length === 0 && (
|
{state.messages.length === 0 && (
|
||||||
<div className="text-center py-20 card">
|
<div className="text-center py-20 card">
|
||||||
<h2 className="text-2xl font-extrabold mb-2">Planning Council Agent</h2>
|
<h2 className={`font-extrabold mb-2 ${state.workspace.isOpen ? 'text-lg' : 'text-2xl'}`}>规划委员会代理</h2>
|
||||||
<p className="text-txt-muted text-sm">
|
<p className="text-txt-muted text-sm">
|
||||||
Share a high-level Epic and the agent will plan through PM, Architect, and RTE perspectives.
|
提供一个高层 Epic,代理将从产品、架构与 RTE 视角进行规划。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -285,18 +514,43 @@ export default function PlanningAgent() {
|
|||||||
{state.messages.map((msg) => (
|
{state.messages.map((msg) => (
|
||||||
<div
|
<div
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
className={`max-w-[78%] px-6 py-5 text-[0.95rem] leading-relaxed whitespace-pre-wrap rounded-2xl shadow-[0_2px_14px_rgba(0,0,0,0.04)] ${
|
className={`${state.workspace.isOpen ? 'max-w-full' : 'max-w-[78%]'} px-6 py-5 text-[0.95rem] leading-relaxed rounded-2xl shadow-[0_2px_14px_rgba(0,0,0,0.04)] ${
|
||||||
msg.role === "user"
|
msg.role === "user"
|
||||||
? "self-end mr-1 bg-surface-muted text-txt border border-border"
|
? "self-end mr-1 bg-surface-muted text-txt border border-border whitespace-pre-wrap"
|
||||||
: "self-start bg-white border border-border border-l-4 border-l-magenta"
|
: "self-start bg-white border border-border border-l-4 border-l-magenta"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{msg.role === "assistant" && (
|
{msg.role === "assistant" && (
|
||||||
<span className="badge mb-3 block w-fit">SYSTEM / RE-ACT LOOP</span>
|
<span className="badge mb-3 block w-fit">系统 / 迭代闭环</span>
|
||||||
|
)}
|
||||||
|
{msg.role === "assistant" && state.reasoning[msg.id] && (
|
||||||
|
<details
|
||||||
|
className="mb-4 bg-surface-muted/50 rounded-lg border border-border overflow-hidden"
|
||||||
|
open={!msg.content}
|
||||||
|
>
|
||||||
|
<summary className="px-4 py-2 text-xs font-medium text-txt-muted cursor-pointer hover:bg-border/20 transition-colors list-none flex items-center gap-2">
|
||||||
|
<span className={`w-1.5 h-1.5 rounded-full bg-magenta ${!msg.content ? 'animate-pulse' : ''}`}></span>
|
||||||
|
查看思考过程
|
||||||
|
</summary>
|
||||||
|
<div className="px-4 py-3 bg-white/50 text-[0.8rem] font-mono whitespace-pre-wrap border-t border-border text-txt-muted">
|
||||||
|
{state.reasoning[msg.id]}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
{msg.role === "assistant" ? (
|
||||||
|
<div className="prose prose-sm max-w-none prose-p:my-0.5 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 || (state.chatting ? "思考中..." : "")}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
msg.content || "Thinking..."
|
||||||
)}
|
)}
|
||||||
{msg.content || "Thinking..."}
|
|
||||||
{msg.status === "failed" && (
|
{msg.status === "failed" && (
|
||||||
<span className="text-red-500 text-xs block mt-2">Failed to send</span>
|
<span className="text-red-500 text-xs block mt-2">发送失败</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -304,9 +558,24 @@ export default function PlanningAgent() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Workspace recall button */}
|
||||||
|
{wsHasContent && (
|
||||||
|
<div className={`${state.workspace.isOpen ? 'mx-4' : 'mx-[72px]'} mb-2`}>
|
||||||
|
<button
|
||||||
|
onClick={() => dispatch({ type: "ws-toggle", open: true })}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-magenta/5 border border-magenta/30 rounded-xl text-sm text-magenta hover:bg-magenta/10 transition-colors w-full justify-center"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
查看文档输出 — {state.workspace.title}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Error */}
|
{/* Error */}
|
||||||
{state.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={`${state.workspace.isOpen ? 'mx-4' : 'mx-[72px]'} mb-2 px-4 py-2 bg-red-50 text-red-700 text-sm flex justify-between items-center`}>
|
||||||
{state.error}
|
{state.error}
|
||||||
<button
|
<button
|
||||||
className="text-red-400 hover:text-red-600 font-bold"
|
className="text-red-400 hover:text-red-600 font-bold"
|
||||||
@@ -318,21 +587,43 @@ export default function PlanningAgent() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Input area */}
|
{/* Input area */}
|
||||||
<div className="px-10 pb-6 pt-4 border-t border-border">
|
<div className={`pb-6 pt-4 border-t border-border ${state.workspace.isOpen ? 'px-4' : 'px-10'}`}>
|
||||||
{/* File upload row */}
|
{/* File upload row */}
|
||||||
<div className="flex items-center gap-3 mb-3 text-sm">
|
<div className="flex items-center gap-3 mb-3 text-sm">
|
||||||
<label className="btn-outline text-xs px-3 py-1.5 cursor-pointer">
|
<label className="btn-outline text-xs px-3 py-1.5 cursor-pointer">
|
||||||
{uploading ? "Uploading..." : "Attach File"}
|
{uploading ? "上传中..." : "附加文件"}
|
||||||
<input type="file" className="hidden" onChange={handleUpload} disabled={uploading} />
|
<input type="file" className="hidden" onChange={handleUpload} disabled={uploading} />
|
||||||
</label>
|
</label>
|
||||||
{files.map((f) => (
|
{files.map((f) => (
|
||||||
<span key={f.id} className="badge text-[0.7rem]">{f.name}</span>
|
<span key={f.id} className="badge text-[0.7rem]">{f.name}</span>
|
||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
className="ml-auto text-xs text-txt-muted hover:text-magenta"
|
className="ml-auto text-xs text-txt-muted hover:text-magenta flex items-center gap-1.5"
|
||||||
|
onClick={() => setInput(`# Epic: Q3 智能云端电池预测性维护系统 (BMS-AI)
|
||||||
|
|
||||||
|
## 业务背景
|
||||||
|
目前我们接入云端的 50 万台新能源车辆中,电池因单体压差过大导致的突发性故障率有上升趋势,造成了较高的售后索赔成本和用户客诉。我们需要在 Q3 PI (Program Increment) 落地一套“电池预测性维护系统”。
|
||||||
|
|
||||||
|
## 核心目标
|
||||||
|
1. **数据实时采集**:云端需要能够高频接收车辆上报的电池运行遥测数据(包含单体电压、温度、充放电电流等)。
|
||||||
|
2. **云端 AI 分析**:利用云端的 AI 异常检测模型,对电池数据进行实时流式计算,预测未来 7 天内可能发生故障的电池包。
|
||||||
|
3. **多端预警联动**:一旦云端判定存在高风险,需立即向车机端(座舱屏幕)和车主的手机 App 同时下发告警弹窗,并生成一条云端售后维修建议工单。
|
||||||
|
|
||||||
|
## 约束与期望
|
||||||
|
- **合规性**:采集的车辆 VIN 码和位置信息必须符合个人信息保护法(PIPL)的数据脱敏要求。
|
||||||
|
- **性能**:云端必须能扛住 50 万台车同时在线高频上报数据的并发压力。告警下发延迟不能超过 3 秒。`)}
|
||||||
|
title="快速输入: BMS-AI 预测性维护 Epic 示例"
|
||||||
|
>
|
||||||
|
<svg className="w-3.5 h-3.5 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||||
|
</svg>
|
||||||
|
快捷指令
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="text-xs text-txt-muted hover:text-magenta"
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
>
|
>
|
||||||
Reset Session
|
重置会话
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -347,15 +638,58 @@ export default function PlanningAgent() {
|
|||||||
send();
|
send();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="Describe your Epic or provide guidance..."
|
placeholder="描述你的 Epic 或提供指引..."
|
||||||
disabled={state.chatting}
|
disabled={state.chatting}
|
||||||
/>
|
/>
|
||||||
<button className="btn-magenta" onClick={send} disabled={state.chatting}>
|
<button className="btn-magenta" onClick={send} disabled={state.chatting}>
|
||||||
{state.chatting ? "Planning..." : "Send"}
|
{state.chatting ? "规划中..." : "发送"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ─── Right Panel: Workspace ─── */}
|
||||||
|
{state.workspace.isOpen && (
|
||||||
|
<div className="w-[65%] h-full flex flex-col bg-white border-l border-border">
|
||||||
|
{/* Workspace Header */}
|
||||||
|
<div className="h-14 border-b border-border flex items-center px-6 justify-between bg-white shrink-0">
|
||||||
|
<h3 className="font-semibold text-txt flex items-center gap-2 truncate">
|
||||||
|
<svg className="w-5 h-5 text-magenta shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||||
|
</svg>
|
||||||
|
{state.workspace.title}
|
||||||
|
{state.workspace.isGenerating && (
|
||||||
|
<span className="text-sm text-magenta animate-pulse flex items-center gap-1 ml-2">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-magenta animate-pulse" />
|
||||||
|
生成中...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => dispatch({ type: "ws-toggle", open: false })}
|
||||||
|
className="p-2 hover:bg-surface-muted rounded-full transition-colors shrink-0"
|
||||||
|
title="关闭面板"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 text-txt-muted hover:text-txt" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* Workspace Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-8">
|
||||||
|
<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 prose-headings:text-txt">
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>
|
||||||
|
{state.workspace.content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
{state.workspace.isGenerating && !state.workspace.content && (
|
||||||
|
<div className="flex items-center justify-center py-20 text-txt-muted">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-magenta" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,22 +31,216 @@ type ChangedFile = {
|
|||||||
deletions: number;
|
deletions: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FileContent = {
|
type FileContentIssue = {
|
||||||
content: string;
|
line: number;
|
||||||
issues: Array<{
|
severity: string;
|
||||||
line: number;
|
message: string;
|
||||||
severity: string;
|
rule?: string;
|
||||||
message: string;
|
scanner?: string;
|
||||||
rule?: 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 View = "dashboard" | "pr-list" | "settings";
|
||||||
|
|
||||||
|
type PRHistoryItem = {
|
||||||
|
pr_number: number;
|
||||||
|
error_count?: number;
|
||||||
|
warning_count?: number;
|
||||||
|
total_issues?: number;
|
||||||
|
};
|
||||||
|
|
||||||
type QualityGateProps = {
|
type QualityGateProps = {
|
||||||
view: View;
|
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;
|
const BASE = API.quality;
|
||||||
|
|
||||||
async function apiGet<T>(path: string): Promise<T> {
|
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 [fileContent, setFileContent] = useState<FileContent | null>(null);
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const [trendHistory, setTrendHistory] = useState<PRHistoryItem[]>([]);
|
||||||
|
const [trendLoading, setTrendLoading] = useState(false);
|
||||||
|
|
||||||
const fetchPRs = useCallback(async () => {
|
const fetchPRs = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
@@ -91,6 +288,23 @@ export default function QualityGate({ view }: QualityGateProps) {
|
|||||||
fetchPRs();
|
fetchPRs();
|
||||||
}, [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) => {
|
const openPRDetail = useCallback(async (prId: number) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
@@ -116,8 +330,8 @@ export default function QualityGate({ view }: QualityGateProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!modalOpen || !selectedPR || !selectedFile) return;
|
if (!modalOpen || !selectedPR || !selectedFile) return;
|
||||||
setFileContent(null);
|
setFileContent(null);
|
||||||
apiGet<FileContent>(`/prs/${selectedPR.id}/file?path=${encodeURIComponent(selectedFile)}`)
|
apiGet<Parameters<typeof normalizeFileContent>[0]>(`/prs/${selectedPR.id}/file?path=${encodeURIComponent(selectedFile)}`)
|
||||||
.then(setFileContent)
|
.then((data) => setFileContent(normalizeFileContent(data)))
|
||||||
.catch(() => setFileContent({ content: "// Failed to load file", issues: [] }));
|
.catch(() => setFileContent({ content: "// Failed to load file", issues: [] }));
|
||||||
}, [modalOpen, selectedPR, selectedFile]);
|
}, [modalOpen, selectedPR, selectedFile]);
|
||||||
|
|
||||||
@@ -167,14 +381,14 @@ export default function QualityGate({ view }: QualityGateProps) {
|
|||||||
|
|
||||||
{view === "dashboard" && (
|
{view === "dashboard" && (
|
||||||
<>
|
<>
|
||||||
<h2 className="text-xl font-extrabold mb-6">Overview</h2>
|
<h2 className="text-xl font-extrabold mb-6">总览</h2>
|
||||||
|
|
||||||
<div className="grid grid-cols-4 gap-5 mb-8">
|
<div className="grid grid-cols-4 gap-5 mb-8">
|
||||||
{[
|
{[
|
||||||
{ label: "Open PRs", value: stats.pending, color: "text-yellow-600", bg: "bg-yellow-50" },
|
{ label: "打开 PR", value: stats.pending, color: "text-yellow-600", bg: "bg-yellow-50" },
|
||||||
{ label: "Merged", value: stats.passed, color: "text-green-600", bg: "bg-green-50" },
|
{ label: "已合并", value: stats.passed, color: "text-green-600", bg: "bg-green-50" },
|
||||||
{ label: "Rejected", value: stats.rejected, color: "text-red-600", bg: "bg-red-50" },
|
{ label: "已拒绝", value: stats.rejected, color: "text-red-600", bg: "bg-red-50" },
|
||||||
{ label: "Total Issues", value: stats.totalIssues, color: "text-magenta", bg: "bg-magenta-50" },
|
{ label: "问题总数", value: stats.totalIssues, color: "text-magenta", bg: "bg-magenta-50" },
|
||||||
].map((s) => (
|
].map((s) => (
|
||||||
<div key={s.label} className={`${s.bg} p-6 border border-border rounded-2xl shadow-[0_4px_16px_rgba(0,0,0,0.04)]`}>
|
<div key={s.label} className={`${s.bg} p-6 border border-border rounded-2xl shadow-[0_4px_16px_rgba(0,0,0,0.04)]`}>
|
||||||
<div className={`text-3xl font-extrabold ${s.color}`}>{s.value}</div>
|
<div className={`text-3xl font-extrabold ${s.color}`}>{s.value}</div>
|
||||||
@@ -183,7 +397,11 @@ export default function QualityGate({ view }: QualityGateProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 className="text-lg font-extrabold mb-4">Recent Pull Requests</h3>
|
<div className="mt-8 mb-8">
|
||||||
|
<ProblemTrendChart history={trendHistory} loading={trendLoading} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-lg font-extrabold mb-4">近期 Pull Request</h3>
|
||||||
<PRTable prs={prs.slice(0, 8)} loading={loading} onView={(pr) => openPRDetail(pr.id)} />
|
<PRTable prs={prs.slice(0, 8)} loading={loading} onView={(pr) => openPRDetail(pr.id)} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -191,7 +409,7 @@ export default function QualityGate({ view }: QualityGateProps) {
|
|||||||
{view === "pr-list" && (
|
{view === "pr-list" && (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<h2 className="text-xl font-extrabold">Pull Request List</h2>
|
<h2 className="text-xl font-extrabold">Pull Request 列表</h2>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{(["all", "open", "merged", "closed"] as const).map((f) => (
|
{(["all", "open", "merged", "closed"] as const).map((f) => (
|
||||||
<button
|
<button
|
||||||
@@ -203,11 +421,11 @@ export default function QualityGate({ view }: QualityGateProps) {
|
|||||||
: "border-border text-txt-muted hover:border-magenta"
|
: "border-border text-txt-muted hover:border-magenta"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{f === "all" ? "All" : f === "open" ? "Open" : f === "merged" ? "Merged" : "Closed"}
|
{f === "all" ? "全部" : f === "open" ? "打开" : f === "merged" ? "已合并" : "已关闭"}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
<button className="btn-outline text-xs px-3 py-1.5" onClick={fetchPRs}>
|
<button className="btn-outline text-xs px-3 py-1.5" onClick={fetchPRs}>
|
||||||
Refresh
|
刷新
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -217,21 +435,21 @@ export default function QualityGate({ view }: QualityGateProps) {
|
|||||||
|
|
||||||
{view === "settings" && (
|
{view === "settings" && (
|
||||||
<div className="max-w-xl">
|
<div className="max-w-xl">
|
||||||
<h2 className="text-xl font-extrabold mb-6">Settings</h2>
|
<h2 className="text-xl font-extrabold mb-6">设置</h2>
|
||||||
<div className="card mb-4">
|
<div className="card mb-4">
|
||||||
<h3 className="font-bold mb-2">Webhook Configuration</h3>
|
<h3 className="font-bold mb-2">Webhook 配置</h3>
|
||||||
<p className="text-sm text-txt-muted mb-3">Add this URL to your Gitea repository webhook settings:</p>
|
<p className="text-sm text-txt-muted mb-3">将此 URL 添加到 Gitea 仓库的 webhook 设置:</p>
|
||||||
<code className="block bg-surface-muted p-3 text-sm font-mono break-all rounded-lg">
|
<code className="block bg-surface-muted p-3 text-sm font-mono break-all rounded-lg">
|
||||||
POST {window.location.origin}/quality-api/webhook/gitea
|
POST {window.location.origin}/quality-api/webhook/gitea
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h3 className="font-bold mb-2">Quick Notes</h3>
|
<h3 className="font-bold mb-2">快速说明</h3>
|
||||||
<ul className="text-sm text-txt-muted list-disc list-inside space-y-1">
|
<ul className="text-sm text-txt-muted list-disc list-inside space-y-1">
|
||||||
<li>Supports Gitea Push and Pull Request events</li>
|
<li>支持 Gitea Push 与 Pull Request 事件</li>
|
||||||
<li>Runs Pylint, Flake8, ESLint, and Bandit automatically</li>
|
<li>自动运行 Pylint、Flake8、ESLint 与 Bandit</li>
|
||||||
<li>Optional AI review using DeepSeek-V3</li>
|
<li>可选 AI 审查(DeepSeek-V3)</li>
|
||||||
<li>Scan summary can be pushed to Feishu channels</li>
|
<li>扫描摘要可推送到飞书通知</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -259,7 +477,7 @@ export default function QualityGate({ view }: QualityGateProps) {
|
|||||||
|
|
||||||
<div className="flex flex-1 min-h-0">
|
<div className="flex flex-1 min-h-0">
|
||||||
<div className="w-[260px] shrink-0 border-r border-border overflow-y-auto p-4 bg-surface-muted/35">
|
<div className="w-[260px] shrink-0 border-r border-border overflow-y-auto p-4 bg-surface-muted/35">
|
||||||
<h3 className="text-xs font-bold text-txt-muted uppercase mb-3">Changed Files</h3>
|
<h3 className="text-xs font-bold text-txt-muted uppercase mb-3">变更文件</h3>
|
||||||
{changedFiles.map((f) => (
|
{changedFiles.map((f) => (
|
||||||
<button
|
<button
|
||||||
key={f.filename}
|
key={f.filename}
|
||||||
@@ -281,73 +499,104 @@ export default function QualityGate({ view }: QualityGateProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-auto bg-white">
|
{/* 代码区:完整代码 + 每行右侧缺陷标注,一行一个滚动条(参考 index copy.html) */}
|
||||||
{fileContent ? (
|
<div className="flex-1 min-h-0 min-w-0 flex flex-col overflow-hidden">
|
||||||
<pre className="text-sm font-mono leading-6 p-4">
|
<div className="flex-1 min-h-0 overflow-auto bg-[#1e1e1e] font-mono text-[13px] leading-[1.5]">
|
||||||
{fileContent.content.split("\n").map((line, i) => {
|
{fileContent ? (
|
||||||
const lineNum = i + 1;
|
<div className="min-w-min">
|
||||||
const issues = fileContent.issues.filter((iss) => iss.line === lineNum);
|
{(fileContent.content ?? "").split("\n").map((line, i) => {
|
||||||
return (
|
const lineNum = i + 1;
|
||||||
<div key={i} className={`flex ${issues.length > 0 ? "bg-red-50/80" : ""}`}>
|
const lineIssues = (fileContent.issues ?? []).filter((iss) => iss.line === lineNum);
|
||||||
<span className="w-12 shrink-0 text-right pr-4 text-txt-muted select-none text-xs leading-6">
|
const hasIssue = lineIssues.length > 0;
|
||||||
{lineNum}
|
const reasonText = hasIssue
|
||||||
</span>
|
? lineIssues.map((iss) => (iss.scanner ? `[${iss.scanner}] ` : "") + (iss.message || "")).join(";")
|
||||||
<span className="flex-1 whitespace-pre-wrap">{line}</span>
|
: "";
|
||||||
{issues.length > 0 && (
|
const displayText = line === "" ? "\u00A0" : line;
|
||||||
<span className="shrink-0 px-2 text-xs text-red-600 max-w-xs truncate" title={issues[0].message}>
|
return (
|
||||||
● {issues[0].message}
|
<div
|
||||||
</span>
|
key={i}
|
||||||
)}
|
className={`flex min-h-[1.5em] items-stretch ${hasIssue ? "code-line-has-issue" : ""}`}
|
||||||
</div>
|
>
|
||||||
);
|
{/* 行号区 */}
|
||||||
})}
|
<div className="w-12 min-w-[48px] shrink-0 flex items-center justify-end pr-2 text-[#6c757d] bg-[#252526] select-none">
|
||||||
</pre>
|
<span className="mr-1">{lineNum}</span>
|
||||||
) : (
|
{hasIssue && (
|
||||||
<div className="flex items-center justify-center h-full text-txt-muted text-sm">
|
<span
|
||||||
{selectedFile ? "Loading file..." : "Select a file to inspect"}
|
className={
|
||||||
</div>
|
lineIssues[0]?.severity === "error" || lineIssues[0]?.severity === "high"
|
||||||
)}
|
? "text-[#f14c4c]"
|
||||||
</div>
|
: "text-[#cca700]"
|
||||||
|
}
|
||||||
<div className="w-[220px] shrink-0 border-l border-border overflow-y-auto p-4 bg-surface-muted/60">
|
title={reasonText}
|
||||||
<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>
|
</span>
|
||||||
)}
|
)}
|
||||||
{fileContent?.issues.map((iss, i) => (
|
</div>
|
||||||
<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">
|
<div
|
||||||
<span className={`font-bold ${
|
className={`flex-1 min-w-0 px-3 py-0 whitespace-pre-wrap break-all text-[#d4d4d4] ${
|
||||||
iss.severity === "error" ? "text-red-600" : iss.severity === "warning" ? "text-yellow-600" : "text-blue-600"
|
hasIssue ? "bg-red-900/20 border-l-2 border-l-red-500 pl-2" : ""
|
||||||
}`}>
|
}`}
|
||||||
{iss.severity === "error" ? "❌" : iss.severity === "warning" ? "⚠️" : "ℹ️"}
|
>
|
||||||
</span>
|
{displayText}
|
||||||
<span className="text-txt-muted">L{iss.line}</span>
|
</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>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-txt">{iss.message}</p>
|
) : (
|
||||||
{iss.rule && <p className="text-txt-muted mt-0.5">{iss.rule}</p>}
|
<div className="flex items-center justify-center h-full min-h-[200px] text-[#6c757d] text-sm">
|
||||||
</div>
|
{selectedFile ? "加载文件..." : "请选择要检查的文件"}
|
||||||
))}
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 px-6 py-4 border-t border-border shrink-0">
|
<div className="flex justify-end gap-3 px-6 py-4 border-t border-border shrink-0">
|
||||||
<button className="btn-outline" onClick={() => setModalOpen(false)}>
|
<button className="btn-outline" onClick={() => setModalOpen(false)}>
|
||||||
Close
|
关闭
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="bg-red-600 text-white font-bold text-sm px-6 py-2.5 border-none cursor-pointer hover:opacity-90 disabled:opacity-50 rounded-xl"
|
className="bg-red-600 text-white font-bold text-sm px-6 py-2.5 border-none cursor-pointer hover:opacity-90 disabled:opacity-50 rounded-xl"
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
disabled={loading || selectedPR.state !== "open"}
|
disabled={loading || selectedPR.state !== "open"}
|
||||||
>
|
>
|
||||||
Reject
|
拒绝
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn-magenta"
|
className="btn-magenta"
|
||||||
onClick={handleMerge}
|
onClick={handleMerge}
|
||||||
disabled={loading || selectedPR.state !== "open"}
|
disabled={loading || selectedPR.state !== "open"}
|
||||||
>
|
>
|
||||||
Approve & Merge
|
批准并合并
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -367,27 +616,27 @@ function PRTable({
|
|||||||
onView: (pr: PRScan) => void;
|
onView: (pr: PRScan) => void;
|
||||||
}) {
|
}) {
|
||||||
if (loading && prs.length === 0) {
|
if (loading && prs.length === 0) {
|
||||||
return <div className="text-center py-8 text-txt-muted text-sm">Loading...</div>;
|
return <div className="text-center py-8 text-txt-muted text-sm">加载中...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prs.length === 0) {
|
if (prs.length === 0) {
|
||||||
return <div className="text-center py-8 text-txt-muted text-sm">No PR records found</div>;
|
return <div className="text-center py-8 text-txt-muted text-sm">暂无 PR 记录</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto rounded-2xl border border-border bg-white shadow-[0_6px_22px_rgba(0,0,0,0.04)]">
|
<div className="overflow-x-auto rounded-2xl border border-border bg-white shadow-[0_6px_22px_rgba(0,0,0,0.04)]">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-surface-muted text-left">
|
<tr className="bg-surface-muted text-left">
|
||||||
<th className="px-4 py-3 border-b border-border font-bold">PR#</th>
|
<th className="px-4 py-3 border-b border-border font-bold">PR#</th>
|
||||||
<th className="px-4 py-3 border-b border-border font-bold">Title</th>
|
<th className="px-4 py-3 border-b border-border font-bold">标题</th>
|
||||||
<th className="px-4 py-3 border-b border-border font-bold">Repository</th>
|
<th className="px-4 py-3 border-b border-border font-bold">仓库</th>
|
||||||
<th className="px-4 py-3 border-b border-border font-bold">Author</th>
|
<th className="px-4 py-3 border-b border-border font-bold">作者</th>
|
||||||
<th className="px-4 py-3 border-b border-border font-bold">Branch</th>
|
<th className="px-4 py-3 border-b border-border font-bold">分支</th>
|
||||||
<th className="px-4 py-3 border-b border-border font-bold">State</th>
|
<th className="px-4 py-3 border-b border-border font-bold">状态</th>
|
||||||
<th className="px-4 py-3 border-b border-border font-bold">Issues</th>
|
<th className="px-4 py-3 border-b border-border font-bold">问题数</th>
|
||||||
<th className="px-4 py-3 border-b border-border font-bold">Created</th>
|
<th className="px-4 py-3 border-b border-border font-bold">创建时间</th>
|
||||||
<th className="px-4 py-3 border-b border-border font-bold">Action</th>
|
<th className="px-4 py-3 border-b border-border font-bold">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -408,7 +657,7 @@ function PRTable({
|
|||||||
? "bg-green-100 text-green-700"
|
? "bg-green-100 text-green-700"
|
||||||
: "bg-red-100 text-red-700"
|
: "bg-red-100 text-red-700"
|
||||||
}`}>
|
}`}>
|
||||||
{pr.state === "open" ? "Open" : pr.state === "merged" ? "Merged" : "Closed"}
|
{pr.state === "open" ? "打开" : pr.state === "merged" ? "已合并" : "已关闭"}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
@@ -417,14 +666,14 @@ function PRTable({
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-xs text-txt-muted">
|
<td className="px-4 py-3 text-xs text-txt-muted">
|
||||||
{new Date(pr.created_at).toLocaleString("en-US")}
|
{new Date(pr.created_at).toLocaleString("zh-CN")}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<button
|
<button
|
||||||
className="text-magenta font-bold text-xs hover:underline"
|
className="text-magenta font-bold text-xs hover:underline"
|
||||||
onClick={() => onView(pr)}
|
onClick={() => onView(pr)}
|
||||||
>
|
>
|
||||||
Inspect →
|
查看 →
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ export default {
|
|||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
magenta: {
|
magenta: {
|
||||||
DEFAULT: "#E20074",
|
DEFAULT: "#F43F5E",
|
||||||
50: "#FFF0F7",
|
50: "#FFF1F2",
|
||||||
100: "#FFE0EF",
|
100: "#FFE4E6",
|
||||||
600: "#E20074",
|
600: "#F43F5E",
|
||||||
700: "#B8005E",
|
700: "#BE123C",
|
||||||
},
|
},
|
||||||
surface: {
|
surface: {
|
||||||
DEFAULT: "#FFFFFF",
|
DEFAULT: "#FFFFFF",
|
||||||
@@ -30,5 +30,7 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [
|
||||||
|
require('@tailwindcss/typography'),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
198
temp_output.txt
Normal file
198
temp_output.txt
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
abortRef.current?.abort();
|
||||||
|
const fresh = { sessionId: makeId("sess"), userId: makeId("user") };
|
||||||
|
dispatch({ type: "reset", ...fresh });
|
||||||
|
setFiles([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full relative overflow-hidden">
|
||||||
|
{/* ─── Sidebar Overlay ─── */}
|
||||||
|
{isHistoryOpen && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black/20 z-40 transition-opacity duration-300"
|
||||||
|
onClick={() => setIsHistoryOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`absolute top-0 left-0 h-full w-1/4 bg-white border-r border-border z-50 transform transition-transform duration-300 ease-in-out shadow-xl ${
|
||||||
|
isHistoryOpen ? "translate-x-0" : "-translate-x-full"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="p-6 h-full flex flex-col">
|
||||||
|
<div className="flex items-center justify-between mb-8">
|
||||||
|
<h3 className="text-lg font-bold">历史对话</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsHistoryOpen(false)}
|
||||||
|
className="p-2 hover:bg-surface-muted rounded-full transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{state.historySessions.length === 0 ? (
|
||||||
|
<div className="text-center py-10 text-txt-muted text-sm italic">
|
||||||
|
暂无历史记录
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-3 py-2">
|
||||||
|
{state.historySessions.map((session) => (
|
||||||
|
<div
|
||||||
|
key={session.id}
|
||||||
|
onClick={() => loadHistorySession(session.sessionId)}
|
||||||
|
className={`p-4 rounded-xl border border-border cursor-pointer transition-all hover:bg-surface-muted hover:border-magenta group ${
|
||||||
|
state.sessionId === session.sessionId ? 'bg-magenta/5 border-magenta ring-1 ring-magenta' : 'bg-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start mb-2">
|
||||||
|
<span className="text-[0.7rem] font-mono text-txt-muted truncate max-w-[120px]">
|
||||||
|
ID: {session.sessionId.slice(0, 8)}...
|
||||||
|
</span>
|
||||||
|
<span className="text-[0.65rem] text-txt-muted">
|
||||||
|
{new Date(session.ts).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-txt leading-snug line-clamp-2 group-hover:text-magenta transition-colors">
|
||||||
|
{session.summary}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{loadingHistory && (
|
||||||
|
<div className="absolute inset-0 bg-white/60 flex items-center justify-center z-[60]">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-magenta"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ─── Main Chat ─── */}
|
||||||
|
<div className="h-full flex flex-col min-w-0">
|
||||||
|
<div className="flex-1 overflow-y-auto px-[72px] py-8">
|
||||||
|
{/* History Toggle Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setIsHistoryOpen(true)}
|
||||||
|
className="absolute top-6 left-6 p-2 bg-white border border-border rounded-lg shadow-sm hover:bg-surface-muted transition-colors z-30 group"
|
||||||
|
title="查看历史记录"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 text-txt-muted group-hover:text-magenta" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<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">规划委员会代理</h2>
|
||||||
|
<p className="text-txt-muted text-sm">
|
||||||
|
提供一个高层 Epic,代理将从产品、架构与 RTE 视角进行规划。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{state.messages.map((msg) => (
|
||||||
|
<div
|
||||||
|
key={msg.id}
|
||||||
|
className={`max-w-[78%] px-6 py-5 text-[0.95rem] leading-relaxed rounded-2xl shadow-[0_2px_14px_rgba(0,0,0,0.04)] ${
|
||||||
|
msg.role === "user"
|
||||||
|
? "self-end mr-1 bg-surface-muted text-txt border border-border whitespace-pre-wrap"
|
||||||
|
: "self-start bg-white border border-border border-l-4 border-l-magenta"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{msg.role === "assistant" && (
|
||||||
|
<span className="badge mb-3 block w-fit">系统 / 迭代闭环</span>
|
||||||
|
)}
|
||||||
|
{msg.role === "assistant" && state.reasoning[msg.id] && (
|
||||||
|
<details
|
||||||
|
className="mb-4 bg-surface-muted/50 rounded-lg border border-border overflow-hidden"
|
||||||
|
open={!msg.content}
|
||||||
|
>
|
||||||
|
<summary className="px-4 py-2 text-xs font-medium text-txt-muted cursor-pointer hover:bg-border/20 transition-colors list-none flex items-center gap-2">
|
||||||
|
<span className={`w-1.5 h-1.5 rounded-full bg-magenta ${!msg.content ? 'animate-pulse' : ''}`}></span>
|
||||||
|
查看思考过程
|
||||||
|
</summary>
|
||||||
|
<div className="px-4 py-3 bg-white/50 text-[0.8rem] font-mono whitespace-pre-wrap border-t border-border text-txt-muted">
|
||||||
|
{state.reasoning[msg.id]}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
{msg.role === "assistant" ? (
|
||||||
|
<div className="prose prose-sm max-w-none prose-p:my-0.5 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 || (state.chatting ? "思考中..." : "")}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
msg.content || "Thinking..."
|
||||||
|
)}
|
||||||
|
{msg.status === "failed" && (
|
||||||
|
<span className="text-red-500 text-xs block mt-2">发送失败</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div ref={bottomRef} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error */}
|
||||||
|
{state.error && (
|
||||||
|
<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"
|
||||||
|
onClick={() => dispatch({ type: "set-error" })}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Input area */}
|
||||||
|
<div className="px-10 pb-6 pt-4 border-t border-border">
|
||||||
|
{/* File upload row */}
|
||||||
|
<div className="flex items-center gap-3 mb-3 text-sm">
|
||||||
|
<label className="btn-outline text-xs px-3 py-1.5 cursor-pointer">
|
||||||
|
{uploading ? "上传中..." : "附加文件"}
|
||||||
|
<input type="file" className="hidden" onChange={handleUpload} disabled={uploading} />
|
||||||
|
</label>
|
||||||
|
{files.map((f) => (
|
||||||
|
<span key={f.id} className="badge text-[0.7rem]">{f.name}</span>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
className="ml-auto text-xs text-txt-muted hover:text-magenta"
|
||||||
|
onClick={handleReset}
|
||||||
|
>
|
||||||
|
重置会话
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4 items-end">
|
||||||
|
<textarea
|
||||||
|
className="input-field flex-1 min-h-[88px] max-h-56 resize-y"
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
send();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="描述你的 Epic 或提供指引..."
|
||||||
|
disabled={state.chatting}
|
||||||
|
/>
|
||||||
|
<button className="btn-magenta" onClick={send} disabled={state.chatting}>
|
||||||
|
{state.chatting ? "规划中..." : "发送"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
144
template/devops.html
Normal file
144
template/devops.html
Normal 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> 战略规划
|
||||||
|
</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> 开发运维
|
||||||
|
</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> 质量门控
|
||||||
|
</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> 开发代理</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
237
template/index.html
Normal 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> 战略规划
|
||||||
|
</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> 开发运维
|
||||||
|
</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> 质量门控
|
||||||
|
</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">
|
||||||
|
进入规划看板 →
|
||||||
|
</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">
|
||||||
|
开启开发流水线 →
|
||||||
|
</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">
|
||||||
|
查看质量大盘 →
|
||||||
|
</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
134
template/planning.html
Normal 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> 战略规划
|
||||||
|
</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> 开发运维
|
||||||
|
</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> 质量门控
|
||||||
|
</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
151
template/quality.html
Normal 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> 战略规划
|
||||||
|
</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> 开发运维
|
||||||
|
</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> 质量门控
|
||||||
|
</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">质量门控 大盘</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">通过</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">合并请求审查</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>
|
||||||
108
template/工程页面内容总结.md
Normal file
108
template/工程页面内容总结.md
Normal 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 工程师理解产品目标和用户需求,从而设计出更符合用户期望的界面和交互。
|
||||||
Reference in New Issue
Block a user