using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Threading; using AxCopilot.Services.Agent; namespace AxCopilot.Views; public partial class ChatWindow { private DispatcherTimer? _v2LiveElapsedTimer; private DateTime _v2LiveStartTime; private TextBlock? _v2LiveElapsedText; private Border? _v2LiveStatusCard; private TextBlock? _v2LiveStatusText; private TextBlock? _v2LiveStatusDetailText; private TextBlock? _v2LiveStatusMetaText; /// V2: 스트리밍 시작 시 라이브 진행 컨테이너 생성 private void ShowAgentLiveCardV2(string runTab) { if (MessageList == null) return; if (!string.Equals(runTab, _activeTab, StringComparison.OrdinalIgnoreCase)) return; RemoveAgentLiveCardV2(animated: false); _v2LiveStartTime = DateTime.UtcNow; _v2LiveToolCards.Clear(); _v2LastLiveToolCallId = null; var msgMaxWidth = GetMessageMaxWidth(); var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var accentColor = ResolveLiveProgressAccentColor(accentBrush); var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; _v2LiveContainer = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center, Width = msgMaxWidth, MaxWidth = msgMaxWidth, Margin = new Thickness(0, 4, 0, 6), }; var headerGrid = new Grid { Margin = new Thickness(2, 0, 0, 4) }; headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); var (agentName, _, _) = GetAgentIdentity(); var (liveIconHost, livePixels, liveGlows, liveRotate, liveScale) = CreateMiniLauncherIconEx(4.0, "none"); var canvas = liveIconHost.Children.OfType().FirstOrDefault(); if (canvas != null) { var animState = new ChatIconAnimState { Host = liveIconHost, Canvas = canvas, Pixels = livePixels, Glows = liveGlows, Rotate = liveRotate, Scale = liveScale, IsRandomMode = _settings.Settings.Launcher.EnableChatIconRandomAnimation, }; StartChatIconAnimation(animState); } Grid.SetColumn(liveIconHost, 0); headerGrid.Children.Add(liveIconHost); var nameTb = new TextBlock { Text = agentName, FontSize = 11, FontWeight = FontWeights.SemiBold, Foreground = secondaryText, Margin = new Thickness(6, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center, }; Grid.SetColumn(nameTb, 1); headerGrid.Children.Add(nameTb); _v2LiveElapsedText = new TextBlock { Text = "", FontSize = 10, Foreground = secondaryText, Opacity = 0.70, HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center, }; Grid.SetColumn(_v2LiveElapsedText, 2); headerGrid.Children.Add(_v2LiveElapsedText); _v2LiveContainer.Children.Add(headerGrid); _v2LiveStatusText = new TextBlock { FontSize = 14, FontWeight = FontWeights.SemiBold, Foreground = primaryText, TextWrapping = TextWrapping.Wrap, LineHeight = 20, }; _v2LiveStatusDetailText = new TextBlock { FontSize = 11, Foreground = secondaryText, Opacity = 0.88, TextWrapping = TextWrapping.Wrap, LineHeight = 18, Margin = new Thickness(0, 4, 0, 0), }; _v2LiveStatusMetaText = new TextBlock { FontSize = 10, Foreground = secondaryText, Opacity = 0.72, Margin = new Thickness(0, 8, 0, 0), }; _v2LiveStatusCard = new Border { Background = new SolidColorBrush(Color.FromArgb(0x12, accentColor.R, accentColor.G, accentColor.B)), BorderBrush = new SolidColorBrush(Color.FromArgb(0x36, accentColor.R, accentColor.G, accentColor.B)), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(12), Padding = new Thickness(14, 12, 14, 12), Margin = new Thickness(0, 4, 0, 6), Child = new StackPanel { Children = { _v2LiveStatusText, _v2LiveStatusDetailText, _v2LiveStatusMetaText, } } }; _v2LiveContainer.Children.Add(_v2LiveStatusCard); RefreshV2LiveStatusCard(runTab); AddTranscriptElement(_v2LiveContainer); ForceScrollToEnd(); _v2LiveElapsedTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; _v2LiveElapsedTimer.Tick += (_, _) => { if (_v2LiveElapsedText == null) return; var sec = (int)(DateTime.UtcNow - _v2LiveStartTime).TotalSeconds; _v2LiveElapsedText.Text = sec > 0 ? $"{sec}초 경과" : ""; }; _v2LiveElapsedTimer.Start(); } /// V2: 에이전트 이벤트 수신 시 라이브 카드 업데이트 private void UpdateAgentLiveCardV2(AgentEvent agentEvent) { if (_v2LiveContainer == null) return; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var msgMaxWidth = GetMessageMaxWidth(); UpdateV2LiveStatusCardFromEvent(agentEvent); switch (agentEvent.Type) { case AgentEventType.ToolCall: { var (icon, iconColor) = GetV2ToolIcon(agentEvent.ToolName); var toolId = $"{agentEvent.ToolName}_{agentEvent.Timestamp.Ticks}"; _v2LastLiveToolCallId = toolId; var outerGrid = new Grid { Margin = new Thickness(0, 2, 0, 2) }; outerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); outerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); var accentColor = ResolveLiveProgressAccentColor( TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue); var pulseLine = new Border { Width = 2, Background = new SolidColorBrush(Color.FromArgb(0x80, accentColor.R, accentColor.G, accentColor.B)), CornerRadius = new CornerRadius(1), Margin = new Thickness(12, 0, 8, 0), }; var pulseAnim = new DoubleAnimation(0.4, 1.0, TimeSpan.FromMilliseconds(800)) { AutoReverse = true, RepeatBehavior = RepeatBehavior.Forever, EasingFunction = new SineEase(), }; pulseLine.BeginAnimation(UIElement.OpacityProperty, pulseAnim); Grid.SetColumn(pulseLine, 0); outerGrid.Children.Add(pulseLine); var card = new Border { Background = new SolidColorBrush(Color.FromArgb(0x1C, accentColor.R, accentColor.G, accentColor.B)), BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, accentColor.R, accentColor.G, accentColor.B)), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 6, 10, 6), Tag = "pending", }; Grid.SetColumn(card, 1); var sp = new StackPanel { Orientation = Orientation.Horizontal }; sp.Children.Add(new TextBlock { Text = icon, FontSize = 12, Foreground = iconColor, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), }); sp.Children.Add(new TextBlock { Text = GetV2ToolDisplayName(agentEvent.ToolName), FontSize = 11, FontWeight = FontWeights.SemiBold, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, }); if (!string.IsNullOrWhiteSpace(agentEvent.FilePath)) { sp.Children.Add(new TextBlock { Text = TruncateFilePath(agentEvent.FilePath, 50), FontSize = 10, Foreground = secondaryText, Opacity = 0.7, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(8, 0, 0, 0), }); } sp.Children.Add(new TextBlock { Text = "...", FontSize = 11, Foreground = secondaryText, Opacity = 0.5, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(6, 0, 0, 0), }); card.Child = sp; outerGrid.Children.Add(card); _v2LiveToolCards[toolId] = card; _v2LiveContainer.Children.Add(outerGrid); AutoScrollIfNeeded(); break; } case AgentEventType.ToolResult: { if (_v2LastLiveToolCallId != null && _v2LiveToolCards.TryGetValue(_v2LastLiveToolCallId, out var pendingCard)) { var isSuccess = agentEvent.Success; var statusIcon = isSuccess ? "\uE73E" : "\uE711"; var statusColor = isSuccess ? new SolidColorBrush(Color.FromRgb(0x66, 0xBB, 0x6A)) : new SolidColorBrush(Color.FromRgb(0xEF, 0x53, 0x50)); pendingCard.Background = isSuccess ? new SolidColorBrush(Color.FromArgb(0x0A, 0x66, 0xBB, 0x6A)) : new SolidColorBrush(Color.FromArgb(0x0A, 0xEF, 0x53, 0x50)); pendingCard.BorderBrush = isSuccess ? new SolidColorBrush(Color.FromArgb(0x30, 0x66, 0xBB, 0x6A)) : new SolidColorBrush(Color.FromArgb(0x30, 0xEF, 0x53, 0x50)); pendingCard.Tag = "complete"; if (pendingCard.Child is StackPanel sp) { if (sp.Children.Count > 0 && sp.Children[^1] is TextBlock lastTb && lastTb.Text == "...") sp.Children.RemoveAt(sp.Children.Count - 1); sp.Children.Add(new TextBlock { Text = statusIcon, FontFamily = s_segoeIconFont, FontSize = 11, Foreground = statusColor, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(6, 0, 0, 0), }); var elapsed = NormalizeProgressElapsedMs(agentEvent.ElapsedMs); if (elapsed > 0) { sp.Children.Add(new TextBlock { Text = $"{elapsed / 1000.0:F1}s", FontSize = 10, Foreground = secondaryText, Opacity = 0.6, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(4, 0, 0, 0), }); } } var parent = pendingCard.Parent as Grid; if (parent?.Children[0] is Border pulseLine) { pulseLine.BeginAnimation(UIElement.OpacityProperty, null); pulseLine.Opacity = 1; pulseLine.Background = isSuccess ? new SolidColorBrush(Color.FromArgb(0x60, 0x66, 0xBB, 0x6A)) : new SolidColorBrush(Color.FromArgb(0x60, 0xEF, 0x53, 0x50)); } var cardToCollapse = pendingCard; var outerGridToCollapse = parent; var collapseTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(1200) }; collapseTimer.Tick += (_, _) => { collapseTimer.Stop(); if (cardToCollapse == null) return; var padAnim = new ThicknessAnimation( cardToCollapse.Padding, new Thickness(8, 2, 8, 2), TimeSpan.FromMilliseconds(200)) { EasingFunction = new QuadraticEase() }; var opAnim = new DoubleAnimation(1.0, 0.55, TimeSpan.FromMilliseconds(200)) { EasingFunction = new QuadraticEase() }; cardToCollapse.BeginAnimation(Border.PaddingProperty, padAnim); cardToCollapse.BeginAnimation(UIElement.OpacityProperty, opAnim); if (outerGridToCollapse != null) outerGridToCollapse.Margin = new Thickness(0, 1, 0, 1); }; collapseTimer.Start(); _v2LastLiveToolCallId = null; } break; } case AgentEventType.Thinking: { var thinkText = AgentProgressSummarySanitizer.NormalizeThinkingSummary( agentEvent.Summary, agentEvent.ToolName, maxLength: 100); if (string.IsNullOrWhiteSpace(thinkText)) break; var thinkRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(22, 2, 0, 2), }; thinkRow.Children.Add(new TextBlock { Text = "\uE915", FontFamily = s_segoeIconFont, FontSize = 10, Foreground = new SolidColorBrush(Color.FromRgb(0x59, 0xA5, 0xF5)), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0), }); thinkRow.Children.Add(new TextBlock { Text = thinkText, FontSize = 10.5, FontStyle = FontStyles.Italic, Foreground = secondaryText, Opacity = 0.82, TextTrimming = TextTrimming.CharacterEllipsis, MaxWidth = msgMaxWidth - 60, }); _v2LiveContainer.Children.Add(thinkRow); AutoScrollIfNeeded(); break; } } } /// V2: 스트리밍 종료 시 라이브 카드 제거 private void RemoveAgentLiveCardV2(bool animated = true) { _v2LiveElapsedTimer?.Stop(); _v2LiveElapsedTimer = null; _v2LiveElapsedText = null; _v2LiveStatusCard = null; _v2LiveStatusText = null; _v2LiveStatusDetailText = null; _v2LiveStatusMetaText = null; if (_v2LiveContainer == null) return; _chatIconAnimStates.RemoveAll(s => s.Host != null && !s.Host.IsVisible); var toRemove = _v2LiveContainer; _v2LiveContainer = null; _v2LiveToolCards.Clear(); _v2LastLiveToolCallId = null; if (animated && ContainsTranscriptElement(toRemove)) { var anim = new DoubleAnimation(toRemove.Opacity, 0, TimeSpan.FromMilliseconds(160)); anim.Completed += (_, _) => RemoveTranscriptElement(toRemove); toRemove.BeginAnimation(UIElement.OpacityProperty, anim); return; } RemoveTranscriptElement(toRemove); } private void RefreshV2LiveStatusCard(string runTab) { if (_v2LiveStatusText == null) return; var hint = GetLiveAgentProgressHint(runTab); if (hint != null) { var narrative = AgentStatusNarrativeCatalog.BuildFromEvent(hint, runTab); var hintSummary = string.IsNullOrWhiteSpace(hint.Summary) ? narrative.Message : hint.Summary; UpdateV2LiveStatusCard(hintSummary, narrative.Detail, BuildReadableProgressMetaText(hint)); return; } var initial = AgentStatusNarrativeCatalog.BuildInitial(runTab); UpdateV2LiveStatusCard(initial.Message, initial.Detail, null); } private void UpdateV2LiveStatusCardFromEvent(AgentEvent agentEvent) { if (_v2LiveStatusText == null) return; var narrative = AgentStatusNarrativeCatalog.BuildFromEvent(agentEvent, _activeTab); var normalizedThinking = agentEvent.Type == AgentEventType.Thinking ? AgentProgressSummarySanitizer.NormalizeThinkingSummary( agentEvent.Summary, agentEvent.ToolName, maxLength: 160) : string.Empty; var message = string.IsNullOrWhiteSpace(normalizedThinking) ? narrative.Message : normalizedThinking; UpdateV2LiveStatusCard(message, narrative.Detail, BuildReadableProgressMetaText(agentEvent)); } private void UpdateV2LiveStatusCard(string message, string? detail, string? meta) { if (_v2LiveStatusText == null || _v2LiveStatusDetailText == null || _v2LiveStatusMetaText == null) return; _v2LiveStatusText.Text = string.IsNullOrWhiteSpace(message) ? "작업을 이어가고 있습니다..." : message; _v2LiveStatusDetailText.Text = detail ?? string.Empty; _v2LiveStatusDetailText.Visibility = string.IsNullOrWhiteSpace(detail) ? Visibility.Collapsed : Visibility.Visible; _v2LiveStatusMetaText.Text = meta ?? string.Empty; _v2LiveStatusMetaText.Visibility = string.IsNullOrWhiteSpace(meta) ? Visibility.Collapsed : Visibility.Visible; } }