Some checks failed
Release Gate / gate (push) Has been cancelled
- ChatWindow.InlineInteractions를 UserAskPresentation과 PlanApprovalPresentation으로 분리해 사용자 질문 카드와 계획 승인 흐름의 책임을 나눔 - 메시지 타입 renderer 분리 계획의 다음 단계로 ChatWindow.xaml.cs와 mixed inline interaction partial의 결합도를 낮춤 - README, DEVELOPMENT, claw-code parity plan 문서를 2026-04-06 09:44 (KST) 기준으로 갱신함 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
253 lines
8.7 KiB
C#
253 lines
8.7 KiB
C#
using System.Linq;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using System.Windows.Input;
|
|
using System.Windows.Media;
|
|
using System.Windows.Media.Animation;
|
|
|
|
namespace AxCopilot.Views;
|
|
|
|
public partial class ChatWindow
|
|
{
|
|
private async Task<string?> ShowInlineUserAskAsync(string question, List<string> options, string defaultValue)
|
|
{
|
|
var tcs = new TaskCompletionSource<string?>();
|
|
|
|
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<string> options, string defaultValue, TaskCompletionSource<string?> 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;
|
|
}
|
|
}
|