From 7efbf58ddac5baa408432aae74b2ba209e7f88c7 Mon Sep 17 00:00:00 2001 From: minecraft1024a Date: Sat, 29 Nov 2025 09:51:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(report):=20=E9=87=8D=E6=9E=84=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E6=8A=A5=E5=91=8A=EF=BC=8C=E5=A2=9E=E5=8A=A0=E6=95=88?= =?UTF-8?q?=E7=8E=87=E5=88=86=E6=9E=90=E5=B9=B6=E9=87=87=E7=94=A8MD3?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本次更新对统计报告进行了全面的重构和视觉升级,旨在提供更深入的数据洞察和更现代化的用户体验。 主要变更包括: - **新增效率分析模块**: 引入了一系列关键的大模型效率指标,如单条消息成本、Token效率(输出/输入比)、每小时成本/请求数等,并通过新的表格进行详细展示。 - **全新UI设计**: 整体界面采用Material Design 3 (MD3) 风格重新设计,优化了色彩、字体、间距和卡片样式,提升了报告的专业性和可读性。 - **图表功能增强**: 动态图表和静态图表均进行了美化,更新了配色方案,增加了平滑的加载动画和交互效果。饼图升级为更易读的甜甜圈图,并优化了工具提示信息。 - **内容和布局优化**: 增加了更多的摘要卡片以快速概览核心数据。为各个板块标题添加了 Emoji 图标,使信息层次更清晰,并优化了整体布局。 --- src/chat/utils/report_generator.py | 121 +++++- src/chat/utils/statistic_keys.py | 15 + src/chat/utils/templates/charts_tab.html | 27 +- src/chat/utils/templates/report.css | 419 ++++++++++++++++---- src/chat/utils/templates/report.html | 8 +- src/chat/utils/templates/report.js | 212 +++++++++- src/chat/utils/templates/static_charts.html | 8 +- src/chat/utils/templates/tab_content.html | 34 +- 8 files changed, 703 insertions(+), 141 deletions(-) diff --git a/src/chat/utils/report_generator.py b/src/chat/utils/report_generator.py index 8c8756070..48914f2fc 100644 --- a/src/chat/utils/report_generator.py +++ b/src/chat/utils/report_generator.py @@ -135,27 +135,123 @@ class HTMLReportGenerator: for chat_id, count in sorted(stat_data[MSG_CNT_BY_CHAT].items()) ] ) + + # 先计算基础数据 + total_tokens = sum(stat_data.get(TOTAL_TOK_BY_MODEL, {}).values()) + total_requests = stat_data.get(TOTAL_REQ_CNT, 0) + total_cost = stat_data.get(TOTAL_COST, 0) + total_messages = stat_data.get(TOTAL_MSG_CNT, 0) + online_seconds = stat_data.get(ONLINE_TIME, 0) + online_hours = online_seconds / 3600 if online_seconds > 0 else 0 + + # 大模型相关效率指标 + avg_cost_per_req = (total_cost / total_requests) if total_requests > 0 else 0 + avg_cost_per_msg = (total_cost / total_messages) if total_messages > 0 else 0 + avg_tokens_per_msg = (total_tokens / total_messages) if total_messages > 0 else 0 + avg_tokens_per_req = (total_tokens / total_requests) if total_requests > 0 else 0 + msg_to_req_ratio = (total_messages / total_requests) if total_requests > 0 else 0 + cost_per_hour = (total_cost / online_hours) if online_hours > 0 else 0 + req_per_hour = (total_requests / online_hours) if online_hours > 0 else 0 + + # Token效率 (输出/输入比率) + total_in_tokens = sum(stat_data.get(IN_TOK_BY_MODEL, {}).values()) + total_out_tokens = sum(stat_data.get(OUT_TOK_BY_MODEL, {}).values()) + token_efficiency = (total_out_tokens / total_in_tokens) if total_in_tokens > 0 else 0 + + # 生成效率指标表格数据 + efficiency_data = [ + ("💸 平均每条消息成本", f"{avg_cost_per_msg:.6f} ¥", "处理每条用户消息的平均AI成本"), + ("🎯 平均每条消息Token", f"{avg_tokens_per_msg:.0f}", "每条消息平均消耗的Token数量"), + ("📊 平均每次请求Token", f"{avg_tokens_per_req:.0f}", "每次AI请求平均消耗的Token数"), + ("🔄 消息/请求比率", f"{msg_to_req_ratio:.2f}", "平均每个AI请求处理的消息数"), + ("⚡ Token效率(输出/输入)", f"{token_efficiency:.3f}x", "输出Token与输入Token的比率"), + ("💵 每小时运行成本", f"{cost_per_hour:.4f} ¥/h", "在线每小时的AI成本"), + ("🚀 每小时请求数", f"{req_per_hour:.1f} 次/h", "在线每小时的AI请求次数"), + ("💰 每千Token成本", f"{(total_cost / total_tokens * 1000) if total_tokens > 0 else 0:.4f} ¥", "平均每1000个Token的成本"), + ("📈 Token/在线小时", f"{(total_tokens / online_hours) if online_hours > 0 else 0:.0f}", "每在线小时处理的Token数"), + ("💬 消息/在线小时", f"{(total_messages / online_hours) if online_hours > 0 else 0:.1f}", "每在线小时处理的消息数"), + ] + + efficiency_rows = "\n".join( + [ + f"{metric}{value}{desc}" + for metric, value, desc in efficiency_data + ] + ) + + # 计算活跃聊天数和最活跃聊天 + msg_by_chat = stat_data.get(MSG_CNT_BY_CHAT, {}) + active_chats = len(msg_by_chat) + most_active_chat = "" + if msg_by_chat: + most_active_id = max(msg_by_chat, key=msg_by_chat.get) + most_active_chat = self.name_mapping.get(most_active_id, (most_active_id, 0))[0] + most_active_count = msg_by_chat[most_active_id] + most_active_chat = f"{most_active_chat} ({most_active_count}条)" + + avg_msg_per_chat = (total_messages / active_chats) if active_chats > 0 else 0 + summary_cards = f"""
-

总花费

-

{stat_data.get(TOTAL_COST, 0):.4f} ¥

+

💰 总花费

+

{total_cost:.4f} ¥

-

总请求数

-

{stat_data.get(TOTAL_REQ_CNT, 0)}

+

📞 AI请求数

+

{total_requests:,}

-

总Token数

-

{sum(stat_data.get(TOTAL_TOK_BY_MODEL, {}).values())}

-
-
-

总消息数

-

{stat_data.get(TOTAL_MSG_CNT, 0)}

+

🎯 总Token数

+

{total_tokens:,}

-

总在线时间

-

{format_online_time(int(stat_data.get(ONLINE_TIME, 0)))}

+

💬 总消息数

+

{total_messages:,}

+
+
+

⏱️ 在线时间

+

{format_online_time(int(online_seconds))}

+
+
+

💸 每条消息成本

+

{avg_cost_per_msg:.4f} ¥

+
+
+

📊 每请求Token

+

{avg_tokens_per_req:.0f}

+
+
+

� 消息/请求比

+

{msg_to_req_ratio:.2f}

+
+
+

⚡ Token效率

+

{token_efficiency:.2f}x

+
+
+

💵 每小时成本

+

{cost_per_hour:.4f} ¥

+
+
+

🚀 每小时请求

+

{req_per_hour:.1f}

+
+
+

👥 活跃聊天数

+

{active_chats}

+
+
+

🔥 最活跃聊天

+

{most_active_chat if most_active_chat else "无"}

+
+
+

📈 平均消息/聊天

+

{avg_msg_per_chat:.1f}

+
+
+

🎯 每消息Token

+

{avg_tokens_per_msg:.0f}

""" @@ -173,6 +269,7 @@ class HTMLReportGenerator: module_rows=module_rows, type_rows=type_rows, chat_rows=chat_rows, + efficiency_rows=efficiency_rows, ) def _generate_chart_tab(self, chart_data: dict) -> str: diff --git a/src/chat/utils/statistic_keys.py b/src/chat/utils/statistic_keys.py index 2a552ac1a..9630d081b 100644 --- a/src/chat/utils/statistic_keys.py +++ b/src/chat/utils/statistic_keys.py @@ -61,3 +61,18 @@ PIE_CHART_COST_BY_PROVIDER = "pie_chart_cost_by_provider" PIE_CHART_REQ_BY_PROVIDER = "pie_chart_req_by_provider" BAR_CHART_COST_BY_MODEL = "bar_chart_cost_by_model" BAR_CHART_REQ_BY_MODEL = "bar_chart_req_by_model" + +# 新增消息分析指标 +MSG_CNT_BY_USER = "messages_by_user" # 按用户的消息数 +ACTIVE_CHATS_CNT = "active_chats_count" # 活跃聊天数 +MOST_ACTIVE_CHAT = "most_active_chat" # 最活跃的聊天 +AVG_MSG_PER_CHAT = "avg_messages_per_chat" # 平均每个聊天的消息数 + +# 新增大模型效率指标 +AVG_COST_PER_MSG = "avg_cost_per_message" # 平均每条消息成本 +AVG_TOKENS_PER_MSG = "avg_tokens_per_message" # 平均每条消息Token数 +AVG_TOKENS_PER_REQ = "avg_tokens_per_request" # 平均每次请求Token数 +MSG_TO_REQ_RATIO = "message_to_request_ratio" # 消息/请求比率 +COST_PER_ONLINE_HOUR = "cost_per_online_hour" # 每小时在线成本 +REQ_PER_ONLINE_HOUR = "requests_per_online_hour" # 每小时请求数 +TOKEN_EFFICIENCY = "token_efficiency" # Token效率 (输出/输入比率) diff --git a/src/chat/utils/templates/charts_tab.html b/src/chat/utils/templates/charts_tab.html index 52e6c3024..5ad4c8709 100644 --- a/src/chat/utils/templates/charts_tab.html +++ b/src/chat/utils/templates/charts_tab.html @@ -1,16 +1,27 @@
-

数据图表

-
- +

📈 数据图表

+

+ 📊 动态图表: 选择不同的时间范围查看数据趋势变化 +

+
+
-
-
-
-
-
+
+
+ +
+
+ +
+
+ +
+
+ +
\ No newline at end of file diff --git a/src/chat/utils/templates/report.css b/src/chat/utils/templates/report.css index 2229be785..ff8930f23 100644 --- a/src/chat/utils/templates/report.css +++ b/src/chat/utils/templates/report.css @@ -1,111 +1,164 @@ +/* Material Design 3 - Blue White Gray Theme */ +:root { + --md-sys-color-primary: #1976D2; + --md-sys-color-primary-container: #E3F2FD; + --md-sys-color-on-primary: #FFFFFF; + --md-sys-color-secondary: #546E7A; + --md-sys-color-secondary-container: #ECEFF1; + --md-sys-color-surface: #FFFFFF; + --md-sys-color-surface-variant: #F5F5F5; + --md-sys-color-background: #FAFAFA; + --md-sys-color-on-surface: #1C1B1F; + --md-sys-color-outline: #BDBDBD; + --md-sys-color-shadow: rgba(0, 0, 0, 0.1); +} + /* General Body Styles */ body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-family: 'Roboto', -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif; margin: 0; padding: 20px; - background-color: #f0f4f8; /* Light blue-gray background */ - color: #333; /* Darker text for better contrast */ + background: var(--md-sys-color-background); + color: var(--md-sys-color-on-surface); line-height: 1.6; + min-height: 100vh; } /* Main Container */ .container { - max-width: 95%; /* Make container almost full-width */ + max-width: 95%; margin: 20px auto; - background-color: #FFFFFF; /* Pure white background */ - padding: 30px; - border-radius: 12px; /* Slightly more rounded corners */ - box-shadow: 0 8px 25px rgba(0, 0, 0, 0.07); /* Softer, deeper shadow */ + background-color: var(--md-sys-color-surface); + padding: 40px; + border-radius: 28px; + box-shadow: 0 4px 16px var(--md-sys-color-shadow); + animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } } /* Dashboard Layout */ .dashboard-layout { display: flex; - gap: 30px; + flex-direction: column; + gap: 32px; } .main-content { - flex: 65%; + width: 100%; min-width: 0; } .sidebar-content { - flex: 35%; + width: 100%; min-width: 0; } -/* Responsive Design for Mobile */ -@media (max-width: 992px) { - .dashboard-layout { - flex-direction: column; - } - .main-content, .sidebar-content { - flex: 1; - } -} - -/* Typography */ +/* Typography - Material Design 3 */ h1, h2 { - color: #212529; + color: var(--md-sys-color-on-surface); padding-bottom: 10px; margin-top: 0; } h1 { text-align: center; - font-size: 2.2em; - margin-bottom: 20px; - color: #2A6CB5; /* A deeper, more professional blue */ + font-size: 2.5em; + margin-bottom: 30px; + color: var(--md-sys-color-primary); + font-weight: 500; + letter-spacing: 0; } h2 { font-size: 1.5em; margin-top: 40px; - margin-bottom: 15px; - border-bottom: 2px solid #DDE6ED; /* Lighter border color */ + margin-bottom: 20px; + border-bottom: 2px solid var(--md-sys-color-primary); + padding-bottom: 12px; + color: var(--md-sys-color-on-surface); + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; } -/* Info Banners */ +/* Info Banners - MD3 Style */ .info-item { - background-color: #E9F2FA; /* Light blue background */ - padding: 12px 18px; - border-radius: 8px; - margin-bottom: 20px; + background: var(--md-sys-color-primary-container); + padding: 16px 24px; + border-radius: 16px; + margin-bottom: 24px; font-size: 0.95em; - border: 1px solid #D1E3F4; /* Light blue border */ + border-left: 4px solid var(--md-sys-color-primary); + box-shadow: 0 1px 3px var(--md-sys-color-shadow); + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.info-item:hover { + box-shadow: 0 2px 6px var(--md-sys-color-shadow); } .info-item strong { - color: #2A6CB5; /* Deeper blue for emphasis */ + color: var(--md-sys-color-primary); + font-weight: 500; } -/* Tabs */ +/* Tabs - MD3 Style */ .tabs { - border-bottom: 2px solid #DEE2E6; + border-bottom: 1px solid var(--md-sys-color-outline); display: flex; - margin-bottom: 20px; + margin-bottom: 32px; + flex-wrap: wrap; + gap: 8px; } .tabs button { background: none; border: none; outline: none; - padding: 14px 20px; + padding: 16px 24px; cursor: pointer; - transition: all 0.3s ease; - font-size: 16px; - color: #6C757D; - border-bottom: 3px solid transparent; - margin-bottom: -2px; /* Align with container border */ + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + font-size: 14px; + color: var(--md-sys-color-on-surface); + border-radius: 16px 16px 0 0; + margin-bottom: -1px; + font-weight: 500; + position: relative; +} + +.tabs button::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background: var(--md-sys-color-primary); + transform: scaleX(0); + transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); } .tabs button:hover { - color: #2A6CB5; - background-color: #f0f4f8; /* Subtle hover background */ + background: var(--md-sys-color-surface-variant); } .tabs button.active { - color: #2A6CB5; /* Active tab color */ - border-bottom-color: #2A6CB5; /* Active tab border color */ + color: var(--md-sys-color-primary); + font-weight: 600; +} + +.tabs button.active::after { + transform: scaleX(1); } .tab-content { @@ -117,83 +170,277 @@ h2 { display: block; } -/* Summary Cards */ +/* Summary Cards - MD3 Style */ .summary-cards { display: grid; - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); - gap: 20px; - margin: 20px 0; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; + margin: 32px 0; } .card { - background-color: #FFFFFF; - padding: 20px; - border-radius: 8px; + background: var(--md-sys-color-surface); + padding: 24px; + border-radius: 16px; text-align: center; - border: 1px solid #DDE6ED; /* Lighter border */ - transition: all 0.3s ease; + border: 1px solid var(--md-sys-color-outline); + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 1px 3px var(--md-sys-color-shadow); } .card:hover { - transform: translateY(-5px); - box-shadow: 0 6px 15px rgba(0,0,0,0.08); + box-shadow: 0 4px 12px var(--md-sys-color-shadow); + transform: translateY(-4px); + border-color: var(--md-sys-color-primary); } .card h3 { - margin: 0 0 10px; - font-size: 1em; - color: #6C757D; + margin: 0 0 16px; + font-size: 0.875rem; + color: var(--md-sys-color-secondary); + font-weight: 500; + letter-spacing: 0.1px; } .card p { margin: 0; - font-size: 1.8em; - font-weight: bold; - color: #212529; + font-size: 2rem; + font-weight: 600; + color: var(--md-sys-color-primary); + line-height: 1.2; } -/* Tables */ +/* Tables - MD3 Style */ table { width: 100%; border-collapse: collapse; - margin-top: 15px; - font-size: 0.9em; + margin-top: 24px; + font-size: 0.875rem; + border-radius: 16px; + overflow: hidden; + box-shadow: 0 1px 3px var(--md-sys-color-shadow); + border: 1px solid var(--md-sys-color-outline); } th, td { - padding: 12px 15px; + padding: 16px; text-align: left; - border-bottom: 1px solid #EAEAEA; + border-bottom: 1px solid var(--md-sys-color-outline); } + th { - background-color: #4A90E2; /* Main theme blue */ - color: white; - font-weight: bold; - font-size: 0.95em; - text-transform: uppercase; - letter-spacing: 0.5px; + background: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); + font-weight: 500; + font-size: 0.875rem; + letter-spacing: 0.1px; +} + +tbody tr { + transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1); } tr:nth-child(even) { - background-color: #F7FAFC; /* Very light blue for alternate rows */ + background-color: var(--md-sys-color-surface-variant); } -tr:hover { - background-color: #E9F2FA; /* Light blue for hover */ +tbody tr:hover { + background-color: var(--md-sys-color-primary-container); } -/* Chart Container in Sidebar */ +/* Chart Container - MD3 Style */ .chart-container { position: relative; - height: 300px; /* Adjust height as needed */ width: 100%; - margin-bottom: 20px; + padding: 24px; + background: var(--md-sys-color-surface); + border-radius: 16px; + box-shadow: 0 1px 3px var(--md-sys-color-shadow); + border: 1px solid var(--md-sys-color-outline); + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); } -/* Footer */ +.chart-container:hover { + box-shadow: 0 4px 12px var(--md-sys-color-shadow); +} + +.chart-container canvas { + max-width: 100%; + height: auto !important; +} + +/* Chart Wrapper for dynamic charts */ +.chart-wrapper { + position: relative; + width: 100%; + height: 350px; + padding: 24px; + background: var(--md-sys-color-surface); + border-radius: 16px; + box-shadow: 0 1px 3px var(--md-sys-color-shadow); + border: 1px solid var(--md-sys-color-outline); +} + +.chart-wrapper canvas { + max-height: 300px !important; +} + +.chart-grid { + display: flex; + flex-direction: column; + gap: 24px; + margin-top: 24px; +} + +/* Time Range Buttons - MD3 Filled Button */ +.time-range-btn { + background: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); + border: none; + padding: 10px 24px; + margin: 0 8px; + border-radius: 20px; + cursor: pointer; + font-size: 14px; + font-weight: 500; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 1px 3px var(--md-sys-color-shadow); +} + +.time-range-btn:hover { + box-shadow: 0 2px 6px var(--md-sys-color-shadow); +} + +.time-range-btn.active { + background: var(--md-sys-color-secondary); + box-shadow: 0 2px 6px var(--md-sys-color-shadow); +} + +/* Statistics Badge - MD3 */ +.stat-badge { + display: inline-block; + padding: 4px 12px; + background: var(--md-sys-color-primary-container); + color: var(--md-sys-color-primary); + border-radius: 16px; + font-size: 0.875rem; + font-weight: 500; + margin-left: 8px; +} + +/* Loading Animation */ +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.loading { + animation: pulse 1.5s ease-in-out infinite; +} + +/* Scrollbar Styling - MD3 */ +::-webkit-scrollbar { + width: 12px; + height: 12px; +} + +::-webkit-scrollbar-track { + background: var(--md-sys-color-surface-variant); + border-radius: 8px; +} + +::-webkit-scrollbar-thumb { + background: var(--md-sys-color-primary); + border-radius: 8px; + border: 2px solid var(--md-sys-color-surface-variant); +} + +::-webkit-scrollbar-thumb:hover { + background: var(--md-sys-color-secondary); +} + +/* Footer - MD3 */ .footer { text-align: center; - margin-top: 40px; - font-size: 0.85em; - color: #6C757D; + margin-top: 48px; + padding-top: 24px; + border-top: 1px solid var(--md-sys-color-outline); + font-size: 0.875rem; + color: var(--md-sys-color-secondary); +} + +.footer p { + margin: 8px 0; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .container { + padding: 20px; + border-radius: 16px; + } + + h1 { + font-size: 2em; + } + + h2 { + font-size: 1.25em; + } + + .summary-cards { + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 12px; + } + + .card { + padding: 16px; + } + + .card h3 { + font-size: 0.8rem; + margin-bottom: 12px; + } + + .card p { + font-size: 1.5rem; + } + + table { + font-size: 0.8rem; + } + + th, td { + padding: 12px 8px; + } + + .tabs button { + padding: 12px 16px; + font-size: 13px; + } + + .time-range-btn { + padding: 8px 16px; + margin: 4px; + font-size: 13px; + } + + .chart-wrapper, + .chart-container { + height: 250px; + } +} + +@media (max-width: 480px) { + .summary-cards { + grid-template-columns: 1fr 1fr; + } + + .card p { + font-size: 1.25rem; + } } diff --git a/src/chat/utils/templates/report.html b/src/chat/utils/templates/report.html index b70146063..23726641b 100644 --- a/src/chat/utils/templates/report.html +++ b/src/chat/utils/templates/report.html @@ -4,15 +4,21 @@ {{ report_title }} + +

{{ report_title }}

-

统计截止时间: {{ generation_time }}

+

📅 统计截止时间: {{ generation_time }}

{{ tab_list }}
{{ tab_content }} +