diff --git a/web/static/css/style.css b/web/static/css/style.css index 7dcad05e..acc5f288 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -7196,17 +7196,68 @@ header { stroke-width: 2; } -.mcp-stats-timeline-empty, .mcp-stats-timeline-error { margin: 0; - padding: 20px 8px; + padding: 16px 8px; text-align: center; font-size: 0.75rem; - color: var(--text-muted); + color: #b91c1c; } -.mcp-stats-timeline-error { - color: #b91c1c; +.mcp-stats-timeline-empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + flex: 1; + min-height: 88px; + padding: 20px 16px; + text-align: center; + border-radius: 8px; + background: rgba(148, 163, 184, 0.06); + border: 1px dashed rgba(148, 163, 184, 0.28); +} + +.mcp-stats-timeline-empty-state--compact { + min-height: 72px; + padding: 14px 10px; + gap: 4px; +} + +.mcp-stats-timeline-empty-state__icon { + color: rgba(148, 163, 184, 0.75); + flex-shrink: 0; +} + +.mcp-stats-timeline-empty-state--compact .mcp-stats-timeline-empty-state__icon { + width: 28px; + height: 28px; +} + +.mcp-stats-timeline-empty-state__title { + margin: 0; + font-size: 0.8125rem; + font-weight: 500; + color: var(--text-secondary); + line-height: 1.4; +} + +.mcp-stats-timeline-empty-state--compact .mcp-stats-timeline-empty-state__title { + font-size: 0.75rem; +} + +.mcp-stats-timeline-empty-state__hint { + margin: 0; + max-width: 28em; + font-size: 0.6875rem; + color: var(--text-muted); + line-height: 1.45; +} + +.mcp-stats-timeline-empty-state--compact .mcp-stats-timeline-empty-state__hint { + font-size: 0.625rem; + max-width: 100%; } .mcp-stats-timeline-tooltip { diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 56134afe..6d0c4833 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -1583,6 +1583,7 @@ "timelineSummary": "{{total}} calls in range · peak {{peak}}", "timelineSparseHint": "Most buckets are empty; peak {{peak}} calls at {{peakTime}}", "timelineNoData": "No calls in this period", + "timelineEmptyHint": "Switch the time range or invoke MCP tools in chat or tasks", "timelineLoadError": "Failed to load call trend", "timelineTotalLegend": "Total calls", "timelineFailedLegend": "Failed", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 372249e9..81ee5273 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -1571,6 +1571,7 @@ "timelineSummary": "区间内 {{total}} 次 · 峰值 {{peak}}", "timelineSparseHint": "该时段多数时间为 0,峰值 {{peak}} 次出现在 {{peakTime}}", "timelineNoData": "该时段暂无调用", + "timelineEmptyHint": "切换时间范围查看其他时段,或在对话/任务中调用 MCP 工具", "timelineLoadError": "无法加载调用趋势", "timelineTotalLegend": "总调用", "timelineFailedLegend": "失败", diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index 3e0de1fe..3e939197 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -3986,7 +3986,9 @@ async function setMcpMonitorTimelineRange(range) { monitorState.timeline = timelineJson; const timelineInner = document.querySelector('#monitor-stats .mcp-stats-combined__timeline-inner'); if (timelineInner) { - timelineInner.innerHTML = renderMcpStatsTimelineBody(monitorState.timeline, monitorState.timelineError); + const combined = timelineInner.closest('.mcp-stats-combined'); + const compactEmpty = combined && !!combined.querySelector('.mcp-stats-combined__main'); + timelineInner.innerHTML = renderMcpStatsTimelineBody(monitorState.timeline, monitorState.timelineError, compactEmpty); bindMcpStatsTimelineEvents(); syncMcpMonitorTimelineRangeUI(range); } else if (monitorState.stats && Object.keys(monitorState.stats).length > 0) { @@ -3996,7 +3998,9 @@ async function setMcpMonitorTimelineRange(range) { monitorState.timelineError = err.message || 'error'; const timelineInner = document.querySelector('#monitor-stats .mcp-stats-combined__timeline-inner'); if (timelineInner) { - timelineInner.innerHTML = renderMcpStatsTimelineBody(monitorState.timeline, monitorState.timelineError); + const combined = timelineInner.closest('.mcp-stats-combined'); + const compactEmpty = combined && !!combined.querySelector('.mcp-stats-combined__main'); + timelineInner.innerHTML = renderMcpStatsTimelineBody(monitorState.timeline, monitorState.timelineError, compactEmpty); bindMcpStatsTimelineEvents(); syncMcpMonitorTimelineRangeUI(range); } @@ -4014,7 +4018,21 @@ function renderMcpStatsTimelineRangeButtons() { }).join(''); } -function renderMcpStatsTimelineBody(timeline, timelineError) { +const MCP_TIMELINE_EMPTY_ICON = ''; + +function renderMcpStatsTimelineEmptyState(compact) { + const noData = mcpMonitorT('timelineNoData') || monitorFallback('该时段暂无调用', 'No calls in this period'); + const emptyHint = mcpMonitorT('timelineEmptyHint') + || monitorFallback('切换时间范围查看其他时段,或在对话/任务中调用 MCP 工具', 'Switch the time range or invoke MCP tools in chat or tasks'); + const compactClass = compact ? ' mcp-stats-timeline-empty-state--compact' : ''; + return `
+ ${MCP_TIMELINE_EMPTY_ICON} +

${escapeHtml(noData)}

+

${escapeHtml(emptyHint)}

+
`; +} + +function renderMcpStatsTimelineBody(timeline, timelineError, compactEmpty) { const hint = mcpMonitorT('timelineHint') || monitorFallback('全部工具合计', 'All tools combined'); if (timelineError) { @@ -4029,8 +4047,7 @@ function renderMcpStatsTimelineBody(timeline, timelineError) { || `区间内 ${summaryTotal} 次 · 峰值 ${peak}`; if (points.length === 0 || summaryTotal === 0) { - const noData = mcpMonitorT('timelineNoData') || monitorFallback('该时段暂无调用', 'No calls in this period'); - return `

${escapeHtml(noData)}

`; + return renderMcpStatsTimelineEmptyState(!!compactEmpty); } const rangeKey = timeline.range || getMcpMonitorTimelineRange(); @@ -4083,7 +4100,7 @@ function renderMcpStatsCombinedSection(topTools, totals, activeToolFilter, timel const timelineCol = showTimeline ? `

${escapeHtml(timelineTitle)}

-
${renderMcpStatsTimelineBody(timeline, timelineError)}
+
${renderMcpStatsTimelineBody(timeline, timelineError, hasTools)}
` : ''; @@ -4897,7 +4914,8 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) { .sort((a, b) => (b.totalCalls || 0) - (a.totalCalls || 0)) .slice(0, MCP_STATS_TOP_N); - const showCombined = showTimeline || topTools.length > 0; + const hasAnyCalls = totals.total > 0; + const showCombined = hasAnyCalls && (topTools.length > 0 || showTimeline); const html = `
${renderMcpStatsMetricsBar(totals, successRate, rateTone, rateSubText, lastCallText, hasCalls)}