Add AI playground and frontend layout guidelines
This commit is contained in:
16
README.md
16
README.md
@@ -312,6 +312,22 @@ AI_PROVIDER_SERVICE_TOKEN=change_me
|
||||
|
||||
- [docs/aiprovider.md](/home/ray/dev/linkong/planet/docs/aiprovider.md)
|
||||
- [aiprovider/README.md](/home/ray/dev/linkong/planet/aiprovider/README.md)
|
||||
- [docs/frontend-layout-guidelines.md](/home/ray/dev/linkong/planet/docs/frontend-layout-guidelines.md)
|
||||
- [docs/ai-playground-development-plan.md](/home/ray/dev/linkong/planet/docs/ai-playground-development-plan.md)
|
||||
|
||||
## 前端页面布局规范
|
||||
|
||||
管理后台页面默认遵循“单屏工作区”原则:
|
||||
|
||||
- 页头、摘要区、主工作区应在一屏内形成稳定结构
|
||||
- 主表格 / 主图表 / 主分析区应占据页面主要可视空间
|
||||
- 模块内容超出时优先在卡片、表格、标签页内部滚动
|
||||
- 不依赖整页纵向撑开来容纳主要工作区
|
||||
|
||||
当前推荐参考实现:
|
||||
|
||||
- [frontend/src/pages/BGP/BGP.tsx](/home/ray/dev/linkong/planet/frontend/src/pages/BGP/BGP.tsx)
|
||||
- [docs/frontend-layout-guidelines.md](/home/ray/dev/linkong/planet/docs/frontend-layout-guidelines.md)
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: aiprovider/Dockerfile
|
||||
env_file:
|
||||
- ./aiprovider/.env
|
||||
container_name: planet_aiprovider
|
||||
ports:
|
||||
- "8010:8010"
|
||||
|
||||
@@ -7,6 +7,35 @@ This project follows the repository versioning rule:
|
||||
- `feature` -> `+0.1.0`
|
||||
- `bugfix` -> `+0.0.1`
|
||||
|
||||
## 0.24.0
|
||||
|
||||
Released: 2026-04-09
|
||||
|
||||
### Highlights
|
||||
|
||||
- Added a dedicated `AI Playground` admin entry so operators can validate provider connectivity and run controlled situational-analysis prompts from the main frontend without introducing a separate UI service.
|
||||
- Established a first explicit frontend layout rulebook centered on single-screen workspaces, internal module scrolling, and BGP-style page composition for future admin pages.
|
||||
|
||||
### Added
|
||||
|
||||
- Added [frontend/src/pages/Playground/Playground.tsx](/home/ray/dev/linkong/planet/frontend/src/pages/Playground/Playground.tsx), introducing the first dedicated AI testing workspace with provider status visibility, prompt/result tabs, and collapsible operator guidance.
|
||||
- Added [docs/frontend-layout-guidelines.md](/home/ray/dev/linkong/planet/docs/frontend-layout-guidelines.md), documenting the repository standard for one-screen admin workspaces and module-local overflow handling.
|
||||
- Added [docs/ai-playground-development-plan.md](/home/ray/dev/linkong/planet/docs/ai-playground-development-plan.md), capturing the completed AI gateway/UI work and the next delivery phases for BGP briefs, evidence-first inputs, and future agent runtime expansion.
|
||||
|
||||
### Improved
|
||||
|
||||
- Improved [frontend/src/components/AppLayout/AppLayout.tsx](/home/ray/dev/linkong/planet/frontend/src/components/AppLayout/AppLayout.tsx) and [frontend/src/App.tsx](/home/ray/dev/linkong/planet/frontend/src/App.tsx) by wiring `AI Playground` into the main admin navigation and route tree instead of pointing operators to a nonexistent `aiprovider` chat page.
|
||||
- Improved [planet.sh](/home/ray/dev/linkong/planet/planet.sh) by replacing the incorrect `localhost:8010/chat` closeout link with the frontend `AI Playground` entry.
|
||||
- Improved [docker-compose.yml](/home/ray/dev/linkong/planet/docker-compose.yml) by attaching `./aiprovider/.env` to the `aiprovider` service so provider identity, model, and credentials actually reach the running container.
|
||||
- Improved [README.md](/home/ray/dev/linkong/planet/README.md) by linking the new frontend layout guidance and AI Playground development plan.
|
||||
- Improved [frontend/src/index.css](/home/ray/dev/linkong/planet/frontend/src/index.css) by refining the Playground workspace into a notebook-friendly left-sidebar plus right-tabbed layout, reusing thin scrollbars, and making provider/help/result regions degrade more gracefully under constrained height.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the local AI status flow so provider configuration no longer appeared permanently `disabled / not configured` merely because `aiprovider/.env` was not mounted into the container.
|
||||
- Fixed repeated `Provider 状态` refetching when switching away from and back to `/playground` by caching the last known provider status within the browser session until the operator explicitly refreshes it.
|
||||
- Fixed several Playground layout regressions where auxiliary panels could push the result area out of view or clip provider details without exposing internal scrolling.
|
||||
|
||||
## 0.23.4
|
||||
|
||||
Released: 2026-04-08
|
||||
|
||||
318
docs/ai-playground-development-plan.md
Normal file
318
docs/ai-playground-development-plan.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# AI Playground Development Plan
|
||||
|
||||
## 目标
|
||||
|
||||
这份计划用于统一 `aiprovider`、`backend AI facade`、`Playground` 页面,以及后续 `BGP / 告警 / 数据源健康` 等 AI 入口的演进方向。
|
||||
|
||||
当前原则:
|
||||
|
||||
- `aiprovider` 继续作为独立模型网关
|
||||
- `backend` 继续作为稳定业务入口
|
||||
- `frontend` 负责测试台和业务 UI
|
||||
- 先做“可控、可验证、可解释”的 AI 能力,再逐步引入 agent/tool calling
|
||||
|
||||
## 当前已完成
|
||||
|
||||
### 1. AI 网关基础层
|
||||
|
||||
已完成:
|
||||
|
||||
- 独立 `aiprovider` 服务
|
||||
- `backend -> aiprovider -> model provider` 调用链
|
||||
- `provider/status` 与 `situational-awareness/analyze` 稳定接口
|
||||
- `X-Request-ID` 透传
|
||||
- 轻量超时与重试
|
||||
- MiniMax / Anthropic-compatible / OpenAI-compatible / Ollama 适配
|
||||
|
||||
相关文件:
|
||||
|
||||
- [backend/app/api/v1/ai.py](/home/ray/dev/linkong/planet/backend/app/api/v1/ai.py)
|
||||
- [backend/app/services/ai_client.py](/home/ray/dev/linkong/planet/backend/app/services/ai_client.py)
|
||||
- [aiprovider/main.py](/home/ray/dev/linkong/planet/aiprovider/main.py)
|
||||
- [aiprovider/provider_service.py](/home/ray/dev/linkong/planet/aiprovider/provider_service.py)
|
||||
- [docs/aiprovider.md](/home/ray/dev/linkong/planet/docs/aiprovider.md)
|
||||
|
||||
### 2. 本地运行与配置打通
|
||||
|
||||
已完成:
|
||||
|
||||
- `planet.sh` 启动链路纳入 `aiprovider`
|
||||
- `planet.sh` 启动完成后输出 Playground 入口
|
||||
- `docker-compose.yml` 为 `aiprovider` 加入 `env_file`
|
||||
- `backend/.env` 与 `aiprovider/.env` 两侧 service token 对齐
|
||||
- `Playground` 状态缓存,避免页面切换时每次都重新请求 provider 状态
|
||||
|
||||
相关文件:
|
||||
|
||||
- [planet.sh](/home/ray/dev/linkong/planet/planet.sh)
|
||||
- [docker-compose.yml](/home/ray/dev/linkong/planet/docker-compose.yml)
|
||||
- [backend/.env.example](/home/ray/dev/linkong/planet/backend/.env.example)
|
||||
- [aiprovider/.env.example](/home/ray/dev/linkong/planet/aiprovider/.env.example)
|
||||
|
||||
### 3. Playground UI 基础版
|
||||
|
||||
已完成:
|
||||
|
||||
- 新增前端路由 `/playground`
|
||||
- 左侧 `Provider 状态 + 测试说明`
|
||||
- 右侧 `请求 / 结果` Tabs
|
||||
- `Provider 状态` 支持手动刷新
|
||||
- `测试说明` 支持折叠
|
||||
- 内部区域采用细滚动条
|
||||
- 页面布局开始遵循“单屏工作区 + 模块内部滚动”规范
|
||||
|
||||
相关文件:
|
||||
|
||||
- [frontend/src/pages/Playground/Playground.tsx](/home/ray/dev/linkong/planet/frontend/src/pages/Playground/Playground.tsx)
|
||||
- [frontend/src/App.tsx](/home/ray/dev/linkong/planet/frontend/src/App.tsx)
|
||||
- [frontend/src/components/AppLayout/AppLayout.tsx](/home/ray/dev/linkong/planet/frontend/src/components/AppLayout/AppLayout.tsx)
|
||||
- [frontend/src/index.css](/home/ray/dev/linkong/planet/frontend/src/index.css)
|
||||
|
||||
### 4. 前端布局规范沉淀
|
||||
|
||||
已完成:
|
||||
|
||||
- 把“一屏工作区、主模块优先、模块内部滚动”的规范文档化
|
||||
- 明确 `BGP` 页面为当前参考实现
|
||||
|
||||
相关文件:
|
||||
|
||||
- [docs/frontend-layout-guidelines.md](/home/ray/dev/linkong/planet/docs/frontend-layout-guidelines.md)
|
||||
- [frontend/src/pages/BGP/BGP.tsx](/home/ray/dev/linkong/planet/frontend/src/pages/BGP/BGP.tsx)
|
||||
|
||||
## 当前限制
|
||||
|
||||
### 1. Playground 还是 prompt playground,不是 agent playground
|
||||
|
||||
当前 `Playground` 的 `观察项 / 目标 / 约束条件` 都是人工输入。
|
||||
|
||||
模型现在拿到的是:
|
||||
|
||||
- 你手工输入的结构化字段
|
||||
- 后端传递的少量静态上下文
|
||||
|
||||
模型现在拿不到:
|
||||
|
||||
- 实时 BGP 事件
|
||||
- 真实告警列表
|
||||
- 数据源健康状态
|
||||
- 自动检索结果
|
||||
- tool calling / skills / 自主取数
|
||||
|
||||
### 2. `situational-awareness/analyze` 还是通用提示词接口
|
||||
|
||||
当前更适合:
|
||||
|
||||
- 测试链路
|
||||
- 测试模型输出风格
|
||||
- 验证不同 provider 是否正常返回
|
||||
|
||||
当前还不适合:
|
||||
|
||||
- 直接当真实态势系统主入口
|
||||
- 让用户手工维护长期分析模板
|
||||
- 代替专用业务研判接口
|
||||
|
||||
### 3. 还没有可验证的真实业务输入注入
|
||||
|
||||
目前最缺的是:
|
||||
|
||||
- 从业务系统自动整理“事实输入”
|
||||
- 再把这些事实喂给 AI
|
||||
|
||||
而不是继续让用户在 Playground 手工输入真实事件摘要。
|
||||
|
||||
## 短期计划
|
||||
|
||||
### Phase A: Playground 收敛为稳定测试台
|
||||
|
||||
目标:
|
||||
|
||||
- 保持 Playground 简洁可用
|
||||
- 不再继续堆“高级参数”
|
||||
|
||||
工作项:
|
||||
|
||||
- 继续微调左侧 `Provider 状态` 与 `测试说明` 的空间策略
|
||||
- 保持 `请求 / 结果` 为单一主工作区
|
||||
- 不引入盲填式高级字段
|
||||
- 统一滚动条、卡片、溢出行为
|
||||
|
||||
完成标准:
|
||||
|
||||
- 笔记本视口下依然可用
|
||||
- 各模块标题可见
|
||||
- 主要阅读区始终是右侧 Tabs
|
||||
|
||||
### Phase B: BGP AI 简报
|
||||
|
||||
目标:
|
||||
|
||||
- 不再依赖手工填写“观察项”
|
||||
- 让系统自动把真实 BGP 数据注入 AI
|
||||
|
||||
建议实现:
|
||||
|
||||
- 新增专用后端接口,例如:
|
||||
- `POST /api/v1/ai/bgp/brief`
|
||||
- 后端自动读取:
|
||||
- incidents summary
|
||||
- anomalies
|
||||
- recent events
|
||||
- collector coverage summary
|
||||
- 后端将结构化事实注入 `context / observations`
|
||||
- 前端在 BGP 页面增加“生成 AI 简报”
|
||||
|
||||
完成标准:
|
||||
|
||||
- 用户不需要手工录入 BGP 观察项
|
||||
- AI 输出能明确区分“事实”和“研判”
|
||||
|
||||
### Phase C: 告警 / 数据源健康 AI 简报
|
||||
|
||||
目标:
|
||||
|
||||
- 复用同样模式,扩展到其他模块
|
||||
|
||||
建议入口:
|
||||
|
||||
- `Alerts` 页面:异常与告警摘要
|
||||
- `DataSources` 页面:采集失败与健康状态总结
|
||||
|
||||
原则:
|
||||
|
||||
- 每个业务页优先做“专用 AI 简报”
|
||||
- 不优先做“万能大聊天框”
|
||||
|
||||
## 中期计划
|
||||
|
||||
### 1. Assessment Layer
|
||||
|
||||
目标:
|
||||
|
||||
- 不只返回自由文本
|
||||
- 返回结构化的 assessment
|
||||
|
||||
建议输出字段:
|
||||
|
||||
- summary
|
||||
- key_risks
|
||||
- evidence
|
||||
- recommendations
|
||||
- confidence
|
||||
- missing_data
|
||||
|
||||
这样后续才能:
|
||||
|
||||
- 持久化
|
||||
- 回看
|
||||
- 对比不同时间的 AI 结论
|
||||
- 在 Earth / Dashboard / BGP 页面稳定展示
|
||||
|
||||
### 2. Evidence-first Runtime
|
||||
|
||||
目标:
|
||||
|
||||
- 所有 AI 分析先取真实数据,再调模型
|
||||
|
||||
原则:
|
||||
|
||||
- 先 evidence
|
||||
- 再 prompt
|
||||
- 最后才是自由生成
|
||||
|
||||
优先要做的不是更强聊天,而是:
|
||||
|
||||
- 更稳定的数据注入
|
||||
- 更一致的事实模板
|
||||
- 更清晰的结果结构
|
||||
|
||||
### 3. 按页面提供专用入口
|
||||
|
||||
目标:
|
||||
|
||||
- 让 AI 成为业务视图的一部分,而不是孤立 playground
|
||||
|
||||
优先顺序建议:
|
||||
|
||||
1. `BGP` AI 简报
|
||||
2. `Alerts` AI 简报
|
||||
3. `DataSources` 健康研判
|
||||
4. `Dashboard` 总览总结
|
||||
|
||||
## 长期计划
|
||||
|
||||
### 1. Tool Calling / Agent Runtime
|
||||
|
||||
只有在以下基础稳定后再推进:
|
||||
|
||||
- 数据源健康信号稳定
|
||||
- BGP / Alerts / Datasource evidence 注入稳定
|
||||
- assessment 结构稳定
|
||||
|
||||
长期可做能力:
|
||||
|
||||
- AI 调用受控工具查询业务数据
|
||||
- AI 调用检索/web search 做外部验证
|
||||
- AI 生成建议而不是直接修改系统
|
||||
- 审核后触发受控动作
|
||||
|
||||
### 2. 受控动作与闭环
|
||||
|
||||
潜在方向:
|
||||
|
||||
- 根据健康异常生成修复建议
|
||||
- 根据态势变化生成处理建议
|
||||
- 进入 review queue
|
||||
- 审批后执行
|
||||
- 验证结果并形成闭环
|
||||
|
||||
### 3. 多模块统一 AI 体验
|
||||
|
||||
长期目标不是一个孤立 Playground,而是:
|
||||
|
||||
- 每个业务页都有自己的 AI 入口
|
||||
- 共享统一的 backend AI facade
|
||||
- 共享统一的 assessment 结构
|
||||
- 共享统一的 evidence 注入与审计链路
|
||||
|
||||
## 设计决策总结
|
||||
|
||||
### 为什么保留 `aiprovider`
|
||||
|
||||
因为它已经很好地承担了:
|
||||
|
||||
- provider 适配
|
||||
- 协议兼容
|
||||
- service token 边界
|
||||
- 独立重启与部署
|
||||
|
||||
因此短期内不建议把它并回 `backend`。
|
||||
|
||||
### 为什么 Playground 不做成万能聊天页
|
||||
|
||||
因为当前更需要的是:
|
||||
|
||||
- 稳定测试链路
|
||||
- 可验证业务输入
|
||||
- 专用分析入口
|
||||
|
||||
而不是一个泛化但没有真实数据支撑的聊天框。
|
||||
|
||||
### 为什么优先做专用 AI 简报
|
||||
|
||||
因为:
|
||||
|
||||
- 数据可以自动注入
|
||||
- 用户心智更清晰
|
||||
- 输出更容易结构化
|
||||
- 更容易校验事实与研判是否一致
|
||||
|
||||
## 下一步建议
|
||||
|
||||
按优先级建议接下来这样做:
|
||||
|
||||
1. 稳住 `Playground` 当前布局,不再大幅重做
|
||||
2. 在 `BGP` 页面新增专用 “AI 简报” 入口
|
||||
3. 后端新增 `BGP brief` 专用接口,自动注入真实数据
|
||||
4. 把 AI 输出逐步从自由文本升级为结构化 assessment
|
||||
168
docs/frontend-layout-guidelines.md
Normal file
168
docs/frontend-layout-guidelines.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Frontend Layout Guidelines
|
||||
|
||||
本项目后台页面默认遵循“单屏工作区”布局规范。目标不是让页面永远不溢出,而是确保在常见桌面视口下:
|
||||
|
||||
- 页面主结构能在一屏内看清
|
||||
- 用户能同时看到页头、摘要区和主工作区
|
||||
- 超出的内容在模块内部滚动,而不是把整页纵向撑爆
|
||||
|
||||
当前推荐参考实现:
|
||||
|
||||
- [frontend/src/pages/BGP/BGP.tsx](/home/ray/dev/linkong/planet/frontend/src/pages/BGP/BGP.tsx)
|
||||
- [frontend/src/index.css](/home/ray/dev/linkong/planet/frontend/src/index.css)
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 页面优先保证一屏工作区
|
||||
|
||||
管理页默认采用:
|
||||
|
||||
- 页头:标题、说明、主要操作
|
||||
- 主工作区:统计卡、表格、图表、列表、标签页
|
||||
|
||||
推荐结构:
|
||||
|
||||
```tsx
|
||||
<AppLayout>
|
||||
<div className="page-shell">
|
||||
<div className="page-shell__header">...</div>
|
||||
<div className="page-shell__body">...</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
```
|
||||
|
||||
页面总高度应被限制在 `AppLayout` 内容区内,而不是继续让整个页面自然向下增长。
|
||||
|
||||
### 2. 滚动优先发生在模块内部
|
||||
|
||||
如果表格、日志、长列表、图表明细超出空间:
|
||||
|
||||
- 让卡片内部滚动
|
||||
- 让表格内部滚动
|
||||
- 让标签页内容区内部滚动
|
||||
|
||||
不要默认依赖整个页面滚动去“解决”空间问题。
|
||||
|
||||
### 3. 主工作区必须拿到主要空间
|
||||
|
||||
页面里最重要的模块必须是视觉和空间上的主角。通常应保证:
|
||||
|
||||
- 页头始终可见
|
||||
- 摘要区高度被控制
|
||||
- 主表格 / 主图表 / 主分析区占据 50% 以上可视高度
|
||||
|
||||
如果一个页面有多个大模块,优先顺序是:
|
||||
|
||||
1. 先压缩说明区和摘要区
|
||||
2. 再把次级模块收进标签页或切换视图
|
||||
3. 最后才考虑继续增加整页滚动
|
||||
|
||||
### 4. 小屏幕和高缩放必须进入紧凑模式
|
||||
|
||||
在窗口高度较低、宽度较窄、或系统缩放较高时,应主动切换紧凑布局,例如:
|
||||
|
||||
- 缩小卡片 padding
|
||||
- 缩小表头和单元格间距
|
||||
- 将摘要区改为更紧凑的单行/横向滚动布局
|
||||
- 将次级模块移入标签页、抽屉、折叠区
|
||||
|
||||
紧凑模式的目标是保持可用,不是单纯把文字和控件一股脑缩小。
|
||||
|
||||
### 5. overflow 责任必须明确
|
||||
|
||||
页面中的大块内容必须明确:
|
||||
|
||||
- 谁负责占满剩余高度
|
||||
- 谁负责裁剪
|
||||
- 谁负责滚动
|
||||
|
||||
常见要求:
|
||||
|
||||
- 父容器链路需要 `min-height: 0`
|
||||
- 工作区容器通常需要 `display: flex`
|
||||
- 真正的滚动节点要显式 `overflow: auto`
|
||||
|
||||
## 推荐实现模式
|
||||
|
||||
### 页面骨架
|
||||
|
||||
优先复用项目里已有的通用结构:
|
||||
|
||||
- `.dashboard-content-inner`
|
||||
- `.page-shell`
|
||||
- `.page-shell__header`
|
||||
- `.page-shell__body`
|
||||
- `.table-scroll-region`
|
||||
|
||||
不要每个页面都重新发明一套完全不同的高度和滚动语义。
|
||||
|
||||
### 表格工作区
|
||||
|
||||
推荐模式:
|
||||
|
||||
```tsx
|
||||
<Card>
|
||||
<div className="table-scroll-region" ref={tableRegionRef}>
|
||||
<Table
|
||||
pagination={false}
|
||||
scroll={{ x: 1200, y: tableHeight }}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- 表格尽量在卡片内部滚动
|
||||
- `scroll.y` 应来自实际可用高度估算,而不是完全静态的魔法数字
|
||||
- 父容器链路要保证 header、body、content 的 overflow 都在表格内部闭合
|
||||
|
||||
### 多模块页面
|
||||
|
||||
如果一个页面同时有:
|
||||
|
||||
- 摘要卡
|
||||
- 表格
|
||||
- 异常明细
|
||||
- 最近事件
|
||||
|
||||
不建议简单纵向堆叠全部模块。优先使用:
|
||||
|
||||
- 顶部摘要 + 底部单一主工作区
|
||||
- 标签页切换多个次级数据视图
|
||||
- 左右分栏,并保证每栏内部独立滚动
|
||||
|
||||
## 不推荐的做法
|
||||
|
||||
以下模式默认视为不符合本项目页面规范:
|
||||
|
||||
- 依赖整页纵向滚动来显示主要工作区
|
||||
- 一个页面纵向堆 3 到 4 个大卡片,每个都想完整展示
|
||||
- 表格没有内部滚动,导致缩放后只能看到 1 到 2 行数据
|
||||
- 父容器缺少 `min-height: 0`,导致内部滚动失效
|
||||
- 只做视觉缩小,不处理真正的空间分配
|
||||
|
||||
## 页面验收检查清单
|
||||
|
||||
提交前至少检查:
|
||||
|
||||
- 页头、摘要区、主工作区能否同时出现
|
||||
- 主工作区是否拿到了页面中最多的高度
|
||||
- 表格或明细溢出时,滚动条是否出现在模块内部
|
||||
- 浏览器缩放到 `125%` / `150%` 时是否仍可用
|
||||
- 低高度窗口下是否还保有合理的可见内容行数
|
||||
- Tabs、Card、Table 在 overflow 时是否仍可操作
|
||||
|
||||
## 落地顺序
|
||||
|
||||
后续新增或重构后台页时,优先按这个顺序设计:
|
||||
|
||||
1. 先定义主工作区
|
||||
2. 再确定哪些模块必须常驻可见
|
||||
3. 最后再做样式和视觉层次
|
||||
|
||||
简单说:
|
||||
|
||||
- 先保证空间分配正确
|
||||
- 再处理滚动边界
|
||||
- 最后再做美化
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "planet-frontend",
|
||||
"version": "0.23.4",
|
||||
"version": "0.24.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.2.6",
|
||||
|
||||
@@ -8,6 +8,7 @@ import DataList from './pages/DataList/DataList'
|
||||
import Earth from './pages/Earth/Earth'
|
||||
import Settings from './pages/Settings/Settings'
|
||||
import BGP from './pages/BGP/BGP'
|
||||
import Playground from './pages/Playground/Playground'
|
||||
|
||||
function App() {
|
||||
const { token } = useAuthStore()
|
||||
@@ -26,6 +27,7 @@ function App() {
|
||||
<Route path="/datasources" element={<DataSources />} />
|
||||
<Route path="/data" element={<DataList />} />
|
||||
<Route path="/bgp" element={<BGP />} />
|
||||
<Route path="/playground" element={<Playground />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="*" element={<Navigate to="/admin" replace />} />
|
||||
</Routes>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
SettingOutlined,
|
||||
BarChartOutlined,
|
||||
DeploymentUnitOutlined,
|
||||
RobotOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
MenuFoldOutlined,
|
||||
} from '@ant-design/icons'
|
||||
@@ -34,6 +35,7 @@ function AppLayout({ children }: AppLayoutProps) {
|
||||
{ key: '/datasources', icon: <DatabaseOutlined />, label: '数据源' },
|
||||
{ key: '/data', icon: <BarChartOutlined />, label: '采集数据' },
|
||||
{ key: '/bgp', icon: <DeploymentUnitOutlined />, label: 'BGP观测' },
|
||||
{ key: '/playground', icon: <RobotOutlined />, label: 'AI Playground' },
|
||||
{ key: '/users', icon: <UserOutlined />, label: '用户管理' },
|
||||
{ key: '/settings', icon: <SettingOutlined />, label: '系统配置' },
|
||||
]
|
||||
|
||||
@@ -183,6 +183,454 @@ body {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.playground-page__grid {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.playground-page__body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.playground-shell {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.playground-shell__sidebar {
|
||||
flex: 0 0 340px;
|
||||
min-width: 300px;
|
||||
max-width: 360px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.playground-card {
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.playground-card .ant-card-head {
|
||||
min-height: 56px;
|
||||
padding: 0 20px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.playground-card .ant-card-head-title {
|
||||
padding: 14px 0;
|
||||
}
|
||||
|
||||
.playground-card__icon-button {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.playground-card__icon-button:hover {
|
||||
color: #1677ff !important;
|
||||
background: rgba(22, 119, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
.playground-card .ant-card-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
padding: 16px 20px 14px;
|
||||
}
|
||||
|
||||
.playground-card__scroll {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
|
||||
}
|
||||
|
||||
.playground-card__scroll::-webkit-scrollbar,
|
||||
.playground-result__content::-webkit-scrollbar,
|
||||
.playground-result__blocks-scroll::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.playground-card__scroll::-webkit-scrollbar-thumb,
|
||||
.playground-result__content::-webkit-scrollbar-thumb,
|
||||
.playground-result__blocks-scroll::-webkit-scrollbar-thumb {
|
||||
background: rgba(148, 163, 184, 0.82);
|
||||
border-radius: 999px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.playground-card__scroll::-webkit-scrollbar-thumb:hover,
|
||||
.playground-result__content::-webkit-scrollbar-thumb:hover,
|
||||
.playground-result__blocks-scroll::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(100, 116, 139, 0.9);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.playground-card__scroll::-webkit-scrollbar-track,
|
||||
.playground-result__content::-webkit-scrollbar-track,
|
||||
.playground-result__blocks-scroll::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.playground-card--provider {
|
||||
flex: 1 1 auto;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.playground-card--provider .ant-card-body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.playground-card--help {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.playground-card--workspace {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.playground-card--workspace .ant-card-body,
|
||||
.playground-card--result .ant-card-body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.playground-tabs,
|
||||
.playground-tabs .ant-tabs-content-holder,
|
||||
.playground-tabs .ant-tabs-content,
|
||||
.playground-tabs .ant-tabs-tabpane {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.playground-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.playground-tabs .ant-tabs-nav {
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.playground-tabs .ant-tabs-content-holder {
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.playground-tabpane {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-right: 2px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
|
||||
}
|
||||
|
||||
.playground-tabpane::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.playground-tabpane::-webkit-scrollbar-thumb {
|
||||
background: rgba(148, 163, 184, 0.82);
|
||||
border-radius: 999px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.playground-tabpane::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(100, 116, 139, 0.9);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.playground-tabpane::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.playground-form__grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.playground-form__grid--bottom {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.playground-form__grid .ant-form-item {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.playground-tabpane .ant-form {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.playground-form__actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.playground-card--request .ant-form-item {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.playground-card--request .ant-form-item-label {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.playground-card--request .ant-form-item-label > label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.playground-card--result {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.playground-card--result .ant-card-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.playground-provider-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.playground-kv {
|
||||
display: grid;
|
||||
grid-template-columns: 88px minmax(0, 1fr);
|
||||
align-items: start;
|
||||
column-gap: 14px;
|
||||
row-gap: 4px;
|
||||
}
|
||||
|
||||
.playground-kv > .ant-typography:first-child {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.playground-kv > .ant-typography:last-child,
|
||||
.playground-kv .playground-status-tags {
|
||||
min-width: 0;
|
||||
justify-self: end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.playground-card--provider .ant-card-body {
|
||||
padding-top: 18px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.playground-card--provider .playground-kv + .playground-kv {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.playground-kv--top {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.playground-status-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.playground-code-block {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.playground-note {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.playground-help {
|
||||
flex: 0 0 auto;
|
||||
min-height: 0;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.playground-help--collapsed {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.playground-help--expanded {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.playground-help.ant-collapse {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.playground-help > .ant-collapse-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.playground-help .ant-collapse-header {
|
||||
padding: 14px 20px !important;
|
||||
align-items: center !important;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.playground-help .ant-collapse-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.playground-help .ant-collapse-content-box {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
|
||||
padding: 12px 16px 16px !important;
|
||||
}
|
||||
|
||||
.playground-help .ant-collapse-content-box::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.playground-help .ant-collapse-content-box::-webkit-scrollbar-thumb {
|
||||
background: rgba(148, 163, 184, 0.82);
|
||||
border-radius: 999px;
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.playground-help .ant-collapse-content-box::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(100, 116, 139, 0.9);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.playground-help .ant-collapse-content-box::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.playground-note.ant-alert {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.playground-note .ant-alert-icon {
|
||||
margin-inline-end: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.playground-note .ant-alert-message {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.playground-note .ant-alert-description {
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.playground-result__loading {
|
||||
flex: 1 1 auto;
|
||||
min-height: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.playground-result__stack {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.playground-result__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.playground-result__content,
|
||||
.playground-result__blocks .ant-card {
|
||||
border-radius: 14px;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.playground-result__content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
|
||||
}
|
||||
|
||||
.playground-result__content pre,
|
||||
.playground-result__blocks pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.playground-result__blocks {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.playground-result__blocks-scroll {
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.playground-page__header {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.playground-form__grid--bottom {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.playground-shell {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.playground-shell__sidebar {
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.data-source-tabs-shell,
|
||||
.data-source-tabs,
|
||||
|
||||
350
frontend/src/pages/Playground/Playground.tsx
Normal file
350
frontend/src/pages/Playground/Playground.tsx
Normal file
@@ -0,0 +1,350 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import axios from 'axios'
|
||||
import { SyncOutlined } from '@ant-design/icons'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Collapse,
|
||||
Form,
|
||||
Input,
|
||||
Space,
|
||||
Spin,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd'
|
||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
const API_BASE_URL = (import.meta as any).env?.VITE_API_URL || '/api/v1'
|
||||
const PLAYGROUND_PROVIDER_STATUS_STORAGE_KEY = 'playground-provider-status'
|
||||
|
||||
interface AIProviderStatus {
|
||||
provider: string
|
||||
api?: string | null
|
||||
enabled: boolean
|
||||
configured: boolean
|
||||
model?: string | null
|
||||
base_url?: string | null
|
||||
}
|
||||
|
||||
interface AIContentBlock {
|
||||
type: string
|
||||
text?: string | null
|
||||
thinking?: string | null
|
||||
}
|
||||
|
||||
interface SituationalAnalysisResponse {
|
||||
provider: string
|
||||
model: string
|
||||
content: string
|
||||
content_blocks: AIContentBlock[]
|
||||
text_blocks: string[]
|
||||
thinking_blocks: string[]
|
||||
raw_response: Record<string, unknown>
|
||||
}
|
||||
|
||||
interface PlaygroundFormValues {
|
||||
title: string
|
||||
objective: string
|
||||
observations?: string
|
||||
constraints?: string
|
||||
}
|
||||
|
||||
function splitLines(value?: string) {
|
||||
return (value || '')
|
||||
.split('\n')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function Playground() {
|
||||
const [form] = Form.useForm<PlaygroundFormValues>()
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
const [statusLoading, setStatusLoading] = useState(false)
|
||||
const [analyzing, setAnalyzing] = useState(false)
|
||||
const [providerStatus, setProviderStatus] = useState<AIProviderStatus | null>(null)
|
||||
const [analysis, setAnalysis] = useState<SituationalAnalysisResponse | null>(null)
|
||||
const [activeTab, setActiveTab] = useState<'request' | 'result'>('request')
|
||||
const [helpExpanded, setHelpExpanded] = useState(true)
|
||||
|
||||
const loadProviderStatus = async (force = false) => {
|
||||
if (!force) {
|
||||
const cached = sessionStorage.getItem(PLAYGROUND_PROVIDER_STATUS_STORAGE_KEY)
|
||||
if (cached) {
|
||||
try {
|
||||
setProviderStatus(JSON.parse(cached) as AIProviderStatus)
|
||||
return
|
||||
} catch {
|
||||
sessionStorage.removeItem(PLAYGROUND_PROVIDER_STATUS_STORAGE_KEY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setStatusLoading(true)
|
||||
try {
|
||||
const res = await axios.get<AIProviderStatus>(`${API_BASE_URL}/ai/provider/status`)
|
||||
setProviderStatus(res.data)
|
||||
sessionStorage.setItem(PLAYGROUND_PROVIDER_STATUS_STORAGE_KEY, JSON.stringify(res.data))
|
||||
} catch {
|
||||
messageApi.error('AI Provider 状态获取失败')
|
||||
} finally {
|
||||
setStatusLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void loadProviderStatus()
|
||||
}, [])
|
||||
|
||||
const handleAnalyze = async (values: PlaygroundFormValues) => {
|
||||
setAnalyzing(true)
|
||||
setActiveTab('result')
|
||||
try {
|
||||
const res = await axios.post<SituationalAnalysisResponse>(
|
||||
`${API_BASE_URL}/ai/situational-awareness/analyze`,
|
||||
{
|
||||
title: values.title.trim(),
|
||||
objective: values.objective.trim(),
|
||||
observations: splitLines(values.observations),
|
||||
constraints: splitLines(values.constraints),
|
||||
context: {
|
||||
source: 'playground',
|
||||
},
|
||||
},
|
||||
)
|
||||
setAnalysis(res.data)
|
||||
messageApi.success('分析已完成')
|
||||
} catch {
|
||||
messageApi.error('分析失败,请检查 AI Provider 配置或稍后再试')
|
||||
setActiveTab('request')
|
||||
} finally {
|
||||
setAnalyzing(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
{contextHolder}
|
||||
<div className="page-shell playground-page">
|
||||
<div className="page-shell__header playground-page__header">
|
||||
<div>
|
||||
<Title level={3} style={{ marginBottom: 4 }}>AI Playground</Title>
|
||||
<Text type="secondary">
|
||||
在前端测试 AI 能力,但实际请求仍统一经由主后端转发到 AI Provider。
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="page-shell__body playground-page__body">
|
||||
<div className="playground-shell">
|
||||
<div className="playground-shell__sidebar">
|
||||
<Card
|
||||
className="playground-card playground-card--provider"
|
||||
title="Provider 状态"
|
||||
extra={(
|
||||
<Button
|
||||
type="text"
|
||||
shape="circle"
|
||||
icon={<SyncOutlined spin={statusLoading} />}
|
||||
onClick={() => void loadProviderStatus(true)}
|
||||
aria-label="刷新 Provider 状态"
|
||||
className="playground-card__icon-button"
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<div className="playground-card__scroll">
|
||||
<div className="playground-panel__section">
|
||||
<Spin spinning={statusLoading}>
|
||||
{providerStatus ? (
|
||||
<div className="playground-provider-panel">
|
||||
<div className="playground-kv">
|
||||
<Text type="secondary">Provider</Text>
|
||||
<Text strong>{providerStatus.provider || '-'}</Text>
|
||||
</div>
|
||||
<div className="playground-kv">
|
||||
<Text type="secondary">模型</Text>
|
||||
<Text strong>{providerStatus.model || '-'}</Text>
|
||||
</div>
|
||||
<div className="playground-kv">
|
||||
<Text type="secondary">API</Text>
|
||||
<Text code>{providerStatus.api || '-'}</Text>
|
||||
</div>
|
||||
<div className="playground-kv">
|
||||
<Text type="secondary">状态</Text>
|
||||
<div className="playground-status-tags">
|
||||
<Tag color={providerStatus.enabled ? 'green' : 'default'}>
|
||||
{providerStatus.enabled ? 'enabled' : 'disabled'}
|
||||
</Tag>
|
||||
<Tag color={providerStatus.configured ? 'blue' : 'volcano'}>
|
||||
{providerStatus.configured ? 'configured' : 'not configured'}
|
||||
</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div className="playground-kv playground-kv--top">
|
||||
<Text type="secondary">Base URL</Text>
|
||||
<Text code className="playground-code-block">
|
||||
{providerStatus.base_url || '-'}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Alert type="warning" showIcon message="尚未获取到 AI Provider 状态" />
|
||||
)}
|
||||
</Spin>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Collapse
|
||||
className={`playground-help${helpExpanded ? ' playground-help--expanded' : ' playground-help--collapsed'}`}
|
||||
defaultActiveKey={['help']}
|
||||
onChange={(keys) => setHelpExpanded(Array.isArray(keys) ? keys.includes('help') : keys === 'help')}
|
||||
items={[
|
||||
{
|
||||
key: 'help',
|
||||
label: '测试说明',
|
||||
children: (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
className="playground-note"
|
||||
message="用于验证 AI Provider 是否可用,以及 backend -> aiprovider 链路是否通畅。"
|
||||
description={(
|
||||
<span>
|
||||
当前链路:
|
||||
<Text code>frontend /playground</Text>
|
||||
{' -> '}
|
||||
<Text code>backend /api/v1/ai/*</Text>
|
||||
{' -> '}
|
||||
<Text code>aiprovider /v1/*</Text>
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Card className="playground-card playground-card--workspace">
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={(key) => setActiveTab(key as 'request' | 'result')}
|
||||
className="playground-tabs"
|
||||
items={[
|
||||
{
|
||||
key: 'request',
|
||||
label: '请求',
|
||||
children: (
|
||||
<div className="playground-tabpane">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
title: 'BGP 告警态势简报',
|
||||
objective: '总结当前告警的主要风险、优先级与建议动作。',
|
||||
observations: '出现新的高危告警\n部分观测站最近 24h 事件增多',
|
||||
constraints: '结论要简洁\n优先给出操作建议',
|
||||
}}
|
||||
onFinish={handleAnalyze}
|
||||
>
|
||||
<Form.Item
|
||||
label="标题"
|
||||
name="title"
|
||||
rules={[{ required: true, message: '请输入标题' }]}
|
||||
>
|
||||
<Input maxLength={200} placeholder="例如:BGP 告警态势简报" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="目标"
|
||||
name="objective"
|
||||
rules={[{ required: true, message: '请输入分析目标' }]}
|
||||
>
|
||||
<Input.TextArea rows={4} maxLength={1000} placeholder="希望模型帮助完成什么分析" />
|
||||
</Form.Item>
|
||||
|
||||
<div className="playground-form__grid playground-form__grid--bottom">
|
||||
<Form.Item label="观察项" name="observations">
|
||||
<Input.TextArea rows={6} placeholder="每行一条观察,例如:某观测站事件显著增加" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="约束条件" name="constraints">
|
||||
<Input.TextArea rows={6} placeholder="每行一条限制,例如:回答保持简洁" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
<div className="playground-form__actions">
|
||||
<Button type="primary" htmlType="submit" loading={analyzing}>
|
||||
开始分析
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
form.resetFields()
|
||||
setAnalysis(null)
|
||||
}}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'result',
|
||||
label: '结果',
|
||||
children: (
|
||||
<div className="playground-tabpane">
|
||||
{analyzing ? (
|
||||
<div className="playground-result__loading">
|
||||
<Spin />
|
||||
<Text type="secondary">AI 正在生成结果...</Text>
|
||||
</div>
|
||||
) : analysis ? (
|
||||
<Space direction="vertical" size={16} className="playground-result__stack">
|
||||
<div className="playground-result__meta">
|
||||
<Tag color="blue">{analysis.provider}</Tag>
|
||||
<Tag>{analysis.model}</Tag>
|
||||
</div>
|
||||
<div className="playground-result__content">
|
||||
<pre>{analysis.content}</pre>
|
||||
</div>
|
||||
{analysis.text_blocks.length ? (
|
||||
<div className="playground-result__blocks">
|
||||
<Text strong>文本块</Text>
|
||||
<div className="playground-result__blocks-scroll">
|
||||
{analysis.text_blocks.map((block, index) => (
|
||||
<Card key={`${index}-${block.slice(0, 12)}`} size="small">
|
||||
<pre>{block}</pre>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Space>
|
||||
) : (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="这里会显示 AI 返回的分析结果"
|
||||
description="先切到“请求”填写内容并点击“开始分析”。"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default Playground
|
||||
@@ -1236,8 +1236,8 @@ start() {
|
||||
log_success "启动完成"
|
||||
log_note "智能星球计划: http://localhost:${FRONTEND_PORT}/earth"
|
||||
log_note "智能星球仪表盘: http://localhost:${FRONTEND_PORT}/admin"
|
||||
log_note "AI Playground: http://localhost:${FRONTEND_PORT}/playground"
|
||||
log_note "智能星球开发文档: http://localhost:${BACKEND_PORT}/docs"
|
||||
log_note "智能星球AI: http://localhost:${AI_PROVIDER_PORT}/chat"
|
||||
}
|
||||
|
||||
stop() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "planet"
|
||||
version = "0.23.4"
|
||||
version = "0.24.0"
|
||||
description = "智能星球计划 - 态势感知系统"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
|
||||
Reference in New Issue
Block a user