Initial commit to new repository
This commit is contained in:
398
src/AxCopilot/Views/AgentStatsDashboardWindow.xaml.cs
Normal file
398
src/AxCopilot/Views/AgentStatsDashboardWindow.xaml.cs
Normal file
@@ -0,0 +1,398 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>에이전트 실행 통계 대시보드 창.</summary>
|
||||
public partial class AgentStatsDashboardWindow : Window
|
||||
{
|
||||
private int _currentDays = 1; // 현재 필터 (오늘=1)
|
||||
|
||||
public AgentStatsDashboardWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += (_, _) => LoadStats(_currentDays);
|
||||
}
|
||||
|
||||
// ─── 이벤트 핸들러 ────────────────────────────────────────────────────
|
||||
|
||||
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 타이틀바 우측 닫기 버튼 영역에서는 DragMove 실행하지 않음
|
||||
var pos = e.GetPosition(this);
|
||||
if (pos.X > ActualWidth - 50) return;
|
||||
|
||||
if (e.ClickCount == 2)
|
||||
{
|
||||
WindowState = WindowState == WindowState.Maximized
|
||||
? WindowState.Normal : WindowState.Maximized;
|
||||
}
|
||||
else DragMove();
|
||||
}
|
||||
|
||||
private void BtnClose_Click(object sender, MouseButtonEventArgs e) => Close();
|
||||
|
||||
private void Filter_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is not Border b || b.Tag is not string tagStr) return;
|
||||
if (!int.TryParse(tagStr, out var days)) return;
|
||||
|
||||
_currentDays = days;
|
||||
UpdateFilterButtons(b);
|
||||
LoadStats(days);
|
||||
}
|
||||
|
||||
private void BtnClearStats_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var result = CustomMessageBox.Show(
|
||||
"모든 통계 데이터를 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.",
|
||||
"통계 초기화",
|
||||
MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
if (result != MessageBoxResult.Yes) return;
|
||||
|
||||
AgentStatsService.Clear();
|
||||
LoadStats(_currentDays);
|
||||
}
|
||||
|
||||
// ─── 통계 로드/렌더링 ─────────────────────────────────────────────────
|
||||
|
||||
private void LoadStats(int days)
|
||||
{
|
||||
var summary = AgentStatsService.Aggregate(days);
|
||||
RenderSummaryCards(summary);
|
||||
RenderDailyChart(summary, days);
|
||||
RenderToolFreq(summary);
|
||||
RenderBreakdowns(summary);
|
||||
UpdateStatus(summary, days);
|
||||
}
|
||||
|
||||
private void RenderSummaryCards(AgentStatsService.AgentStatsSummary s)
|
||||
{
|
||||
SummaryCards.Children.Clear();
|
||||
SummaryCards.Children.Add(MakeSummaryCard("\uE9D9", "총 실행", s.TotalSessions.ToString("N0"), "#A78BFA"));
|
||||
SummaryCards.Children.Add(MakeSummaryCard("\uE8A7", "도구 호출", s.TotalToolCalls.ToString("N0"), "#3B82F6"));
|
||||
SummaryCards.Children.Add(MakeSummaryCard("\uE7C8", "총 토큰", FormatTokens(s.TotalTokens), "#10B981"));
|
||||
SummaryCards.Children.Add(MakeSummaryCard("\uE73E", "재시도 품질",
|
||||
$"{s.RetryQualityRate * 100:F0}%",
|
||||
"#F59E0B"));
|
||||
}
|
||||
|
||||
private Border MakeSummaryCard(string icon, string label, string value, string colorHex)
|
||||
{
|
||||
var bg = TryFindResource("ItemBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40));
|
||||
var fg = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var sub = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var col = (Color)ColorConverter.ConvertFromString(colorHex);
|
||||
|
||||
var card = new Border
|
||||
{
|
||||
Background = bg,
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(14, 12, 14, 12),
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
};
|
||||
var sp = new StackPanel();
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 16,
|
||||
Foreground = new SolidColorBrush(col),
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
});
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = value,
|
||||
FontSize = 20,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Foreground = fg,
|
||||
});
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 11,
|
||||
Foreground = sub,
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
});
|
||||
card.Child = sp;
|
||||
return card;
|
||||
}
|
||||
|
||||
private void RenderDailyChart(AgentStatsService.AgentStatsSummary s, int days)
|
||||
{
|
||||
DailyBarChart.Children.Clear();
|
||||
DailyBarLabels.Children.Clear();
|
||||
|
||||
// 최근 N일 날짜 범위 구성
|
||||
var endDate = DateTime.Today;
|
||||
int chartDays = days == 1 ? 1 : days == 0 ? 30 : days;
|
||||
var dates = Enumerable.Range(0, chartDays)
|
||||
.Select(i => endDate.AddDays(-(chartDays - 1 - i)))
|
||||
.ToList();
|
||||
|
||||
var maxVal = dates
|
||||
.Select(d => s.DailySessions.GetValueOrDefault(d.ToString("yyyy-MM-dd"), 0))
|
||||
.DefaultIfEmpty(0).Max();
|
||||
if (maxVal == 0) maxVal = 1;
|
||||
|
||||
var barColor = (Color)ColorConverter.ConvertFromString("#4B5EFC");
|
||||
var chartW = 748.0; // 근사값 (Canvas는 레이아웃 후 ActualWidth 확정)
|
||||
var barGap = 2.0;
|
||||
var barW = Math.Max(4, (chartW - barGap * dates.Count) / dates.Count);
|
||||
var chartH = 100.0;
|
||||
|
||||
for (int i = 0; i < dates.Count; i++)
|
||||
{
|
||||
var dateKey = dates[i].ToString("yyyy-MM-dd");
|
||||
var val = s.DailySessions.GetValueOrDefault(dateKey, 0);
|
||||
var barH = Math.Max(2, val / (double)maxVal * (chartH - 4));
|
||||
var x = i * (barW + barGap);
|
||||
|
||||
var rect = new Rectangle
|
||||
{
|
||||
Width = barW,
|
||||
Height = barH,
|
||||
Fill = new SolidColorBrush(Color.FromArgb(0xCC,
|
||||
barColor.R, barColor.G, barColor.B)),
|
||||
RadiusX = 3,
|
||||
RadiusY = 3,
|
||||
ToolTip = $"{dateKey}: {val}회",
|
||||
};
|
||||
Canvas.SetLeft(rect, x);
|
||||
Canvas.SetTop(rect, chartH - barH);
|
||||
DailyBarChart.Children.Add(rect);
|
||||
|
||||
// 레이블: 7일 이하면 모두 표시, 그 이상이면 월요일만
|
||||
if (dates.Count <= 7 || dates[i].DayOfWeek == DayOfWeek.Monday)
|
||||
{
|
||||
var lbl = new TextBlock
|
||||
{
|
||||
Text = dates[i].ToString("MM/dd"),
|
||||
FontSize = 9,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
};
|
||||
Canvas.SetLeft(lbl, x);
|
||||
DailyBarLabels.Children.Add(lbl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderToolFreq(AgentStatsService.AgentStatsSummary s)
|
||||
{
|
||||
ToolFreqPanel.Children.Clear();
|
||||
if (s.ToolFrequency.Count == 0)
|
||||
{
|
||||
ToolFreqPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "데이터 없음",
|
||||
FontSize = 12,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var maxVal = s.ToolFrequency.Values.Max();
|
||||
var barColor = (Color)ColorConverter.ConvertFromString("#3B82F6");
|
||||
|
||||
foreach (var (tool, count) in s.ToolFrequency)
|
||||
{
|
||||
var ratio = (double)count / maxVal;
|
||||
var row = new Grid { Margin = new Thickness(0, 2, 0, 2) };
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(120) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition());
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(36) });
|
||||
|
||||
var nameTb = new TextBlock
|
||||
{
|
||||
Text = tool,
|
||||
FontSize = 11,
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
Grid.SetColumn(nameTb, 0);
|
||||
|
||||
var barBg = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x20, 0x3B, 0x82, 0xF6)),
|
||||
CornerRadius = new CornerRadius(3),
|
||||
Height = 10,
|
||||
Margin = new Thickness(6, 0, 4, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
var barFg = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(0xCC, barColor.R, barColor.G, barColor.B)),
|
||||
CornerRadius = new CornerRadius(3),
|
||||
Height = 10,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
};
|
||||
barFg.Width = ratio * 150; // 최대 150px
|
||||
barBg.Child = barFg;
|
||||
Grid.SetColumn(barBg, 1);
|
||||
|
||||
var countTb = new TextBlock
|
||||
{
|
||||
Text = count.ToString(),
|
||||
FontSize = 11,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
Grid.SetColumn(countTb, 2);
|
||||
|
||||
row.Children.Add(nameTb);
|
||||
row.Children.Add(barBg);
|
||||
row.Children.Add(countTb);
|
||||
ToolFreqPanel.Children.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderBreakdowns(AgentStatsService.AgentStatsSummary s)
|
||||
{
|
||||
// 탭 분포
|
||||
TabBreakPanel.Children.Clear();
|
||||
var tabColors = new Dictionary<string, string>
|
||||
{
|
||||
["Cowork"] = "#A78BFA",
|
||||
["Code"] = "#34D399",
|
||||
["Chat"] = "#3B82F6",
|
||||
};
|
||||
foreach (var (tab, count) in s.TabBreakdown.OrderByDescending(kv => kv.Value))
|
||||
{
|
||||
var c = tabColors.GetValueOrDefault(tab, "#9CA3AF");
|
||||
TabBreakPanel.Children.Add(MakeBreakRow(tab, count, c,
|
||||
s.TabBreakdown.Values.DefaultIfEmpty(1).Max()));
|
||||
}
|
||||
if (s.TabBreakdown.Count == 0)
|
||||
TabBreakPanel.Children.Add(MakeEmptyText());
|
||||
|
||||
// 모델 분포
|
||||
ModelBreakPanel.Children.Clear();
|
||||
foreach (var (model, count) in s.ModelBreakdown.OrderByDescending(kv => kv.Value))
|
||||
{
|
||||
var shortName = model.Length > 18 ? model[..18] + "…" : model;
|
||||
ModelBreakPanel.Children.Add(MakeBreakRow(shortName, count, "#F59E0B",
|
||||
s.ModelBreakdown.Values.DefaultIfEmpty(1).Max()));
|
||||
}
|
||||
if (s.ModelBreakdown.Count == 0)
|
||||
ModelBreakPanel.Children.Add(MakeEmptyText());
|
||||
}
|
||||
|
||||
private UIElement MakeBreakRow(string label, int count, string colorHex, int maxVal)
|
||||
{
|
||||
var ratio = maxVal > 0 ? (double)count / maxVal : 0;
|
||||
var col = (Color)ColorConverter.ConvertFromString(colorHex);
|
||||
|
||||
var row = new StackPanel { Margin = new Thickness(0, 2, 0, 2) };
|
||||
var header = new Grid();
|
||||
header.ColumnDefinitions.Add(new ColumnDefinition());
|
||||
header.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) });
|
||||
|
||||
var lbl = new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 11,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||||
};
|
||||
Grid.SetColumn(lbl, 0);
|
||||
|
||||
var cnt = new TextBlock
|
||||
{
|
||||
Text = count.ToString(),
|
||||
FontSize = 11,
|
||||
FontWeight= FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
};
|
||||
Grid.SetColumn(cnt, 1);
|
||||
header.Children.Add(lbl);
|
||||
header.Children.Add(cnt);
|
||||
row.Children.Add(header);
|
||||
|
||||
// 바 차트
|
||||
var barBg = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x20, col.R, col.G, col.B)),
|
||||
CornerRadius = new CornerRadius(3),
|
||||
Height = 6,
|
||||
Margin = new Thickness(0, 3, 0, 0),
|
||||
};
|
||||
var barFg = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(0xCC, col.R, col.G, col.B)),
|
||||
CornerRadius = new CornerRadius(3),
|
||||
Height = 6,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
};
|
||||
barFg.Width = ratio * 160; // 최대 160px
|
||||
barBg.Child = barFg;
|
||||
row.Children.Add(barBg);
|
||||
return row;
|
||||
}
|
||||
|
||||
private TextBlock MakeEmptyText() => new()
|
||||
{
|
||||
Text = "데이터 없음",
|
||||
FontSize = 11,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
};
|
||||
|
||||
private void UpdateFilterButtons(Border active)
|
||||
{
|
||||
foreach (var btn in new[] { FilterToday, Filter7d, Filter30d, FilterAll })
|
||||
{
|
||||
var isActive = btn == active;
|
||||
btn.Background = isActive
|
||||
? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC))
|
||||
: TryFindResource("ItemBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x30, 0x30, 0x45));
|
||||
if (btn.Child is TextBlock tb)
|
||||
{
|
||||
tb.Foreground = isActive
|
||||
? Brushes.White
|
||||
: TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStatus(AgentStatsService.AgentStatsSummary s, int days)
|
||||
{
|
||||
var period = days switch
|
||||
{
|
||||
1 => "오늘",
|
||||
7 => "최근 7일",
|
||||
30 => "최근 30일",
|
||||
_ => "전체 기간",
|
||||
};
|
||||
var fileSize = AgentStatsService.GetFileSize();
|
||||
var taskQuality = string.Join(", ",
|
||||
s.RetryQualityByTaskType
|
||||
.OrderByDescending(kv => kv.Value)
|
||||
.ThenBy(kv => kv.Key, StringComparer.Ordinal)
|
||||
.Take(3)
|
||||
.Select(kv => $"{kv.Key} {kv.Value * 100:F0}%"));
|
||||
var taskQualityText = string.IsNullOrWhiteSpace(taskQuality)
|
||||
? "task 품질 데이터 없음"
|
||||
: $"task 품질 {taskQuality}";
|
||||
StatusText.Text =
|
||||
$"{period} 기준 | 세션 {s.TotalSessions}회, 도구 호출 {s.TotalToolCalls}회, " +
|
||||
$"토큰 {FormatTokens(s.TotalTokens)} | 재시도 복구 {s.TotalRecoveredAfterFailure}, 반복차단 {s.TotalRepeatedFailureBlocked} | {taskQualityText} | 저장 파일 {FormatFileSize(fileSize)}";
|
||||
}
|
||||
|
||||
private static string FormatTokens(int t) =>
|
||||
t >= 1_000_000 ? $"{t / 1_000_000.0:F1}M"
|
||||
: t >= 1_000 ? $"{t / 1_000.0:F1}K"
|
||||
: t.ToString();
|
||||
|
||||
private static string FormatFileSize(long bytes) =>
|
||||
bytes >= 1024 * 1024 ? $"{bytes / (1024.0 * 1024):F1}MB"
|
||||
: bytes >= 1024 ? $"{bytes / 1024.0:F1}KB"
|
||||
: $"{bytes}B";
|
||||
}
|
||||
Reference in New Issue
Block a user