using System.Windows; using System.Windows.Input; using System.Windows.Media; namespace AxCopilot.Views; public partial class ChatWindow { // 메시지 개수와 대화 ID가 바뀔 때만 재계산한다. private int _cachedMessageTokens; private int _cachedMessageCountForTokens = -1; private string? _cachedConvIdForTokens; private void RefreshContextUsageVisual() { if (TokenUsageCard == null || TokenUsageArc == null || TokenUsagePercentText == null || TokenUsageSummaryText == null || TokenUsageHintText == null || CompactNowLabel == null) return; var showContextUsage = _activeTab is "Cowork" or "Code"; TokenUsageCard.Visibility = showContextUsage ? Visibility.Visible : Visibility.Collapsed; if (!showContextUsage) { if (TokenUsagePopup != null) TokenUsagePopup.IsOpen = false; return; } var llm = _settings.Settings.Llm; var expressionLevel = GetAgentUiExpressionLevel(); var maxContextTokens = Math.Clamp(llm.MaxContextTokens, 1024, 1_000_000); var triggerPercent = Math.Clamp(llm.ContextCompactTriggerPercent, 10, 95); var triggerRatio = triggerPercent / 100.0; int messageTokens; lock (_convLock) { var convId = _currentConversation?.Id; var msgCount = _currentConversation?.Messages?.Count ?? 0; if (convId != _cachedConvIdForTokens || msgCount != _cachedMessageCountForTokens || _isStreaming) { _cachedMessageTokens = msgCount > 0 ? Services.TokenEstimator.EstimateMessages(_currentConversation!.Messages) : 0; _cachedConvIdForTokens = convId; _cachedMessageCountForTokens = msgCount; } messageTokens = _cachedMessageTokens; } var draftText = InputBox?.Text ?? ""; var draftTokens = string.IsNullOrWhiteSpace(draftText) ? 0 : Services.TokenEstimator.Estimate(draftText) + 4; var hasAnyMessages = messageTokens > 0 || _isStreaming; var baseOverhead = 0; if (hasAnyMessages) { var sysPromptLen = _llm?.SystemPrompt?.Length ?? 0; var toolCount = _toolRegistry?.GetActiveToolsForTab(_activeTab ?? "Chat")?.Count ?? 0; baseOverhead = Services.TokenEstimator.EstimateBaseOverhead(sysPromptLen, toolCount); } var currentTokens = Math.Max(0, messageTokens + draftTokens + baseOverhead); var usageRatio = Services.TokenEstimator.GetContextUsage(currentTokens, maxContextTokens); var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.DodgerBlue; Brush progressBrush = accentBrush; string summary; string compactLabel; if (usageRatio >= 1.0) { progressBrush = Brushes.IndianRed; summary = "컨텍스트 한도 초과"; compactLabel = "지금 압축"; } else if (usageRatio >= triggerRatio) { progressBrush = Brushes.DarkOrange; summary = llm.EnableProactiveContextCompact ? "곧 자동 압축" : "압축 임계 도달"; compactLabel = "압축 권장"; } else if (usageRatio >= triggerRatio * 0.7) { progressBrush = Brushes.Goldenrod; summary = "컨텍스트 사용 증가"; compactLabel = "미리 압축"; } else { summary = "컨텍스트 여유"; compactLabel = "압축"; } string detailText; if (_lastCompactionAt.HasValue && _lastCompactionBeforeTokens.HasValue && _lastCompactionAfterTokens.HasValue) { var compactType = _lastCompactionWasAutomatic ? "자동" : "수동"; detailText = $"{compactType} 압축 {Services.TokenEstimator.Format(_lastCompactionBeforeTokens.Value)} -> {Services.TokenEstimator.Format(_lastCompactionAfterTokens.Value)}"; } else { detailText = $"자동 압축 시작 {triggerPercent}%"; } TokenUsageArc.Stroke = progressBrush; var percentText = $"{System.Math.Round(usageRatio * 100):0}%"; TokenUsagePercentText.Text = percentText; TokenUsageSummaryText.Text = $"컨텍스트 {percentText}"; TokenUsageHintText.Text = $"{Services.TokenEstimator.Format(currentTokens)} / {Services.TokenEstimator.Format(maxContextTokens)}"; CompactNowLabel.Text = compactLabel; ViewModel.ContextTokens = currentTokens; ViewModel.MaxContextTokens = maxContextTokens; if (TokenUsagePopupTitle != null) TokenUsagePopupTitle.Text = $"컨텍스트 창 {percentText}"; if (TokenUsagePopupUsage != null) TokenUsagePopupUsage.Text = $"{Services.TokenEstimator.Format(currentTokens)}/{Services.TokenEstimator.Format(maxContextTokens)}"; if (TokenUsagePopupDetail != null) TokenUsagePopupDetail.Text = expressionLevel == "simple" ? detailText : $"{detailText} · 임계 {triggerPercent}%"; if (TokenUsagePopupCompact != null) { TokenUsagePopupCompact.Text = _sessionCompactionCount > 0 ? expressionLevel switch { "simple" => $"압축 {_sessionCompactionCount}회 · {FormatTokenCount(_sessionCompactionSavedTokens)} 절감", _ => $"누적 압축 {_sessionCompactionCount}회 · 절감 {FormatTokenCount(_sessionCompactionSavedTokens)}" } : expressionLevel == "rich" ? "AX Agent가 컨텍스트를 자동 관리합니다" : "컨텍스트 자동 관리"; } TokenUsageCard.ToolTip = null; UpdateCircularUsageArc(TokenUsageArc, usageRatio, 15, 15, 11); } private void TokenUsageCard_MouseEnter(object sender, MouseEventArgs e) { _tokenUsagePopupCloseTimer.Stop(); if (TokenUsagePopup != null && TokenUsageCard?.Visibility == Visibility.Visible) TokenUsagePopup.IsOpen = true; } private void TokenUsageCard_MouseLeave(object sender, MouseEventArgs e) { _tokenUsagePopupCloseTimer.Stop(); _tokenUsagePopupCloseTimer.Start(); } private void TokenUsagePopup_MouseEnter(object sender, MouseEventArgs e) { _tokenUsagePopupCloseTimer.Stop(); } private void TokenUsagePopup_MouseLeave(object sender, MouseEventArgs e) { _tokenUsagePopupCloseTimer.Stop(); _tokenUsagePopupCloseTimer.Start(); } private void CloseTokenUsagePopupIfIdle() { if (TokenUsagePopup == null) return; var cardHovered = IsMouseInsideElement(TokenUsageCard); var popupHovered = TokenUsagePopup.Child is FrameworkElement popupChild && IsMouseInsideElement(popupChild); if (!cardHovered && !popupHovered) TokenUsagePopup.IsOpen = false; } private static bool IsMouseInsideElement(FrameworkElement? element) { if (element == null || !element.IsVisible || element.ActualWidth <= 0 || element.ActualHeight <= 0) return false; try { var mouse = System.Windows.Forms.Control.MousePosition; var point = element.PointFromScreen(new Point(mouse.X, mouse.Y)); return point.X >= 0 && point.Y >= 0 && point.X <= element.ActualWidth && point.Y <= element.ActualHeight; } catch { return element.IsMouseOver; } } }