using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using AxCopilot.Services.Agent; namespace AxCopilot.Views; public partial class ChatWindow { private async Task ShowInlineUserAskAsync(string question, List options, string defaultValue) { var tcs = new TaskCompletionSource(); await Dispatcher.InvokeAsync(() => { AddUserAskCard(question, options, defaultValue, tcs); }); var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromMinutes(5))); if (completed != tcs.Task) { await Dispatcher.InvokeAsync(RemoveUserAskCard); return null; } return await tcs.Task; } private void RemoveUserAskCard() { if (_userAskCard == null) return; MessagePanel.Children.Remove(_userAskCard); _userAskCard = null; } private void AddUserAskCard(string question, List options, string defaultValue, TaskCompletionSource tcs) { RemoveUserAskCard(); var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var accentColor = ((SolidColorBrush)accentBrush).Color; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromArgb(0x24, accentColor.R, accentColor.G, accentColor.B)); var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x10, accentColor.R, accentColor.G, accentColor.B)); var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x16, 0xFF, 0xFF, 0xFF)); var okBrush = BrushFromHex("#10B981"); var dangerBrush = BrushFromHex("#EF4444"); var container = new Border { Margin = new Thickness(40, 4, 90, 8), HorizontalAlignment = HorizontalAlignment.Left, MaxWidth = Math.Max(420, GetMessageMaxWidth() - 36), Background = itemBg, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(14), Padding = new Thickness(14, 12, 14, 12), }; var outer = new StackPanel(); outer.Children.Add(new TextBlock { Text = "의견 요청", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = primaryText, }); outer.Children.Add(new TextBlock { Text = question, Margin = new Thickness(0, 4, 0, 10), FontSize = 12.5, Foreground = primaryText, TextWrapping = TextWrapping.Wrap, LineHeight = 20, }); Border? selectedOption = null; string selectedResponse = defaultValue; if (options.Count > 0) { var optionPanel = new WrapPanel { Margin = new Thickness(0, 0, 0, 10), ItemWidth = double.NaN, }; foreach (var option in options.Where(static option => !string.IsNullOrWhiteSpace(option))) { var optionLabel = option.Trim(); var optBorder = new Border { Background = Brushes.Transparent, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(999), Padding = new Thickness(10, 6, 10, 6), Margin = new Thickness(0, 0, 8, 8), Cursor = Cursors.Hand, Child = new TextBlock { Text = optionLabel, FontSize = 12, Foreground = primaryText, }, }; optBorder.MouseEnter += (s, _) => { if (!ReferenceEquals(selectedOption, s)) ((Border)s).Background = hoverBg; }; optBorder.MouseLeave += (s, _) => { if (!ReferenceEquals(selectedOption, s)) ((Border)s).Background = Brushes.Transparent; }; optBorder.MouseLeftButtonUp += (_, _) => { if (selectedOption != null) { selectedOption.Background = Brushes.Transparent; selectedOption.BorderBrush = borderBrush; } selectedOption = optBorder; selectedOption.Background = new SolidColorBrush(Color.FromArgb(0x18, accentColor.R, accentColor.G, accentColor.B)); selectedOption.BorderBrush = accentBrush; selectedResponse = optionLabel; }; optionPanel.Children.Add(optBorder); } outer.Children.Add(optionPanel); } outer.Children.Add(new TextBlock { Text = "직접 입력", FontSize = 11.5, Foreground = secondaryText, Margin = new Thickness(0, 0, 0, 6), }); var inputBox = new TextBox { Text = defaultValue, AcceptsReturn = true, TextWrapping = TextWrapping.Wrap, MinHeight = 42, MaxHeight = 100, FontSize = 12.5, Padding = new Thickness(10, 8, 10, 8), Background = Brushes.Transparent, Foreground = primaryText, CaretBrush = primaryText, BorderBrush = borderBrush, BorderThickness = new Thickness(1), }; inputBox.TextChanged += (_, _) => { if (!string.IsNullOrWhiteSpace(inputBox.Text)) { selectedResponse = inputBox.Text.Trim(); if (selectedOption != null) { selectedOption.Background = Brushes.Transparent; selectedOption.BorderBrush = borderBrush; selectedOption = null; } } }; outer.Children.Add(inputBox); var buttonRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 12, 0, 0), }; Border BuildActionButton(string label, Brush bg, Brush fg) { return new Border { Background = bg, BorderBrush = bg, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(999), Padding = new Thickness(12, 7, 12, 7), Margin = new Thickness(8, 0, 0, 0), Cursor = Cursors.Hand, Child = new TextBlock { Text = label, FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = fg, }, }; } var cancelBtn = BuildActionButton("취소", Brushes.Transparent, dangerBrush); cancelBtn.BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, 0xEF, 0x44, 0x44)); cancelBtn.MouseLeftButtonUp += (_, _) => { RemoveUserAskCard(); tcs.TrySetResult(null); }; buttonRow.Children.Add(cancelBtn); var submitBtn = BuildActionButton("전달", okBrush, Brushes.White); submitBtn.MouseLeftButtonUp += (_, _) => { var finalResponse = !string.IsNullOrWhiteSpace(inputBox.Text) ? inputBox.Text.Trim() : selectedResponse?.Trim(); if (string.IsNullOrWhiteSpace(finalResponse)) finalResponse = defaultValue?.Trim(); if (string.IsNullOrWhiteSpace(finalResponse)) return; RemoveUserAskCard(); tcs.TrySetResult(finalResponse); }; buttonRow.Children.Add(submitBtn); outer.Children.Add(buttonRow); container.Child = outer; _userAskCard = container; container.Opacity = 0; container.BeginAnimation(UIElement.OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(180))); MessagePanel.Children.Add(container); ForceScrollToEnd(); inputBox.Focus(); inputBox.CaretIndex = inputBox.Text.Length; } private Func, Task> CreatePlanDecisionCallback() { return async (planSummary, options) => { var tcs = new TaskCompletionSource(); var steps = TaskDecomposer.ExtractSteps(planSummary); await Dispatcher.InvokeAsync(() => { _pendingPlanSummary = planSummary; _pendingPlanSteps = steps.ToList(); EnsurePlanViewerWindow(); _planViewerWindow?.LoadPlan(planSummary, steps, tcs); ShowPlanButton(true); AddDecisionButtons(tcs, options); }); var completed = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromMinutes(5))); if (completed != tcs.Task) { await Dispatcher.InvokeAsync(() => { _planViewerWindow?.Hide(); ResetPendingPlanPresentation(); }); return "취소"; } var result = await tcs.Task; var agentDecision = result; if (result == null) { agentDecision = _planViewerWindow?.BuildApprovedDecisionPayload(AgentLoopService.ApprovedPlanDecisionPrefix); } else if (!string.Equals(result, "취소", StringComparison.OrdinalIgnoreCase) && !string.Equals(result, "승인", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(result)) { agentDecision = $"수정 요청: {result.Trim()}"; } if (result == null) { await Dispatcher.InvokeAsync(() => { _planViewerWindow?.SwitchToExecutionMode(); }); } else { await Dispatcher.InvokeAsync(() => { _planViewerWindow?.Hide(); ResetPendingPlanPresentation(); }); } return agentDecision; }; } private void EnsurePlanViewerWindow() { if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow)) return; _planViewerWindow = new PlanViewerWindow(this); _planViewerWindow.Closing += (_, e) => { e.Cancel = true; _planViewerWindow.Hide(); }; } private void ShowPlanButton(bool show) { if (!show) { for (int i = MoodIconPanel.Children.Count - 1; i >= 0; i--) { if (MoodIconPanel.Children[i] is Border b && b.Tag?.ToString() == "PlanBtn") { if (i > 0 && MoodIconPanel.Children[i - 1] is Border sep && sep.Tag?.ToString() == "PlanSep") MoodIconPanel.Children.RemoveAt(i - 1); if (i < MoodIconPanel.Children.Count) MoodIconPanel.Children.RemoveAt(Math.Min(i, MoodIconPanel.Children.Count - 1)); break; } } return; } foreach (var child in MoodIconPanel.Children) { if (child is Border b && b.Tag?.ToString() == "PlanBtn") return; } var separator = new Border { Width = 1, Height = 18, Background = TryFindResource("SeparatorColor") as Brush ?? Brushes.Gray, Margin = new Thickness(4, 0, 4, 0), VerticalAlignment = VerticalAlignment.Center, Tag = "PlanSep", }; MoodIconPanel.Children.Add(separator); var planBtn = CreateFolderBarButton("\uE9D2", "계획", "실행 계획 보기", "#10B981"); planBtn.Tag = "PlanBtn"; planBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; if (string.IsNullOrWhiteSpace(_pendingPlanSummary) && _pendingPlanSteps.Count == 0) return; EnsurePlanViewerWindow(); if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow)) { if (string.IsNullOrWhiteSpace(_planViewerWindow.PlanText) || _planViewerWindow.PlanText != (_pendingPlanSummary ?? string.Empty) || !_planViewerWindow.Steps.SequenceEqual(_pendingPlanSteps)) { _planViewerWindow.LoadPlanPreview(_pendingPlanSummary ?? "", _pendingPlanSteps); } _planViewerWindow.Show(); _planViewerWindow.Activate(); } }; MoodIconPanel.Children.Add(planBtn); } private void UpdatePlanViewerStep(AgentEvent evt) { if (_planViewerWindow == null || !IsWindowAlive(_planViewerWindow)) return; if (evt.StepCurrent > 0) _planViewerWindow.UpdateCurrentStep(evt.StepCurrent - 1); } private void CompletePlanViewer() { if (_planViewerWindow != null && IsWindowAlive(_planViewerWindow)) _planViewerWindow.MarkComplete(); ResetPendingPlanPresentation(); } private void ResetPendingPlanPresentation() { _pendingPlanSummary = null; _pendingPlanSteps.Clear(); ShowPlanButton(false); } private static bool IsWindowAlive(Window? w) { if (w == null) return false; try { var _ = w.IsVisible; return true; } catch { return false; } } }