Initial commit to new repository
This commit is contained in:
345
src/AxCopilot/Views/UserAskDialog.cs
Normal file
345
src/AxCopilot/Views/UserAskDialog.cs
Normal file
@@ -0,0 +1,345 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Effects;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 에이전트가 사용자에게 질문할 때 표시하는 커스텀 대화 상자.
|
||||
/// 선택지 버튼 + 직접 입력 텍스트 박스를 제공합니다.
|
||||
/// </summary>
|
||||
internal sealed class UserAskDialog : Window
|
||||
{
|
||||
private string _selectedResponse = "";
|
||||
private readonly TextBox _customInput;
|
||||
private readonly StackPanel _optionPanel;
|
||||
|
||||
public string SelectedResponse => _selectedResponse;
|
||||
|
||||
private UserAskDialog(string question, List<string> options, string defaultValue)
|
||||
{
|
||||
Width = 460;
|
||||
MinWidth = 380;
|
||||
MaxWidth = 560;
|
||||
SizeToContent = SizeToContent.Height;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||
ResizeMode = ResizeMode.NoResize;
|
||||
WindowStyle = WindowStyle.None;
|
||||
AllowsTransparency = true;
|
||||
Background = Brushes.Transparent;
|
||||
Topmost = true;
|
||||
|
||||
var bgBrush = Application.Current.TryFindResource("LauncherBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E));
|
||||
var primaryText = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var secondaryText = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
||||
var borderBrush = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var itemBg = Application.Current.TryFindResource("ItemBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40));
|
||||
var hoverBg = Application.Current.TryFindResource("ItemHoverBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
|
||||
// 루트
|
||||
var root = new Border
|
||||
{
|
||||
Background = bgBrush,
|
||||
CornerRadius = new CornerRadius(16),
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(24, 20, 24, 18),
|
||||
Effect = new DropShadowEffect
|
||||
{
|
||||
BlurRadius = 24, ShadowDepth = 4, Opacity = 0.3, Color = Colors.Black,
|
||||
},
|
||||
};
|
||||
|
||||
var stack = new StackPanel();
|
||||
|
||||
// 타이틀 바 (드래그 가능)
|
||||
var titleBar = new Grid { Margin = new Thickness(0, 0, 0, 12) };
|
||||
titleBar.MouseLeftButtonDown += (_, _) => { try { DragMove(); } catch { } };
|
||||
var titleSp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
titleSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE9CE",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 16,
|
||||
Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
});
|
||||
titleSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "AX Agent — 질문",
|
||||
FontSize = 14,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
titleBar.Children.Add(titleSp);
|
||||
stack.Children.Add(titleBar);
|
||||
|
||||
// 구분선
|
||||
stack.Children.Add(new Border
|
||||
{
|
||||
Height = 1,
|
||||
Background = borderBrush,
|
||||
Opacity = 0.3,
|
||||
Margin = new Thickness(0, 0, 0, 14),
|
||||
});
|
||||
|
||||
// 질문 텍스트
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = question,
|
||||
FontSize = 13.5,
|
||||
Foreground = primaryText,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(0, 0, 0, 16),
|
||||
LineHeight = 20,
|
||||
});
|
||||
|
||||
// 선택지 버튼들
|
||||
_optionPanel = new StackPanel { Margin = new Thickness(0, 0, 0, 12) };
|
||||
Border? selectedBorder = null;
|
||||
|
||||
foreach (var option in options)
|
||||
{
|
||||
var capturedOption = option;
|
||||
var optBorder = new Border
|
||||
{
|
||||
Background = itemBg,
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(14, 10, 14, 10),
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
Cursor = Cursors.Hand,
|
||||
BorderBrush = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(1.5),
|
||||
};
|
||||
|
||||
var optSp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
var radioCircle = new Border
|
||||
{
|
||||
Width = 18, Height = 18,
|
||||
CornerRadius = new CornerRadius(9),
|
||||
BorderBrush = secondaryText,
|
||||
BorderThickness = new Thickness(1.5),
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Child = new Border
|
||||
{
|
||||
Width = 10, Height = 10,
|
||||
CornerRadius = new CornerRadius(5),
|
||||
Background = Brushes.Transparent,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
};
|
||||
optSp.Children.Add(radioCircle);
|
||||
optSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = capturedOption,
|
||||
FontSize = 13,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
optBorder.Child = optSp;
|
||||
|
||||
optBorder.MouseEnter += (s, _) =>
|
||||
{
|
||||
var b = (Border)s;
|
||||
if (b.BorderBrush != accentBrush)
|
||||
b.Background = hoverBg;
|
||||
};
|
||||
optBorder.MouseLeave += (s, _) =>
|
||||
{
|
||||
var b = (Border)s;
|
||||
if (b.BorderBrush != accentBrush)
|
||||
b.Background = itemBg;
|
||||
};
|
||||
optBorder.MouseLeftButtonUp += (s, _) =>
|
||||
{
|
||||
// 이전 선택 해제
|
||||
if (selectedBorder != null)
|
||||
{
|
||||
selectedBorder.BorderBrush = Brushes.Transparent;
|
||||
selectedBorder.Background = itemBg;
|
||||
var prevCircle = FindRadioCircle(selectedBorder);
|
||||
if (prevCircle != null) prevCircle.Background = Brushes.Transparent;
|
||||
}
|
||||
|
||||
// 현재 선택
|
||||
var cur = (Border)s;
|
||||
cur.BorderBrush = accentBrush;
|
||||
cur.Background = new SolidColorBrush(Color.FromArgb(0x15,
|
||||
((SolidColorBrush)accentBrush).Color.R,
|
||||
((SolidColorBrush)accentBrush).Color.G,
|
||||
((SolidColorBrush)accentBrush).Color.B));
|
||||
var circle = FindRadioCircle(cur);
|
||||
if (circle != null) circle.Background = accentBrush;
|
||||
selectedBorder = cur;
|
||||
|
||||
_selectedResponse = capturedOption;
|
||||
if (_customInput != null) _customInput.Text = ""; // 선택지 고르면 직접 입력 초기화
|
||||
};
|
||||
|
||||
_optionPanel.Children.Add(optBorder);
|
||||
}
|
||||
stack.Children.Add(_optionPanel);
|
||||
|
||||
// 직접 입력 영역
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "또는 직접 입력:",
|
||||
FontSize = 11.5,
|
||||
Foreground = secondaryText,
|
||||
Margin = new Thickness(2, 0, 0, 6),
|
||||
});
|
||||
|
||||
_customInput = new TextBox
|
||||
{
|
||||
MinHeight = 40,
|
||||
MaxHeight = 100,
|
||||
AcceptsReturn = true,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
FontSize = 13,
|
||||
Background = itemBg,
|
||||
Foreground = primaryText,
|
||||
CaretBrush = primaryText,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(10, 8, 10, 8),
|
||||
Text = defaultValue,
|
||||
};
|
||||
_customInput.TextChanged += (_, _) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_customInput.Text))
|
||||
{
|
||||
// 직접 입력 시 선택 해제
|
||||
if (selectedBorder != null)
|
||||
{
|
||||
selectedBorder.BorderBrush = Brushes.Transparent;
|
||||
selectedBorder.Background = itemBg;
|
||||
var prevCircle = FindRadioCircle(selectedBorder);
|
||||
if (prevCircle != null) prevCircle.Background = Brushes.Transparent;
|
||||
selectedBorder = null;
|
||||
}
|
||||
_selectedResponse = _customInput.Text;
|
||||
}
|
||||
};
|
||||
stack.Children.Add(_customInput);
|
||||
|
||||
// 하단 버튼
|
||||
var btnPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(0, 16, 0, 0),
|
||||
};
|
||||
|
||||
// 확인 버튼
|
||||
var confirmBtn = new Border
|
||||
{
|
||||
Background = accentBrush,
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(20, 9, 20, 9),
|
||||
Cursor = Cursors.Hand,
|
||||
Margin = new Thickness(8, 0, 0, 0),
|
||||
};
|
||||
confirmBtn.Child = new TextBlock
|
||||
{
|
||||
Text = "확인",
|
||||
FontSize = 13,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = Brushes.White,
|
||||
};
|
||||
confirmBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85;
|
||||
confirmBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
||||
confirmBtn.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_selectedResponse) && !string.IsNullOrWhiteSpace(defaultValue))
|
||||
_selectedResponse = defaultValue;
|
||||
DialogResult = true;
|
||||
Close();
|
||||
};
|
||||
btnPanel.Children.Add(confirmBtn);
|
||||
|
||||
// 취소 버튼
|
||||
var cancelBtn = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(16, 9, 16, 9),
|
||||
Cursor = Cursors.Hand,
|
||||
BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, 0xDC, 0x26, 0x26)),
|
||||
BorderThickness = new Thickness(1),
|
||||
};
|
||||
cancelBtn.Child = new TextBlock
|
||||
{
|
||||
Text = "취소",
|
||||
FontSize = 13,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)),
|
||||
};
|
||||
cancelBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85;
|
||||
cancelBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
||||
cancelBtn.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
_selectedResponse = "";
|
||||
DialogResult = false;
|
||||
Close();
|
||||
};
|
||||
// 취소를 왼쪽에 배치
|
||||
btnPanel.Children.Insert(0, cancelBtn);
|
||||
|
||||
stack.Children.Add(btnPanel);
|
||||
|
||||
root.Child = stack;
|
||||
Content = root;
|
||||
|
||||
// 등장 애니메이션
|
||||
root.RenderTransformOrigin = new Point(0.5, 0.5);
|
||||
root.RenderTransform = new ScaleTransform(0.95, 0.95);
|
||||
root.Opacity = 0;
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
var fade = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(150));
|
||||
root.BeginAnimation(OpacityProperty, fade);
|
||||
var scaleX = new DoubleAnimation(0.95, 1, TimeSpan.FromMilliseconds(200))
|
||||
{ EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } };
|
||||
var scaleY = new DoubleAnimation(0.95, 1, TimeSpan.FromMilliseconds(200))
|
||||
{ EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } };
|
||||
((ScaleTransform)root.RenderTransform).BeginAnimation(ScaleTransform.ScaleXProperty, scaleX);
|
||||
((ScaleTransform)root.RenderTransform).BeginAnimation(ScaleTransform.ScaleYProperty, scaleY);
|
||||
};
|
||||
|
||||
// ESC → 취소
|
||||
KeyDown += (_, e) =>
|
||||
{
|
||||
if (e.Key == Key.Escape) { _selectedResponse = ""; DialogResult = false; Close(); }
|
||||
if (e.Key == Key.Enter && !_customInput.IsFocused) { DialogResult = true; Close(); }
|
||||
};
|
||||
}
|
||||
|
||||
private static Border? FindRadioCircle(Border optionBorder)
|
||||
{
|
||||
if (optionBorder.Child is StackPanel sp && sp.Children.Count > 0 && sp.Children[0] is Border outer
|
||||
&& outer.Child is Border inner)
|
||||
return inner;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>에이전트 질문 다이얼로그를 표시합니다.</summary>
|
||||
/// <returns>사용자 응답 문자열. 취소 시 null.</returns>
|
||||
public static string? Show(string question, List<string> options, string defaultValue = "")
|
||||
{
|
||||
var dlg = new UserAskDialog(question, options, defaultValue);
|
||||
var result = dlg.ShowDialog();
|
||||
return result == true ? dlg.SelectedResponse : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user