diff --git a/docs/changelog/update.md b/docs/changelog/update.md index f5b5b2de..6422dc58 100644 --- a/docs/changelog/update.md +++ b/docs/changelog/update.md @@ -1,33 +1,26 @@ 目前已有的开发计划包括: -📝 0.2.2 version +📝 0.3 version 不再新增任何特性,仅作功能调整以及 bug 修复。 🐛**BUGs** - [ ] 部分 doc 格式的文件支持有问题 - [ ] 当出现不支持的文件类型的时候,前端没有限制 -- [ ] 当消息生成的时候有报错的时候,前端无显示(需要解决存储的问题) +- [ ] 当消息生成的时候有报错的时候,前端无显示 +- [ ] 另外一个智能体的历史对话无法显示 --- 💭 **Features Todo** -- [x] LangGraph 升级到 0.6+ 版本,并适配新特性,如 context 等,添加 MCP 工具的支持。 - - [x] 支持动态工具配置的同时,将 Configuration替换为 Context 后能够正常使用 -- [x] 使用更好的知识图谱检索方法(二跳结果查询) - [ ] 使用其他的聊天记录管理方法,解决两个问题,一个是上下文长度过长,一个是上下文的类型变得更加丰富,比如多模态等等。(现在是基于 LangGraph 的 Memory 实现的,v0.2.3 版本实现),暂定使用 [mem0](github.com/mem0ai/mem0) 来实现。但是目前了解下来,还不是我想要的那种方案。可能会基于这个实现一个 ThreadConvManager 这个类。 - [ ] 添加对于上传文件的支持:这里的复杂的地方就在于如何和历史记录结合在一起(v0.2.3 版本实现,放在记忆管理后面) -- [x] 将现在的 graphview.vue 文件中 GraphContainer相关的代码分离到一个单独的组件中,然后 actions 和 footer 都是作为 slot top/bottom 提供的 -- [x] 然后应用到 web/src/components/ToolCallingResult/KnowledgeGraphResult.vue 中,替换现有的 GraphContainer。 - [ ] 知识图谱的上传和可视化,支持属性,标签的展示 - [ ] 集成智能体评估,首先使用命令行来实现,然后考虑放在 UI 里面展示 - [ ] 开发与生产环境隔离 - [ ] 添加统计信息 -- [x] 添加内容审查功能 -- [x] 补充 embedding 模型和 reranker 模型的配置说明 - [ ] 支持 MinerU 的解析方法 -- [x] 优化 knowledge 文件夹下面的架构 - [ ] Options 中添加网络搜索和绘制图片的选项,分别是用来调用工具 📝 **Base** @@ -40,10 +33,6 @@ 下面的功能**可能**会放在后续版本实现,暂时未定 -- [x] 支持数据库查询功能 -- [x] 消息内容支持图片显示 -- [x] 添加 SQL 读取工具 -- [x] 添加绘图工具(这里的绘图是指绘制固定格式的图和表等,暂时没想好如何支持自定义绘图) - [ ] 添加测试脚本,覆盖最常见的功能 - [ ] 优化对文档信息的检索展示(检索结果页、详情页) - [ ] 集成 LangFuse (观望)添加用户日志与用户反馈模块,可以在 AgentView 中查看信息 \ No newline at end of file diff --git a/server/routers/dashboard_router.py b/server/routers/dashboard_router.py index c7ded568..5779755a 100644 --- a/server/routers/dashboard_router.py +++ b/server/routers/dashboard_router.py @@ -27,6 +27,48 @@ dashboard = APIRouter(prefix="/dashboard", tags=["Dashboard"]) # ============================================================================= +class UserActivityStats(BaseModel): + """用户活跃度统计""" + + total_users: int + active_users_24h: int + active_users_30d: int + daily_active_users: list[dict] # 最近7天每日活跃用户 + + +class ToolCallStats(BaseModel): + """工具调用统计""" + + total_calls: int + successful_calls: int + failed_calls: int + success_rate: float + most_used_tools: list[dict] + tool_error_distribution: dict + daily_tool_calls: list[dict] # 最近7天每日工具调用数 + + +class KnowledgeStats(BaseModel): + """知识库统计""" + + total_databases: int + total_files: int + total_nodes: int + total_storage_size: int # 字节 + databases_by_type: dict + file_type_distribution: dict + + +class AgentAnalytics(BaseModel): + """AI智能体分析""" + + total_agents: int + agent_conversation_counts: list[dict] + agent_satisfaction_rates: list[dict] + agent_tool_usage: list[dict] + top_performing_agents: list[dict] + + class ConversationListItem(BaseModel): """Conversation list item""" @@ -176,7 +218,373 @@ async def get_conversation_detail( # ============================================================================= -# Statistics +# User Activity Statistics +# ============================================================================= + + +@dashboard.get("/stats/users", response_model=UserActivityStats) +async def get_user_activity_stats( + db: Session = Depends(get_db), + current_user: User = Depends(get_admin_user), +): + """Get user activity statistics (Admin only)""" + try: + from src.storage.db.models import User, Conversation + + now = datetime.utcnow() + + # 基础用户统计 + total_users = db.query(func.count(User.id)).scalar() or 0 + + # 不同时间段的活跃用户数(基于对话活动) + active_users_24h = ( + db.query(func.count(distinct(Conversation.user_id))) + .filter(Conversation.updated_at >= now - timedelta(days=1)) + .scalar() + or 0 + ) + + active_users_30d = ( + db.query(func.count(distinct(Conversation.user_id))) + .filter(Conversation.updated_at >= now - timedelta(days=30)) + .scalar() + or 0 + ) + # 最近7天每日活跃用户 + daily_active_users = [] + for i in range(7): + day_start = now - timedelta(days=i + 1) + day_end = now - timedelta(days=i) + + active_count = ( + db.query(func.count(distinct(Conversation.user_id))) + .filter(Conversation.updated_at >= day_start, Conversation.updated_at < day_end) + .scalar() + or 0 + ) + + daily_active_users.append({"date": day_start.strftime("%Y-%m-%d"), "active_users": active_count}) + + return UserActivityStats( + total_users=total_users, + active_users_24h=active_users_24h, + active_users_30d=active_users_30d, + daily_active_users=list(reversed(daily_active_users)), # 按时间正序 + ) + + except Exception as e: + logger.error(f"Error getting user activity stats: {e}") + logger.error(traceback.format_exc()) + raise HTTPException(status_code=500, detail=f"Failed to get user activity stats: {str(e)}") + + +# ============================================================================= +# Tool Call Statistics +# ============================================================================= + + +@dashboard.get("/stats/tools", response_model=ToolCallStats) +async def get_tool_call_stats( + db: Session = Depends(get_db), + current_user: User = Depends(get_admin_user), +): + """Get tool call statistics (Admin only)""" + try: + from src.storage.db.models import ToolCall + + now = datetime.utcnow() + + # 基础工具调用统计 + total_calls = db.query(func.count(ToolCall.id)).scalar() or 0 + successful_calls = db.query(func.count(ToolCall.id)).filter(ToolCall.status == "success").scalar() or 0 + failed_calls = total_calls - successful_calls + success_rate = round((successful_calls / total_calls * 100), 2) if total_calls > 0 else 0 + + # 最常用工具 + most_used_tools = ( + db.query(ToolCall.tool_name, func.count(ToolCall.id).label("count")) + .group_by(ToolCall.tool_name) + .order_by(func.count(ToolCall.id).desc()) + .limit(10) + .all() + ) + most_used_tools = [{"tool_name": name, "count": count} for name, count in most_used_tools] + + # 工具错误分布 + tool_errors = ( + db.query(ToolCall.tool_name, func.count(ToolCall.id).label("error_count")) + .filter(ToolCall.status == "error") + .group_by(ToolCall.tool_name) + .all() + ) + tool_error_distribution = {name: count for name, count in tool_errors} + + # 最近7天每日工具调用数 + daily_tool_calls = [] + for i in range(7): + day_start = now - timedelta(days=i + 1) + day_end = now - timedelta(days=i) + + daily_count = ( + db.query(func.count(ToolCall.id)) + .filter(ToolCall.created_at >= day_start, ToolCall.created_at < day_end) + .scalar() + or 0 + ) + + daily_tool_calls.append({"date": day_start.strftime("%Y-%m-%d"), "call_count": daily_count}) + + return ToolCallStats( + total_calls=total_calls, + successful_calls=successful_calls, + failed_calls=failed_calls, + success_rate=success_rate, + most_used_tools=most_used_tools, + tool_error_distribution=tool_error_distribution, + daily_tool_calls=list(reversed(daily_tool_calls)), + ) + + except Exception as e: + logger.error(f"Error getting tool call stats: {e}") + logger.error(traceback.format_exc()) + raise HTTPException(status_code=500, detail=f"Failed to get tool call stats: {str(e)}") + + +# ============================================================================= +# Knowledge Base Statistics +# ============================================================================= + + +@dashboard.get("/stats/knowledge", response_model=KnowledgeStats) +async def get_knowledge_stats( + db: Session = Depends(get_db), + current_user: User = Depends(get_admin_user), +): + """Get knowledge base statistics (Admin only)""" + try: + from src.knowledge.manager import KnowledgeBaseManager + import json + import os + + # 从知识库管理系统获取数据 + kb_manager = KnowledgeBaseManager(work_dir="/app/saves/knowledge_base_data") + + # 读取全局元数据文件 + metadata_file = "/app/saves/knowledge_base_data/global_metadata.json" + if os.path.exists(metadata_file): + with open(metadata_file, encoding="utf-8") as f: + global_metadata = json.load(f) + + databases = global_metadata.get("databases", {}) + total_databases = len(databases) + + # 统计不同类型的知识库 + databases_by_type = {} + files_by_type = {} + total_files = 0 + total_nodes = 0 + total_storage_size = 0 + + # 文件类型映射到中文友好名称 + file_type_mapping = { + "txt": "文本文件", + "pdf": "PDF文档", + "docx": "Word文档", + "doc": "Word文档", + "md": "Markdown", + "html": "HTML网页", + "htm": "HTML网页", + "json": "JSON数据", + "csv": "CSV表格", + "xlsx": "Excel表格", + "xls": "Excel表格", + "pptx": "PowerPoint", + "ppt": "PowerPoint", + "png": "PNG图片", + "jpg": "JPEG图片", + "jpeg": "JPEG图片", + "gif": "GIF图片", + "svg": "SVG图片", + "mp4": "MP4视频", + "mp3": "MP3音频", + "zip": "ZIP压缩包", + "rar": "RAR压缩包", + "7z": "7Z压缩包", + } + + # 统计上传文件 + uploads_dir = "/app/saves/knowledge_base_data/uploads" + if os.path.exists(uploads_dir): + for root, dirs, files in os.walk(uploads_dir): + for file in files: + # 支持更多文件类型 + if file.lower().endswith(tuple(file_type_mapping.keys())): + total_files += 1 + file_ext = file.lower().split(".")[-1] + # 使用映射后的友好名称 + display_name = file_type_mapping.get(file_ext, file_ext.upper() + "文件") + files_by_type[display_name] = files_by_type.get(display_name, 0) + 1 + + # 估算文件大小 + file_path = os.path.join(root, file) + try: + file_size = os.path.getsize(file_path) + total_storage_size += file_size + except OSError: + total_storage_size += 1024 # 默认1KB + + # 统计知识库类型分布 + for kb_id, kb_info in databases.items(): + kb_type = kb_info.get("kb_type", "unknown") + display_type = { + "lightrag": "LightRAG", + "chroma": "Chroma向量库", + "faiss": "FAISS索引", + "milvus": "Milvus向量库", + "qdrant": "Qdrant向量库", + "elasticsearch": "Elasticsearch", + "unknown": "未知类型", + }.get(kb_type.lower(), kb_type) + databases_by_type[display_type] = databases_by_type.get(display_type, 0) + 1 + + # 尝试从各个知识库系统获取更详细的统计 + try: + kb_instance = kb_manager.get_kb(kb_id) + if kb_instance and hasattr(kb_instance, "get_stats"): + stats = kb_instance.get_stats() + total_nodes += stats.get("node_count", 0) + except Exception as e: + logger.warning(f"Failed to get stats for KB {kb_id}: {e}") + continue + + else: + # 如果没有元数据文件,返回空数据 + total_databases = 0 + total_files = 0 + total_nodes = 0 + total_storage_size = 0 + databases_by_type = {} + files_by_type = {} + + return KnowledgeStats( + total_databases=total_databases, + total_files=total_files, + total_nodes=total_nodes, + total_storage_size=total_storage_size, + databases_by_type=databases_by_type, + file_type_distribution=files_by_type, # 保持API兼容,但使用新的数据 + ) + + except Exception as e: + logger.error(f"Error getting knowledge stats: {e}") + logger.error(traceback.format_exc()) + raise HTTPException(status_code=500, detail=f"Failed to get knowledge stats: {str(e)}") + + +# ============================================================================= +# Agent Analytics +# ============================================================================= + + +@dashboard.get("/stats/agents", response_model=AgentAnalytics) +async def get_agent_analytics( + db: Session = Depends(get_db), + current_user: User = Depends(get_admin_user), +): + """Get AI agent analytics (Admin only)""" + try: + from src.storage.db.models import Conversation, MessageFeedback, Message, ToolCall + + # 获取所有智能体 + agents = ( + db.query(Conversation.agent_id, func.count(Conversation.id).label("conversation_count")) + .group_by(Conversation.agent_id) + .all() + ) + + total_agents = len(agents) + agent_conversation_counts = [{"agent_id": agent_id, "conversation_count": count} for agent_id, count in agents] + + # 智能体满意度统计 + agent_satisfaction = [] + for agent_id, _ in agents: + total_feedbacks = ( + db.query(func.count(MessageFeedback.id)) + .join(Message, MessageFeedback.message_id == Message.id) + .join(Conversation, Message.conversation_id == Conversation.id) + .filter(Conversation.agent_id == agent_id) + .scalar() + or 0 + ) + + positive_feedbacks = ( + db.query(func.count(MessageFeedback.id)) + .join(Message, MessageFeedback.message_id == Message.id) + .join(Conversation, Message.conversation_id == Conversation.id) + .filter(Conversation.agent_id == agent_id, MessageFeedback.rating == "like") + .scalar() + or 0 + ) + + satisfaction_rate = round((positive_feedbacks / total_feedbacks * 100), 2) if total_feedbacks > 0 else 0 + + agent_satisfaction.append( + {"agent_id": agent_id, "satisfaction_rate": satisfaction_rate, "total_feedbacks": total_feedbacks} + ) + + # 智能体工具使用统计 + agent_tool_usage = [] + for agent_id, _ in agents: + tool_usage_count = ( + db.query(func.count(ToolCall.id)) + .join(Message, ToolCall.message_id == Message.id) + .join(Conversation, Message.conversation_id == Conversation.id) + .filter(Conversation.agent_id == agent_id) + .scalar() + or 0 + ) + + agent_tool_usage.append({"agent_id": agent_id, "tool_usage_count": tool_usage_count}) + + # 表现最佳的智能体(综合评分) + top_performing_agents = [] + for i, (agent_id, conv_count) in enumerate(agents): + # 综合评分 = 对话数权重 + 满意度权重 + satisfaction_data = next( + (s for s in agent_satisfaction if s["agent_id"] == agent_id), {"satisfaction_rate": 0} + ) + + score = conv_count * 0.3 + satisfaction_data["satisfaction_rate"] * 0.7 + + top_performing_agents.append( + { + "agent_id": agent_id, + "score": round(score, 2), + "conversation_count": conv_count, + "satisfaction_rate": satisfaction_data["satisfaction_rate"], + } + ) + + # 按评分排序,取前5名 + top_performing_agents.sort(key=lambda x: x["score"], reverse=True) + top_performing_agents = top_performing_agents[:5] + + return AgentAnalytics( + total_agents=total_agents, + agent_conversation_counts=agent_conversation_counts, + agent_satisfaction_rates=agent_satisfaction, + agent_tool_usage=agent_tool_usage, + top_performing_agents=top_performing_agents, + ) + + except Exception as e: + logger.error(f"Error getting agent analytics: {e}") + logger.error(traceback.format_exc()) + raise HTTPException(status_code=500, detail=f"Failed to get agent analytics: {str(e)}") + + +# ============================================================================= +# Basic Statistics (保留原有接口) # ============================================================================= @@ -197,54 +605,21 @@ async def get_dashboard_stats( total_messages = db.query(func.count(Message.id)).scalar() or 0 total_users = db.query(func.count(distinct(Conversation.user_id))).scalar() or 0 - # Conversations by agent - conversations_by_agent = ( - db.query(Conversation.agent_id, func.count(Conversation.id).label("count")) - .group_by(Conversation.agent_id) - .all() - ) - - # Recent activity (last 24 hours) - yesterday = datetime.utcnow() - timedelta(days=1) - recent_conversations = ( - db.query(func.count(Conversation.id)).filter(Conversation.created_at >= yesterday).scalar() or 0 - ) - recent_messages = db.query(func.count(Message.id)).filter(Message.created_at >= yesterday).scalar() or 0 - # Feedback statistics total_feedbacks = db.query(func.count(MessageFeedback.id)).scalar() or 0 like_count = db.query(func.count(MessageFeedback.id)).filter(MessageFeedback.rating == "like").scalar() or 0 - dislike_count = ( - db.query(func.count(MessageFeedback.id)).filter(MessageFeedback.rating == "dislike").scalar() or 0 - ) # Calculate satisfaction rate satisfaction_rate = round((like_count / total_feedbacks * 100), 2) if total_feedbacks > 0 else 0 - # Recent feedback (last 24 hours) - recent_feedbacks = ( - db.query(func.count(MessageFeedback.id)).filter(MessageFeedback.created_at >= yesterday).scalar() or 0 - ) - return { - "timestamp": datetime.utcnow().isoformat(), "total_conversations": total_conversations, "active_conversations": active_conversations, "total_messages": total_messages, "total_users": total_users, - "conversations_by_agent": [ - {"agent_id": agent_id or "unknown", "count": count} for agent_id, count in conversations_by_agent - ], - "recent_activity": { - "conversations_24h": recent_conversations, - "messages_24h": recent_messages, - }, "feedback_stats": { "total_feedbacks": total_feedbacks, - "like_count": like_count, - "dislike_count": dislike_count, "satisfaction_rate": satisfaction_rate, - "recent_feedbacks_24h": recent_feedbacks, }, } except Exception as e: @@ -262,7 +637,6 @@ class FeedbackListItem(BaseModel): """Feedback list item""" id: int - message_id: int user_id: str rating: str reason: str | None diff --git a/src/config/app.py b/src/config/app.py index 0edb87f0..3c8781c3 100644 --- a/src/config/app.py +++ b/src/config/app.py @@ -236,4 +236,5 @@ class Config(SimpleConfig): return value + config = Config() diff --git a/web/src/apis/dashboard_api.js b/web/src/apis/dashboard_api.js index 5fc9890e..fb9a3fb4 100644 --- a/web/src/apis/dashboard_api.js +++ b/web/src/apis/dashboard_api.js @@ -59,6 +59,67 @@ export const dashboardApi = { if (params.offset) queryParams.append('offset', params.offset) return apiAdminGet(`/api/dashboard/feedbacks?${queryParams.toString()}`) + }, + + // ========== 新增并行API接口 ========== + + /** + * 获取用户活跃度统计 + * @returns {Promise} - 用户活跃度统计信息 + */ + getUserStats: () => { + return apiAdminGet('/api/dashboard/stats/users') + }, + + /** + * 获取工具调用统计 + * @returns {Promise} - 工具调用统计信息 + */ + getToolStats: () => { + return apiAdminGet('/api/dashboard/stats/tools') + }, + + /** + * 获取知识库统计 + * @returns {Promise} - 知识库统计信息 + */ + getKnowledgeStats: () => { + return apiAdminGet('/api/dashboard/stats/knowledge') + }, + + /** + * 获取AI智能体分析数据 + * @returns {Promise} - AI智能体分析信息 + */ + getAgentStats: () => { + return apiAdminGet('/api/dashboard/stats/agents') + }, + + /** + * 批量获取所有统计数据(并行请求) + * @returns {Promise} - 所有统计数据 + */ + getAllStats: async () => { + try { + const [basicStats, userStats, toolStats, knowledgeStats, agentStats] = await Promise.all([ + apiAdminGet('/api/dashboard/stats'), + apiAdminGet('/api/dashboard/stats/users'), + apiAdminGet('/api/dashboard/stats/tools'), + apiAdminGet('/api/dashboard/stats/knowledge'), + apiAdminGet('/api/dashboard/stats/agents') + ]) + + return { + basic: basicStats, + users: userStats, + tools: toolStats, + knowledge: knowledgeStats, + agents: agentStats + } + } catch (error) { + console.error('批量获取统计数据失败:', error) + throw error + } } } diff --git a/web/src/assets/css/base.css b/web/src/assets/css/base.css index cb83dd0a..d1d6b76e 100644 --- a/web/src/assets/css/base.css +++ b/web/src/assets/css/base.css @@ -51,6 +51,46 @@ --color-error: #c73234; --color-info: #0058d4; + /* Chart Colors - 图表颜色系统 */ + --chart-primary: var(--main-500); /* 主色调 - 用于主要数据 */ + --chart-primary-light: var(--main-400); /* 主色调浅色 - 用于渐变 */ + --chart-primary-dark: var(--main-600); /* 主色调深色 - 用于强调 */ + + --chart-success: #52c41a; /* 成功色 - 绿色 */ + --chart-success-light: #95de64; /* 成功色浅色 */ + --chart-success-dark: #389e0d; /* 成功色深色 */ + + --chart-warning: #faad14; /* 警告色 - 橙色 */ + --chart-warning-light: #ffd666; /* 警告色浅色 */ + --chart-warning-dark: #d48806; /* 警告色深色 */ + + --chart-error: #f5222d; /* 错误色 - 红色 */ + --chart-error-light: #ff7875; /* 错误色浅色 */ + --chart-error-dark: #cf1322; /* 错误色深色 */ + + --chart-info: #1890ff; /* 信息色 - 蓝色 */ + --chart-info-light: #69c0ff; /* 信息色浅色 */ + --chart-info-dark: #096dd9; /* 信息色深色 */ + + --chart-secondary: #722ed1; /* 次要色 - 紫色 */ + --chart-secondary-light: #b37feb; /* 次要色浅色 */ + --chart-secondary-dark: #531dab; /* 次要色深色 */ + + --chart-accent: #13c2c2; /* 强调色 - 青色 */ + --chart-accent-light: #5cdbd3; /* 强调色浅色 */ + --chart-accent-dark: #08979c; /* 强调色深色 */ + + /* Chart Palette - 图表调色板 */ + --chart-palette-1: var(--chart-primary); + --chart-palette-2: var(--chart-success); + --chart-palette-3: var(--chart-warning); + --chart-palette-4: var(--chart-error); + --chart-palette-5: var(--chart-info); + --chart-palette-6: var(--chart-secondary); + --chart-palette-7: var(--chart-accent); + --chart-palette-8: #fa541c; /* 橙红色 */ + --chart-palette-9: #eb2f96; /* 粉红色 */ + --chart-palette-10: #a0d911; /* 黄绿色 */ --bg-sider: var(--main-5); --color-text: var(--c-black); diff --git a/web/src/assets/css/dashboard.css b/web/src/assets/css/dashboard.css new file mode 100644 index 00000000..95b77b23 --- /dev/null +++ b/web/src/assets/css/dashboard.css @@ -0,0 +1,464 @@ +/* Dashboard 共享样式文件 */ + +/* Chart Color System - 图表颜色系统 */ +:root { + /* ECharts 图表颜色配置 */ + --echarts-primary-gradient: { + type: 'linear', + x: 0, y: 0, x2: 0, y2: 1, + colorStops: [ + { offset: 0, color: 'var(--chart-primary)' }, + { offset: 1, color: 'var(--chart-primary-light)' } + ] + }; + + --echarts-success-gradient: { + type: 'linear', + x: 0, y: 0, x2: 0, y2: 1, + colorStops: [ + { offset: 0, color: 'var(--chart-success)' }, + { offset: 1, color: 'var(--chart-success-light)' } + ] + }; + + --echarts-warning-gradient: { + type: 'linear', + x: 0, y: 0, x2: 0, y2: 1, + colorStops: [ + { offset: 0, color: 'var(--chart-warning)' }, + { offset: 1, color: 'var(--chart-warning-light)' } + ] + }; + + --echarts-error-gradient: { + type: 'linear', + x: 0, y: 0, x2: 0, y2: 1, + colorStops: [ + { offset: 0, color: 'var(--chart-error)' }, + { offset: 1, color: 'var(--chart-error-light)' } + ] + }; + + --echarts-info-gradient: { + type: 'linear', + x: 0, y: 0, x2: 0, y2: 1, + colorStops: [ + { offset: 0, color: 'var(--chart-info)' }, + { offset: 1, color: 'var(--chart-info-light)' } + ] + }; + + --echarts-secondary-gradient: { + type: 'linear', + x: 0, y: 0, x2: 0, y2: 1, + colorStops: [ + { offset: 0, color: 'var(--chart-secondary)' }, + { offset: 1, color: 'var(--chart-secondary-light)' } + ] + }; + + --echarts-accent-gradient: { + type: 'linear', + x: 0, y: 0, x2: 0, y2: 1, + colorStops: [ + { offset: 0, color: 'var(--chart-accent)' }, + { offset: 1, color: 'var(--chart-accent-light)' } + ] + }; +} + +/* Chart Color Palette - 图表调色板 */ +.chart-color-palette { + --palette-1: var(--chart-palette-1); + --palette-2: var(--chart-palette-2); + --palette-3: var(--chart-palette-3); + --palette-4: var(--chart-palette-4); + --palette-5: var(--chart-palette-5); + --palette-6: var(--chart-palette-6); + --palette-7: var(--chart-palette-7); + --palette-8: var(--chart-palette-8); + --palette-9: var(--chart-palette-9); + --palette-10: var(--chart-palette-10); +} + +/* Chart Area Gradient - 图表区域渐变 */ +.chart-area-gradient-primary { + background: linear-gradient(180deg, + rgba(57, 150, 174, 0.3) 0%, + rgba(57, 150, 174, 0.05) 100% + ); +} + +.chart-area-gradient-success { + background: linear-gradient(180deg, + rgba(82, 196, 26, 0.3) 0%, + rgba(82, 196, 26, 0.05) 100% + ); +} + +.chart-area-gradient-warning { + background: linear-gradient(180deg, + rgba(250, 173, 20, 0.3) 0%, + rgba(250, 173, 20, 0.05) 100% + ); +} + +.chart-area-gradient-error { + background: linear-gradient(180deg, + rgba(245, 34, 45, 0.3) 0%, + rgba(245, 34, 45, 0.05) 100% + ); +} + +/* Chart Shadow Effects - 图表阴影效果 */ +.chart-shadow-primary { + box-shadow: 0 4px 12px rgba(57, 150, 174, 0.15); +} + +.chart-shadow-success { + box-shadow: 0 4px 12px rgba(82, 196, 26, 0.15); +} + +.chart-shadow-warning { + box-shadow: 0 4px 12px rgba(250, 173, 20, 0.15); +} + +.chart-shadow-error { + box-shadow: 0 4px 12px rgba(245, 34, 45, 0.15); +} + +/* 共享的卡片样式 */ +.dashboard-card { + background-color: var(--gray-0); + border: 1px solid var(--gray-200); + border-radius: 8px; + transition: all 0.2s ease; + height: 100%; + display: flex; + flex-direction: column; + + &:hover { + border-color: var(--gray-300); + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); + } + + :deep(.ant-card-head) { + border-bottom: 1px solid var(--gray-200); + min-height: 56px; + padding: 0 20px; + flex-shrink: 0; + background-color: var(--gray-0); + + .ant-card-head-title { + font-size: 16px; + font-weight: 600; + color: var(--gray-1000); + } + } + + :deep(.ant-card-body) { + padding: 20px; + flex: 1; + overflow: auto; + background-color: var(--gray-0); + } +} + +/* 统计概览样式 */ +.stats-overview { + margin-bottom: 16px; + + :deep(.ant-statistic) { + .ant-statistic-title { + font-size: 13px; + font-weight: 500; + color: var(--gray-600); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 8px; + } + + .ant-statistic-content { + font-size: 24px; + font-weight: 700; + color: var(--gray-1000); + } + } +} + +/* 图表容器样式 */ +.chart-container { + h4, h5 { + margin-bottom: 16px; + font-weight: 600; + color: var(--gray-1000); + } + + h4 { + font-size: 16px; + } + + h5 { + font-size: 14px; + color: var(--gray-600); + } + + .chart { + height: 250px; + width: 100%; + background-color: var(--gray-0); + border-radius: 8px; + padding: 10px; + border: 1px solid var(--gray-100); + } + + .chart-medium { + height: 180px; + width: 100%; + background-color: var(--gray-0); + border-radius: 8px; + padding: 8px; + border: 1px solid var(--gray-100); + } + + .chart-small { + height: 180px; + width: 100%; + } +} + +/* Ant Design 组件样式覆盖 */ +:deep(.ant-statistic-title) { + color: var(--gray-600) !important; + font-weight: 500 !important; +} + +:deep(.ant-statistic-content) { + color: var(--gray-1000) !important; +} + +:deep(.ant-divider) { + border-color: var(--gray-200) !important; +} + +:deep(.ant-table) { + background-color: var(--gray-0); + border-radius: 8px; + overflow: hidden; + + .ant-table-thead > tr > th { + background-color: var(--gray-25); + border-bottom: 1px solid var(--gray-200); + font-weight: 600; + padding: 12px 16px; + font-size: 12px; + color: var(--gray-700); + text-transform: uppercase; + letter-spacing: 0.05em; + } + + .ant-table-tbody > tr { + transition: background-color 0.2s ease; + + > td { + border-bottom: 1px solid var(--gray-100); + color: var(--gray-1000); + padding: 12px 16px; + font-weight: 400; + } + + &:hover > td { + background-color: var(--gray-25); + } + } +} + +:deep(.ant-progress) { + .ant-progress-bg { + background-color: var(--main-500) !important; + border-radius: 4px; + } + + .ant-progress-text { + color: var(--gray-1000) !important; + font-weight: 500; + font-size: 12px; + } +} + +/* 紧凑型统计网格 */ +.compact-stats-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + margin-bottom: 24px; + + .mini-stat-card { + background-color: var(--gray-0); + border: 1px solid var(--gray-200); + border-radius: 6px; + padding: 16px; + text-align: center; + transition: all 0.2s ease; + + &:hover { + border-color: var(--gray-300); + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); + } + + .mini-stat-value { + font-size: 20px; + font-weight: 600; + color: var(--gray-1000); + line-height: 1.2; + margin-bottom: 4px; + } + + .mini-stat-label { + font-size: 12px; + color: var(--gray-600); + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.025em; + } + } +} + +/* 紧凑型图表容器 */ +.compact-chart-container { + background-color: var(--gray-0); + border: 1px solid var(--gray-200); + border-radius: 6px; + padding: 16px; + height: 240px; + transition: all 0.2s ease; + + &:hover { + border-color: var(--gray-300); + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); + } + + .chart-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 12px; + + .chart-title { + font-size: 14px; + font-weight: 600; + color: var(--gray-1000); + } + + .chart-subtitle { + font-size: 11px; + color: var(--gray-600); + background-color: var(--gray-100); + padding: 2px 6px; + border-radius: 4px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.025em; + } + } + + .compact-chart { + height: 180px; + width: 100%; + } +} + +/* 排名显示样式 */ +.rank-display { + display: flex; + align-items: center; + justify-content: center; + + .rank-medal { + font-size: 20px; + } + + .rank-number { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + background-color: var(--gray-100); + border-radius: 50%; + font-size: 12px; + font-weight: 600; + color: var(--gray-600); + border: 1px solid var(--gray-200); + } +} + +/* 指标值样式 */ +.metric-value { + font-weight: 500; + color: var(--gray-1000); + font-size: 14px; +} + +/* 排名样式 */ +:deep(.ant-table-tbody > tr.top-rank.first) { + background-color: #fef3c7; + border-radius: 4px; +} + +:deep(.ant-table-tbody > tr.top-rank.second) { + background-color: var(--gray-100); + border-radius: 4px; +} + +:deep(.ant-table-tbody > tr.top-rank.third) { + background-color: #fed7aa; + border-radius: 4px; +} + +/* 响应式设计 */ +@media (max-width: 1200px) { + .compact-stats-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + .compact-stats-grid { + grid-template-columns: repeat(2, 1fr); + gap: 8px; + + .mini-stat-card { + padding: 8px; + + .mini-stat-value { + font-size: 16px; + } + + .mini-stat-label { + font-size: 10px; + } + } + } + + .compact-chart-container { + height: 180px; + padding: 8px; + + .compact-chart { + height: 130px; + } + + .chart-header { + margin-bottom: 4px; + + .chart-title { + font-size: 11px; + } + + .chart-subtitle { + font-size: 9px; + } + } + } +} diff --git a/web/src/components/StatusBar.vue b/web/src/components/StatusBar.vue new file mode 100644 index 00000000..a3dfece4 --- /dev/null +++ b/web/src/components/StatusBar.vue @@ -0,0 +1,246 @@ + + + + + diff --git a/web/src/components/dashboard/AgentStatsComponent.vue b/web/src/components/dashboard/AgentStatsComponent.vue new file mode 100644 index 00000000..49947619 --- /dev/null +++ b/web/src/components/dashboard/AgentStatsComponent.vue @@ -0,0 +1,415 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/dashboard/KnowledgeStatsComponent.vue b/web/src/components/dashboard/KnowledgeStatsComponent.vue new file mode 100644 index 00000000..a6937bb6 --- /dev/null +++ b/web/src/components/dashboard/KnowledgeStatsComponent.vue @@ -0,0 +1,390 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/dashboard/ToolStatsComponent.vue b/web/src/components/dashboard/ToolStatsComponent.vue new file mode 100644 index 00000000..24aebd28 --- /dev/null +++ b/web/src/components/dashboard/ToolStatsComponent.vue @@ -0,0 +1,334 @@ + + + + + \ No newline at end of file diff --git a/web/src/components/dashboard/UserStatsComponent.vue b/web/src/components/dashboard/UserStatsComponent.vue new file mode 100644 index 00000000..0a547210 --- /dev/null +++ b/web/src/components/dashboard/UserStatsComponent.vue @@ -0,0 +1,194 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/DashboardView.vue b/web/src/views/DashboardView.vue index 5b9447d8..56e445a8 100644 --- a/web/src/views/DashboardView.vue +++ b/web/src/views/DashboardView.vue @@ -1,137 +1,192 @@ @@ -180,24 +235,35 @@ +