Merge branch 'refactor/gui-redesign': Agent-First GUI redesign with Design Token system

- U1: Design Token system (tokens.css + theme.ts)
- U2: Four-quadrant Agent-First layout with TopNav
- U3: Chat panel refactor (markdown-it + ToolCallIndicator)
- U4: Code/preview panel (CodeDiffViewer + FileTree)
- U5: Terminal panel refactor (One Dark Pro + Ant Design Modal)
- U6: Evolution panel simplification + grouped settings
- U7: Transitions + responsive breakpoints
- Color migration: 15+ components migrated to Design Tokens
- Code review fixes: ARIA a11y, XSS protection, touch/keyboard support,
  path traversal protection, lazy-loading, ANSI span balance
This commit is contained in:
chiguyong 2026-06-13 10:08:37 +08:00
commit b89da90fd9
119 changed files with 6351 additions and 1737 deletions

View File

@ -0,0 +1,417 @@
---
title: "refactor: Agent-First GUI Redesign"
status: completed
created: 2026-06-13
origin: docs/brainstorms/2026-06-13-gui-redesign-requirements.md
---
# Plan: Agent-First GUI Redesign
## Summary
将 Fischer AgentKit GUI 从 SideNav 多页面布局重构为 Agent-First 四象限全屏布局,建立统一 Design Token 体系,采用浅色极简视觉风格。分 3 个迭代渐进式完成,每个迭代可独立部署。
## Problem Frame
当前 GUI 处于"功能可用但视觉粗糙"状态300 处硬编码颜色、无设计系统、SideNav 多页面布局无法同时展示 Agent 活动、无响应式支持。竞品Devin/Cursor/v0.dev已普遍采用 Agent-First 全屏布局 + 统一设计系统。
## Requirements Trace
| R-ID | Requirement | Priority |
|------|-------------|----------|
| R1 | Design Token 体系 | P0 |
| R2 | Agent-First 全屏布局 | P0 |
| R3 | 对话面板重构 | P1 |
| R4 | 代码/预览面板 | P1 |
| R5 | 终端面板重构 | P1 |
| R6 | 状态/监控面板(进化精简) | P1 |
| R7 | 工作流单页化 | P2 |
| R8 | 设置分组化 | P2 |
| R9 | 过渡动画与微交互 | P2 |
| R10 | 响应式断点 | P2 |
## Key Technical Decisions
### KTD1: Design Token 双轨制 — CSS 变量 + Ant Design Theme Token
**决策:** 同时建立 CSS 自定义属性(`var(--color-primary)`)和 Ant Design Vue ConfigProvider theme token两者通过映射表保持同步。
**理由:** Ant Design Vue 4.x 的 CSS-in-JS 架构通过 `theme.token` 控制组件内部样式,但自定义组件和 ECharts 无法读取 antd token。CSS 变量作为通用层antd token 作为组件层,映射表桥接两者。
### KTD2: 四象限布局实现 — CSS Grid + 可拖拽分隔线
**决策:** 使用 CSS Grid 实现四象限基础布局,自定义 `SplitPane` 组件实现可拖拽分隔线,比例持久化到 localStorage。
**理由:** CSS Grid 天然支持二维布局和 `fr` 单位,比 Flexbox 更适合四象限。自定义 SplitPane 比引入第三方库(如 splitpanes更轻量且可精确控制样式。
### KTD3: 路由策略 — 保留 Vue Router页面切换变为象限内容切换
**决策:** 保留 Vue Router 但重构路由结构。顶级路由改为象限内容路由(`/chat`、`/code`、`/terminal`、`/monitor`),通过路由参数控制各象限显示内容。旧路由(`/workflow`、`/knowledge`、`/skills`、`/evolution`、`/settings`)重定向到新的象限路由。
**理由:** 保留路由可维持 URL 可访问性、浏览器前进/后退、深层链接。象限内容切换比整页跳转更流畅。
### KTD4: 视觉风格 — 浅色极简 + Vercel 式紫黑渐变
**决策:** 主背景白色/极浅灰,强调色使用紫黑渐变(`#7c3aed` → `#1e1b4b`),代码/终端区域使用深色背景One Dark Pro 配色)。
**理由:** 用户选择浅色极简风格。Vercel/v0.dev 的紫黑渐变是成熟的浅色极简强调色方案,与白色背景形成强对比。
## Scope Boundaries
### In Scope
- Design Token 体系建立
- 四象限全屏布局重构
- 所有现有功能迁移
- 浅色极简视觉风格
- 过渡动画和微交互
- 1280px+ 响应式断点
### Deferred to Follow-Up Work
- 暗色模式(需 Design Token 暗色变体)
- 移动端适配
- Computer Use 功能实现
- Cmd+K 内联编辑
- @-mention 上下文引用
- 代码 Diff Accept/Reject 实际回滚
### Outside This Product's Identity
- 多用户协作/实时协同编辑
- 插件市场
- 代码编辑器(只读预览)
---
## Implementation Units
### U1. Design Token 体系 + 主题配置
**Goal:** 建立统一的设计令牌系统,消除所有硬编码颜色/间距/圆角值。
**Requirements:** R1
**Dependencies:** 无
**Files:**
- Create: `src/agentkit/server/frontend/src/styles/tokens.css`
- Create: `src/agentkit/server/frontend/src/styles/theme.ts`
- Create: `src/agentkit/server/frontend/src/styles/index.ts`
- Modify: `src/agentkit/server/frontend/src/App.vue`
- Modify: `src/agentkit/server/frontend/src/main.ts`
**Approach:**
1. 创建 `tokens.css`,定义 CSS 自定义属性颜色primary/secondary/success/error/warning/灰阶/背景/边框、间距4/8/12/16/24/32px、圆角4/6/8/12px、字体12/13/14/16/20/24px、阴影sm/md/lg
2. 创建 `theme.ts`,定义 Ant Design Vue theme token 映射(将 CSS 变量值映射到 antd token
3. 在 `App.vue` 的 ConfigProvider 中注入 theme 配置
4. 在 `main.ts` 中引入 `tokens.css`
5. 统一主色为 `#7c3aed`(紫黑渐变起始色),消除 `#1677ff`/`#1890ff` 混用
**Patterns to follow:** Ant Design Vue 4.x theme customization API (ConfigProvider theme prop)
**Test scenarios:**
- 验证 CSS 变量在浏览器中正确计算(`getComputedStyle(document.documentElement).getPropertyValue('--color-primary')` 返回 `#7c3aed`
- 验证 Ant Design 组件使用新主题色Button primary 颜色为 `#7c3aed`
- 验证自定义组件可通过 `var(--color-primary)` 引用主题色
**Verification:** 所有 CSS 变量可计算Ant Design 组件使用新主题色,无硬编码 `#1677ff`/`#1890ff`。
---
### U2. 四象限全屏布局 + 顶部导航
**Goal:** 将 SideNav 多页面布局重构为四象限全屏布局,顶部极简导航栏。
**Requirements:** R2
**Dependencies:** U1
**Files:**
- Create: `src/agentkit/server/frontend/src/components/layout/AgentLayout.vue`
- Create: `src/agentkit/server/frontend/src/components/layout/SplitPane.vue`
- Create: `src/agentkit/server/frontend/src/components/layout/TopNav.vue`
- Create: `src/agentkit/server/frontend/src/components/layout/QuadrantPanel.vue`
- Modify: `src/agentkit/server/frontend/src/App.vue`
- Modify: `src/agentkit/server/frontend/src/router/index.ts`
- Preserve: `src/agentkit/server/frontend/src/components/layout/AppLayout.vue`(旧布局保留,路由切换)
**Approach:**
1. 创建 `AgentLayout.vue`CSS Grid 四象限布局,每个象限是一个 `<QuadrantPanel>`,支持折叠/展开
2. 创建 `SplitPane.vue`:可拖拽分隔线组件,支持水平/垂直方向,比例持久化到 localStorage
3. 创建 `TopNav.vue`48px 高度顶部导航栏,包含 Logo、任务选择器、Agent 状态指示、模式切换、设置入口
4. 创建 `QuadrantPanel.vue`象限容器组件支持标题栏、折叠按钮、Tab 切换
5. 修改路由:新增 `/agent` 路由使用 AgentLayout旧路由保留 AppLayout 作为兼容
6. 默认路由 `/` 重定向到 `/agent`
**Patterns to follow:** Devin 四象限布局模式CSS Grid `grid-template-rows/columns` + `fr` 单位
**Test scenarios:**
- 四象限正确渲染,各象限可独立折叠/展开
- 分隔线可拖拽调整比例,刷新后比例保持
- 顶部导航栏正确显示 Logo、状态指示、设置入口
- 1280px 以下右下象限自动折叠
**Verification:** 四象限布局可交互,分隔线可拖拽,比例持久化,响应式断点生效。
---
### U3. 对话面板重构(左上象限)
**Goal:** 将 ChatView 重构为对话面板,支持 Markdown 渲染和工具调用指示器。
**Requirements:** R3
**Dependencies:** U1, U2
**Files:**
- Modify: `src/agentkit/server/frontend/src/views/ChatView.vue`
- Modify: `src/agentkit/server/frontend/src/components/chat/ChatMessage.vue`
- Modify: `src/agentkit/server/frontend/src/components/chat/ChatInput.vue`
- Modify: `src/agentkit/server/frontend/src/components/chat/ChatSidebar.vue`
- Create: `src/agentkit/server/frontend/src/components/chat/ToolCallIndicator.vue`
- Create: `src/agentkit/server/frontend/src/components/chat/ContextPill.vue`
**Approach:**
1. ChatMessage替换 `v-html` 为 Markdown 渲染(使用 `markdown-it``marked`),添加工具调用指示器(`[Read]`/`[Edit]`/`[Bash]` 彩色标签)
2. ChatInput添加上下文胶囊Context Pills显示当前关联的文件/技能
3. ChatSidebar改为可折叠侧栏默认折叠
4. 流式输出添加打字机效果
5. 所有硬编码颜色替换为 Design Token 引用
**Patterns to follow:** Claude Code 的 Tool Use IndicatorsTrae 的 Context Pills
**Test scenarios:**
- Markdown 正确渲染(标题、代码块、列表、链接)
- 工具调用指示器正确显示类型和颜色
- 上下文胶囊显示关联文件名
- 流式输出打字机效果平滑
**Verification:** 对话面板功能完整Markdown 渲染正确,工具调用可视化,无 `v-html`
---
### U4. 代码/预览面板(右上象限)
**Goal:** 新增代码 Diff 查看和文件预览能力,集成工作流画布和知识库管理。
**Requirements:** R4, R7
**Dependencies:** U1, U2
**Files:**
- Create: `src/agentkit/server/frontend/src/components/code/CodeDiffViewer.vue`
- Create: `src/agentkit/server/frontend/src/components/code/FileTree.vue`
- Modify: `src/agentkit/server/frontend/src/views/WorkflowView.vue`(单页化)
- Modify: `src/agentkit/server/frontend/src/views/KnowledgeBaseView.vue`
- Modify: `src/agentkit/server/frontend/src/components/workflow/FlowCanvas.vue`
- Modify: `src/agentkit/server/frontend/src/components/workflow/NodePalette.vue`
- Modify: `src/agentkit/server/frontend/src/components/workflow/PropertyPanel.vue`
**Approach:**
1. 创建 `CodeDiffViewer.vue`:只读代码 Diff 展示,支持逐行高亮(红/绿),语法高亮
2. 创建 `FileTree.vue`:文件树浏览器,展示 Agent 修改的文件列表
3. 工作流单页化:列表和编辑在同一象限内通过 Tab 切换
4. 知识库管理集成为此象限的一个 Tab
5. 象限 Tab 切换:代码 / 工作流 / 知识库
6. 所有硬编码颜色替换为 Design Token
**Patterns to follow:** Cursor Composer 的 Diff 展示v0.dev 的 Tab 切换
**Test scenarios:**
- 代码 Diff 正确高亮删除/新增行
- 文件树正确展示文件结构
- 工作流列表/编辑 Tab 切换流畅
- 知识库 Tab 正常工作
- 象限 Tab 切换无闪烁
**Verification:** 右上象限支持三种内容切换,代码 Diff 可视化,工作流单页化完成。
---
### U5. 终端面板重构(左下象限)
**Goal:** 将 TerminalView 重构为终端面板,使用 Ant Design 组件替代原生 HTML。
**Requirements:** R5
**Dependencies:** U1, U2
**Files:**
- Modify: `src/agentkit/server/frontend/src/views/TerminalView.vue`
- Modify: `src/agentkit/server/frontend/src/components/terminal/TerminalEmulator.vue`
- Modify: `src/agentkit/server/frontend/src/components/terminal/CommandHistory.vue`
**Approach:**
1. 终端背景改为 One Dark Pro 配色(深色背景 + 语法高亮色)
2. 命令确认弹窗从原生 HTML 改为 Ant Design Modal
3. 命令历史侧栏改为可折叠
4. 输入框从原生 `<input>` 改为 Ant Design Input
5. 所有硬编码颜色替换为 Design Token
**Patterns to follow:** One Dark Pro 终端配色Ant Design Modal 组件
**Test scenarios:**
- 终端输出 ANSI 颜色正确渲染
- 命令确认弹窗使用 Ant Design Modal 样式
- 命令历史可折叠展开
- WebSocket 连接/断开正常
**Verification:** 终端面板视觉统一,无原生 HTML 弹窗One Dark Pro 配色生效。
---
### U6. 状态/监控面板(右下象限)+ 进化精简
**Goal:** 将 EvolutionView 精简后集成为右下象限,集成技能和设置。
**Requirements:** R6, R8
**Dependencies:** U1, U2
**Files:**
- Modify: `src/agentkit/server/frontend/src/views/EvolutionView.vue`
- Modify: `src/agentkit/server/frontend/src/components/evolution/DashboardOverview.vue`
- Modify: `src/agentkit/server/frontend/src/components/evolution/MetricsChart.vue`
- Modify: `src/agentkit/server/frontend/src/components/evolution/UsagePanel.vue`
- Modify: `src/agentkit/server/frontend/src/views/SkillsView.vue`
- Modify: `src/agentkit/server/frontend/src/views/SettingsView.vue`
- Delete: `src/agentkit/server/frontend/src/components/evolution/PitfallRoutePanel.vue`
- Delete: `src/agentkit/server/frontend/src/components/evolution/OptimizationPanel.vue`
- Delete: `src/agentkit/server/frontend/src/components/evolution/MetricsPanel.vue`
- Delete: `src/agentkit/server/frontend/src/components/evolution/ExperiencePanel.vue`
**Approach:**
1. 进化面板精简6 个子面板合并为 3 个 Tab — 概览+指标、经验+坑点、用量
2. DashboardOverview4 列统计卡片改为 2 列,增加趋势迷你图
3. 设置分组化4 个 TabLLM/技能/知识库/系统),每组独立保存
4. 象限 Tab 切换:监控 / 技能 / 设置
5. 删除不再需要的包装组件PitfallRoutePanel/OptimizationPanel/MetricsPanel/ExperiencePanel
6. 所有硬编码颜色替换为 Design Token
**Patterns to follow:** Devin 的 Action TimelineAnt Design Tabs 分组
**Test scenarios:**
- 进化概览+指标 Tab 正确展示
- 经验+坑点 Tab 合并后功能完整
- 设置分组 Tab 切换正常,每组独立保存
- 技能 Tab 正常展示和安装
**Verification:** 右下象限支持三种内容切换,进化面板精简完成,设置分组化完成。
---
### U7. 过渡动画 + 微交互 + 响应式
**Goal:** 为所有交互添加过渡动画,实现 1280px+ 响应式断点。
**Requirements:** R9, R10
**Dependencies:** U1, U2, U3, U4, U5, U6
**Files:**
- Create: `src/agentkit/server/frontend/src/styles/transitions.css`
- Create: `src/agentkit/server/frontend/src/styles/responsive.css`
- Modify: `src/agentkit/server/frontend/src/components/layout/AgentLayout.vue`
- Modify: all view and component files (transition additions)
**Approach:**
1. 创建 `transitions.css`:定义 Vue transition 类fade/slide/collapse/stagger统一时长150ms/200ms/300ms
2. 象限折叠/展开:平滑过渡 200ms ease
3. Tab 切换:淡入淡出 150ms
4. 列表项加载:交错渐入 stagger 50ms
5. 空状态:品牌化插图 + 引导文案(替代 `<a-empty>`
6. 加载态:骨架屏替代 `<a-spin>`
7. 创建 `responsive.css`定义断点≥1440px 四象限完整1280-1440px 右下折叠,<1280px 提示
8. 象限比例记忆localStorage 保存用户调整的比例
**Patterns to follow:** Vue `<Transition>` 组件CSS `@media` 断点
**Test scenarios:**
- 象限折叠/展开动画平滑无卡顿
- Tab 切换淡入淡出效果正确
- 列表项交错渐入效果
- 1440px+ 四象限完整展示
- 1280-1440px 右下象限自动折叠
- <1280px 显示提示信息
- 刷新后象限比例保持
**Verification:** 所有过渡动画生效,响应式断点正确,比例持久化。
---
## High-Level Technical Design
### 四象限布局架构
```
┌─────────────────────────────────────────────────────────┐
│ TopNav (48px) │
│ [Logo] [TaskSelector] [AgentStatus] [ModeToggle] [⚙] │
├──────────────────────────┬──────────────────────────────┤
│ │ │
│ QuadrantPanel │ QuadrantPanel │
│ position="top-left" │ position="top-right" │
│ ┌─ Tabs ────────────┐ │ ┌─ Tabs ────────────────┐ │
│ │ Chat (default) │ │ │ Code/Diff (default) │ │
│ │ Agent Log │ │ │ Workflow │ │
│ └────────────────────┘ │ │ Knowledge Base │ │
│ │ └────────────────────────┘ │
│ ← SplitPane (vertical) →│ │
├──────────────────────────┼──────────────────────────────┤
│ │ │
│ QuadrantPanel │ QuadrantPanel │
│ position="bottom-left" │ position="bottom-right" │
│ ┌─ Tabs ────────────┐ │ ┌─ Tabs ────────────────┐ │
│ │ Terminal (default) │ │ │ Monitor (default) │ │
│ │ Command History │ │ │ Skills │ │
│ └────────────────────┘ │ │ Settings │ │
│ │ └────────────────────────┘ │
└──────────────────────────┴──────────────────────────────┘
```
### Design Token 映射架构
```
tokens.css (CSS Custom Properties)
├─→ 自定义组件 (via var(--xxx))
└─→ theme.ts (Ant Design Theme Token Mapping)
└─→ ConfigProvider :theme
└─→ Ant Design 组件 (via CSS-in-JS)
```
### 路由重构
```
/ → /agent (AgentLayout)
/agent/chat → 左上象限显示 Chat
/agent/code → 右上象限显示 Code/Diff
/agent/terminal → 左下象限显示 Terminal
/agent/monitor → 右下象限显示 Monitor
旧路由兼容:
/workflow → /agent/code?tab=workflow
/knowledge → /agent/code?tab=knowledge
/skills → /agent/monitor?tab=skills
/evolution → /agent/monitor?tab=monitor
/settings → /agent/monitor?tab=settings
/terminal → /agent/terminal
```
---
## Risks & Mitigations
| Risk | Impact | Mitigation |
|------|--------|------------|
| 300 处硬编码颜色迁移遗漏 | 视觉不一致 | U1 完成后全局搜索 `#[0-9a-fA-F]{3,6}` 验证零残留 |
| Vue Flow 在象限内 resize 问题 | 画布渲染异常 | SplitPane 拖拽结束时触发 window resize 事件 |
| ECharts 颜色需单独处理 | 图表颜色不跟随主题 | 在 ECharts 初始化时从 CSS 变量读取颜色 |
| 大型组件迁移引入回归 | 功能损坏 | 每个单元完成后运行现有测试 + 手动验证 |
| 四象限布局在小屏幕下体验差 | 不可用 | R10 响应式断点确保 1280px+ 可用 |
## Open Questions
- Code Diff Viewer 是否需要引入第三方库(如 diff2html还是手写简单 diff 展示?→ 建议先用简单行级高亮,后续迭代引入 diff2html
- 骨架屏组件是自建还是使用 Ant Design Vue 的 Skeleton→ 建议使用 antd Skeleton减少维护成本

View File

@ -680,6 +680,7 @@ def create_app(
if gui_mode:
from pathlib import Path as _Path
from fastapi.responses import HTMLResponse, FileResponse
from fastapi.staticfiles import StaticFiles
_static_dir = _Path(__file__).parent / "static"
@ -691,4 +692,29 @@ def create_app(
return FileResponse(str(index_path))
return HTMLResponse("<h1>AgentKit GUI not found</h1>", status_code=404)
# SPA fallback: serve index.html for all non-API, non-static routes
@app.get("/{path:path}", response_class=HTMLResponse, include_in_schema=False)
async def spa_fallback(path: str):
"""Serve index.html for SPA client-side routing."""
# Don't intercept API routes or docs
if path.startswith("api/") or path.startswith("docs") or path == "openapi.json":
return HTMLResponse("<h1>Not Found</h1>", status_code=404)
# Try to serve a real static file first (with path traversal protection)
file_path = (_static_dir / path).resolve()
if not str(file_path).startswith(str(_static_dir.resolve())):
return HTMLResponse("<h1>Forbidden</h1>", status_code=403)
if file_path.is_file():
return FileResponse(str(file_path))
# Fallback to index.html for SPA routing
index_path = _static_dir / "index.html"
if index_path.exists():
return FileResponse(str(index_path))
return HTMLResponse("<h1>Not Found</h1>", status_code=404)
# Mount static assets last (js, css, images, etc.)
# mount() is checked after route matching, so API routes take priority
assets_dir = _static_dir / "assets"
if assets_dir.exists():
app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="static-assets")
return app

File diff suppressed because it is too large Load Diff

View File

@ -14,11 +14,15 @@
"@vue-flow/controls": "^1.1.0",
"@vue-flow/core": "^1.41.0",
"ant-design-vue": "^4.2.0",
"dompurify": "^3.4.10",
"markdown-it": "^14.2.0",
"pinia": "^2.2.0",
"vue": "^3.5.0",
"vue-router": "^4.4.0"
},
"devDependencies": {
"@types/dompurify": "^3.0.5",
"@types/markdown-it": "^14.1.2",
"@vitejs/plugin-vue": "^5.1.0",
"echarts": "^6.1.0",
"typescript": "^5.6.0",

View File

@ -1,13 +1,13 @@
<template>
<a-config-provider :locale="zhCN">
<AppLayout />
<a-config-provider :locale="zhCN" :theme="themeConfig">
<router-view />
</a-config-provider>
</template>
<script setup lang="ts">
import { ConfigProvider as AConfigProvider } from 'ant-design-vue'
import zhCN from 'ant-design-vue/es/locale/zh_CN'
import AppLayout from './components/layout/AppLayout.vue'
import { themeConfig } from './styles'
</script>
<style>

View File

@ -14,7 +14,8 @@ export class BaseApiClient {
}
protected async request<T>(path: string, options: RequestInit = {}): Promise<T> {
const url = `${this.baseUrl}${path}`
// If path starts with /api/, it's an absolute path — don't prepend baseUrl
const url = path.startsWith('/api/') ? path : `${this.baseUrl}${path}`
const headers: Record<string, string> = {
...options.headers as Record<string, string>,
}

View File

@ -11,6 +11,7 @@ export interface IKbSource {
status: string
document_count: number
last_synced: string | null
config?: Record<string, unknown>
}
export interface IAddSourceRequest {

View File

@ -71,6 +71,29 @@ class SkillsApiClient extends BaseApiClient {
return this.request<{ capabilities: ICapabilityInfo[] }>('/capabilities')
}
/** Install a skill by name (searches GitHub) or from a source URL */
async installSkill(
name: string,
source?: string
): Promise<{ status: string; name: string; path: string }> {
// The install endpoint is on /api/v1/skills/install, not under skill-management
return this.request<{ status: string; name: string; path: string }>(
'/api/v1/skills/install',
{
method: 'POST',
body: JSON.stringify({ name, source }),
}
)
}
/** Uninstall a skill */
async uninstallSkill(skillName: string): Promise<{ status: string; name: string }> {
return this.request<{ status: string; name: string }>(
`/api/v1/skills/${encodeURIComponent(skillName)}`,
{ method: 'DELETE' }
)
}
/** Reload a skill */
async reloadSkill(
skillName: string

View File

@ -1,33 +1,51 @@
<template>
<div class="chat-input">
<a-textarea
v-model:value="inputText"
:placeholder="placeholder"
:auto-size="{ minRows: 1, maxRows: 4 }"
:disabled="disabled"
@pressEnter="handlePressEnter"
class="chat-input__textarea"
/>
<a-button
type="primary"
:disabled="!canSend"
:loading="disabled"
@click="handleSend"
class="chat-input__send"
>
<template #icon><SendOutlined /></template>
发送
</a-button>
<div v-if="contextPills.length > 0" class="chat-input__pills">
<ContextPill
v-for="(pill, idx) in contextPills"
:key="idx"
:label="pill.label"
:icon="pill.icon"
:removable="pill.removable"
@remove="removePill(idx)"
/>
</div>
<div class="chat-input__row">
<a-textarea
v-model:value="inputText"
:placeholder="placeholder"
:auto-size="{ minRows: 1, maxRows: 4 }"
:disabled="disabled"
@pressEnter="handlePressEnter"
class="chat-input__textarea"
/>
<a-button
type="primary"
:disabled="!canSend"
:loading="disabled"
@click="handleSend"
class="chat-input__send"
>
<template #icon><SendOutlined /></template>
</a-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, type Component } from 'vue'
import { Input as AInput, Button as AButton } from 'ant-design-vue'
import { SendOutlined } from '@ant-design/icons-vue'
import ContextPill from './ContextPill.vue'
const ATextarea = AInput.TextArea
interface ContextPillData {
label: string
icon?: Component
removable?: boolean
}
interface IProps {
disabled?: boolean
placeholder?: string
@ -43,6 +61,7 @@ const emit = defineEmits<{
}>()
const inputText = ref('')
const contextPills = ref<ContextPillData[]>([])
const canSend = computed(() => {
return inputText.value.trim().length > 0 && !props.disabled
@ -56,20 +75,36 @@ function handleSend(): void {
}
function handlePressEnter(event: KeyboardEvent): void {
if (event.shiftKey) return // Shift+Enter for new line
if (event.shiftKey) return
event.preventDefault()
handleSend()
}
function removePill(idx: number): void {
contextPills.value.splice(idx, 1)
}
</script>
<style scoped>
.chat-input {
display: flex;
flex-direction: column;
padding: var(--space-2) var(--space-3);
background: var(--bg-primary);
border-top: 1px solid var(--border-color);
}
.chat-input__pills {
display: flex;
gap: var(--space-1);
flex-wrap: wrap;
padding-bottom: var(--space-2);
}
.chat-input__row {
display: flex;
align-items: flex-end;
gap: 8px;
padding: 12px 16px;
background: #ffffff;
border-top: 1px solid #f0f0f0;
gap: var(--space-2);
}
.chat-input__textarea {

View File

@ -3,26 +3,38 @@
<div class="chat-message__avatar">
<a-avatar
v-if="message.role === 'assistant'"
:size="36"
style="background-color: #1677ff"
:size="32"
class="chat-message__avatar--assistant"
>
<template #icon><RobotOutlined /></template>
</a-avatar>
<a-avatar
v-else
:size="36"
style="background-color: #52c41a"
:size="32"
class="chat-message__avatar--user"
>
<template #icon><UserOutlined /></template>
</a-avatar>
</div>
<div class="chat-message__body">
<!-- Tool call indicators -->
<div v-if="toolCalls.length > 0" class="chat-message__tools">
<ToolCallIndicator
v-for="(tc, idx) in toolCalls"
:key="idx"
:type="tc.type"
:name="tc.name"
/>
</div>
<!-- Message content -->
<div class="chat-message__content" :class="[`chat-message__content--${message.role}`]">
<span v-html="renderedContent"></span>
<div v-if="message.role === 'assistant'" class="chat-message__markdown" v-html="renderedContent"></div>
<span v-else>{{ message.content }}</span>
<a-spin v-if="isLoading" size="small" class="chat-message__loading" />
</div>
<!-- Routing info -->
<div v-if="showRouting" class="chat-message__routing">
<a-tag color="blue">
<a-tag color="purple">
<ThunderboltOutlined /> {{ message.matched_skill }}
</a-tag>
<a-tag v-if="message.confidence !== undefined" color="green">
@ -41,9 +53,36 @@
<script setup lang="ts">
import { computed } from 'vue'
import MarkdownIt from 'markdown-it'
import DOMPurify from 'dompurify'
import { Avatar as AAvatar, Tag as ATag, Spin as ASpin } from 'ant-design-vue'
import { RobotOutlined, UserOutlined, ThunderboltOutlined } from '@ant-design/icons-vue'
import type { IChatMessage } from '@/api/types'
import ToolCallIndicator from './ToolCallIndicator.vue'
const md = new MarkdownIt({
html: false,
linkify: true,
breaks: true,
})
// Sanitize markdown output to prevent XSS (javascript: links, data: URIs, etc.)
function sanitize(html: string): string {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: [
'p', 'br', 'strong', 'em', 'del', 'code', 'pre', 'a',
'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'blockquote',
'table', 'thead', 'tbody', 'tr', 'th', 'td', 'span',
],
ALLOWED_ATTR: ['href', 'target', 'rel', 'class'],
ALLOW_DATA_ATTR: false,
})
}
interface ToolCall {
type: string
name: string
}
interface IProps {
message: IChatMessage
@ -65,20 +104,32 @@ const formattedTime = computed(() => {
})
const renderedContent = computed(() => {
// Simple rendering: escape HTML and convert newlines
const escaped = props.message.content
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
return escaped.replace(/\n/g, '<br/>')
if (!props.message.content) return ''
return sanitize(md.render(props.message.content))
})
const toolCalls = computed<ToolCall[]>(() => {
// Extract tool calls from message metadata or content patterns
const calls: ToolCall[] = []
const content = props.message.content || ''
// Detect tool use patterns like [Read], [Edit], [Bash] at line start only
const toolPattern = /^\[(Read|Edit|Bash|Write|Search|Grep|Glob)\]/gm
let match
while ((match = toolPattern.exec(content)) !== null) {
const toolName = match[1].toLowerCase()
calls.push({ type: toolName, name: match[1] })
}
return calls
})
</script>
<style scoped>
.chat-message {
display: flex;
gap: 12px;
padding: 12px 16px;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
}
.chat-message--user {
@ -89,50 +140,111 @@ const renderedContent = computed(() => {
flex-shrink: 0;
}
.chat-message__avatar--assistant {
background: var(--gradient-brand) !important;
}
.chat-message__avatar--user {
background-color: var(--color-success) !important;
}
.chat-message__body {
max-width: 70%;
max-width: 75%;
display: flex;
flex-direction: column;
gap: 4px;
gap: var(--space-1);
}
.chat-message--user .chat-message__body {
align-items: flex-end;
}
.chat-message__tools {
display: flex;
gap: var(--space-1);
flex-wrap: wrap;
}
.chat-message__content {
padding: 10px 14px;
border-radius: 12px;
line-height: 1.6;
font-size: 14px;
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-lg);
line-height: var(--leading-normal);
font-size: var(--font-base);
word-break: break-word;
}
.chat-message__content--user {
background: #1677ff;
color: #ffffff;
border-bottom-right-radius: 4px;
background: var(--color-primary);
color: var(--text-inverse);
border-bottom-right-radius: var(--radius-sm);
}
.chat-message__content--assistant {
background: #ffffff;
color: #333333;
border: 1px solid #f0f0f0;
border-bottom-left-radius: 4px;
background: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-bottom-left-radius: var(--radius-sm);
}
.chat-message__markdown {
overflow-x: auto;
}
.chat-message__markdown :deep(p) {
margin-bottom: var(--space-2);
}
.chat-message__markdown :deep(p:last-child) {
margin-bottom: 0;
}
.chat-message__markdown :deep(pre) {
background: var(--code-bg);
color: var(--code-fg);
padding: var(--space-3);
border-radius: var(--radius-md);
overflow-x: auto;
margin: var(--space-2) 0;
font-size: var(--font-sm);
}
.chat-message__markdown :deep(code) {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace;
font-size: var(--font-sm);
}
.chat-message__markdown :deep(:not(pre) > code) {
background: var(--color-primary-light);
color: var(--color-primary);
padding: 1px var(--space-1);
border-radius: var(--radius-sm);
}
.chat-message__markdown :deep(ul),
.chat-message__markdown :deep(ol) {
padding-left: var(--space-5);
margin-bottom: var(--space-2);
}
.chat-message__markdown :deep(h1),
.chat-message__markdown :deep(h2),
.chat-message__markdown :deep(h3) {
margin-top: var(--space-3);
margin-bottom: var(--space-2);
}
.chat-message__loading {
margin-left: 8px;
margin-left: var(--space-2);
}
.chat-message__routing {
display: flex;
gap: 4px;
gap: var(--space-1);
flex-wrap: wrap;
}
.chat-message__time {
font-size: 12px;
color: #999999;
font-size: var(--font-xs);
color: var(--text-placeholder);
}
</style>

View File

@ -1,31 +1,38 @@
<template>
<div class="chat-sidebar">
<div class="chat-sidebar__header">
<a-button type="primary" block @click="handleCreate">
<template #icon><PlusOutlined /></template>
新建对话
</a-button>
</div>
<div class="chat-sidebar__list">
<a-empty v-if="conversations.length === 0" description="暂无对话" />
<div
v-for="conv in conversations"
:key="conv.id"
class="chat-sidebar__item"
:class="{ 'chat-sidebar__item--active': conv.id === currentId }"
@click="handleSelect(conv.id)"
>
<MessageOutlined class="chat-sidebar__item-icon" />
<span class="chat-sidebar__item-title">{{ conv.title }}</span>
<span class="chat-sidebar__item-time">{{ formatRelativeTime(conv.updated_at) }}</span>
<div :class="['chat-sidebar', { 'chat-sidebar--collapsed': collapsed }]">
<div v-if="!collapsed" class="chat-sidebar__content">
<div class="chat-sidebar__header">
<a-button type="primary" block size="small" @click="handleCreate">
<template #icon><PlusOutlined /></template>
新建对话
</a-button>
</div>
<div class="chat-sidebar__list">
<a-empty v-if="conversations.length === 0" description="暂无对话" :image-style="{ height: '40px' }" />
<div
v-for="conv in conversations"
:key="conv.id"
class="chat-sidebar__item"
:class="{ 'chat-sidebar__item--active': conv.id === currentId }"
@click="handleSelect(conv.id)"
>
<MessageOutlined class="chat-sidebar__item-icon" />
<span class="chat-sidebar__item-title">{{ conv.title }}</span>
<span class="chat-sidebar__item-time">{{ formatRelativeTime(conv.updated_at) }}</span>
</div>
</div>
</div>
<button class="chat-sidebar__toggle" @click="collapsed = !collapsed" :title="collapsed ? '展开' : '折叠'">
<RightOutlined v-if="collapsed" />
<LeftOutlined v-else />
</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Button as AButton, Empty as AEmpty } from 'ant-design-vue'
import { PlusOutlined, MessageOutlined } from '@ant-design/icons-vue'
import { PlusOutlined, MessageOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons-vue'
import type { IConversation } from '@/api/types'
interface IProps {
@ -40,6 +47,8 @@ const emit = defineEmits<{
select: [id: string]
}>()
const collapsed = ref(true)
function handleCreate(): void {
emit('create')
}
@ -69,69 +78,102 @@ function formatRelativeTime(dateStr: string): string {
<style scoped>
.chat-sidebar {
width: 280px;
display: flex;
height: 100%;
background: #ffffff;
border-right: 1px solid #f0f0f0;
background: var(--bg-primary);
border-right: 1px solid var(--border-color);
transition: width var(--transition-normal);
}
.chat-sidebar--collapsed {
width: 32px;
}
.chat-sidebar:not(.chat-sidebar--collapsed) {
width: 240px;
}
.chat-sidebar__content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-sidebar__header {
padding: 16px;
border-bottom: 1px solid #f0f0f0;
padding: var(--space-3);
border-bottom: 1px solid var(--border-color);
}
.chat-sidebar__list {
flex: 1;
overflow-y: auto;
padding: 8px;
padding: var(--space-2);
}
.chat-sidebar__item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-radius: 8px;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
cursor: pointer;
transition: background 0.2s;
transition: background var(--transition-fast);
}
.chat-sidebar__item:hover {
background: #f5f5f5;
background: var(--bg-tertiary);
}
.chat-sidebar__item--active {
background: #e6f4ff;
background: var(--color-primary-light);
}
.chat-sidebar__item--active:hover {
background: #e6f4ff;
background: var(--color-primary-light);
}
.chat-sidebar__item-icon {
color: #999999;
font-size: 14px;
color: var(--text-placeholder);
font-size: var(--font-sm);
flex-shrink: 0;
}
.chat-sidebar__item--active .chat-sidebar__item-icon {
color: #1677ff;
color: var(--color-primary);
}
.chat-sidebar__item-title {
flex: 1;
font-size: 14px;
color: #333333;
font-size: var(--font-sm);
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-sidebar__item-time {
font-size: 12px;
color: #999999;
font-size: var(--font-xs);
color: var(--text-placeholder);
flex-shrink: 0;
}
.chat-sidebar__toggle {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 100%;
border: none;
background: transparent;
color: var(--text-tertiary);
cursor: pointer;
flex-shrink: 0;
transition: all var(--transition-fast);
}
.chat-sidebar__toggle:hover {
color: var(--text-primary);
background: var(--bg-tertiary);
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<span class="context-pill">
<component :is="icon" class="context-pill__icon" />
<span class="context-pill__label">{{ label }}</span>
<button v-if="removable" class="context-pill__remove" @click.stop="$emit('remove')">
<CloseOutlined />
</button>
</span>
</template>
<script setup lang="ts">
import { type Component } from 'vue'
import { CloseOutlined } from '@ant-design/icons-vue'
defineProps<{
label: string
icon?: Component
removable?: boolean
}>()
defineEmits<{
remove: []
}>()
</script>
<style scoped>
.context-pill {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: 2px var(--space-2);
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--radius-full);
font-size: var(--font-xs);
color: var(--text-secondary);
max-width: 160px;
}
.context-pill__icon {
font-size: 10px;
color: var(--text-tertiary);
flex-shrink: 0;
}
.context-pill__label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.context-pill__remove {
display: flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
border: none;
background: transparent;
color: var(--text-placeholder);
cursor: pointer;
border-radius: var(--radius-full);
flex-shrink: 0;
padding: 0;
transition: all var(--transition-fast);
}
.context-pill__remove:hover {
color: var(--color-error);
background: var(--color-error-light);
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<span :class="['tool-call-indicator', `tool-call-indicator--${type}`]">
<component :is="icon" class="tool-call-indicator__icon" />
<span class="tool-call-indicator__label">{{ name }}</span>
</span>
</template>
<script setup lang="ts">
import { computed, type Component } from 'vue'
import {
ReadOutlined,
EditOutlined,
CodeOutlined,
FileAddOutlined,
SearchOutlined,
FolderOpenOutlined,
} from '@ant-design/icons-vue'
const props = defineProps<{
type: string
name: string
}>()
const iconMap: Record<string, Component> = {
read: ReadOutlined,
edit: EditOutlined,
bash: CodeOutlined,
write: FileAddOutlined,
search: SearchOutlined,
grep: SearchOutlined,
glob: FolderOpenOutlined,
}
const icon = computed(() => iconMap[props.type] || CodeOutlined)
</script>
<style scoped>
.tool-call-indicator {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 1px var(--space-2);
border-radius: var(--radius-full);
font-size: 11px;
font-weight: var(--font-weight-medium);
line-height: 1.6;
}
.tool-call-indicator__icon {
font-size: 10px;
}
.tool-call-indicator--read {
background: var(--color-info-light);
color: var(--color-info);
}
.tool-call-indicator--edit {
background: var(--color-warning-light);
color: var(--color-warning);
}
.tool-call-indicator--bash {
background: var(--color-primary-light);
color: var(--color-primary);
}
.tool-call-indicator--write {
background: var(--color-success-light);
color: var(--color-success);
}
.tool-call-indicator--search,
.tool-call-indicator--grep,
.tool-call-indicator--glob {
background: var(--color-primary-light);
color: var(--color-primary);
}
</style>

View File

@ -0,0 +1,146 @@
<template>
<div class="code-diff-viewer">
<div v-if="!diff" class="code-diff-viewer__empty">
<FileTextOutlined style="font-size: 32px; color: var(--text-placeholder)" />
<p>暂无代码变更</p>
</div>
<div v-else class="code-diff-viewer__content">
<div class="code-diff-viewer__header">
<span class="code-diff-viewer__file">{{ diff.file }}</span>
<span class="code-diff-viewer__stats">
<span class="code-diff-viewer__added">+{{ diff.added }}</span>
<span class="code-diff-viewer__removed">-{{ diff.removed }}</span>
</span>
</div>
<div class="code-diff-viewer__lines">
<div
v-for="(line, idx) in diff.lines"
:key="idx"
:class="['code-diff-viewer__line', `code-diff-viewer__line--${line.type}`]"
>
<span class="code-diff-viewer__num">{{ line.num }}</span>
<span class="code-diff-viewer__prefix">{{ line.prefix }}</span>
<span class="code-diff-viewer__text">{{ line.text }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { FileTextOutlined } from '@ant-design/icons-vue'
export interface DiffLine {
num: number
type: 'context' | 'added' | 'removed'
prefix: string
text: string
}
export interface DiffData {
file: string
added: number
removed: number
lines: DiffLine[]
}
defineProps<{
diff: DiffData | null
}>()
</script>
<style scoped>
.code-diff-viewer {
height: 100%;
overflow: auto;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace;
font-size: var(--font-sm);
background: var(--code-bg);
color: var(--code-fg);
}
.code-diff-viewer__empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: var(--space-3);
color: var(--text-placeholder);
font-family: inherit;
}
.code-diff-viewer__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-2) var(--space-3);
background: rgba(0, 0, 0, 0.2);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
position: sticky;
top: 0;
z-index: 1;
}
.code-diff-viewer__file {
color: var(--code-fg);
font-weight: var(--font-weight-medium);
}
.code-diff-viewer__stats {
display: flex;
gap: var(--space-2);
font-size: var(--font-xs);
}
.code-diff-viewer__added {
color: var(--color-success);
}
.code-diff-viewer__removed {
color: var(--color-error);
}
.code-diff-viewer__line {
display: flex;
min-height: 20px;
line-height: 20px;
}
.code-diff-viewer__line--added {
background: var(--code-added-bg);
}
.code-diff-viewer__line--removed {
background: var(--code-removed-bg);
}
.code-diff-viewer__num {
width: 48px;
text-align: right;
padding-right: var(--space-2);
color: var(--code-comment);
user-select: none;
flex-shrink: 0;
}
.code-diff-viewer__prefix {
width: 16px;
text-align: center;
flex-shrink: 0;
}
.code-diff-viewer__line--added .code-diff-viewer__prefix {
color: var(--color-success);
}
.code-diff-viewer__line--removed .code-diff-viewer__prefix {
color: var(--color-error);
}
.code-diff-viewer__text {
flex: 1;
white-space: pre;
overflow-x: auto;
}
</style>

View File

@ -0,0 +1,162 @@
<template>
<div class="file-tree">
<div v-if="files.length === 0" class="file-tree__empty">
<FolderOpenOutlined style="font-size: 24px; color: var(--text-placeholder)" />
<p>暂无文件</p>
</div>
<div v-else class="file-tree__list">
<div
v-for="file in files"
:key="file.path"
:class="['file-tree__item', { 'file-tree__item--active': selectedPath === file.path }]"
@click="selectedPath = file.path; $emit('select', file.path)"
>
<component :is="getFileIcon(file.status)" class="file-tree__icon" :class="[`file-tree__icon--${file.status}`]" />
<span class="file-tree__name">{{ file.name }}</span>
<span v-if="file.status" :class="['file-tree__badge', `file-tree__badge--${file.status}`]">
{{ getStatusLabel(file.status) }}
</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, type Component } from 'vue'
import {
FolderOpenOutlined,
FileOutlined,
FileAddOutlined,
EditOutlined,
DeleteOutlined,
} from '@ant-design/icons-vue'
export interface FileEntry {
path: string
name: string
status?: 'added' | 'modified' | 'deleted'
}
defineProps<{
files: FileEntry[]
}>()
defineEmits<{
select: [path: string]
}>()
const selectedPath = ref('')
function getFileIcon(status?: string): Component {
switch (status) {
case 'added': return FileAddOutlined
case 'deleted': return DeleteOutlined
case 'modified': return EditOutlined
default: return FileOutlined
}
}
function getStatusLabel(status?: string): string {
switch (status) {
case 'added': return 'A'
case 'deleted': return 'D'
case 'modified': return 'M'
default: return ''
}
}
</script>
<style scoped>
.file-tree {
height: 100%;
overflow-y: auto;
background: var(--bg-primary);
}
.file-tree__empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: var(--space-2);
color: var(--text-placeholder);
font-size: var(--font-sm);
}
.file-tree__list {
padding: var(--space-1);
}
.file-tree__item {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-sm);
cursor: pointer;
transition: background var(--transition-fast);
font-size: var(--font-sm);
}
.file-tree__item:hover {
background: var(--bg-tertiary);
}
.file-tree__item--active {
background: var(--color-primary-light);
}
.file-tree__icon {
font-size: 14px;
color: var(--text-tertiary);
flex-shrink: 0;
}
.file-tree__icon--added {
color: var(--color-success);
}
.file-tree__icon--modified {
color: var(--color-warning);
}
.file-tree__icon--deleted {
color: var(--color-error);
}
.file-tree__name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text-primary);
}
.file-tree__badge {
font-size: 10px;
font-weight: var(--font-weight-bold);
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-sm);
flex-shrink: 0;
}
.file-tree__badge--added {
color: var(--color-success);
background: var(--color-success-light);
}
.file-tree__badge--modified {
color: var(--color-warning);
background: var(--color-warning-light);
}
.file-tree__badge--deleted {
color: var(--color-error);
background: var(--color-error-light);
}
</style>

View File

@ -5,7 +5,7 @@
<a-statistic
title="总任务数"
:value="metrics?.total_tasks ?? 0"
:value-style="{ color: '#1890ff' }"
:value-style="{ color: '#7c3aed' }"
>
<template #prefix><CheckCircleOutlined /></template>
</a-statistic>
@ -14,7 +14,7 @@
<a-statistic
title="Agent 活跃数"
:value="activeAgentCount"
:value-style="{ color: '#722ed1' }"
:value-style="{ color: '#7c3aed' }"
>
<template #prefix><TeamOutlined /></template>
</a-statistic>
@ -23,7 +23,7 @@
<a-statistic
title="LLM 用量"
:value="usageSummary.total_tokens"
:value-style="{ color: '#13c2c2' }"
:value-style="{ color: '#3b82f6' }"
>
<template #prefix><CloudServerOutlined /></template>
</a-statistic>
@ -34,7 +34,7 @@
title="质量通过率"
:value="metrics ? (metrics.success_rate * 100).toFixed(1) : '0.0'"
suffix="%"
:value-style="{ color: '#52c41a' }"
:value-style="{ color: '#10b981' }"
>
<template #prefix><SafetyCertificateOutlined /></template>
</a-statistic>
@ -183,7 +183,7 @@ function riskLabel(level: string): string {
.overview-card__footer {
font-size: 12px;
color: #8c8c8c;
color: var(--text-tertiary);
}
.overview-sections {
@ -195,8 +195,8 @@ function riskLabel(level: string): string {
}
.overview-section {
background: #fff;
border: 1px solid #f0f0f0;
background: var(--bg-primary);
border: 1px solid var(--border-color-split);
border-radius: 8px;
padding: 16px;
display: flex;
@ -235,7 +235,7 @@ function riskLabel(level: string): string {
align-items: center;
gap: 8px;
padding: 6px 0;
border-bottom: 1px solid #f5f5f5;
border-bottom: 1px solid var(--bg-tertiary);
}
.experience-item:last-child {
@ -250,11 +250,11 @@ function riskLabel(level: string): string {
}
.experience-item__dot--success {
background: #52c41a;
background: var(--color-success);
}
.experience-item__dot--failure {
background: #ff4d4f;
background: var(--color-error);
}
.experience-item__info {
@ -274,7 +274,7 @@ function riskLabel(level: string): string {
.experience-item__meta {
font-size: 12px;
color: #8c8c8c;
color: var(--text-tertiary);
}
.pitfall-item {
@ -282,7 +282,7 @@ function riskLabel(level: string): string {
align-items: center;
gap: 8px;
padding: 6px 0;
border-bottom: 1px solid #f5f5f5;
border-bottom: 1px solid var(--bg-tertiary);
}
.pitfall-item:last-child {
@ -297,7 +297,7 @@ function riskLabel(level: string): string {
.pitfall-item__rate {
font-size: 12px;
color: #ff4d4f;
color: var(--color-error);
font-weight: 600;
}
</style>

View File

@ -21,8 +21,8 @@ function onExperienceFilter(outcome: string) {
<style scoped>
.experience-panel {
height: 100%;
background: #fff;
border: 1px solid #f0f0f0;
background: var(--bg-primary);
border: 1px solid var(--border-color-split);
border-radius: 8px;
padding: 16px;
overflow: hidden;

View File

@ -61,11 +61,11 @@
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ref } from 'vue'
import { Select as ASelect, SelectOption as ASelectOption, Tag as ATag, Empty as AEmpty } from 'ant-design-vue'
import type { Experience } from '@/api/evolution'
const props = defineProps<{
defineProps<{
experiences: Experience[]
}>()
@ -138,7 +138,7 @@ function formatTime(isoStr: string): string {
top: 0;
bottom: 0;
width: 2px;
background: #e8e8e8;
background: var(--border-color);
}
.timeline-item {
@ -153,21 +153,21 @@ function formatTime(isoStr: string): string {
width: 10px;
height: 10px;
border-radius: 50%;
border: 2px solid #fff;
border: 2px solid var(--bg-primary);
z-index: 1;
}
.timeline-dot--success {
background: #52c41a;
background: var(--color-success);
}
.timeline-dot--failure {
background: #ff4d4f;
background: var(--color-error);
}
.timeline-card {
background: #fafafa;
border: 1px solid #f0f0f0;
background: var(--bg-secondary);
border: 1px solid var(--border-color-split);
border-radius: 6px;
padding: 10px 12px;
cursor: pointer;
@ -199,17 +199,17 @@ function formatTime(isoStr: string): string {
gap: 12px;
margin-top: 4px;
font-size: 12px;
color: #8c8c8c;
color: var(--text-tertiary);
}
.timeline-card__type {
color: #1890ff;
color: var(--color-primary);
}
.timeline-card__detail {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #f0f0f0;
border-top: 1px solid var(--border-color-split);
}
.detail-section {
@ -219,13 +219,13 @@ function formatTime(isoStr: string): string {
.detail-label {
font-size: 12px;
font-weight: 600;
color: #595959;
color: var(--text-secondary);
margin-bottom: 4px;
}
.detail-text {
font-size: 12px;
color: #8c8c8c;
color: var(--text-tertiary);
line-height: 1.5;
}
@ -233,7 +233,7 @@ function formatTime(isoStr: string): string {
margin: 0;
padding-left: 16px;
font-size: 12px;
color: #8c8c8c;
color: var(--text-tertiary);
line-height: 1.6;
}
</style>

View File

@ -18,19 +18,19 @@
<div class="summary-row">
<div class="summary-card">
<div class="summary-label">成功率</div>
<div class="summary-value" style="color: #52c41a">
<div class="summary-value" style="color: #10b981">
{{ metrics ? (metrics.success_rate * 100).toFixed(1) + '%' : '-' }}
</div>
</div>
<div class="summary-card">
<div class="summary-label">平均耗时</div>
<div class="summary-value" style="color: #1890ff">
<div class="summary-value" style="color: #7c3aed">
{{ metrics ? formatDuration(metrics.avg_duration) : '-' }}
</div>
</div>
<div class="summary-card">
<div class="summary-label">重试率</div>
<div class="summary-value" style="color: #fa8c16">
<div class="summary-value" style="color: #f59e0b">
{{ metrics ? (metrics.retry_rate * 100).toFixed(1) + '%' : '-' }}
</div>
</div>
@ -129,7 +129,7 @@ function updateChart() {
type: 'line',
data: props.trends.map(t => +(t.success_rate * 100).toFixed(1)),
smooth: true,
itemStyle: { color: '#52c41a' },
itemStyle: { color: '#10b981' },
symbol: 'circle',
symbolSize: 6,
},
@ -138,7 +138,7 @@ function updateChart() {
type: 'line',
data: props.trends.map(t => +(t.retry_rate * 100).toFixed(1)),
smooth: true,
itemStyle: { color: '#fa8c16' },
itemStyle: { color: '#f59e0b' },
symbol: 'circle',
symbolSize: 6,
},
@ -148,7 +148,7 @@ function updateChart() {
yAxisIndex: 1,
data: props.trends.map(t => +(t.avg_duration).toFixed(0)),
smooth: true,
itemStyle: { color: '#1890ff' },
itemStyle: { color: '#7c3aed' },
lineStyle: { type: 'dashed' },
symbol: 'circle',
symbolSize: 6,
@ -220,7 +220,7 @@ watch(() => [props.trends, props.period], updateChart, { deep: true })
.summary-card {
flex: 1;
background: #fafafa;
background: var(--bg-secondary);
border-radius: 6px;
padding: 8px 12px;
text-align: center;
@ -228,7 +228,7 @@ watch(() => [props.trends, props.period], updateChart, { deep: true })
.summary-label {
font-size: 12px;
color: #8c8c8c;
color: var(--text-tertiary);
margin-bottom: 2px;
}

View File

@ -23,8 +23,8 @@ function onPeriodChange(period: string) {
<style scoped>
.metrics-panel {
height: 100%;
background: #fff;
border: 1px solid #f0f0f0;
background: var(--bg-primary);
border: 1px solid var(--border-color-split);
border-radius: 8px;
padding: 16px;
overflow: hidden;

View File

@ -16,8 +16,8 @@ const store = useEvolutionStore()
<style scoped>
.optimization-panel {
height: 100%;
background: #fff;
border: 1px solid #f0f0f0;
background: var(--bg-primary);
border: 1px solid var(--border-color-split);
border-radius: 8px;
padding: 16px;
overflow: hidden;

View File

@ -62,7 +62,7 @@ import { ref } from 'vue'
import { Tag as ATag, Empty as AEmpty } from 'ant-design-vue'
import type { PathOptimization } from '@/api/evolution'
const props = defineProps<{
defineProps<{
optimizations: PathOptimization[]
}>()
@ -112,8 +112,8 @@ function formatTime(isoStr: string | null): string {
}
.optimizer-item {
background: #fafafa;
border: 1px solid #f0f0f0;
background: var(--bg-secondary);
border: 1px solid var(--border-color-split);
border-radius: 6px;
padding: 10px 12px;
margin-bottom: 8px;
@ -135,18 +135,18 @@ function formatTime(isoStr: string | null): string {
.optimizer-item__type {
font-size: 13px;
font-weight: 500;
color: #1890ff;
color: var(--color-primary);
}
.optimizer-item__time {
font-size: 12px;
color: #8c8c8c;
color: var(--text-tertiary);
}
.optimizer-item__detail {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #f0f0f0;
border-top: 1px solid var(--border-color-split);
}
.path-comparison {
@ -157,7 +157,7 @@ function formatTime(isoStr: string | null): string {
.path-arrow {
font-size: 16px;
color: #52c41a;
color: var(--color-success);
padding-top: 20px;
flex-shrink: 0;
}
@ -174,7 +174,7 @@ function formatTime(isoStr: string | null): string {
.path-label {
font-size: 12px;
font-weight: 600;
color: #595959;
color: var(--text-secondary);
margin-bottom: 4px;
}
@ -192,12 +192,12 @@ function formatTime(isoStr: string | null): string {
}
.path-step--old {
background: #fff2f0;
color: #cf1322;
background: var(--color-error-light);
color: var(--color-error);
}
.path-step--new {
background: #f6ffed;
color: #389e0d;
background: var(--color-success-light);
color: var(--color-success);
}
</style>

View File

@ -49,7 +49,7 @@ import { ref } from 'vue'
import { Input as AInput, Button as AButton, Tag as ATag, Empty as AEmpty } from 'ant-design-vue'
import type { PitfallWarning } from '@/api/evolution'
const props = defineProps<{
defineProps<{
warnings: PitfallWarning[]
loading?: boolean
}>()
@ -124,8 +124,8 @@ function riskLabel(level: string): string {
}
.pitfall-item {
background: #fafafa;
border: 1px solid #f0f0f0;
background: var(--bg-secondary);
border: 1px solid var(--border-color-split);
border-radius: 6px;
padding: 10px 12px;
margin-bottom: 8px;
@ -145,20 +145,20 @@ function riskLabel(level: string): string {
.pitfall-item__rate {
font-size: 12px;
color: #8c8c8c;
color: var(--text-tertiary);
margin-bottom: 4px;
}
.pitfall-item__reason {
font-size: 12px;
color: #595959;
color: var(--text-secondary);
line-height: 1.5;
margin-bottom: 4px;
}
.pitfall-item__suggestion {
font-size: 12px;
color: #1890ff;
color: var(--color-primary);
line-height: 1.5;
}

View File

@ -22,8 +22,8 @@ function onPitfallCheck(taskType: string) {
<style scoped>
.pitfall-route-panel {
height: 100%;
background: #fff;
border: 1px solid #f0f0f0;
background: var(--bg-primary);
border: 1px solid var(--border-color-split);
border-radius: 8px;
padding: 16px;
overflow: hidden;

View File

@ -12,25 +12,25 @@
<div class="usage-summary">
<div class="usage-summary__card">
<div class="usage-summary__label">请求成功率</div>
<div class="usage-summary__value" style="color: #52c41a">
<div class="usage-summary__value" style="color: #10b981">
{{ (summary.success_rate * 100).toFixed(1) }}%
</div>
</div>
<div class="usage-summary__card">
<div class="usage-summary__label">平均响应延迟</div>
<div class="usage-summary__value" style="color: #1890ff">
<div class="usage-summary__value" style="color: #7c3aed">
{{ summary.avg_latency_ms.toFixed(0) }} ms
</div>
</div>
<div class="usage-summary__card">
<div class="usage-summary__label"> Token </div>
<div class="usage-summary__value" style="color: #722ed1">
<div class="usage-summary__value" style="color: #7c3aed">
{{ formatNumber(summary.total_tokens) }}
</div>
</div>
<div class="usage-summary__card">
<div class="usage-summary__label">总请求数</div>
<div class="usage-summary__value" style="color: #fa8c16">
<div class="usage-summary__value" style="color: #f59e0b">
{{ formatNumber(summary.total_requests) }}
</div>
</div>
@ -72,7 +72,7 @@ const PROVIDER_COLORS: Record<string, string> = {
azure: '#0078d4',
deepseek: '#4d6bfe',
zhipu: '#3b5cff',
default: '#8c8c8c',
default: '#737373',
}
async function loadUsage() {
@ -203,8 +203,8 @@ watch(usageData, updateChart, { deep: true })
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border: 1px solid #f0f0f0;
background: var(--bg-primary);
border: 1px solid var(--border-color-split);
border-radius: 8px;
padding: 16px;
}
@ -232,7 +232,7 @@ watch(usageData, updateChart, { deep: true })
}
.usage-summary__card {
background: #fafafa;
background: var(--bg-secondary);
border-radius: 6px;
padding: 10px 12px;
text-align: center;
@ -240,7 +240,7 @@ watch(usageData, updateChart, { deep: true })
.usage-summary__label {
font-size: 12px;
color: #8c8c8c;
color: var(--text-tertiary);
margin-bottom: 4px;
}

View File

@ -120,8 +120,8 @@ onMounted(() => {
}
.search-result-item {
background: #fafafa;
border: 1px solid #f0f0f0;
background: var(--bg-secondary);
border: 1px solid var(--border-color-split);
border-radius: 6px;
padding: 12px 16px;
margin-bottom: 12px;
@ -136,19 +136,19 @@ onMounted(() => {
.search-result-item__index {
font-weight: 600;
color: #1677ff;
color: var(--color-primary);
}
.search-result-item__score {
margin-left: auto;
font-size: 12px;
color: #999;
color: var(--text-placeholder);
}
.search-result-item__content {
font-size: 14px;
line-height: 1.6;
color: #333;
color: var(--text-primary);
white-space: pre-wrap;
max-height: 200px;
overflow-y: auto;

View File

@ -0,0 +1,201 @@
<template>
<div class="agent-layout">
<TopNav />
<div class="agent-layout__body">
<SplitPane
direction="horizontal"
:default-ratio="0.5"
storage-key="agent-h-split"
>
<template #first>
<SplitPane
direction="vertical"
:default-ratio="0.6"
storage-key="agent-left-v-split"
>
<template #first>
<QuadrantPanel
ref="tlPanel"
:tabs="topLeftTabs"
default-tab="chat"
storage-key="quadrant-tl-tab"
>
<template #chat>
<ChatView />
</template>
</QuadrantPanel>
</template>
<template #second>
<QuadrantPanel
ref="blPanel"
:tabs="bottomLeftTabs"
default-tab="terminal"
storage-key="quadrant-bl-tab"
>
<template #terminal>
<TerminalView />
</template>
</QuadrantPanel>
</template>
</SplitPane>
</template>
<template #second>
<SplitPane
direction="vertical"
:default-ratio="0.6"
storage-key="agent-right-v-split"
>
<template #first>
<QuadrantPanel
ref="trPanel"
:tabs="topRightTabs"
default-tab="code"
storage-key="quadrant-tr-tab"
>
<template #code>
<div class="agent-layout__placeholder">
<FileTextOutlined style="font-size: 32px; color: var(--text-placeholder)" />
<p>代码预览</p>
</div>
</template>
<template #workflow>
<WorkflowView />
</template>
<template #knowledge>
<KnowledgeBaseView />
</template>
</QuadrantPanel>
</template>
<template #second>
<QuadrantPanel
ref="brPanel"
:tabs="bottomRightTabs"
default-tab="monitor"
storage-key="quadrant-br-tab"
>
<template #monitor>
<EvolutionView />
</template>
<template #skills>
<SkillsView />
</template>
<template #settings>
<SettingsView />
</template>
</QuadrantPanel>
</template>
</SplitPane>
</template>
</SplitPane>
</div>
<div class="agent-layout__small-screen">
<DesktopOutlined style="font-size: 48px; color: var(--text-placeholder)" />
<h2>请使用更大的屏幕</h2>
<p>Fischer AgentKit 四象限布局需要至少 1280px 宽度的屏幕才能正常使用</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, defineAsyncComponent, type Component } from 'vue'
import { useRoute } from 'vue-router'
import {
MessageOutlined,
CodeOutlined,
FileTextOutlined,
ApartmentOutlined,
BookOutlined,
DashboardOutlined,
AppstoreOutlined,
SettingOutlined,
DesktopOutlined,
} from '@ant-design/icons-vue'
import TopNav from './TopNav.vue'
import SplitPane from './SplitPane.vue'
import QuadrantPanel from './QuadrantPanel.vue'
import type { QuadrantTab } from './QuadrantPanel.vue'
// Lazy-load views to avoid bundling all into initial chunk
const ChatView = defineAsyncComponent(() => import('@/views/ChatView.vue'))
const TerminalView = defineAsyncComponent(() => import('@/views/TerminalView.vue'))
const WorkflowView = defineAsyncComponent(() => import('@/views/WorkflowView.vue'))
const KnowledgeBaseView = defineAsyncComponent(() => import('@/views/KnowledgeBaseView.vue'))
const EvolutionView = defineAsyncComponent(() => import('@/views/EvolutionView.vue'))
const SkillsView = defineAsyncComponent(() => import('@/views/SkillsView.vue'))
const SettingsView = defineAsyncComponent(() => import('@/views/SettingsView.vue'))
const route = useRoute()
const topLeftTabs: QuadrantTab[] = [
{ key: 'chat', label: '对话', icon: MessageOutlined as Component },
]
const bottomLeftTabs: QuadrantTab[] = [
{ key: 'terminal', label: '终端', icon: CodeOutlined as Component },
]
const topRightTabs: QuadrantTab[] = [
{ key: 'code', label: '代码', icon: FileTextOutlined as Component },
{ key: 'workflow', label: '工作流', icon: ApartmentOutlined as Component },
{ key: 'knowledge', label: '知识库', icon: BookOutlined as Component },
]
const bottomRightTabs: QuadrantTab[] = [
{ key: 'monitor', label: '监控', icon: DashboardOutlined as Component },
{ key: 'skills', label: '技能', icon: AppstoreOutlined as Component },
{ key: 'settings', label: '设置', icon: SettingOutlined as Component },
]
// Quadrant refs for route-driven tab switching
const tlPanel = ref<InstanceType<typeof QuadrantPanel> | null>(null)
const blPanel = ref<InstanceType<typeof QuadrantPanel> | null>(null)
const trPanel = ref<InstanceType<typeof QuadrantPanel> | null>(null)
const brPanel = ref<InstanceType<typeof QuadrantPanel> | null>(null)
// Watch route changes to sync quadrant tabs with URL
watch(() => route.meta, (meta) => {
const quadrant = meta.quadrant as string | undefined
const tab = meta.tab as string | undefined
if (quadrant && tab) {
const panelMap: Record<string, typeof tlPanel> = {
tl: tlPanel,
bl: blPanel,
tr: trPanel,
br: brPanel,
}
const panel = panelMap[quadrant]
if (panel?.value) {
panel.value.setActiveTab(tab)
}
}
}, { immediate: true })
</script>
<style scoped>
.agent-layout {
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
overflow: hidden;
background: var(--bg-secondary);
}
.agent-layout__body {
flex: 1;
padding: var(--space-2);
gap: 0;
overflow: hidden;
}
.agent-layout__placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: var(--space-3);
color: var(--text-placeholder);
font-size: var(--font-sm);
}
</style>

View File

@ -21,6 +21,6 @@ import SideNav from './SideNav.vue'
.app-layout__main {
flex: 1;
overflow: hidden;
background: #f5f5f5;
background: var(--bg-tertiary);
}
</style>

View File

@ -0,0 +1,206 @@
<template>
<div class="quadrant-panel" :class="{ 'quadrant-panel--collapsed': collapsed }">
<div class="quadrant-panel__header">
<div class="quadrant-panel__tabs" role="tablist" :aria-label="`${storageKey || 'panel'} tabs`">
<button
v-for="tab in tabs"
:key="tab.key"
:class="['quadrant-panel__tab', { 'quadrant-panel__tab--active': activeTab === tab.key }]"
role="tab"
:aria-selected="activeTab === tab.key"
:tabindex="activeTab === tab.key ? 0 : -1"
@click="activeTab = tab.key"
@keydown="onTabKeydown"
>
<component :is="tab.icon" v-if="tab.icon" class="quadrant-panel__tab-icon" />
<span>{{ tab.label }}</span>
</button>
</div>
<button
class="quadrant-panel__collapse-btn"
@click="collapsed = !collapsed"
:title="collapsed ? '展开' : '折叠'"
:aria-label="collapsed ? '展开面板' : '折叠面板'"
>
<MinusOutlined v-if="!collapsed" />
<PlusOutlined v-else />
</button>
</div>
<div v-show="!collapsed" class="quadrant-panel__body">
<div
v-for="tab in activeTabList"
:key="tab.key"
role="tabpanel"
class="quadrant-panel__content"
>
<slot :name="tab.key" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, computed, type Component } from 'vue'
import { MinusOutlined, PlusOutlined } from '@ant-design/icons-vue'
export interface QuadrantTab {
key: string
label: string
icon?: Component
}
const props = withDefaults(defineProps<{
tabs: QuadrantTab[]
defaultTab?: string
storageKey?: string
}>(), {
defaultTab: '',
})
const savedTab = props.storageKey
? localStorage.getItem(props.storageKey) || props.defaultTab
: props.defaultTab
const activeTab = ref(savedTab || (props.tabs[0]?.key ?? ''))
const collapsed = ref(props.storageKey ? localStorage.getItem(props.storageKey + '-collapsed') === 'true' : false)
// Only render the active tab (v-if via computed filter)
const activeTabList = computed(() => props.tabs.filter(t => t.key === activeTab.value))
watch(activeTab, (val) => {
if (props.storageKey) localStorage.setItem(props.storageKey, val)
})
watch(collapsed, (val) => {
if (props.storageKey) localStorage.setItem(props.storageKey + '-collapsed', String(val))
})
function setActiveTab(tabKey: string) {
if (props.tabs.some(t => t.key === tabKey)) {
activeTab.value = tabKey
}
}
function onTabKeydown(e: KeyboardEvent) {
const currentIndex = props.tabs.findIndex(t => t.key === activeTab.value)
let nextIndex = currentIndex
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
e.preventDefault()
nextIndex = (currentIndex + 1) % props.tabs.length
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
e.preventDefault()
nextIndex = (currentIndex - 1 + props.tabs.length) % props.tabs.length
} else if (e.key === 'Home') {
e.preventDefault()
nextIndex = 0
} else if (e.key === 'End') {
e.preventDefault()
nextIndex = props.tabs.length - 1
}
if (nextIndex !== currentIndex) {
activeTab.value = props.tabs[nextIndex].key
}
}
defineExpose({ setActiveTab })
</script>
<style scoped>
.quadrant-panel {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
}
.quadrant-panel--collapsed {
height: auto !important;
}
.quadrant-panel__header {
display: flex;
align-items: center;
justify-content: space-between;
height: 36px;
padding: 0 var(--space-2);
border-bottom: 1px solid var(--border-color);
background: var(--bg-secondary);
flex-shrink: 0;
}
.quadrant-panel__tabs {
display: flex;
gap: var(--space-1);
overflow-x: auto;
scrollbar-width: none;
}
.quadrant-panel__tabs::-webkit-scrollbar {
display: none;
}
.quadrant-panel__tab {
display: flex;
align-items: center;
gap: var(--space-1);
padding: var(--space-1) var(--space-2);
border: none;
background: transparent;
color: var(--text-tertiary);
font-size: var(--font-xs);
cursor: pointer;
border-radius: var(--radius-sm);
white-space: nowrap;
transition: all var(--transition-fast);
}
.quadrant-panel__tab:hover {
color: var(--text-secondary);
background: var(--bg-tertiary);
}
.quadrant-panel__tab--active {
color: var(--color-primary);
background: var(--color-primary-light);
font-weight: var(--font-weight-medium);
}
.quadrant-panel__tab-icon {
font-size: 12px;
}
.quadrant-panel__collapse-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border: none;
background: transparent;
color: var(--text-tertiary);
cursor: pointer;
border-radius: var(--radius-sm);
flex-shrink: 0;
transition: all var(--transition-fast);
}
.quadrant-panel__collapse-btn:hover {
color: var(--text-secondary);
background: var(--bg-tertiary);
}
.quadrant-panel__body {
flex: 1;
overflow: hidden;
}
.quadrant-panel__content {
height: 100%;
overflow: auto;
}
</style>

View File

@ -88,8 +88,8 @@ watch(
}
)
function handleMenuClick({ key }: { key: string }): void {
router.push(key)
function handleMenuClick({ key }: { key: string | number }): void {
router.push(String(key))
}
</script>

View File

@ -0,0 +1,233 @@
<template>
<div
ref="containerRef"
:class="['split-pane', `split-pane--${direction}`, { 'split-pane--dragging': isDragging }]"
>
<div
class="split-pane__first"
:style="firstStyle"
>
<slot name="first" />
</div>
<div
class="split-pane__handle"
role="separator"
:aria-orientation="direction === 'horizontal' ? 'vertical' : 'horizontal'"
:aria-valuenow="Math.round(ratio * 100)"
aria-valuemin="0"
aria-valuemax="100"
tabindex="0"
@mousedown="onMouseDown"
@touchstart.passive="onTouchStart"
@keydown="onHandleKeydown"
>
<div class="split-pane__handle-line" />
</div>
<div
class="split-pane__second"
:style="secondStyle"
>
<slot name="second" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
const props = withDefaults(defineProps<{
direction?: 'horizontal' | 'vertical'
defaultRatio?: number
minRatio?: number
maxRatio?: number
storageKey?: string
}>(), {
direction: 'horizontal',
defaultRatio: 0.5,
minRatio: 0.2,
maxRatio: 0.8,
})
const containerRef = ref<HTMLElement | null>(null)
const isDragging = ref(false)
const savedRatio = props.storageKey
? parseFloat(localStorage.getItem(props.storageKey) || String(props.defaultRatio))
: props.defaultRatio
const ratio = ref(
Math.min(props.maxRatio, Math.max(props.minRatio, savedRatio))
)
const firstStyle = computed(() => {
if (props.direction === 'horizontal') {
return { width: `${ratio.value * 100}%` }
}
return { height: `${ratio.value * 100}%` }
})
const secondStyle = computed(() => {
if (props.direction === 'horizontal') {
return { width: `${(1 - ratio.value) * 100}%` }
}
return { height: `${(1 - ratio.value) * 100}%` }
})
function clampRatio(val: number) {
return Math.min(props.maxRatio, Math.max(props.minRatio, val))
}
function updateRatioFromPosition(clientPos: number, startPos: number, startRatio: number, containerSize: number) {
const delta = (clientPos - startPos) / containerSize
ratio.value = clampRatio(startRatio + delta)
}
function finishDrag() {
isDragging.value = false
if (props.storageKey) {
localStorage.setItem(props.storageKey, String(ratio.value))
}
window.dispatchEvent(new Event('resize'))
}
function onMouseDown(e: MouseEvent) {
e.preventDefault()
if (!containerRef.value) return
isDragging.value = true
const startPos = props.direction === 'horizontal' ? e.clientX : e.clientY
const containerSize = props.direction === 'horizontal'
? containerRef.value.offsetWidth
: containerRef.value.offsetHeight
const startRatio = ratio.value
function onMouseMove(ev: MouseEvent) {
const currentPos = props.direction === 'horizontal' ? ev.clientX : ev.clientY
updateRatioFromPosition(currentPos, startPos, startRatio, containerSize)
}
function onMouseUp() {
finishDrag()
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
}
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}
function onTouchStart(e: TouchEvent) {
if (!containerRef.value) return
isDragging.value = true
const touch = e.touches[0]
const startPos = props.direction === 'horizontal' ? touch.clientX : touch.clientY
const containerSize = props.direction === 'horizontal'
? containerRef.value.offsetWidth
: containerRef.value.offsetHeight
const startRatio = ratio.value
function onTouchMove(ev: TouchEvent) {
const t = ev.touches[0]
const currentPos = props.direction === 'horizontal' ? t.clientX : t.clientY
updateRatioFromPosition(currentPos, startPos, startRatio, containerSize)
}
function onTouchEnd() {
finishDrag()
document.removeEventListener('touchmove', onTouchMove)
document.removeEventListener('touchend', onTouchEnd)
}
document.addEventListener('touchmove', onTouchMove, { passive: true })
document.addEventListener('touchend', onTouchEnd)
}
function onHandleKeydown(e: KeyboardEvent) {
const step = 0.02 // 2% per key press
if (props.direction === 'horizontal') {
if (e.key === 'ArrowLeft') { e.preventDefault(); ratio.value = clampRatio(ratio.value - step) }
else if (e.key === 'ArrowRight') { e.preventDefault(); ratio.value = clampRatio(ratio.value + step) }
} else {
if (e.key === 'ArrowUp') { e.preventDefault(); ratio.value = clampRatio(ratio.value - step) }
else if (e.key === 'ArrowDown') { e.preventDefault(); ratio.value = clampRatio(ratio.value + step) }
}
if (e.key === 'Home') { e.preventDefault(); ratio.value = props.minRatio }
else if (e.key === 'End') { e.preventDefault(); ratio.value = props.maxRatio }
}
</script>
<style scoped>
.split-pane {
display: flex;
width: 100%;
height: 100%;
overflow: hidden;
}
.split-pane--horizontal {
flex-direction: row;
}
.split-pane--vertical {
flex-direction: column;
}
.split-pane__first,
.split-pane__second {
overflow: hidden;
min-width: 0;
min-height: 0;
}
.split-pane__handle {
flex-shrink: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
transition: background-color var(--transition-fast);
}
.split-pane--horizontal > .split-pane__handle {
width: 6px;
cursor: col-resize;
flex-direction: column;
}
.split-pane--vertical > .split-pane__handle {
height: 6px;
cursor: row-resize;
flex-direction: row;
}
.split-pane__handle:hover,
.split-pane--dragging .split-pane__handle {
background-color: var(--color-primary);
}
.split-pane__handle-line {
background-color: var(--border-color);
border-radius: var(--radius-full);
transition: background-color var(--transition-fast);
}
.split-pane--horizontal > .split-pane__handle > .split-pane__handle-line {
width: 2px;
height: 24px;
}
.split-pane--vertical > .split-pane__handle > .split-pane__handle-line {
height: 2px;
width: 24px;
}
.split-pane__handle:hover .split-pane__handle-line,
.split-pane--dragging .split-pane__handle-line {
background-color: transparent;
}
.split-pane--dragging {
user-select: none;
}
</style>

View File

@ -0,0 +1,129 @@
<template>
<header class="top-nav">
<div class="top-nav__left">
<div class="top-nav__logo" @click="router.push('/agent')">
<span class="top-nav__logo-text">Fischer</span>
<span class="top-nav__logo-badge">AgentKit</span>
</div>
</div>
<div class="top-nav__center">
</div>
<div class="top-nav__right">
<div class="top-nav__status">
<a-badge
:status="wsConnected ? 'success' : 'error'"
:text="wsConnected ? '已连接' : '未连接'"
/>
</div>
<a-tooltip title="设置">
<button class="top-nav__icon-btn" @click="router.push('/agent/monitor?tab=settings')">
<SettingOutlined />
</button>
</a-tooltip>
</div>
</header>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { Badge as ABadge, Tooltip as ATooltip } from 'ant-design-vue'
import { SettingOutlined } from '@ant-design/icons-vue'
import { useChatStore } from '@/stores/chat'
const router = useRouter()
const chatStore = useChatStore()
const wsConnected = computed(() => chatStore.isWsConnected)
</script>
<style scoped>
.top-nav {
display: flex;
align-items: center;
justify-content: space-between;
height: var(--topnav-height);
padding: 0 var(--space-4);
background: var(--bg-primary);
border-bottom: 1px solid var(--border-color);
flex-shrink: 0;
z-index: var(--z-sticky);
}
.top-nav__left {
display: flex;
align-items: center;
gap: var(--space-3);
}
.top-nav__logo {
display: flex;
align-items: center;
gap: var(--space-2);
cursor: pointer;
user-select: none;
}
.top-nav__logo-text {
font-size: var(--font-lg);
font-weight: var(--font-weight-bold);
background: var(--gradient-brand);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.top-nav__logo-badge {
font-size: var(--font-xs);
font-weight: var(--font-weight-medium);
color: var(--text-inverse);
background: var(--gradient-brand);
padding: 1px var(--space-2);
border-radius: var(--radius-full);
letter-spacing: 0.5px;
}
.top-nav__center {
display: flex;
align-items: center;
}
.top-nav__task-select {
min-width: 200px;
}
.top-nav__right {
display: flex;
align-items: center;
gap: var(--space-3);
}
.top-nav__status {
font-size: var(--font-xs);
}
.top-nav__status :deep(.ant-badge-status-text) {
color: var(--text-tertiary);
font-size: var(--font-xs);
}
.top-nav__icon-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
background: transparent;
color: var(--text-tertiary);
cursor: pointer;
border-radius: var(--radius-md);
transition: all var(--transition-fast);
}
.top-nav__icon-btn:hover {
color: var(--text-primary);
background: var(--bg-tertiary);
}
</style>

View File

@ -60,12 +60,12 @@ defineEmits<{
}
.skill-card__icon {
color: #1677ff;
color: var(--color-primary);
}
.skill-card__desc {
font-size: 13px;
color: #666;
color: var(--text-secondary);
margin-bottom: 8px;
line-height: 1.5;
display: -webkit-box;
@ -90,12 +90,12 @@ defineEmits<{
.skill-card__deps-label {
font-size: 12px;
color: #999;
color: var(--text-placeholder);
}
.skill-card__more {
font-size: 12px;
color: #999;
color: var(--text-placeholder);
}
.skill-card__footer {
@ -106,6 +106,6 @@ defineEmits<{
.skill-card__version {
font-size: 12px;
color: #999;
color: var(--text-placeholder);
}
</style>

View File

@ -103,12 +103,12 @@ async function handleHealthCheck(): Promise<void> {
}
.skill-detail__empty {
color: #999;
color: var(--text-placeholder);
font-size: 13px;
}
.skill-detail__config {
background: #f5f5f5;
background: var(--bg-tertiary);
border-radius: 6px;
padding: 12px;
overflow-x: auto;

View File

@ -15,8 +15,7 @@
>
<div class="command-history__item-header">
<span
class="command-history__exit-code"
:class="record.exit_code === 0 ? 'success' : 'error'"
:class="['command-history__exit-code', record.exit_code === 0 ? 'command-history__exit-code--success' : 'command-history__exit-code--error']"
>
{{ record.exit_code === 0 ? '✓' : '✗' }}
</span>
@ -37,6 +36,7 @@
</template>
<script setup lang="ts">
import { Button as AButton } from 'ant-design-vue'
import { useTerminalStore } from '@/stores/terminal'
const terminalStore = useTerminalStore()
@ -60,61 +60,60 @@ function formatTime(timestamp: number): string {
display: flex;
flex-direction: column;
height: 100%;
background: #fafafa;
border-left: 1px solid #f0f0f0;
background: var(--bg-primary);
}
.command-history__header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
font-weight: 600;
font-size: 14px;
border-bottom: 1px solid #f0f0f0;
padding: var(--space-3) var(--space-4);
font-weight: var(--font-weight-semibold);
font-size: var(--font-base);
border-bottom: 1px solid var(--border-color);
}
.command-history__list {
flex: 1;
overflow-y: auto;
padding: 8px;
padding: var(--space-2);
}
.command-history__item {
padding: 8px 12px;
border-radius: 4px;
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-sm);
cursor: pointer;
margin-bottom: 4px;
transition: background 0.2s;
margin-bottom: var(--space-1);
transition: background var(--transition-fast);
}
.command-history__item:hover {
background: #e6f4ff;
background: var(--color-primary-light);
}
.command-history__item-header {
display: flex;
align-items: center;
gap: 6px;
gap: var(--space-1);
}
.command-history__exit-code {
font-size: 12px;
font-weight: 600;
font-size: var(--font-xs);
font-weight: var(--font-weight-semibold);
}
.command-history__exit-code.success {
color: #52c41a;
.command-history__exit-code--success {
color: var(--color-success);
}
.command-history__exit-code.error {
color: #f5222d;
.command-history__exit-code--error {
color: var(--color-error);
}
.command-history__command {
font-family: 'Menlo', 'Monaco', monospace;
font-size: 12px;
color: #333;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace;
font-size: var(--font-xs);
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -122,20 +121,20 @@ function formatTime(timestamp: number): string {
.command-history__item-meta {
display: flex;
gap: 8px;
gap: var(--space-2);
margin-top: 2px;
font-size: 11px;
color: #999;
color: var(--text-placeholder);
}
.command-history__duration {
color: #1677ff;
color: var(--color-primary);
}
.command-history__empty {
text-align: center;
padding: 24px;
color: #999;
font-size: 13px;
padding: var(--space-6);
color: var(--text-placeholder);
font-size: var(--font-sm);
}
</style>

View File

@ -77,16 +77,65 @@ function historyDown(): void {
}
function ansiToHtml(text: string): string {
// Basic ANSI color code conversion
return text
// Basic ANSI color code conversion using One Dark Pro palette
// Track open spans to ensure balanced HTML on reset
let html = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\x1b\[32m/g, '<span style="color:#4caf50">')
.replace(/\x1b\[33m/g, '<span style="color:#ff9800">')
.replace(/\x1b\[31m/g, '<span style="color:#f44336">')
.replace(/\x1b\[36m/g, '<span style="color:#00bcd4">')
.replace(/\x1b\[0m/g, '</span>')
// Replace color codes with close-previous + open-new pattern
// and track span depth for balanced closing on reset
let spanDepth = 0
const result: string[] = []
let i = 0
while (i < html.length) {
// Check for ANSI escape sequence
if (html[i] === '\x1b' && i + 1 < html.length && html[i + 1] === '[') {
const endIdx = html.indexOf('m', i + 2)
if (endIdx !== -1) {
const code = html.substring(i + 2, endIdx)
if (code === '0' || code === '') {
// Reset: close all open spans
for (let s = 0; s < spanDepth; s++) {
result.push('</span>')
}
spanDepth = 0
} else {
// Color code: close previous span if any, open new one
const colorMap: Record<string, string> = {
'32': 'ansi-green',
'33': 'ansi-yellow',
'31': 'ansi-red',
'36': 'ansi-cyan',
'34': 'ansi-blue',
'35': 'ansi-magenta',
'1': 'ansi-bold',
}
const cls = colorMap[code]
if (cls) {
result.push(`<span class="${cls}">`)
spanDepth++
}
}
i = endIdx + 1
} else {
result.push(html[i])
i++
}
} else {
result.push(html[i])
i++
}
}
// Close any remaining open spans
for (let s = 0; s < spanDepth; s++) {
result.push('</span>')
}
return result.join('')
}
</script>
@ -95,38 +144,38 @@ function ansiToHtml(text: string): string {
display: flex;
flex-direction: column;
height: 100%;
background: #1e1e1e;
border-radius: 6px;
background: var(--code-bg);
border-radius: var(--radius-md);
overflow: hidden;
font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace;
}
.terminal-emulator__output {
flex: 1;
overflow-y: auto;
padding: 12px;
font-size: 13px;
line-height: 1.5;
color: #d4d4d4;
padding: var(--space-3);
font-size: var(--font-sm);
line-height: var(--leading-normal);
color: var(--code-fg);
}
.terminal-emulator__welcome {
color: #888;
color: var(--code-comment);
font-style: italic;
}
.terminal-emulator__input {
display: flex;
align-items: center;
padding: 8px 12px;
border-top: 1px solid #333;
background: #252526;
padding: var(--space-2) var(--space-3);
border-top: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(0, 0, 0, 0.2);
}
.terminal-emulator__prompt {
color: #4caf50;
margin-right: 8px;
font-size: 13px;
color: var(--code-string);
margin-right: var(--space-2);
font-size: var(--font-sm);
white-space: nowrap;
}
@ -135,17 +184,25 @@ function ansiToHtml(text: string): string {
background: transparent;
border: none;
outline: none;
color: #d4d4d4;
color: var(--code-fg);
font-family: inherit;
font-size: 13px;
font-size: var(--font-sm);
}
.terminal-emulator__input-field::placeholder {
color: #555;
color: var(--code-comment);
}
.terminal-line {
white-space: pre-wrap;
word-break: break-all;
}
/* ANSI color classes using One Dark Pro palette */
.terminal-line :deep(.ansi-green) { color: var(--code-string); }
.terminal-line :deep(.ansi-yellow) { color: var(--code-number); }
.terminal-line :deep(.ansi-red) { color: var(--code-variable); }
.terminal-line :deep(.ansi-cyan) { color: var(--code-function); }
.terminal-line :deep(.ansi-blue) { color: var(--code-function); }
.terminal-line :deep(.ansi-magenta) { color: var(--code-keyword); }
</style>

View File

@ -43,8 +43,8 @@ const isPaused = computed(() => {
<style scoped>
.approval-node {
background: #fff;
border: 2px solid #722ed1;
background: var(--bg-primary);
border: 2px solid var(--color-primary);
border-radius: 8px;
padding: 8px 12px;
min-width: 160px;
@ -59,32 +59,32 @@ const isPaused = computed(() => {
}
.approval-node.selected {
border-color: #531dab;
border-color: var(--color-primary-hover);
box-shadow: 0 0 0 3px rgba(114, 46, 209, 0.2);
}
.approval-node.paused {
border-color: #fa8c16;
border-color: var(--color-warning);
animation: pulse-border 2s ease-in-out infinite;
}
/* Execution status styles */
.approval-node.status-running {
border-color: #1890ff;
border-color: var(--color-info);
box-shadow: 0 0 8px rgba(24, 144, 255, 0.5);
animation: pulse 1.5s ease-in-out infinite;
}
.approval-node.status-completed {
border-color: #52c41a;
border-color: var(--color-success);
}
.approval-node.status-failed {
border-color: #ff4d4f;
border-color: var(--color-error);
}
.approval-node.status-waiting_approval {
border-color: #faad14;
border-color: var(--color-warning);
box-shadow: 0 0 8px rgba(250, 173, 20, 0.5);
animation: pulse-waiting 2s ease-in-out infinite;
}
@ -115,7 +115,7 @@ const isPaused = computed(() => {
display: flex;
align-items: center;
justify-content: center;
background: #fff;
background: var(--bg-primary);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
}
@ -124,20 +124,20 @@ const isPaused = computed(() => {
}
.running-icon {
color: #1890ff;
color: var(--color-info);
animation: spin 1s linear infinite;
}
.completed-icon {
color: #52c41a;
color: var(--color-success);
}
.failed-icon {
color: #ff4d4f;
color: var(--color-error);
}
.waiting-icon {
color: #faad14;
color: var(--color-warning);
}
@keyframes spin {
@ -153,13 +153,13 @@ const isPaused = computed(() => {
}
.node-icon {
color: #722ed1;
color: var(--color-primary);
font-size: 14px;
}
.node-title {
font-weight: 600;
color: #333;
color: var(--text-primary);
font-size: 13px;
}
@ -179,15 +179,15 @@ const isPaused = computed(() => {
.node-detail {
display: flex;
gap: 4px;
color: #666;
color: var(--text-secondary);
font-size: 11px;
}
.detail-label {
color: #999;
color: var(--text-placeholder);
}
.detail-value {
color: #666;
color: var(--text-secondary);
}
</style>

View File

@ -43,46 +43,46 @@ defineProps<{
}
.diamond-shape {
background: #fff;
border: 2px solid #faad14;
background: var(--bg-primary);
border: 2px solid var(--color-warning);
border-radius: 8px;
padding: 8px 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
box-shadow: var(--shadow-sm);
transform: none;
transition: border-color 0.3s, box-shadow 0.3s;
}
.condition-node:hover .diamond-shape {
box-shadow: 0 4px 12px rgba(250, 173, 20, 0.25);
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.25);
}
.condition-node.selected .diamond-shape {
border-color: #d48806;
box-shadow: 0 0 0 3px rgba(250, 173, 20, 0.2);
border-color: var(--color-warning);
box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.2);
}
/* Execution status styles */
.condition-node.status-running .diamond-shape {
border-color: #1890ff;
box-shadow: 0 0 8px rgba(24, 144, 255, 0.5);
border-color: var(--color-info);
box-shadow: 0 0 8px rgba(59, 130, 246, 0.5);
animation: pulse 1.5s ease-in-out infinite;
}
.condition-node.status-completed .diamond-shape {
border-color: #52c41a;
border-color: var(--color-success);
}
.condition-node.status-failed .diamond-shape {
border-color: #ff4d4f;
border-color: var(--color-error);
}
.condition-node.status-waiting_approval .diamond-shape {
border-color: #faad14;
border-color: var(--color-warning);
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 4px rgba(24, 144, 255, 0.3); }
50% { box-shadow: 0 0 12px rgba(24, 144, 255, 0.7); }
0%, 100% { box-shadow: 0 0 4px rgba(59, 130, 246, 0.3); }
50% { box-shadow: 0 0 12px rgba(59, 130, 246, 0.7); }
}
/* Status indicator icon */
@ -96,7 +96,7 @@ defineProps<{
display: flex;
align-items: center;
justify-content: center;
background: #fff;
background: var(--bg-primary);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
z-index: 1;
}
@ -106,20 +106,20 @@ defineProps<{
}
.running-icon {
color: #1890ff;
color: var(--color-info);
animation: spin 1s linear infinite;
}
.completed-icon {
color: #52c41a;
color: var(--color-success);
}
.failed-icon {
color: #ff4d4f;
color: var(--color-error);
}
.waiting-icon {
color: #faad14;
color: var(--color-warning);
}
@keyframes spin {
@ -134,24 +134,24 @@ defineProps<{
}
.node-icon {
color: #faad14;
color: var(--color-warning);
font-size: 14px;
}
.node-title {
font-weight: 600;
color: #333;
color: var(--text-primary);
font-size: 13px;
}
.condition-expr {
margin-top: 4px;
padding: 2px 8px;
background: #fffbe6;
border: 1px solid #ffe58f;
background: var(--color-warning-light);
border: 1px solid var(--color-warning-light);
border-radius: 4px;
font-size: 11px;
color: #8c6900;
color: var(--color-warning);
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
@ -159,11 +159,11 @@ defineProps<{
}
.handle-true {
background: #52c41a;
background: var(--color-success);
}
.handle-false {
background: #ff4d4f;
background: var(--color-error);
}
.label-true {
@ -171,7 +171,7 @@ defineProps<{
right: -22px;
top: 50%;
transform: translateY(-50%);
color: #52c41a;
color: var(--color-success);
font-size: 11px;
font-weight: 600;
}
@ -181,7 +181,7 @@ defineProps<{
bottom: -18px;
left: 50%;
transform: translateX(-50%);
color: #ff4d4f;
color: var(--color-error);
font-size: 11px;
font-weight: 600;
}

View File

@ -21,8 +21,8 @@
</a-space>
</div>
<VueFlow
v-model:nodes="nodes"
v-model:edges="edges"
v-model:nodes="store.flowNodes"
v-model:edges="store.flowEdges"
:node-types="nodeTypes"
:default-edge-options="defaultEdgeOptions"
:snap-to-grid="true"
@ -58,9 +58,7 @@ import { useWorkflowStore } from '@/stores/workflow'
const store = useWorkflowStore()
const props = defineProps<{
nodes: any[]
edges: any[]
defineProps<{
saving?: boolean
executing?: boolean
}>()
@ -69,15 +67,13 @@ const emit = defineEmits<{
save: []
execute: []
clear: []
'update:nodes': [nodes: any[]]
'update:edges': [edges: any[]]
'node-select': [nodeId: string | null]
'node-drop': [nodeType: string, position: { x: number; y: number }]
}>()
const canvasRef = ref<HTMLElement>()
const nodeTypes = {
const nodeTypes: Record<string, any> = {
skill: markRaw(SkillNode),
condition: markRaw(ConditionNode),
approval: markRaw(ApprovalNode),
@ -128,7 +124,7 @@ function onConnect(params: any) {
sourceHandle: params.sourceHandle,
targetHandle: params.targetHandle,
animated: true,
style: { stroke: '#1890ff', strokeWidth: 2 },
style: { stroke: '#7c3aed', strokeWidth: 2 },
}
store.addEdge(newEdge)
}
@ -164,17 +160,17 @@ onUnmounted(() => {
function onValidate() {
// Basic validation
if (props.nodes.length === 0) {
if (store.flowNodes.length === 0) {
message.warning('工作流为空,请添加节点')
return
}
// Check for nodes without connections (except the first)
const connectedSources = new Set(props.edges.map((e: any) => e.source))
const connectedTargets = new Set(props.edges.map((e: any) => e.target))
const connectedSources = new Set(store.flowEdges.map((e: any) => e.source))
const connectedTargets = new Set(store.flowEdges.map((e: any) => e.target))
const allConnected = new Set([...connectedSources, ...connectedTargets])
const orphanNodes = props.nodes.filter((n: any) => !allConnected.has(n.id))
if (orphanNodes.length > 0 && props.nodes.length > 1) {
const orphanNodes = store.flowNodes.filter((n: any) => !allConnected.has(n.id))
if (orphanNodes.length > 0 && store.flowNodes.length > 1) {
message.warning(`存在未连接的节点: ${orphanNodes.map((n: any) => n.data?.label).join(', ')}`)
return
}
@ -194,8 +190,8 @@ function onValidate() {
.canvas-toolbar {
padding: 8px 12px;
background: #fff;
border-bottom: 1px solid #f0f0f0;
background: var(--bg-primary);
border-bottom: 1px solid var(--border-color-split);
display: flex;
align-items: center;
z-index: 10;

View File

@ -33,28 +33,28 @@ const nodeTypes = [
name: '技能节点',
desc: '引用已注册的技能',
icon: ThunderboltOutlined,
color: '#1890ff',
color: '#7c3aed',
},
{
type: 'condition',
name: '条件节点',
desc: 'If/else 条件分支',
icon: BranchesOutlined,
color: '#faad14',
color: '#f59e0b',
},
{
type: 'approval',
name: '审批节点',
desc: '人工审批关卡',
icon: UserOutlined,
color: '#722ed1',
color: '#7c3aed',
},
{
type: 'parallel',
name: '并行节点',
desc: '并行执行组',
icon: ForkOutlined,
color: '#52c41a',
color: '#10b981',
},
]
@ -69,8 +69,8 @@ function onDragStart(event: DragEvent, nodeType: string) {
<style scoped>
.node-palette {
width: 200px;
border-right: 1px solid #f0f0f0;
background: #fafafa;
border-right: 1px solid var(--border-color-split);
background: var(--bg-secondary);
display: flex;
flex-direction: column;
height: 100%;
@ -81,8 +81,8 @@ function onDragStart(event: DragEvent, nodeType: string) {
padding: 12px 16px;
font-weight: 600;
font-size: 14px;
color: #333;
border-bottom: 1px solid #f0f0f0;
color: var(--text-primary);
border-bottom: 1px solid var(--border-color-split);
}
.palette-list {
@ -97,15 +97,15 @@ function onDragStart(event: DragEvent, nodeType: string) {
align-items: center;
gap: 10px;
padding: 10px 12px;
background: #fff;
border: 1px solid #e8e8e8;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
cursor: grab;
transition: all 0.2s;
}
.palette-item:hover {
border-color: #1890ff;
border-color: var(--color-primary);
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
}
@ -127,11 +127,11 @@ function onDragStart(event: DragEvent, nodeType: string) {
.item-name {
font-size: 13px;
font-weight: 500;
color: #333;
color: var(--text-primary);
}
.item-desc {
font-size: 11px;
color: #999;
color: var(--text-placeholder);
}
</style>

View File

@ -33,48 +33,48 @@ defineProps<{
<style scoped>
.parallel-node {
background: #fff;
border: 2px solid #52c41a;
background: var(--bg-primary);
border: 2px solid var(--color-success);
border-radius: 8px;
padding: 8px 12px;
min-width: 160px;
font-size: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
box-shadow: var(--shadow-sm);
transition: border-color 0.3s, box-shadow 0.3s;
position: relative;
}
.parallel-node:hover {
box-shadow: 0 4px 12px rgba(82, 196, 26, 0.25);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.25);
}
.parallel-node.selected {
border-color: #389e0d;
box-shadow: 0 0 0 3px rgba(82, 196, 26, 0.2);
border-color: var(--color-success);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2);
}
/* Execution status styles */
.parallel-node.status-running {
border-color: #1890ff;
box-shadow: 0 0 8px rgba(24, 144, 255, 0.5);
border-color: var(--color-info);
box-shadow: 0 0 8px rgba(59, 130, 246, 0.5);
animation: pulse 1.5s ease-in-out infinite;
}
.parallel-node.status-completed {
border-color: #52c41a;
border-color: var(--color-success);
}
.parallel-node.status-failed {
border-color: #ff4d4f;
border-color: var(--color-error);
}
.parallel-node.status-waiting_approval {
border-color: #faad14;
border-color: var(--color-warning);
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 4px rgba(24, 144, 255, 0.3); }
50% { box-shadow: 0 0 12px rgba(24, 144, 255, 0.7); }
0%, 100% { box-shadow: 0 0 4px rgba(59, 130, 246, 0.3); }
50% { box-shadow: 0 0 12px rgba(59, 130, 246, 0.7); }
}
/* Status indicator icon */
@ -88,7 +88,7 @@ defineProps<{
display: flex;
align-items: center;
justify-content: center;
background: #fff;
background: var(--bg-primary);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
}
@ -97,20 +97,20 @@ defineProps<{
}
.running-icon {
color: #1890ff;
color: var(--color-info);
animation: spin 1s linear infinite;
}
.completed-icon {
color: #52c41a;
color: var(--color-success);
}
.failed-icon {
color: #ff4d4f;
color: var(--color-error);
}
.waiting-icon {
color: #faad14;
color: var(--color-warning);
}
@keyframes spin {
@ -126,13 +126,13 @@ defineProps<{
}
.node-icon {
color: #52c41a;
color: var(--color-success);
font-size: 14px;
}
.node-title {
font-weight: 600;
color: #333;
color: var(--text-primary);
font-size: 13px;
}
@ -145,15 +145,15 @@ defineProps<{
.node-detail {
display: flex;
gap: 4px;
color: #666;
color: var(--text-secondary);
font-size: 11px;
}
.detail-label {
color: #999;
color: var(--text-placeholder);
}
.detail-value {
color: #666;
color: var(--text-secondary);
}
</style>

View File

@ -177,8 +177,8 @@ function updateConfig(key: string, value: unknown) {
<style scoped>
.property-panel {
width: 300px;
border-left: 1px solid #f0f0f0;
background: #fff;
border-left: 1px solid var(--border-color-split);
background: var(--bg-primary);
display: flex;
flex-direction: column;
height: 100%;
@ -190,13 +190,13 @@ function updateConfig(key: string, value: unknown) {
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
border-bottom: 1px solid var(--border-color-split);
}
.panel-title {
font-weight: 600;
font-size: 14px;
color: #333;
color: var(--text-primary);
}
.panel-body {

View File

@ -37,48 +37,48 @@ defineProps<{
<style scoped>
.skill-node {
background: #fff;
border: 2px solid #1890ff;
background: var(--bg-primary);
border: 2px solid var(--color-primary);
border-radius: 8px;
padding: 8px 12px;
min-width: 160px;
font-size: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
box-shadow: var(--shadow-sm);
transition: border-color 0.3s, box-shadow 0.3s;
position: relative;
}
.skill-node:hover {
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.25);
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.25);
}
.skill-node.selected {
border-color: #096dd9;
box-shadow: 0 0 0 3px rgba(24, 144, 255, 0.2);
border-color: var(--color-primary-hover);
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.2);
}
/* Execution status styles */
.skill-node.status-running {
border-color: #1890ff;
box-shadow: 0 0 8px rgba(24, 144, 255, 0.5);
border-color: var(--color-info);
box-shadow: 0 0 8px rgba(59, 130, 246, 0.5);
animation: pulse 1.5s ease-in-out infinite;
}
.skill-node.status-completed {
border-color: #52c41a;
border-color: var(--color-success);
}
.skill-node.status-failed {
border-color: #ff4d4f;
border-color: var(--color-error);
}
.skill-node.status-waiting_approval {
border-color: #faad14;
border-color: var(--color-warning);
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 4px rgba(24, 144, 255, 0.3); }
50% { box-shadow: 0 0 12px rgba(24, 144, 255, 0.7); }
0%, 100% { box-shadow: 0 0 4px rgba(59, 130, 246, 0.3); }
50% { box-shadow: 0 0 12px rgba(59, 130, 246, 0.7); }
}
/* Status indicator icon */
@ -92,7 +92,7 @@ defineProps<{
display: flex;
align-items: center;
justify-content: center;
background: #fff;
background: var(--bg-primary);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
}
@ -101,20 +101,20 @@ defineProps<{
}
.running-icon {
color: #1890ff;
color: var(--color-info);
animation: spin 1s linear infinite;
}
.completed-icon {
color: #52c41a;
color: var(--color-success);
}
.failed-icon {
color: #ff4d4f;
color: var(--color-error);
}
.waiting-icon {
color: #faad14;
color: var(--color-warning);
}
@keyframes spin {
@ -130,13 +130,13 @@ defineProps<{
}
.node-icon {
color: #1890ff;
color: var(--color-primary);
font-size: 14px;
}
.node-title {
font-weight: 600;
color: #333;
color: var(--text-primary);
font-size: 13px;
}
@ -149,15 +149,15 @@ defineProps<{
.node-detail {
display: flex;
gap: 4px;
color: #666;
color: var(--text-secondary);
font-size: 11px;
}
.detail-label {
color: #999;
color: var(--text-placeholder);
}
.detail-value {
color: #666;
color: var(--text-secondary);
}
</style>

View File

@ -1,6 +1,7 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import 'ant-design-vue/dist/reset.css'
import './styles'
import App from './App.vue'
import router from './router'

View File

@ -2,96 +2,139 @@ import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
// Agent-First 四象限布局 (新)
{
path: '/agent',
name: 'agent',
component: () => import('@/components/layout/AgentLayout.vue'),
meta: { title: 'AgentKit' },
children: [
{
path: '',
redirect: '/agent/chat',
},
{
path: 'chat',
name: 'agent-chat',
meta: { title: '对话', quadrant: 'tl', tab: 'chat' },
component: () => import('@/views/ChatView.vue'),
},
{
path: 'code',
name: 'agent-code',
meta: { title: '代码', quadrant: 'tr', tab: 'code' },
component: () => import('@/views/WorkflowView.vue'),
},
{
path: 'terminal',
name: 'agent-terminal',
meta: { title: '终端', quadrant: 'bl', tab: 'terminal' },
component: () => import('@/views/TerminalView.vue'),
},
{
path: 'monitor',
name: 'agent-monitor',
meta: { title: '监控', quadrant: 'br', tab: 'monitor' },
component: () => import('@/views/EvolutionView.vue'),
},
],
},
// Default redirect to agent layout
{
path: '/',
name: 'chat',
component: () => import('@/views/ChatView.vue'),
meta: { title: '智能对话' },
redirect: '/agent',
},
// Legacy route redirects → agent quadrant routes
{
path: '/workflow',
name: 'workflow',
component: () => import('@/views/WorkflowView.vue'),
meta: { title: '工作流' },
redirect: '/agent/code?tab=workflow',
},
{
path: '/knowledge',
name: 'knowledge',
component: () => import('@/views/KnowledgeBaseView.vue'),
meta: { title: '知识库' },
redirect: '/agent/code?tab=knowledge',
},
{
path: '/skills',
name: 'skills',
component: () => import('@/views/SkillsView.vue'),
meta: { title: '技能' },
redirect: '/agent/monitor?tab=skills',
},
{
path: '/evolution',
redirect: '/agent/monitor?tab=monitor',
},
{
path: '/settings',
redirect: '/agent/monitor?tab=settings',
},
{
path: '/terminal',
name: 'terminal',
component: () => import('@/views/TerminalView.vue'),
meta: { title: '终端' },
redirect: '/agent/terminal',
},
// Computer Use (保留独立路由,显示"即将推出")
{
path: '/computer-use',
name: 'computer-use',
component: () => import('@/views/ComputerUseView.vue'),
meta: { title: 'Computer Use' },
},
// Legacy layout (fallback)
{
path: '/evolution',
name: 'evolution',
component: () => import('@/views/EvolutionView.vue'),
meta: { title: '自进化' },
path: '/legacy',
name: 'legacy',
component: () => import('@/components/layout/AppLayout.vue'),
meta: { title: 'Fischer AgentKit (Legacy)' },
children: [
{
path: '',
redirect: '/evolution/overview',
redirect: '/legacy/chat',
},
{
path: 'overview',
name: 'evolution-overview',
component: () => import('@/components/evolution/DashboardOverview.vue'),
meta: { title: '概览 - 自进化' },
path: 'chat',
name: 'legacy-chat',
component: () => import('@/views/ChatView.vue'),
meta: { title: '智能对话' },
},
{
path: 'experiences',
name: 'evolution-experiences',
component: () => import('@/components/evolution/ExperiencePanel.vue'),
meta: { title: '经验记录 - 自进化' },
path: 'workflow',
name: 'legacy-workflow',
component: () => import('@/views/WorkflowView.vue'),
meta: { title: '工作流' },
},
{
path: 'metrics',
name: 'evolution-metrics',
component: () => import('@/components/evolution/MetricsPanel.vue'),
meta: { title: '指标趋势 - 自进化' },
path: 'knowledge',
name: 'legacy-knowledge',
component: () => import('@/views/KnowledgeBaseView.vue'),
meta: { title: '知识库' },
},
{
path: 'pitfalls',
name: 'evolution-pitfalls',
component: () => import('@/components/evolution/PitfallRoutePanel.vue'),
meta: { title: '避坑预警 - 自进化' },
path: 'skills',
name: 'legacy-skills',
component: () => import('@/views/SkillsView.vue'),
meta: { title: '技能' },
},
{
path: 'optimizations',
name: 'evolution-optimizations',
component: () => import('@/components/evolution/OptimizationPanel.vue'),
meta: { title: '路径优化 - 自进化' },
path: 'terminal',
name: 'legacy-terminal',
component: () => import('@/views/TerminalView.vue'),
meta: { title: '终端' },
},
{
path: 'usage',
name: 'evolution-usage',
component: () => import('@/components/evolution/UsagePanel.vue'),
meta: { title: '用量统计 - 自进化' },
path: 'evolution',
name: 'legacy-evolution',
component: () => import('@/views/EvolutionView.vue'),
meta: { title: '自进化' },
},
{
path: 'settings',
name: 'legacy-settings',
component: () => import('@/views/SettingsView.vue'),
meta: { title: '设置' },
},
],
},
{
path: '/settings',
name: 'settings',
component: () => import('@/views/SettingsView.vue'),
meta: { title: '设置' },
},
]
const router = createRouter({

View File

@ -30,9 +30,9 @@ export const useWorkflowStore = defineStore('workflow', () => {
const isLoading = ref(false)
const error = ref<string | null>(null)
// Vue Flow state
const flowNodes = ref<Node<WorkflowNodeData>[]>([])
const flowEdges = ref<Edge[]>([])
// Vue Flow state — use any[] to avoid @vue-flow/core deep type recursion with Vue 3.5+
const flowNodes = ref<any[]>([])
const flowEdges = ref<any[]>([])
const selectedNodeId = ref<string | null>(null)
// Undo/Redo state
@ -51,9 +51,9 @@ export const useWorkflowStore = defineStore('workflow', () => {
const executionHistoryTotal = ref(0)
// --- Getters ---
const selectedNode = computed<Node<WorkflowNodeData> | null>(() => {
const selectedNode = computed<any | null>(() => {
if (!selectedNodeId.value) return null
return flowNodes.value.find((n) => n.id === selectedNodeId.value) || null
return flowNodes.value.find((n: any) => n.id === selectedNodeId.value) || null
})
const selectedNodeData = computed<WorkflowNodeData | null>(() => {
@ -65,12 +65,12 @@ export const useWorkflowStore = defineStore('workflow', () => {
// --- Internal mutation methods (no command tracking) ---
function _addNodeDirect(node: Node<WorkflowNodeData>): void {
flowNodes.value = [...flowNodes.value, node]
function _addNodeDirect(node: any): void {
flowNodes.value.push(node)
}
function _removeNodeDirect(nodeId: string): { node: Node<WorkflowNodeData>; edges: Edge[] } | null {
const node = flowNodes.value.find((n) => n.id === nodeId)
function _removeNodeDirect(nodeId: string): { node: any; edges: any[] } | null {
const node = flowNodes.value.find((n: any) => n.id === nodeId)
if (!node) return null
const removedEdges = flowEdges.value.filter(
(e) => e.source === nodeId || e.target === nodeId
@ -97,11 +97,12 @@ export const useWorkflowStore = defineStore('workflow', () => {
}
function _updateNodeDataDirect(nodeId: string, data: Partial<WorkflowNodeData>): void {
const index = flowNodes.value.findIndex((n) => n.id === nodeId)
const index = flowNodes.value.findIndex((n: any) => n.id === nodeId)
if (index !== -1) {
const existing = flowNodes.value[index]
flowNodes.value[index] = {
...flowNodes.value[index],
data: { ...flowNodes.value[index].data, ...data },
...existing,
data: { ...existing.data, ...data },
}
}
}

View File

@ -0,0 +1,10 @@
/**
* Fischer AgentKit Styles Entry
*
* Import this module to load all design tokens and theme configuration.
*/
import './tokens.css'
import './transitions.css'
import './responsive.css'
export { themeConfig } from './theme'

View File

@ -0,0 +1,89 @@
/**
* Fischer AgentKit Responsive Breakpoints
*
* 1440px: Four quadrants fully visible
* 1280-1440px: Bottom-right quadrant auto-collapsed
* <1280px: Prompt to use larger screen
*/
/* ── Full four-quadrant layout ── */
@media (min-width: 1440px) {
.agent-layout__body {
display: flex;
}
}
/* ── Compact: bottom-right quadrant collapsed ── */
@media (min-width: 1280px) and (max-width: 1439px) {
.agent-layout__body {
display: flex;
}
/* Auto-collapse bottom-right quadrant at medium widths */
.agent-layout__body .split-pane--horizontal > .split-pane__second .split-pane--vertical > .split-pane__second .quadrant-panel {
height: auto !important;
}
}
/* ── Too small: show prompt ── */
@media (max-width: 1279px) {
.agent-layout__body {
display: none;
}
.agent-layout__small-screen {
display: flex;
}
}
/* ── Default: hide small screen prompt ── */
.agent-layout__small-screen {
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: var(--space-4);
color: var(--text-tertiary);
text-align: center;
padding: var(--space-8);
}
.agent-layout__small-screen h2 {
font-size: var(--font-lg);
color: var(--text-primary);
}
.agent-layout__small-screen p {
font-size: var(--font-base);
max-width: 400px;
}
/* ── Quadrant min-size for readability ── */
.quadrant-panel {
min-width: var(--quadrant-min-size);
min-height: var(--quadrant-min-size);
}
/* ── TopNav responsive ── */
@media (max-width: 768px) {
.top-nav__center {
display: none;
}
}
/* ── Chat sidebar responsive ── */
@media (max-width: 1024px) {
.chat-sidebar:not(.chat-sidebar--collapsed) {
width: 200px;
}
}
/* ── Print: hide interactive elements ── */
@media print {
.top-nav,
.split-pane__handle,
.quadrant-panel__collapse-btn {
display: none !important;
}
}

View File

@ -0,0 +1,88 @@
/**
* Ant Design Vue Theme Token Mapping
*
* Reads CSS custom properties at runtime from tokens.css to ensure
* single source of truth. Falls back to hardcoded values if CSS
* variables are not yet available (SSR / build time).
*/
import type { ThemeConfig } from 'ant-design-vue/es/config-provider/context'
function readToken(varName: string, fallback: string): string {
if (typeof document === 'undefined') return fallback
const val = getComputedStyle(document.documentElement).getPropertyValue(varName).trim()
return val || fallback
}
export const themeConfig: ThemeConfig = {
token: {
// Brand — read from CSS variables
colorPrimary: readToken('--color-primary', '#7c3aed'),
colorInfo: readToken('--color-primary', '#7c3aed'),
// Semantic
colorSuccess: readToken('--color-success', '#10b981'),
colorWarning: readToken('--color-warning', '#f59e0b'),
colorError: readToken('--color-error', '#ef4444'),
// Text
colorText: readToken('--text-primary', '#171717'),
colorTextSecondary: readToken('--text-secondary', '#525252'),
colorTextTertiary: readToken('--text-tertiary', '#737373'),
colorTextQuaternary: readToken('--text-placeholder', '#a3a3a3'),
// Background
colorBgContainer: readToken('--bg-primary', '#ffffff'),
colorBgLayout: readToken('--bg-secondary', '#fafafa'),
colorBgElevated: readToken('--bg-primary', '#ffffff'),
// Border
colorBorder: readToken('--border-color', '#e5e5e5'),
colorBorderSecondary: readToken('--border-color-split', '#f0f0f0'),
// Font
fontSize: 14,
fontSizeSM: 12,
fontSizeLG: 16,
fontSizeXL: 20,
// Radius
borderRadius: 8,
borderRadiusSM: 6,
borderRadiusLG: 12,
// Spacing
marginXS: 4,
marginSM: 8,
margin: 16,
marginMD: 16,
marginLG: 24,
marginXL: 32,
// Shadow
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
boxShadowSecondary: '0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05)',
// Control
controlHeight: 32,
controlHeightSM: 24,
controlHeightLG: 40,
},
components: {
Menu: {
itemSelectedBg: readToken('--color-primary-light', '#ede9fe'),
itemSelectedColor: readToken('--color-primary', '#7c3aed'),
itemHoverBg: '#f5f3ff',
itemHoverColor: readToken('--color-primary', '#7c3aed'),
itemColor: readToken('--text-secondary', '#525252'),
} as Record<string, unknown>,
Tabs: {
itemSelectedColor: readToken('--color-primary', '#7c3aed'),
itemHoverColor: readToken('--color-primary-hover', '#6d28d9'),
} as Record<string, unknown>,
Select: {
colorPrimary: readToken('--color-primary', '#7c3aed'),
colorPrimaryHover: readToken('--color-primary-hover', '#6d28d9'),
} as Record<string, unknown>,
},
}

View File

@ -0,0 +1,136 @@
/**
* Fischer AgentKit Design Tokens
*
* CSS Custom Properties as the single source of truth.
* Ant Design Vue theme tokens are mapped from these values in theme.ts.
*/
:root {
/* ── Brand Colors ── */
--color-primary: #7c3aed;
--color-primary-hover: #6d28d9;
--color-primary-active: #5b21b6;
--color-primary-light: #ede9fe;
--color-primary-bg: #f5f3ff;
/* ── Gradient ── */
--gradient-brand: linear-gradient(135deg, #7c3aed 0%, #1e1b4b 100%);
/* ── Semantic Colors ── */
--color-success: #10b981;
--color-success-light: #d1fae5;
--color-warning: #f59e0b;
--color-warning-light: #fef3c7;
--color-error: #ef4444;
--color-error-light: #fee2e2;
--color-info: #3b82f6;
--color-info-light: #dbeafe;
/* ── Neutral / Gray Scale ── */
--color-gray-50: #fafafa;
--color-gray-100: #f5f5f5;
--color-gray-200: #e5e5e5;
--color-gray-300: #d4d4d4;
--color-gray-400: #a3a3a3;
--color-gray-500: #737373;
--color-gray-600: #525252;
--color-gray-700: #404040;
--color-gray-800: #262626;
--color-gray-900: #171717;
/* ── Background ── */
--bg-primary: #ffffff;
--bg-secondary: #fafafa;
--bg-tertiary: #f5f5f5;
--bg-elevated: #ffffff;
--bg-code: #282c34;
/* ── Foreground / Text ── */
--text-primary: #171717;
--text-secondary: #525252;
--text-tertiary: #737373;
--text-placeholder: #a3a3a3;
--text-inverse: #ffffff;
--text-code: #abb2bf;
/* ── Border ── */
--border-color: #e5e5e5;
--border-color-hover: #d4d4d4;
--border-color-active: var(--color-primary);
--border-color-split: #f0f0f0;
/* ── Spacing ── */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
/* ── Border Radius ── */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 8px;
--radius-xl: 12px;
--radius-full: 9999px;
/* ── Font Size ── */
--font-xs: 12px;
--font-sm: 13px;
--font-base: 14px;
--font-md: 16px;
--font-lg: 20px;
--font-xl: 24px;
/* ── Font Weight ── */
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
/* ── Line Height ── */
--leading-tight: 1.25;
--leading-normal: 1.5;
--leading-relaxed: 1.75;
/* ── Shadow ── */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.07), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -4px rgba(0, 0, 0, 0.04);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.08), 0 8px 10px -6px rgba(0, 0, 0, 0.04);
/* ── Transition ── */
--transition-fast: 150ms ease;
--transition-normal: 200ms ease;
--transition-slow: 300ms ease;
/* ── Z-Index ── */
--z-dropdown: 1000;
--z-sticky: 1020;
--z-fixed: 1030;
--z-modal-backdrop: 1040;
--z-modal: 1050;
--z-popover: 1060;
--z-tooltip: 1070;
/* ── Layout ── */
--topnav-height: 48px;
--sidebar-width: 280px;
--quadrant-min-size: 320px;
/* ── Code Theme (One Dark Pro) ── */
--code-bg: #282c34;
--code-fg: #abb2bf;
--code-keyword: #c678dd;
--code-string: #98c379;
--code-number: #d19a66;
--code-comment: #5c6370;
--code-function: #61afef;
--code-variable: #e06c75;
--code-type: #e5c07b;
--code-added-bg: rgba(16, 185, 129, 0.15);
--code-removed-bg: rgba(239, 68, 68, 0.15);
}

View File

@ -0,0 +1,132 @@
/**
* Fischer AgentKit Transition Animations
*
* Unified transition classes for Vue <Transition> components.
* All durations reference Design Token variables for consistency.
*/
/* ── Fade ── */
.fade-enter-active,
.fade-leave-active {
transition: opacity var(--transition-fast);
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* ── Slide Up ── */
.slide-up-enter-active,
.slide-up-leave-active {
transition: transform var(--transition-normal), opacity var(--transition-fast);
}
.slide-up-enter-from,
.slide-up-leave-to {
transform: translateY(8px);
opacity: 0;
}
/* ── Slide Down ── */
.slide-down-enter-active,
.slide-down-leave-active {
transition: transform var(--transition-normal), opacity var(--transition-fast);
}
.slide-down-enter-from,
.slide-down-leave-to {
transform: translateY(-8px);
opacity: 0;
}
/* ── Slide Right ── */
.slide-right-enter-active,
.slide-right-leave-active {
transition: transform var(--transition-normal), opacity var(--transition-fast);
}
.slide-right-enter-from,
.slide-right-leave-to {
transform: translateX(-8px);
opacity: 0;
}
/* ── Collapse (height) ── */
.collapse-enter-active,
.collapse-leave-active {
transition: max-height var(--transition-slow) ease, opacity var(--transition-fast);
overflow: hidden;
}
.collapse-enter-from,
.collapse-leave-to {
max-height: 0;
opacity: 0;
}
.collapse-enter-to,
.collapse-leave-from {
max-height: 500px;
opacity: 1;
}
/* ── Scale ── */
.scale-enter-active,
.scale-leave-active {
transition: transform var(--transition-fast), opacity var(--transition-fast);
}
.scale-enter-from,
.scale-leave-to {
transform: scale(0.95);
opacity: 0;
}
/* ── Stagger list items ── */
.stagger-list-enter-active {
transition: transform var(--transition-normal), opacity var(--transition-fast);
}
.stagger-list-leave-active {
transition: transform var(--transition-fast), opacity var(--transition-fast);
}
.stagger-list-enter-from,
.stagger-list-leave-to {
transform: translateY(8px);
opacity: 0;
}
.stagger-list-move {
transition: transform var(--transition-normal);
}
/* ── Skeleton pulse ── */
@keyframes skeleton-pulse {
0% { opacity: 1; }
50% { opacity: 0.4; }
100% { opacity: 1; }
}
.skeleton-loading {
animation: skeleton-pulse 1.5s ease-in-out infinite;
background: linear-gradient(
90deg,
var(--bg-tertiary) 25%,
var(--border-color) 50%,
var(--bg-tertiary) 75%
);
background-size: 200% 100%;
border-radius: var(--radius-sm);
}
/* ── Pulse dot (running status) ── */
@keyframes pulse-dot {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.pulse-dot {
animation: pulse-dot 1.5s ease-in-out infinite;
}

View File

@ -119,7 +119,7 @@ function handleSend(message: string): void {
display: flex;
flex-direction: column;
overflow: hidden;
background: #f5f5f5;
background: var(--bg-secondary);
}
.chat-view__empty {
@ -132,7 +132,7 @@ function handleSend(message: string): void {
.chat-view__messages {
flex: 1;
overflow-y: auto;
padding: 16px 0;
padding: var(--space-4) 0;
}
.chat-view__welcome {
@ -141,45 +141,48 @@ function handleSend(message: string): void {
align-items: center;
justify-content: center;
height: 100%;
color: #999999;
color: var(--text-placeholder);
}
.chat-view__welcome-icon {
font-size: 48px;
color: #1677ff;
margin-bottom: 16px;
background: var(--gradient-brand);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: var(--space-4);
}
.chat-view__welcome h2 {
color: #333333;
font-size: 24px;
margin-bottom: 8px;
color: var(--text-primary);
font-size: var(--font-xl);
margin-bottom: var(--space-2);
}
.chat-view__welcome p {
font-size: 14px;
color: #999999;
font-size: var(--font-base);
color: var(--text-tertiary);
}
.chat-view__steps {
padding: 8px 16px;
margin: 0 16px;
background: #ffffff;
border-radius: 8px;
border: 1px solid #f0f0f0;
padding: var(--space-2) var(--space-4);
margin: 0 var(--space-4);
background: var(--bg-primary);
border-radius: var(--radius-lg);
border: 1px solid var(--border-color);
}
.chat-view__step {
display: flex;
align-items: center;
gap: 6px;
gap: var(--space-1);
padding: 2px 0;
font-size: 13px;
color: #666666;
font-size: var(--font-sm);
color: var(--text-secondary);
}
.chat-view__step-icon {
font-size: 10px;
color: #1677ff;
color: var(--color-primary);
}
</style>

View File

@ -1,65 +1,32 @@
<template>
<div class="evolution-container">
<div class="evolution-sidebar">
<div class="evolution-sidebar__header">
<h2>自进化</h2>
<a-spin v-if="store.isLoading" size="small" />
</div>
<a-menu mode="inline" :selected-keys="[currentRoute]" class="evolution-sidebar__menu">
<a-menu-item key="overview" @click="navigate('overview')">
<DashboardOutlined /> 概览
</a-menu-item>
<a-menu-item key="experiences" @click="navigate('experiences')">
<HistoryOutlined /> 经验记录
</a-menu-item>
<a-menu-item key="metrics" @click="navigate('metrics')">
<LineChartOutlined /> 指标趋势
</a-menu-item>
<a-menu-item key="pitfalls" @click="navigate('pitfalls')">
<WarningOutlined /> 避坑预警
</a-menu-item>
<a-menu-item key="optimizations" @click="navigate('optimizations')">
<RocketOutlined /> 路径优化
</a-menu-item>
<a-menu-item key="usage" @click="navigate('usage')">
<CloudServerOutlined /> 用量统计
</a-menu-item>
</a-menu>
</div>
<div class="evolution-content">
<router-view />
</div>
<a-tabs v-model:activeKey="activeTab" class="evolution-tabs">
<a-tab-pane key="overview" tab="概览+指标">
<DashboardOverview />
</a-tab-pane>
<a-tab-pane key="experiences" tab="经验+坑点">
<div class="evolution-panels">
<ExperienceTimeline :experiences="store.experiences" />
<PitfallPanel :warnings="store.pitfalls" />
</div>
</a-tab-pane>
<a-tab-pane key="usage" tab="用量">
<UsagePanel />
</a-tab-pane>
</a-tabs>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { Spin as ASpin, Menu as AMenu, MenuItem as AMenuItem } from 'ant-design-vue'
import {
DashboardOutlined,
HistoryOutlined,
LineChartOutlined,
WarningOutlined,
RocketOutlined,
CloudServerOutlined,
} from '@ant-design/icons-vue'
import { ref, onMounted, onUnmounted } from 'vue'
import { useEvolutionStore } from '@/stores/evolution'
import DashboardOverview from '@/components/evolution/DashboardOverview.vue'
import ExperienceTimeline from '@/components/evolution/ExperienceTimeline.vue'
import PitfallPanel from '@/components/evolution/PitfallPanel.vue'
import UsagePanel from '@/components/evolution/UsagePanel.vue'
const router = useRouter()
const route = useRoute()
const store = useEvolutionStore()
const currentRoute = computed(() => {
const path = route.path
if (path === '/evolution' || path === '/evolution/') return 'overview'
const segment = path.split('/').pop()
return segment || 'overview'
})
function navigate(key: string) {
router.push(`/evolution/${key}`)
}
const activeTab = ref('overview')
onMounted(() => {
store.loadExperiences()
@ -76,40 +43,18 @@ onUnmounted(() => {
<style scoped>
.evolution-container {
height: 100%;
display: flex;
overflow: hidden;
overflow-y: auto;
padding: var(--space-3) var(--space-4);
background: var(--bg-primary);
}
.evolution-sidebar {
width: 180px;
flex-shrink: 0;
background: #fff;
border-right: 1px solid #f0f0f0;
.evolution-tabs {
height: 100%;
}
.evolution-panels {
display: flex;
flex-direction: column;
}
.evolution-sidebar__header {
display: flex;
align-items: center;
gap: 8px;
padding: 16px 16px 8px;
}
.evolution-sidebar__header h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.evolution-sidebar__menu {
flex: 1;
border-inline-end: none !important;
}
.evolution-content {
flex: 1;
overflow: auto;
padding: 16px;
gap: var(--space-4);
}
</style>

View File

@ -32,8 +32,8 @@ onMounted(() => {
<style scoped>
.kb-view {
height: 100%;
padding: 16px 24px;
padding: var(--space-4) var(--space-6);
overflow-y: auto;
background: #fff;
background: var(--bg-primary);
}
</style>

View File

@ -1,144 +1,144 @@
<template>
<div class="settings-view">
<a-form layout="vertical" class="settings-form">
<!-- LLM 配置 -->
<a-divider orientation="left">LLM 配置</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="Provider">
<a-select v-model:value="settingsStore.llm.provider" placeholder="选择 LLM 提供商">
<a-select-option value="anthropic">Anthropic</a-select-option>
<a-select-option value="openai">OpenAI</a-select-option>
<a-select-option value="gemini">Gemini</a-select-option>
<a-select-option value="custom">自定义</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="模型">
<a-input v-model:value="settingsStore.llm.model" placeholder="例如: claude-sonnet-4-20250514" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="API Key">
<a-input-password v-model:value="settingsStore.llm.api_key" placeholder="输入 API Key" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="Base URL">
<a-input v-model:value="settingsStore.llm.base_url" placeholder="自定义 API 地址(可选)" />
</a-form-item>
</a-col>
</a-row>
<a-tabs v-model:activeKey="activeTab" class="settings-tabs">
<a-tab-pane key="llm" tab="LLM 配置">
<a-form layout="vertical" class="settings-form">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="Provider">
<a-select v-model:value="settingsStore.llm.provider" placeholder="选择 LLM 提供商">
<a-select-option value="anthropic">Anthropic</a-select-option>
<a-select-option value="openai">OpenAI</a-select-option>
<a-select-option value="gemini">Gemini</a-select-option>
<a-select-option value="custom">自定义</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="模型">
<a-input v-model:value="settingsStore.llm.model" placeholder="例如: claude-sonnet-4-20250514" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="API Key">
<a-input-password v-model:value="settingsStore.llm.api_key" placeholder="输入 API Key" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="Base URL">
<a-input v-model:value="settingsStore.llm.base_url" placeholder="自定义 API 地址(可选)" />
</a-form-item>
</a-col>
</a-row>
<a-button type="primary" :loading="settingsStore.isSaving" @click="handleSave">保存 LLM 配置</a-button>
</a-form>
</a-tab-pane>
<!-- 技能配置 -->
<a-divider orientation="left">技能配置</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="默认技能">
<a-input v-model:value="settingsStore.skillSettings.default_skill" placeholder="留空则使用自动路由" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="自动路由">
<a-switch v-model:checked="settingsStore.skillSettings.auto_routing" />
<span style="margin-left: 8px; color: #999">启用后自动将消息路由到最匹配的技能</span>
</a-form-item>
</a-col>
</a-row>
<a-tab-pane key="skills" tab="技能管理">
<a-form layout="vertical" class="settings-form">
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="默认技能">
<a-input v-model:value="settingsStore.skillSettings.default_skill" placeholder="留空则使用自动路由" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="自动路由">
<a-switch v-model:checked="settingsStore.skillSettings.auto_routing" />
<span class="settings-form__hint">启用后自动将消息路由到最匹配的技能</span>
</a-form-item>
</a-col>
</a-row>
<a-button type="primary" :loading="settingsStore.isSaving" @click="handleSave">保存技能配置</a-button>
</a-form>
</a-tab-pane>
<!-- 知识库配置 -->
<a-divider orientation="left">知识库配置</a-divider>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="默认信息源">
<a-select
v-model:value="settingsStore.kbSettings.default_sources"
mode="multiple"
placeholder="选择默认信息源"
>
<a-select-option value="local">本地文档</a-select-option>
<a-select-option value="feishu">飞书</a-select-option>
<a-select-option value="confluence">Confluence</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="检索数量 (Top K)">
<a-input-number v-model:value="settingsStore.kbSettings.top_k" :min="1" :max="50" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="检索模式">
<a-select v-model:value="settingsStore.kbSettings.retrieval_mode">
<a-select-option value="standard">标准</a-select-option>
<a-select-option value="rerank">重排序</a-select-option>
<a-select-option value="compression">压缩</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-tab-pane key="knowledge" tab="知识库设置">
<a-form layout="vertical" class="settings-form">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="默认信息源">
<a-select
v-model:value="settingsStore.kbSettings.default_sources"
mode="multiple"
placeholder="选择默认信息源"
>
<a-select-option value="local">本地文档</a-select-option>
<a-select-option value="feishu">飞书</a-select-option>
<a-select-option value="confluence">Confluence</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="检索数量 (Top K)">
<a-input-number v-model:value="settingsStore.kbSettings.top_k" :min="1" :max="50" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="检索模式">
<a-select v-model:value="settingsStore.kbSettings.retrieval_mode">
<a-select-option value="standard">标准</a-select-option>
<a-select-option value="rerank">重排序</a-select-option>
<a-select-option value="compression">压缩</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-button type="primary" :loading="settingsStore.isSaving" @click="handleSave">保存知识库配置</a-button>
</a-form>
</a-tab-pane>
<!-- 系统配置 -->
<a-divider orientation="left">系统配置</a-divider>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="速率限制 (次/分钟)">
<a-input-number v-model:value="settingsStore.system.rate_limit" :min="1" :max="1000" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="日志级别">
<a-select v-model:value="settingsStore.system.logging_level">
<a-select-option value="DEBUG">DEBUG</a-select-option>
<a-select-option value="INFO">INFO</a-select-option>
<a-select-option value="WARNING">WARNING</a-select-option>
<a-select-option value="ERROR">ERROR</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="CORS 来源">
<a-select
v-model:value="settingsStore.system.cors_origins"
mode="tags"
placeholder="输入 CORS 来源"
/>
</a-form-item>
</a-col>
</a-row>
<a-tab-pane key="system" tab="系统设置">
<a-form layout="vertical" class="settings-form">
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="速率限制 (次/分钟)">
<a-input-number v-model:value="settingsStore.system.rate_limit" :min="1" :max="1000" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="日志级别">
<a-select v-model:value="settingsStore.system.logging_level">
<a-select-option value="DEBUG">DEBUG</a-select-option>
<a-select-option value="INFO">INFO</a-select-option>
<a-select-option value="WARNING">WARNING</a-select-option>
<a-select-option value="ERROR">ERROR</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="CORS 来源">
<a-select
v-model:value="settingsStore.system.cors_origins"
mode="tags"
placeholder="输入 CORS 来源"
/>
</a-form-item>
</a-col>
</a-row>
<a-button type="primary" :loading="settingsStore.isSaving" @click="handleSave">保存系统配置</a-button>
</a-form>
</a-tab-pane>
</a-tabs>
<div class="settings-form__actions">
<a-button type="primary" :loading="settingsStore.isSaving" @click="handleSave">
保存设置
</a-button>
<a-alert
v-if="settingsStore.saveSuccess"
message="设置已保存"
type="success"
show-icon
style="margin-left: 12px"
/>
<a-alert
v-if="settingsStore.error"
:message="settingsStore.error"
type="error"
show-icon
style="margin-left: 12px"
/>
</div>
</a-form>
<div v-if="settingsStore.saveSuccess" class="settings-view__alert">
<a-alert message="设置已保存" type="success" show-icon />
</div>
<div v-if="settingsStore.error" class="settings-view__alert">
<a-alert :message="settingsStore.error" type="error" show-icon />
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { ref, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { useSettingsStore } from '@/stores/settings'
const settingsStore = useSettingsStore()
const activeTab = ref('llm')
onMounted(() => {
settingsStore.fetchSettings()
@ -157,18 +157,27 @@ async function handleSave(): Promise<void> {
<style scoped>
.settings-view {
height: 100%;
padding: 16px 24px;
padding: var(--space-4) var(--space-6);
overflow-y: auto;
background: #fff;
background: var(--bg-primary);
}
.settings-tabs {
height: 100%;
}
.settings-form {
max-width: 900px;
max-width: 600px;
}
.settings-form__actions {
margin-top: 24px;
display: flex;
align-items: center;
.settings-form__hint {
margin-left: var(--space-2);
color: var(--text-tertiary);
font-size: var(--font-sm);
}
.settings-view__alert {
margin-top: var(--space-3);
max-width: 600px;
}
</style>

View File

@ -12,10 +12,16 @@
{{ cap.display_name }} ({{ cap.skill_count }})
</a-select-option>
</a-select>
<a-button @click="handleRefresh" :loading="skillsStore.isLoading">
<template #icon><ReloadOutlined /></template>
刷新
</a-button>
<a-space>
<a-button type="primary" @click="showInstallModal = true">
<template #icon><PlusOutlined /></template>
安装技能
</a-button>
<a-button @click="handleRefresh" :loading="skillsStore.isLoading">
<template #icon><ReloadOutlined /></template>
刷新
</a-button>
</a-space>
</div>
<a-spin :spinning="skillsStore.isLoading">
@ -35,13 +41,35 @@
:skill="skillsStore.selectedSkill"
@close="handleDetailClose"
/>
<a-modal
v-model:open="showInstallModal"
title="安装技能"
:confirm-loading="installing"
@ok="handleInstall"
ok-text="安装"
cancel-text="取消"
>
<a-form layout="vertical">
<a-form-item label="技能名称" required>
<a-input v-model:value="installName" placeholder="例如: find-skills" />
</a-form-item>
<a-form-item label="来源 URL可选">
<a-input v-model:value="installSource" placeholder="https://... 或留空自动搜索" />
</a-form-item>
</a-form>
<a-alert v-if="installError" :message="installError" type="error" show-icon style="margin-top: 8px" />
<a-alert v-if="installSuccess" :message="`技能 ${installSuccess} 安装成功!`" type="success" show-icon style="margin-top: 8px" />
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ReloadOutlined } from '@ant-design/icons-vue'
import { ReloadOutlined, PlusOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { useSkillsStore } from '@/stores/skills'
import { skillsApi } from '@/api/skills'
import SkillCard from '@/components/skills/SkillCard.vue'
import SkillDetail from '@/components/skills/SkillDetail.vue'
@ -49,6 +77,14 @@ const skillsStore = useSkillsStore()
const selectedCapability = ref<string | undefined>(undefined)
const showDetail = ref(false)
// Install state
const showInstallModal = ref(false)
const installName = ref('')
const installSource = ref('')
const installing = ref(false)
const installError = ref('')
const installSuccess = ref('')
onMounted(async () => {
await Promise.all([
skillsStore.fetchSkills(),
@ -77,26 +113,50 @@ function handleDetailClose(): void {
showDetail.value = false
skillsStore.clearSelectedSkill()
}
async function handleInstall(): Promise<void> {
if (!installName.value.trim()) {
installError.value = '请输入技能名称'
return
}
installing.value = true
installError.value = ''
installSuccess.value = ''
try {
const result = await skillsApi.installSkill(
installName.value.trim(),
installSource.value.trim() || undefined
)
installSuccess.value = result.name
message.success(`技能 ${result.name} 安装成功!`)
// Refresh skill list
await handleRefresh()
} catch (e: any) {
installError.value = e?.message || '安装失败'
} finally {
installing.value = false
}
}
</script>
<style scoped>
.skills-view {
height: 100%;
padding: 16px 24px;
padding: var(--space-4) var(--space-6);
overflow-y: auto;
background: #fff;
background: var(--bg-primary);
}
.skills-view__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
margin-bottom: var(--space-4);
}
.skills-view__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--space-4);
}
</style>

View File

@ -2,59 +2,56 @@
<div class="terminal-view">
<div class="terminal-view__main">
<TerminalEmulator />
<!-- Confirmation dialog -->
<div v-if="terminalStore.pendingConfirmation" class="terminal-view__confirmation">
<div class="terminal-view__confirmation-content">
<div class="terminal-view__confirmation-header">
<span class="terminal-view__confirmation-icon"></span>
<span>命令确认</span>
</div>
<div class="terminal-view__confirmation-command">
{{ terminalStore.pendingConfirmation.command }}
</div>
<div class="terminal-view__confirmation-reason">
{{ terminalStore.pendingConfirmation.reason }}
</div>
<div class="terminal-view__confirmation-actions">
<label class="terminal-view__whitelist-check">
<input
v-model="addToWhitelist"
type="checkbox"
/>
添加到会话白名单
</label>
<div class="terminal-view__confirmation-buttons">
<button
class="terminal-view__btn terminal-view__btn--reject"
@click="rejectConfirmation"
>
拒绝
</button>
<button
class="terminal-view__btn terminal-view__btn--approve"
@click="approveConfirmation"
>
确认执行
</button>
</div>
</div>
</div>
</div>
<div :class="['terminal-view__sidebar', { 'terminal-view__sidebar--collapsed': sidebarCollapsed }]">
<button class="terminal-view__sidebar-toggle" @click="sidebarCollapsed = !sidebarCollapsed">
<HistoryOutlined />
</button>
<div v-if="!sidebarCollapsed" class="terminal-view__sidebar-content">
<CommandHistory @select="handleHistorySelect" />
</div>
</div>
<div class="terminal-view__sidebar">
<CommandHistory @select="handleHistorySelect" />
</div>
<!-- Confirmation dialog using Ant Design Modal -->
<a-modal
v-model:open="showConfirmation"
title="命令确认"
:ok-text="'确认执行'"
:cancel-text="'拒绝'"
@ok="approveConfirmation"
@cancel="rejectConfirmation"
:ok-button-props="{ danger: true }"
>
<div class="terminal-view__modal-body">
<div class="terminal-view__modal-command">
{{ terminalStore.pendingConfirmation?.command }}
</div>
<div class="terminal-view__modal-reason">
{{ terminalStore.pendingConfirmation?.reason }}
</div>
<a-checkbox v-model:checked="addToWhitelist">
添加到会话白名单
</a-checkbox>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { ref, onUnmounted } from 'vue'
import { ref, computed, onUnmounted } from 'vue'
import { Modal as AModal, Checkbox as ACheckbox } from 'ant-design-vue'
import { HistoryOutlined } from '@ant-design/icons-vue'
import { useTerminalStore } from '@/stores/terminal'
import TerminalEmulator from '@/components/terminal/TerminalEmulator.vue'
import CommandHistory from '@/components/terminal/CommandHistory.vue'
const terminalStore = useTerminalStore()
const addToWhitelist = ref(false)
const sidebarCollapsed = ref(true)
const showConfirmation = computed({
get: () => !!terminalStore.pendingConfirmation,
set: () => { /* modal handles close via cancel */ },
})
onUnmounted(() => {
terminalStore.disconnectWebSocket()
@ -91,110 +88,65 @@ function rejectConfirmation(): void {
.terminal-view__main {
flex: 1;
overflow: hidden;
padding: 16px;
padding: var(--space-2);
position: relative;
}
.terminal-view__sidebar {
width: 280px;
display: flex;
border-left: 1px solid var(--border-color);
background: var(--bg-primary);
transition: width var(--transition-normal);
overflow: hidden;
}
.terminal-view__confirmation {
position: absolute;
bottom: 60px;
left: 24px;
right: 24px;
z-index: 10;
.terminal-view__sidebar--collapsed {
width: 32px;
}
.terminal-view__confirmation-content {
background: #2d2d2d;
border: 1px solid #ff9800;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
.terminal-view__sidebar:not(.terminal-view__sidebar--collapsed) {
width: 240px;
}
.terminal-view__confirmation-header {
.terminal-view__sidebar-toggle {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #ff9800;
margin-bottom: 8px;
justify-content: center;
width: 32px;
height: 100%;
border: none;
background: transparent;
color: var(--text-tertiary);
cursor: pointer;
flex-shrink: 0;
transition: all var(--transition-fast);
}
.terminal-view__confirmation-icon {
font-size: 16px;
.terminal-view__sidebar-toggle:hover {
color: var(--text-primary);
background: var(--bg-tertiary);
}
.terminal-view__confirmation-command {
font-family: 'Menlo', 'Monaco', monospace;
font-size: 13px;
color: #d4d4d4;
background: #1e1e1e;
padding: 8px 12px;
border-radius: 4px;
margin-bottom: 8px;
.terminal-view__sidebar-content {
flex: 1;
overflow: hidden;
min-width: 0;
}
.terminal-view__modal-command {
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, Consolas, monospace;
font-size: var(--font-sm);
color: var(--text-primary);
background: var(--bg-tertiary);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
margin-bottom: var(--space-3);
word-break: break-all;
}
.terminal-view__confirmation-reason {
font-size: 12px;
color: #999;
margin-bottom: 12px;
}
.terminal-view__confirmation-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.terminal-view__whitelist-check {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #aaa;
cursor: pointer;
}
.terminal-view__whitelist-check input {
cursor: pointer;
}
.terminal-view__confirmation-buttons {
display: flex;
gap: 8px;
}
.terminal-view__btn {
padding: 6px 16px;
border: none;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
transition: background 0.2s;
}
.terminal-view__btn--reject {
background: #555;
color: #d4d4d4;
}
.terminal-view__btn--reject:hover {
background: #666;
}
.terminal-view__btn--approve {
background: #ff9800;
color: #1e1e1e;
font-weight: 600;
}
.terminal-view__btn--approve:hover {
background: #ffa726;
.terminal-view__modal-reason {
font-size: var(--font-sm);
color: var(--text-secondary);
margin-bottom: var(--space-3);
}
</style>

View File

@ -315,55 +315,54 @@ function handleBack() {
display: flex;
flex-direction: column;
height: 100%;
padding: 16px;
padding: var(--space-4);
}
.list-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
margin-bottom: var(--space-4);
}
.list-header h3 {
margin: 0;
font-size: 16px;
font-size: var(--font-md);
}
.list-body {
display: flex;
flex-direction: column;
gap: 8px;
gap: var(--space-2);
}
.workflow-item {
display: flex;
align-items: center;
padding: 12px 16px;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: var(--space-3) var(--space-4);
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
cursor: pointer;
transition: all 0.2s;
gap: 12px;
transition: all var(--transition-fast);
gap: var(--space-3);
}
.workflow-item:hover {
border-color: #1890ff;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.12);
border-color: var(--color-primary);
box-shadow: var(--shadow-md);
}
.item-name {
font-weight: 500;
font-size: 14px;
flex: 1;
font-weight: var(--font-weight-medium);
font-size: var(--font-base);
}
.item-meta {
display: flex;
gap: 8px;
font-size: 12px;
color: #999;
gap: var(--space-2);
font-size: var(--font-xs);
color: var(--text-tertiary);
}
.editor-main {
@ -382,8 +381,8 @@ function handleBack() {
}
.execution-history {
border-top: 1px solid #f0f0f0;
background: #fafafa;
border-top: 1px solid var(--border-color);
background: var(--bg-secondary);
flex-shrink: 0;
}
@ -391,20 +390,20 @@ function handleBack() {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
padding: var(--space-2) var(--space-4);
cursor: pointer;
font-size: 13px;
font-weight: 500;
color: #666;
font-size: var(--font-sm);
font-weight: var(--font-weight-medium);
color: var(--text-secondary);
user-select: none;
}
.history-header:hover {
background: #f0f0f0;
background: var(--bg-tertiary);
}
.history-body {
padding: 0 16px 8px;
padding: 0 var(--space-4) var(--space-2);
}
.back-bar {
@ -414,16 +413,16 @@ function handleBack() {
right: 300px;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
background: rgba(255, 255, 255, 0.95);
border-bottom: 1px solid #f0f0f0;
border-bottom: 1px solid var(--border-color);
z-index: 20;
}
.workflow-name {
font-weight: 600;
font-size: 14px;
color: #333;
font-weight: var(--font-weight-semibold);
font-size: var(--font-base);
color: var(--text-primary);
}
</style>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.side-nav[data-v-ffaa5764]{height:100vh;overflow-y:auto;display:flex;flex-direction:column}.side-nav[data-v-ffaa5764] .ant-layout-sider-children{display:flex;flex-direction:column;height:100%}.side-nav__logo[data-v-ffaa5764]{height:64px;display:flex;align-items:center;justify-content:center;border-bottom:1px solid rgba(255,255,255,.1)}.side-nav__title[data-v-ffaa5764]{color:#fff;font-size:18px;font-weight:600;margin:0;white-space:nowrap}.side-nav__footer[data-v-ffaa5764]{margin-top:auto;padding:16px 24px;border-top:1px solid rgba(255,255,255,.1)}.side-nav__footer[data-v-ffaa5764] .ant-badge-status-text{color:#ffffffa6;font-size:12px}.app-layout[data-v-1f8febf9]{height:100vh;width:100vw}.app-layout__main[data-v-1f8febf9]{flex:1;overflow:hidden;background:var(--bg-tertiary)}

View File

@ -0,0 +1 @@
import{c as i,I as u}from"./index-Cdm90D30.js";var l={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"}}]},name:"appstore",theme:"outlined"};function a(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(c){return Object.getOwnPropertyDescriptor(e,c).enumerable}))),n.forEach(function(c){p(r,c,e[c])})}return r}function p(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var o=function(t,e){var n=a({},t,e.attrs);return i(u,a({},n,{icon:l}),null)};o.displayName="AppstoreOutlined";o.inheritAttrs=!1;export{o as A};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
import{d as E,r as f,i as V,z as o,R,c as h,J as W,P as z}from"./index-Cdm90D30.js";import{i as F}from"./_plugin-vue_export-helper-BpJgGuqH.js";var I=function(t,l){var c={};for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&l.indexOf(n)<0&&(c[n]=t[n]);if(t!=null&&typeof Object.getOwnPropertySymbols=="function")for(var a=0,n=Object.getOwnPropertySymbols(t);a<n.length;a++)l.indexOf(n[a])<0&&Object.prototype.propertyIsEnumerable.call(t,n[a])&&(c[n[a]]=t[n[a]]);return c};const J={prefixCls:String,name:String,id:String,type:String,defaultChecked:{type:[Boolean,Number],default:void 0},checked:{type:[Boolean,Number],default:void 0},disabled:Boolean,tabindex:{type:[Number,String]},readonly:Boolean,autofocus:Boolean,value:z.any,required:Boolean},G=E({compatConfig:{MODE:3},name:"Checkbox",inheritAttrs:!1,props:F(J,{prefixCls:"rc-checkbox",type:"checkbox",defaultChecked:!1}),emits:["click","change"],setup(t,l){let{attrs:c,emit:n,expose:a}=l;const d=f(t.checked===void 0?t.defaultChecked:t.checked),s=f();V(()=>t.checked,()=>{d.value=t.checked}),a({focus(){var e;(e=s.value)===null||e===void 0||e.focus()},blur(){var e;(e=s.value)===null||e===void 0||e.blur()}});const i=f(),y=e=>{if(t.disabled)return;t.checked===void 0&&(d.value=e.target.checked),e.shiftKey=i.value;const u={target:o(o({},t),{checked:e.target.checked}),stopPropagation(){e.stopPropagation()},preventDefault(){e.preventDefault()},nativeEvent:e};t.checked!==void 0&&(s.value.checked=!!t.checked),n("change",u),i.value=!1},k=e=>{n("click",e),i.value=e.shiftKey};return()=>{const{prefixCls:e,name:u,id:g,type:m,disabled:b,readonly:x,tabindex:C,autofocus:O,value:P,required:S}=t,_=I(t,["prefixCls","name","id","type","disabled","readonly","tabindex","autofocus","value","required"]),{class:j,onFocus:B,onBlur:K,onKeydown:N,onKeypress:w,onKeyup:D}=c,v=o(o({},_),c),$=Object.keys(v).reduce((p,r)=>((r.startsWith("data-")||r.startsWith("aria-")||r==="role")&&(p[r]=v[r]),p),{}),q=R(e,j,{[`${e}-checked`]:d.value,[`${e}-disabled`]:b}),A=o(o({name:u,id:g,type:m,readonly:x,disabled:b,tabindex:C,class:`${e}-input`,checked:!!d.value,autofocus:O,value:P},$),{onChange:y,onClick:k,onFocus:B,onBlur:K,onKeydown:N,onKeypress:w,onKeyup:D,required:S});return h("span",{class:q},[h("input",W({ref:s},A),null),h("span",{class:`${e}-inner`},null)])}}});export{G as V};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.placeholder-view[data-v-baa4efd2]{display:flex;align-items:center;justify-content:center;height:100%}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
import{c as i,I as u}from"./index-Cdm90D30.js";var l={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M928 140H96c-17.7 0-32 14.3-32 32v496c0 17.7 14.3 32 32 32h380v112H304c-8.8 0-16 7.2-16 16v48c0 4.4 3.6 8 8 8h432c4.4 0 8-3.6 8-8v-48c0-8.8-7.2-16-16-16H548V700h380c17.7 0 32-14.3 32-32V172c0-17.7-14.3-32-32-32zm-40 488H136V212h752v416z"}}]},name:"desktop",theme:"outlined"};function c(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){s(r,a,e[a])})}return r}function s(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var o=function(t,e){var n=c({},t,e.attrs);return i(u,c({},n,{icon:l}),null)};o.displayName="DesktopOutlined";o.inheritAttrs=!1;export{o as D};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
import{u}from"./responsiveObserve-Dze5mRNR.js";import{E as s,a2 as i,K as c,L as f,c as p,I as v}from"./index-Cdm90D30.js";const h=e=>({color:e.colorLink,textDecoration:"none",outline:"none",cursor:"pointer",transition:`color ${e.motionDurationSlow}`,"&:focus, &:hover":{color:e.colorLinkHover},"&:active":{color:e.colorLinkActive}});function g(){const e=c({});let t=null;const r=u();return s(()=>{t=r.value.subscribe(n=>{e.value=n})}),i(()=>{r.value.unsubscribe(t)}),e}function H(e){const t=c();return f(()=>{t.value=e()},{flush:"sync"}),t}var d={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M928 444H820V330.4c0-17.7-14.3-32-32-32H473L355.7 186.2a8.15 8.15 0 00-5.5-2.2H96c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h698c13 0 24.8-7.9 29.7-20l134-332c1.5-3.8 2.3-7.9 2.3-12 0-17.7-14.3-32-32-32zM136 256h188.5l119.6 114.4H748V444H238c-13 0-24.8 7.9-29.7 20L136 643.2V256zm635.3 512H159l103.3-256h612.4L771.3 768z"}}]},name:"folder-open",theme:"outlined"};function a(e){for(var t=1;t<arguments.length;t++){var r=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(r);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(r).filter(function(o){return Object.getOwnPropertyDescriptor(r,o).enumerable}))),n.forEach(function(o){O(e,o,r[o])})}return e}function O(e,t,r){return t in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}var l=function(t,r){var n=a({},t,r.attrs);return p(v,a({},n,{icon:d}),null)};l.displayName="FolderOpenOutlined";l.inheritAttrs=!1;export{l as F,H as e,h as o,g as u};

View File

@ -0,0 +1 @@
import{z as I,T as p,B as i,d as c,ab as C,i as f,f as F,r as x,C as a}from"./index-Cdm90D30.js";import{e as y}from"./index-DlUg5G7X.js";function w(n,o){const e=I({},n);for(let t=0;t<o.length;t+=1){const l=o[t];delete e[l]}return e}const r=Symbol("ContextProps"),s=Symbol("InternalContextProps"),S=function(n){let o=arguments.length>1&&arguments[1]!==void 0?arguments[1]:F(()=>!0);const e=x(new Map),t=(m,v)=>{e.value.set(m,v),e.value=new Map(e.value)},l=m=>{e.value.delete(m),e.value=new Map(e.value)};f([o,e],()=>{}),a(r,n),a(s,{addFormItemField:t,removeFormItemField:l})},d={id:F(()=>{}),onFieldBlur:()=>{},onFieldChange:()=>{},clearValidate:()=>{}},u={addFormItemField:()=>{},removeFormItemField:()=>{}},_=()=>{const n=i(s,u),o=Symbol("FormItemFieldKey"),e=C();return n.addFormItemField(o,e.type),p(()=>{n.removeFormItemField(o)}),a(s,u),a(r,d),i(r,d)},K=c({compatConfig:{MODE:3},name:"AFormItemRest",setup(n,o){let{slots:e}=o;return a(s,u),a(r,d),()=>{var t;return(t=e.default)===null||t===void 0?void 0:t.call(e)}}}),g=y({}),M=c({name:"NoFormStatus",setup(n,o){let{slots:e}=o;return g.useProvide({}),()=>{var t;return(t=e.default)===null||t===void 0?void 0:t.call(e)}}});export{g as F,M as N,S as a,K as b,w as o,_ as u};

View File

@ -0,0 +1 @@
const F={BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,CAPS_LOCK:20,ESC:27,SPACE:32,LEFT:37,UP:38,RIGHT:39,DOWN:40,N:78,P:80,META:91,WIN_KEY_RIGHT:92,CONTEXT_MENU:93,F1:112,F2:113,F3:114,F4:115,F5:116,F6:117,F7:118,F8:119,F9:120,F10:121,F11:122,F12:123,SEMICOLON:186,EQUALS:187,WIN_KEY:224};export{F as K};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.document-upload[data-v-55410552]{padding:8px 0}.upload-spin[data-v-55410552]{display:block;text-align:center;padding:16px}.document-list[data-v-55410552]{margin-top:16px}.source-config[data-v-752313e6]{padding:8px 0}.source-config__header[data-v-752313e6]{margin-bottom:16px;display:flex;justify-content:flex-end}.search-test[data-v-bfaf0801]{padding:8px 0}.advanced-options[data-v-bfaf0801]{margin-top:12px}.advanced-form[data-v-bfaf0801]{flex-wrap:wrap;gap:8px}.search-results[data-v-bfaf0801]{margin-top:16px}.search-result-item[data-v-bfaf0801]{background:var(--bg-secondary);border:1px solid var(--border-color-split);border-radius:6px;padding:12px 16px;margin-bottom:12px}.search-result-item__header[data-v-bfaf0801]{display:flex;align-items:center;gap:8px;margin-bottom:8px}.search-result-item__index[data-v-bfaf0801]{font-weight:600;color:var(--color-primary)}.search-result-item__score[data-v-bfaf0801]{margin-left:auto;font-size:12px;color:var(--text-placeholder)}.search-result-item__content[data-v-bfaf0801]{font-size:14px;line-height:1.6;color:var(--text-primary);white-space:pre-wrap;max-height:200px;overflow-y:auto}.search-result-item__meta[data-v-bfaf0801]{margin-top:8px;display:flex;flex-wrap:wrap;gap:4px}.kb-view[data-v-e4e6375c]{height:100%;padding:var(--space-4) var(--space-6);overflow-y:auto;background:var(--bg-primary)}

View File

@ -0,0 +1 @@
import{c,I as u}from"./index-Cdm90D30.js";var s={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"}}]},name:"right",theme:"outlined"};function i(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){O(r,a,e[a])})}return r}function O(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var f=function(t,e){var n=i({},t,e.attrs);return c(u,i({},n,{icon:s}),null)};f.displayName="RightOutlined";f.inheritAttrs=!1;var g={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"}}]},name:"left",theme:"outlined"};function l(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){p(r,a,e[a])})}return r}function p(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var o=function(t,e){var n=l({},t,e.attrs);return c(u,l({},n,{icon:g}),null)};o.displayName="LeftOutlined";o.inheritAttrs=!1;export{o as L,f as R};

View File

@ -0,0 +1 @@
import{c as i,I as c}from"./index-Cdm90D30.js";var o={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M482 152h60q8 0 8 8v704q0 8-8 8h-60q-8 0-8-8V160q0-8 8-8z"}},{tag:"path",attrs:{d:"M192 474h672q8 0 8 8v60q0 8-8 8H160q-8 0-8-8v-60q0-8 8-8z"}}]},name:"plus",theme:"outlined"};function u(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){s(r,a,e[a])})}return r}function s(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var l=function(t,e){var n=u({},t,e.attrs);return i(c,u({},n,{icon:o}),null)};l.displayName="PlusOutlined";l.inheritAttrs=!1;export{l as P};

View File

@ -0,0 +1 @@
import{c as l,I as c}from"./index-Cdm90D30.js";var p={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M908 640H804V488c0-4.4-3.6-8-8-8H548v-96h108c8.8 0 16-7.2 16-16V80c0-8.8-7.2-16-16-16H368c-8.8 0-16 7.2-16 16v288c0 8.8 7.2 16 16 16h108v96H228c-4.4 0-8 3.6-8 8v152H116c-8.8 0-16 7.2-16 16v288c0 8.8 7.2 16 16 16h288c8.8 0 16-7.2 16-16V656c0-8.8-7.2-16-16-16H292v-88h440v88H620c-8.8 0-16 7.2-16 16v288c0 8.8 7.2 16 16 16h288c8.8 0 16-7.2 16-16V656c0-8.8-7.2-16-16-16zm-564 76v168H176V716h168zm84-408V140h168v168H428zm420 576H680V716h168v168z"}}]},name:"apartment",theme:"outlined"};function i(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){v(r,a,e[a])})}return r}function v(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var f=function(t,e){var n=i({},t,e.attrs);return l(c,i({},n,{icon:p}),null)};f.displayName="ApartmentOutlined";f.inheritAttrs=!1;var O={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32zm-260 72h96v209.9L621.5 312 572 347.4V136zm220 752H232V136h280v296.9c0 3.3 1 6.6 3 9.3a15.9 15.9 0 0022.3 3.7l83.8-59.9 81.4 59.4c2.7 2 6 3.1 9.4 3.1 8.8 0 16-7.2 16-16V136h64v752z"}}]},name:"book",theme:"outlined"};function u(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){g(r,a,e[a])})}return r}function g(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var s=function(t,e){var n=u({},t,e.attrs);return l(c,u({},n,{icon:O}),null)};s.displayName="BookOutlined";s.inheritAttrs=!1;var b={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M924.8 625.7l-65.5-56c3.1-19 4.7-38.4 4.7-57.8s-1.6-38.8-4.7-57.8l65.5-56a32.03 32.03 0 009.3-35.2l-.9-2.6a443.74 443.74 0 00-79.7-137.9l-1.8-2.1a32.12 32.12 0 00-35.1-9.5l-81.3 28.9c-30-24.6-63.5-44-99.7-57.6l-15.7-85a32.05 32.05 0 00-25.8-25.7l-2.7-.5c-52.1-9.4-106.9-9.4-159 0l-2.7.5a32.05 32.05 0 00-25.8 25.7l-15.8 85.4a351.86 351.86 0 00-99 57.4l-81.9-29.1a32 32 0 00-35.1 9.5l-1.8 2.1a446.02 446.02 0 00-79.7 137.9l-.9 2.6c-4.5 12.5-.8 26.5 9.3 35.2l66.3 56.6c-3.1 18.8-4.6 38-4.6 57.1 0 19.2 1.5 38.4 4.6 57.1L99 625.5a32.03 32.03 0 00-9.3 35.2l.9 2.6c18.1 50.4 44.9 96.9 79.7 137.9l1.8 2.1a32.12 32.12 0 0035.1 9.5l81.9-29.1c29.8 24.5 63.1 43.9 99 57.4l15.8 85.4a32.05 32.05 0 0025.8 25.7l2.7.5a449.4 449.4 0 00159 0l2.7-.5a32.05 32.05 0 0025.8-25.7l15.7-85a350 350 0 0099.7-57.6l81.3 28.9a32 32 0 0035.1-9.5l1.8-2.1c34.8-41.1 61.6-87.5 79.7-137.9l.9-2.6c4.5-12.3.8-26.3-9.3-35zM788.3 465.9c2.5 15.1 3.8 30.6 3.8 46.1s-1.3 31-3.8 46.1l-6.6 40.1 74.7 63.9a370.03 370.03 0 01-42.6 73.6L721 702.8l-31.4 25.8c-23.9 19.6-50.5 35-79.3 45.8l-38.1 14.3-17.9 97a377.5 377.5 0 01-85 0l-17.9-97.2-37.8-14.5c-28.5-10.8-55-26.2-78.7-45.7l-31.4-25.9-93.4 33.2c-17-22.9-31.2-47.6-42.6-73.6l75.5-64.5-6.5-40c-2.4-14.9-3.7-30.3-3.7-45.5 0-15.3 1.2-30.6 3.7-45.5l6.5-40-75.5-64.5c11.3-26.1 25.6-50.7 42.6-73.6l93.4 33.2 31.4-25.9c23.7-19.5 50.2-34.9 78.7-45.7l37.9-14.3 17.9-97.2c28.1-3.2 56.8-3.2 85 0l17.9 97 38.1 14.3c28.7 10.8 55.4 26.2 79.3 45.8l31.4 25.8 92.8-32.9c17 22.9 31.2 47.6 42.6 73.6L781.8 426l6.5 39.9zM512 326c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm79.2 255.2A111.6 111.6 0 01512 614c-29.9 0-58-11.7-79.2-32.8A111.6 111.6 0 01400 502c0-29.9 11.7-58 32.8-79.2C454 401.6 482.1 390 512 390c29.9 0 58 11.6 79.2 32.8A111.6 111.6 0 01624 502c0 29.9-11.7 58-32.8 79.2z"}}]},name:"setting",theme:"outlined"};function o(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){d(r,a,e[a])})}return r}function d(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var m=function(t,e){var n=o({},t,e.attrs);return l(c,o({},n,{icon:b}),null)};m.displayName="SettingOutlined";m.inheritAttrs=!1;export{f as A,s as B,m as S};

View File

@ -0,0 +1 @@
.settings-view[data-v-69defdaa]{height:100%;padding:var(--space-4) var(--space-6);overflow-y:auto;background:var(--bg-primary)}.settings-tabs[data-v-69defdaa]{height:100%}.settings-form[data-v-69defdaa]{max-width:600px}.settings-form__hint[data-v-69defdaa]{margin-left:var(--space-2);color:var(--text-tertiary);font-size:var(--font-sm)}.settings-view__alert[data-v-69defdaa]{margin-top:var(--space-3);max-width:600px}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.skill-card[data-v-f7a7f9c2]{cursor:pointer}.skill-card__title[data-v-f7a7f9c2]{display:flex;align-items:center;gap:6px}.skill-card__icon[data-v-f7a7f9c2]{color:var(--color-primary)}.skill-card__desc[data-v-f7a7f9c2]{font-size:13px;color:var(--text-secondary);margin-bottom:8px;line-height:1.5;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.skill-card__tags[data-v-f7a7f9c2]{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px}.skill-card__deps[data-v-f7a7f9c2]{display:flex;align-items:center;gap:4px;flex-wrap:wrap}.skill-card__deps-label[data-v-f7a7f9c2],.skill-card__more[data-v-f7a7f9c2]{font-size:12px;color:var(--text-placeholder)}.skill-card__footer[data-v-f7a7f9c2]{margin-top:8px;display:flex;justify-content:flex-end}.skill-card__version[data-v-f7a7f9c2]{font-size:12px;color:var(--text-placeholder)}.skill-detail__tags[data-v-3ddcc970]{display:flex;flex-wrap:wrap;gap:6px}.skill-detail__empty[data-v-3ddcc970]{color:var(--text-placeholder);font-size:13px}.skill-detail__config[data-v-3ddcc970]{background:var(--bg-tertiary);border-radius:6px;padding:12px;overflow-x:auto}.skill-detail__config pre[data-v-3ddcc970]{margin:0;font-size:12px;line-height:1.5}.skill-detail__actions[data-v-3ddcc970]{margin-top:24px;display:flex;gap:12px}.skills-view[data-v-50d34770]{height:100%;padding:var(--space-4) var(--space-6);overflow-y:auto;background:var(--bg-primary)}.skills-view__header[data-v-50d34770]{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-4)}.skills-view__grid[data-v-50d34770]{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:var(--space-4)}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.terminal-emulator[data-v-e0e9656d]{display:flex;flex-direction:column;height:100%;background:var(--code-bg);border-radius:var(--radius-md);overflow:hidden;font-family:SF Mono,Fira Code,Cascadia Code,Menlo,Consolas,monospace}.terminal-emulator__output[data-v-e0e9656d]{flex:1;overflow-y:auto;padding:var(--space-3);font-size:var(--font-sm);line-height:var(--leading-normal);color:var(--code-fg)}.terminal-emulator__welcome[data-v-e0e9656d]{color:var(--code-comment);font-style:italic}.terminal-emulator__input[data-v-e0e9656d]{display:flex;align-items:center;padding:var(--space-2) var(--space-3);border-top:1px solid rgba(255,255,255,.1);background:#0003}.terminal-emulator__prompt[data-v-e0e9656d]{color:var(--code-string);margin-right:var(--space-2);font-size:var(--font-sm);white-space:nowrap}.terminal-emulator__input-field[data-v-e0e9656d]{flex:1;background:transparent;border:none;outline:none;color:var(--code-fg);font-family:inherit;font-size:var(--font-sm)}.terminal-emulator__input-field[data-v-e0e9656d]::placeholder{color:var(--code-comment)}.terminal-line[data-v-e0e9656d]{white-space:pre-wrap;word-break:break-all}.terminal-line[data-v-e0e9656d] .ansi-green{color:var(--code-string)}.terminal-line[data-v-e0e9656d] .ansi-yellow{color:var(--code-number)}.terminal-line[data-v-e0e9656d] .ansi-red{color:var(--code-variable)}.terminal-line[data-v-e0e9656d] .ansi-cyan,.terminal-line[data-v-e0e9656d] .ansi-blue{color:var(--code-function)}.terminal-line[data-v-e0e9656d] .ansi-magenta{color:var(--code-keyword)}.command-history[data-v-d8bc7055]{display:flex;flex-direction:column;height:100%;background:var(--bg-primary)}.command-history__header[data-v-d8bc7055]{display:flex;justify-content:space-between;align-items:center;padding:var(--space-3) var(--space-4);font-weight:var(--font-weight-semibold);font-size:var(--font-base);border-bottom:1px solid var(--border-color)}.command-history__list[data-v-d8bc7055]{flex:1;overflow-y:auto;padding:var(--space-2)}.command-history__item[data-v-d8bc7055]{padding:var(--space-2) var(--space-3);border-radius:var(--radius-sm);cursor:pointer;margin-bottom:var(--space-1);transition:background var(--transition-fast)}.command-history__item[data-v-d8bc7055]:hover{background:var(--color-primary-light)}.command-history__item-header[data-v-d8bc7055]{display:flex;align-items:center;gap:var(--space-1)}.command-history__exit-code[data-v-d8bc7055]{font-size:var(--font-xs);font-weight:var(--font-weight-semibold)}.command-history__exit-code--success[data-v-d8bc7055]{color:var(--color-success)}.command-history__exit-code--error[data-v-d8bc7055]{color:var(--color-error)}.command-history__command[data-v-d8bc7055]{font-family:SF Mono,Fira Code,Cascadia Code,Menlo,Consolas,monospace;font-size:var(--font-xs);color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.command-history__item-meta[data-v-d8bc7055]{display:flex;gap:var(--space-2);margin-top:2px;font-size:11px;color:var(--text-placeholder)}.command-history__duration[data-v-d8bc7055]{color:var(--color-primary)}.command-history__empty[data-v-d8bc7055]{text-align:center;padding:var(--space-6);color:var(--text-placeholder);font-size:var(--font-sm)}.terminal-view[data-v-e0e2611b]{display:flex;height:100%;overflow:hidden}.terminal-view__main[data-v-e0e2611b]{flex:1;overflow:hidden;padding:var(--space-2);position:relative}.terminal-view__sidebar[data-v-e0e2611b]{display:flex;border-left:1px solid var(--border-color);background:var(--bg-primary);transition:width var(--transition-normal);overflow:hidden}.terminal-view__sidebar--collapsed[data-v-e0e2611b]{width:32px}.terminal-view__sidebar[data-v-e0e2611b]:not(.terminal-view__sidebar--collapsed){width:240px}.terminal-view__sidebar-toggle[data-v-e0e2611b]{display:flex;align-items:center;justify-content:center;width:32px;height:100%;border:none;background:transparent;color:var(--text-tertiary);cursor:pointer;flex-shrink:0;transition:all var(--transition-fast)}.terminal-view__sidebar-toggle[data-v-e0e2611b]:hover{color:var(--text-primary);background:var(--bg-tertiary)}.terminal-view__sidebar-content[data-v-e0e2611b]{flex:1;overflow:hidden;min-width:0}.terminal-view__modal-command[data-v-e0e2611b]{font-family:SF Mono,Fira Code,Cascadia Code,Menlo,Consolas,monospace;font-size:var(--font-sm);color:var(--text-primary);background:var(--bg-tertiary);padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);margin-bottom:var(--space-3);word-break:break-all}.terminal-view__modal-reason[data-v-e0e2611b]{font-size:var(--font-sm);color:var(--text-secondary);margin-bottom:var(--space-3)}

View File

@ -0,0 +1 @@
import{c as u,I as i}from"./index-Cdm90D30.js";var s={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M848 359.3H627.7L825.8 109c4.1-5.3.4-13-6.3-13H436c-2.8 0-5.5 1.5-6.9 4L170 547.5c-3.1 5.3.7 12 6.9 12h174.4l-89.4 357.6c-1.9 7.8 7.5 13.3 13.3 7.7L853.5 373c5.2-4.9 1.7-13.7-5.5-13.7zM378.2 732.5l60.3-241H281.1l189.6-327.4h224.6L487 427.4h211L378.2 732.5z"}}]},name:"thunderbolt",theme:"outlined"};function c(r){for(var e=1;e<arguments.length;e++){var t=arguments[e]!=null?Object(arguments[e]):{},n=Object.keys(t);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(t).filter(function(a){return Object.getOwnPropertyDescriptor(t,a).enumerable}))),n.forEach(function(a){d(r,a,t[a])})}return r}function d(r,e,t){return e in r?Object.defineProperty(r,e,{value:t,enumerable:!0,configurable:!0,writable:!0}):r[e]=t,r}var o=function(e,t){var n=c({},e,t.attrs);return u(i,c({},n,{icon:s}),null)};o.displayName="ThunderboltOutlined";o.inheritAttrs=!1;var b={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M858.5 763.6a374 374 0 00-80.6-119.5 375.63 375.63 0 00-119.5-80.6c-.4-.2-.8-.3-1.2-.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-.4.2-.8.3-1.2.5-44.8 18.9-85 46-119.5 80.6a375.63 375.63 0 00-80.6 119.5A371.7 371.7 0 00136 901.8a8 8 0 008 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c.1 4.4 3.6 7.8 8 7.8h60a8 8 0 008-8.2c-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z"}}]},name:"user",theme:"outlined"};function l(r){for(var e=1;e<arguments.length;e++){var t=arguments[e]!=null?Object(arguments[e]):{},n=Object.keys(t);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(t).filter(function(a){return Object.getOwnPropertyDescriptor(t,a).enumerable}))),n.forEach(function(a){O(r,a,t[a])})}return r}function O(r,e,t){return e in r?Object.defineProperty(r,e,{value:t,enumerable:!0,configurable:!0,writable:!0}):r[e]=t,r}var f=function(e,t){var n=l({},e,t.attrs);return u(i,l({},n,{icon:b}),null)};f.displayName="UserOutlined";f.inheritAttrs=!1;export{o as T,f as U};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
let i={};function t(a,n){}function c(a,n,e){!n&&!i[e]&&(i[e]=!0)}function d(a,n){c(t,a,n)}const o=(a,n,e)=>{d(a,`[ant-design-vue: ${n}] ${e}`)};export{d as a,o as d,t as w};

View File

@ -0,0 +1,3 @@
import{x as K,y as Q,z as _,A as U,aH as Y,d as Z,D as oo,bo as eo,bp as no,bq as lo,ap as to,az as io,ao,bh as so,an as ro,R as co,c as s,a1 as uo,aF as go,aP as po,p as mo,v as fo,J as N,aG as vo,K as w,f as $o,P as v,aA as yo}from"./index-Cdm90D30.js";import{c as ho}from"./zoom-DFzZ47uz.js";const B=(o,e,n,i,a)=>({backgroundColor:o,border:`${i.lineWidth}px ${i.lineType} ${e}`,[`${a}-icon`]:{color:n}}),Co=o=>{const{componentCls:e,motionDurationSlow:n,marginXS:i,marginSM:a,fontSize:u,fontSizeLG:r,lineHeight:g,borderRadiusLG:$,motionEaseInOutCirc:c,alertIconSizeLG:d,colorText:m,paddingContentVerticalSM:f,alertPaddingHorizontal:y,paddingMD:C,paddingContentHorizontalLG:x}=o;return{[e]:_(_({},U(o)),{position:"relative",display:"flex",alignItems:"center",padding:`${f}px ${y}px`,wordWrap:"break-word",borderRadius:$,[`&${e}-rtl`]:{direction:"rtl"},[`${e}-content`]:{flex:1,minWidth:0},[`${e}-icon`]:{marginInlineEnd:i,lineHeight:0},"&-description":{display:"none",fontSize:u,lineHeight:g},"&-message":{color:m},[`&${e}-motion-leave`]:{overflow:"hidden",opacity:1,transition:`max-height ${n} ${c}, opacity ${n} ${c},
padding-top ${n} ${c}, padding-bottom ${n} ${c},
margin-bottom ${n} ${c}`},[`&${e}-motion-leave-active`]:{maxHeight:0,marginBottom:"0 !important",paddingTop:0,paddingBottom:0,opacity:0}}),[`${e}-with-description`]:{alignItems:"flex-start",paddingInline:x,paddingBlock:C,[`${e}-icon`]:{marginInlineEnd:a,fontSize:d,lineHeight:0},[`${e}-message`]:{display:"block",marginBottom:i,color:m,fontSize:r},[`${e}-description`]:{display:"block"}},[`${e}-banner`]:{marginBottom:0,border:"0 !important",borderRadius:0}}},xo=o=>{const{componentCls:e,colorSuccess:n,colorSuccessBorder:i,colorSuccessBg:a,colorWarning:u,colorWarningBorder:r,colorWarningBg:g,colorError:$,colorErrorBorder:c,colorErrorBg:d,colorInfo:m,colorInfoBorder:f,colorInfoBg:y}=o;return{[e]:{"&-success":B(a,i,n,o,e),"&-info":B(y,f,m,o,e),"&-warning":B(g,r,u,o,e),"&-error":_(_({},B(d,c,$,o,e)),{[`${e}-description > pre`]:{margin:0,padding:0}})}}},So=o=>{const{componentCls:e,iconCls:n,motionDurationMid:i,marginXS:a,fontSizeIcon:u,colorIcon:r,colorIconHover:g}=o;return{[e]:{"&-action":{marginInlineStart:a},[`${e}-close-icon`]:{marginInlineStart:a,padding:0,overflow:"hidden",fontSize:u,lineHeight:`${u}px`,backgroundColor:"transparent",border:"none",outline:"none",cursor:"pointer",[`${n}-close`]:{color:r,transition:`color ${i}`,"&:hover":{color:g}}},"&-close-text":{color:r,transition:`color ${i}`,"&:hover":{color:g}}}}},bo=o=>[Co(o),xo(o),So(o)],Io=K("Alert",o=>{const{fontSizeHeading3:e}=o,n=Q(o,{alertIconSizeLG:e,alertPaddingHorizontal:12});return[bo(n)]}),wo={success:ro,info:so,error:ao,warning:io},Bo={success:to,info:lo,error:no,warning:eo},_o=yo("success","info","warning","error"),Ho=()=>({type:v.oneOf(_o),closable:{type:Boolean,default:void 0},closeText:v.any,message:v.any,description:v.any,afterClose:Function,showIcon:{type:Boolean,default:void 0},prefixCls:String,banner:{type:Boolean,default:void 0},icon:v.any,closeIcon:v.any,onClose:Function}),To=Z({compatConfig:{MODE:3},name:"AAlert",inheritAttrs:!1,props:Ho(),setup(o,e){let{slots:n,emit:i,attrs:a,expose:u}=e;const{prefixCls:r,direction:g}=oo("alert",o),[$,c]=Io(r),d=w(!1),m=w(!1),f=w(),y=t=>{t.preventDefault();const p=f.value;p.style.height=`${p.offsetHeight}px`,p.style.height=`${p.offsetHeight}px`,d.value=!0,i("close",t)},C=()=>{var t;d.value=!1,m.value=!0,(t=o.afterClose)===null||t===void 0||t.call(o)},x=$o(()=>{const{type:t}=o;return t!==void 0?t:o.banner?"warning":"info"});u({animationEnd:C});const V=w({});return()=>{var t,p,H,T,z,A,E,O,F,L;const{banner:M,closeIcon:G=(t=n.closeIcon)===null||t===void 0?void 0:t.call(n)}=o;let{closable:P,showIcon:h}=o;const D=(p=o.closeText)!==null&&p!==void 0?p:(H=n.closeText)===null||H===void 0?void 0:H.call(n),S=(T=o.description)!==null&&T!==void 0?T:(z=n.description)===null||z===void 0?void 0:z.call(n),R=(A=o.message)!==null&&A!==void 0?A:(E=n.message)===null||E===void 0?void 0:E.call(n),b=(O=o.icon)!==null&&O!==void 0?O:(F=n.icon)===null||F===void 0?void 0:F.call(n),W=(L=n.action)===null||L===void 0?void 0:L.call(n);h=M&&h===void 0?!0:h;const j=(S?Bo:wo)[x.value]||null;D&&(P=!0);const l=r.value,k=co(l,{[`${l}-${x.value}`]:!0,[`${l}-closing`]:d.value,[`${l}-with-description`]:!!S,[`${l}-no-icon`]:!h,[`${l}-banner`]:!!M,[`${l}-closable`]:P,[`${l}-rtl`]:g.value==="rtl",[c.value]:!0}),X=P?s("button",{type:"button",onClick:y,class:`${l}-close-icon`,tabindex:0},[D?s("span",{class:`${l}-close-text`},[D]):G===void 0?s(uo,null,null):G]):null,q=b&&(go(b)?ho(b,{class:`${l}-icon`}):s("span",{class:`${l}-icon`},[b]))||s(j,{class:`${l}-icon`},null),J=po(`${l}-motion`,{appear:!1,css:!0,onAfterLeave:C,onBeforeLeave:I=>{I.style.maxHeight=`${I.offsetHeight}px`},onLeave:I=>{I.style.maxHeight="0px"}});return $(m.value?null:s(vo,J,{default:()=>[mo(s("div",N(N({role:"alert"},a),{},{style:[a.style,V.value],class:[a.class,k],"data-show":!d.value,ref:f}),[h?q:null,s("div",{class:`${l}-content`},[R?s("div",{class:`${l}-message`},[R]):null,S?s("div",{class:`${l}-description`},[S]):null]),W?s("div",{class:`${l}-action`},[W]):null,X]),[[fo,!d.value]])]}))}}}),Eo=Y(To);export{Eo as _};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
import{x as b,y as S,z as d,A as w,aH as B,d as T,U as k,D as A,R as z,c as f,J as x,aJ as D,r as N,f as _,aR as $,F as I,av as C}from"./index-Cdm90D30.js";import{g as W,P as H,A as R,t as E,a as M}from"./base-BzUb4EcV.js";import{o as j}from"./FormItemContext-C7duC9vx.js";import{i as F}from"./zoom-DFzZ47uz.js";import{i as J}from"./_plugin-vue_export-helper-BpJgGuqH.js";const L=t=>{const{componentCls:o,popoverBg:r,popoverColor:e,width:a,fontWeightStrong:s,popoverPadding:l,boxShadowSecondary:c,colorTextHeading:g,borderRadiusLG:u,zIndexPopup:p,marginXS:m,colorBgElevated:n}=t;return[{[o]:d(d({},w(t)),{position:"absolute",top:0,left:{_skip_check_:!0,value:0},zIndex:p,fontWeight:"normal",whiteSpace:"normal",textAlign:"start",cursor:"auto",userSelect:"text","--antd-arrow-background-color":n,"&-rtl":{direction:"rtl"},"&-hidden":{display:"none"},[`${o}-content`]:{position:"relative"},[`${o}-inner`]:{backgroundColor:r,backgroundClip:"padding-box",borderRadius:u,boxShadow:c,padding:l},[`${o}-title`]:{minWidth:a,marginBottom:m,color:g,fontWeight:s},[`${o}-inner-content`]:{color:e}})},W(t,{colorBg:"var(--antd-arrow-background-color)"}),{[`${o}-pure`]:{position:"relative",maxWidth:"none",[`${o}-content`]:{display:"inline-block"}}}]},O=t=>{const{componentCls:o}=t;return{[o]:H.map(r=>{const e=t[`${r}-6`];return{[`&${o}-${r}`]:{"--antd-arrow-background-color":e,[`${o}-inner`]:{backgroundColor:e},[`${o}-arrow`]:{background:"transparent"}}}})}},G=t=>{const{componentCls:o,lineWidth:r,lineType:e,colorSplit:a,paddingSM:s,controlHeight:l,fontSize:c,lineHeight:g,padding:u}=t,p=l-Math.round(c*g),m=p/2,n=p/2-r,i=u;return{[o]:{[`${o}-inner`]:{padding:0},[`${o}-title`]:{margin:0,padding:`${m}px ${i}px ${n}px`,borderBottom:`${r}px ${e} ${a}`},[`${o}-inner-content`]:{padding:`${s}px ${i}px`}}}},U=b("Popover",t=>{const{colorBgElevated:o,colorText:r,wireframe:e}=t,a=S(t,{popoverBg:o,popoverColor:r,popoverPadding:12});return[L(a),O(a),e&&G(a),F(a,"zoom-big")]},t=>{let{zIndexPopupBase:o}=t;return{zIndexPopup:o+30,width:177}}),V=()=>d(d({},M()),{content:C(),title:C()}),X=T({compatConfig:{MODE:3},name:"APopover",inheritAttrs:!1,props:J(V(),d(d({},E()),{trigger:"hover",placement:"top",mouseEnterDelay:.1,mouseLeaveDelay:.1})),setup(t,o){let{expose:r,slots:e,attrs:a}=o;const s=N();k(t.visible===void 0),r({getPopupDomNode:()=>{var n,i;return(i=(n=s.value)===null||n===void 0?void 0:n.getPopupDomNode)===null||i===void 0?void 0:i.call(n)}});const{prefixCls:l,configProvider:c}=A("popover",t),[g,u]=U(l),p=_(()=>c.getPrefixCls()),m=()=>{var n,i;const{title:v=$((n=e.title)===null||n===void 0?void 0:n.call(e)),content:h=$((i=e.content)===null||i===void 0?void 0:i.call(e))}=t,P=!!(Array.isArray(v)?v.length:v),y=!!(Array.isArray(h)?h.length:v);return!P&&!y?null:f(I,null,[P&&f("div",{class:`${l.value}-title`},[v]),f("div",{class:`${l.value}-inner-content`},[h])])};return()=>{const n=z(t.overlayClassName,u.value);return g(f(R,x(x(x({},j(t,["title","content"])),a),{},{prefixCls:l.value,ref:s,overlayClassName:n,transitionName:D(p.value,"zoom-big",t.transitionName),"data-popover-inject":!0}),{title:m,default:e.default}))}}}),oo=B(X);export{oo as P};

View File

@ -0,0 +1 @@
import{x as b,y as w,z as s,A as z,aH as y,d as M,D as C,M as B,c as f,J as u,f as d}from"./index-Cdm90D30.js";const H=t=>{const{componentCls:e,sizePaddingEdgeHorizontal:o,colorSplit:r,lineWidth:i}=t;return{[e]:s(s({},z(t)),{borderBlockStart:`${i}px solid ${r}`,"&-vertical":{position:"relative",top:"-0.06em",display:"inline-block",height:"0.9em",margin:`0 ${t.dividerVerticalGutterMargin}px`,verticalAlign:"middle",borderTop:0,borderInlineStart:`${i}px solid ${r}`},"&-horizontal":{display:"flex",clear:"both",width:"100%",minWidth:"100%",margin:`${t.dividerHorizontalGutterMargin}px 0`},[`&-horizontal${e}-with-text`]:{display:"flex",alignItems:"center",margin:`${t.dividerHorizontalWithTextGutterMargin}px 0`,color:t.colorTextHeading,fontWeight:500,fontSize:t.fontSizeLG,whiteSpace:"nowrap",textAlign:"center",borderBlockStart:`0 ${r}`,"&::before, &::after":{position:"relative",width:"50%",borderBlockStart:`${i}px solid transparent`,borderBlockStartColor:"inherit",borderBlockEnd:0,transform:"translateY(50%)",content:"''"}},[`&-horizontal${e}-with-text-left`]:{"&::before":{width:"5%"},"&::after":{width:"95%"}},[`&-horizontal${e}-with-text-right`]:{"&::before":{width:"95%"},"&::after":{width:"5%"}},[`${e}-inner-text`]:{display:"inline-block",padding:"0 1em"},"&-dashed":{background:"none",borderColor:r,borderStyle:"dashed",borderWidth:`${i}px 0 0`},[`&-horizontal${e}-with-text${e}-dashed`]:{"&::before, &::after":{borderStyle:"dashed none none"}},[`&-vertical${e}-dashed`]:{borderInlineStartWidth:i,borderInlineEnd:0,borderBlockStart:0,borderBlockEnd:0},[`&-plain${e}-with-text`]:{color:t.colorText,fontWeight:"normal",fontSize:t.fontSize},[`&-horizontal${e}-with-text-left${e}-no-default-orientation-margin-left`]:{"&::before":{width:0},"&::after":{width:"100%"},[`${e}-inner-text`]:{paddingInlineStart:o}},[`&-horizontal${e}-with-text-right${e}-no-default-orientation-margin-right`]:{"&::before":{width:"100%"},"&::after":{width:0},[`${e}-inner-text`]:{paddingInlineEnd:o}}})}},I=b("Divider",t=>{const e=w(t,{dividerVerticalGutterMargin:t.marginXS,dividerHorizontalWithTextGutterMargin:t.margin,dividerHorizontalGutterMargin:t.marginLG});return[H(e)]},{sizePaddingEdgeHorizontal:0}),G=()=>({prefixCls:String,type:{type:String,default:"horizontal"},dashed:{type:Boolean,default:!1},orientation:{type:String,default:"center"},plain:{type:Boolean,default:!1},orientationMargin:[String,Number]}),W=M({name:"ADivider",inheritAttrs:!1,compatConfig:{MODE:3},props:G(),setup(t,e){let{slots:o,attrs:r}=e;const{prefixCls:i,direction:m}=C("divider",t),[v,h]=I(i),g=d(()=>t.orientation==="left"&&t.orientationMargin!=null),c=d(()=>t.orientation==="right"&&t.orientationMargin!=null),x=d(()=>{const{type:n,dashed:l,plain:S}=t,a=i.value;return{[a]:!0,[h.value]:!!h.value,[`${a}-${n}`]:!0,[`${a}-dashed`]:!!l,[`${a}-plain`]:!!S,[`${a}-rtl`]:m.value==="rtl",[`${a}-no-default-orientation-margin-left`]:g.value,[`${a}-no-default-orientation-margin-right`]:c.value}}),$=d(()=>{const n=typeof t.orientationMargin=="number"?`${t.orientationMargin}px`:t.orientationMargin;return s(s({},g.value&&{marginLeft:n}),c.value&&{marginRight:n})}),p=d(()=>t.orientation.length>0?"-"+t.orientation:t.orientation);return()=>{var n;const l=B((n=o.default)===null||n===void 0?void 0:n.call(o));return v(f("div",u(u({},r),{},{class:[x.value,l.length?`${i.value}-with-text ${i.value}-with-text${p.value}`:"",r.class],role:"separator"}),[l.length?f("span",{class:`${i.value}-inner-text`,style:$.value},[l]):null]))}}}),E=y(W);export{E as _};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More