Files
AX-Copilot-Codex/src/AxCopilot/Views/ChatWindow.TopicPresetPresentation.cs
lacvet 1b4566d192
Some checks failed
Release Gate / gate (push) Has been cancelled
AX Agent 프리셋 렌더와 주제 선택 흐름을 분리해 메인 창 책임을 줄인다
- ChatWindow.TopicPresetPresentation을 추가해 프리셋 카드 생성, 커스텀 프리셋 메뉴, 주제 선택 적용 흐름을 별도 프레젠테이션 계층으로 이동한다

- ChatWindow.xaml.cs에서는 프리셋 UI 조립 코드를 제거하고 대화 orchestration 중심 구조를 더 강화한다

- claw-code parity plan과 개발 문서에 구조 개선 진행 상황을 반영해 큰 구조 개선 항목이 사실상 마감 단계임을 기록한다

- 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0)
2026-04-06 10:15:22 +09:00

524 lines
18 KiB
C#

using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using AxCopilot.Models;
using AxCopilot.Services;
namespace AxCopilot.Views;
public partial class ChatWindow
{
/// <summary>프리셋에서 대화 주제 버튼을 동적으로 생성합니다.</summary>
private void BuildTopicButtons()
{
TopicButtonPanel.Children.Clear();
TopicButtonPanel.Visibility = Visibility.Visible;
if (TopicPresetScrollViewer != null)
TopicPresetScrollViewer.Visibility = Visibility.Visible;
if (_activeTab == "Cowork" || _activeTab == "Code")
{
if (EmptyStateTitle != null) EmptyStateTitle.Text = _activeTab == "Code"
? "코드 작업을 입력하세요"
: "작업 유형을 선택하세요";
if (EmptyStateDesc != null) EmptyStateDesc.Text = _activeTab == "Code"
? "코딩 에이전트가 코드 분석, 수정, 빌드, 테스트를 수행합니다"
: "에이전트가 상세한 데이터를 작성합니다";
}
else
{
if (EmptyStateTitle != null) EmptyStateTitle.Text = "대화 주제를 선택하세요";
if (EmptyStateDesc != null) EmptyStateDesc.Text = "주제에 맞는 전문 프리셋이 자동 적용됩니다";
}
if (_activeTab == "Code")
{
TopicButtonPanel.Visibility = Visibility.Collapsed;
if (TopicPresetScrollViewer != null)
TopicPresetScrollViewer.Visibility = Visibility.Collapsed;
return;
}
var presets = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets);
var cardBackground = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent;
var cardHoverBackground = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
var cardBorder = TryFindResource("BorderColor") as Brush ?? BrushFromHex("#E5E7EB");
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
void AttachTopicCardHover(Border card, Brush normalBackground, Brush hoverBackground)
{
card.MouseEnter += (sender, _) =>
{
if (sender is Border hovered)
{
hovered.Background = hoverBackground;
hovered.BorderBrush = TryFindResource("AccentColor") as Brush ?? cardBorder;
}
};
card.MouseLeave += (sender, _) =>
{
if (sender is Border hovered)
{
hovered.Background = normalBackground;
hovered.BorderBrush = cardBorder;
}
};
}
foreach (var preset in presets)
{
var capturedPreset = preset;
var buttonColor = BrushFromHex(preset.Color);
var border = new Border
{
Background = cardBackground,
BorderBrush = cardBorder,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(14),
Padding = new Thickness(14, 14, 14, 12),
Margin = new Thickness(6, 6, 6, 8),
Cursor = Cursors.Hand,
Width = 148,
Height = 124,
ClipToBounds = true,
};
var contentGrid = new Grid();
var stack = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
var iconCircle = new Border
{
Width = 34,
Height = 34,
CornerRadius = new CornerRadius(17),
Background = new SolidColorBrush(((SolidColorBrush)buttonColor).Color) { Opacity = 0.15 },
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 0, 0, 9),
};
var iconBlock = new TextBlock
{
Text = preset.Symbol,
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 15,
Foreground = buttonColor,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
iconCircle.Child = iconBlock;
stack.Children.Add(iconCircle);
stack.Children.Add(new TextBlock
{
Text = preset.Label,
FontSize = 15,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
HorizontalAlignment = HorizontalAlignment.Center,
TextAlignment = TextAlignment.Center,
TextWrapping = TextWrapping.Wrap,
MaxWidth = 112,
});
if (capturedPreset.IsCustom)
{
contentGrid.Children.Add(stack);
var badge = new Border
{
Width = 16,
Height = 16,
CornerRadius = new CornerRadius(4),
Background = new SolidColorBrush(Color.FromArgb(0x60, 0xFF, 0xFF, 0xFF)),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(2, 2, 0, 0),
};
badge.Child = new TextBlock
{
Text = "\uE710",
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 8,
Foreground = buttonColor,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
contentGrid.Children.Add(badge);
}
else
{
contentGrid.Children.Add(stack);
}
border.Child = contentGrid;
AttachTopicCardHover(border, cardBackground, cardHoverBackground);
border.MouseLeftButtonDown += (_, _) => SelectTopic(capturedPreset);
if (capturedPreset.IsCustom)
{
border.MouseRightButtonUp += (sender, args) =>
{
args.Handled = true;
ShowCustomPresetContextMenu(sender as Border, capturedPreset);
};
}
TopicButtonPanel.Children.Add(border);
}
var etcColor = BrushFromHex("#6B7280");
var etcBorder = new Border
{
Background = cardBackground,
BorderBrush = cardBorder,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(14),
Padding = new Thickness(14, 14, 14, 12),
Margin = new Thickness(6, 6, 6, 8),
Cursor = Cursors.Hand,
Width = 148,
Height = 124,
ClipToBounds = true,
};
var etcGrid = new Grid();
var etcStack = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
var etcIconCircle = new Border
{
Width = 34,
Height = 34,
CornerRadius = new CornerRadius(17),
Background = new SolidColorBrush(((SolidColorBrush)etcColor).Color) { Opacity = 0.15 },
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 0, 0, 9),
};
etcIconCircle.Child = new TextBlock
{
Text = "\uE70F",
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 15,
Foreground = etcColor,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
etcStack.Children.Add(etcIconCircle);
etcStack.Children.Add(new TextBlock
{
Text = "기타",
FontSize = 15,
FontWeight = FontWeights.SemiBold,
Foreground = primaryText,
HorizontalAlignment = HorizontalAlignment.Center,
TextAlignment = TextAlignment.Center,
});
etcGrid.Children.Add(etcStack);
etcBorder.Child = etcGrid;
AttachTopicCardHover(etcBorder, cardBackground, cardHoverBackground);
etcBorder.MouseLeftButtonDown += (_, _) =>
{
EmptyState.Visibility = Visibility.Collapsed;
InputBox.Focus();
};
TopicButtonPanel.Children.Add(etcBorder);
var addBorder = new Border
{
Background = Brushes.Transparent,
BorderBrush = cardBorder,
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(14),
Padding = new Thickness(14, 14, 14, 12),
Margin = new Thickness(6, 6, 6, 8),
Cursor = Cursors.Hand,
Width = 148,
Height = 124,
ClipToBounds = true,
};
var addGrid = new Grid();
var addStack = new StackPanel
{
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
addStack.Children.Add(new TextBlock
{
Text = "\uE710",
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 18,
Foreground = secondaryText,
HorizontalAlignment = HorizontalAlignment.Center,
Margin = new Thickness(0, 8, 0, 8),
});
addStack.Children.Add(new TextBlock
{
Text = "프리셋 추가",
FontSize = 14,
FontWeight = FontWeights.SemiBold,
Foreground = secondaryText,
HorizontalAlignment = HorizontalAlignment.Center,
});
addGrid.Children.Add(addStack);
addBorder.Child = addGrid;
AttachTopicCardHover(addBorder, Brushes.Transparent, cardHoverBackground);
addBorder.MouseLeftButtonDown += (_, _) => ShowCustomPresetDialog();
TopicButtonPanel.Children.Add(addBorder);
UpdateTopicPresetScrollMode();
}
private void UpdateTopicPresetScrollMode()
{
if (TopicPresetScrollViewer == null || TopicButtonPanel == null)
return;
Dispatcher.BeginInvoke(new Action(() =>
{
if (TopicPresetScrollViewer == null || TopicButtonPanel == null)
return;
TopicPresetScrollViewer.UpdateLayout();
var shouldScroll = TopicPresetScrollViewer.ExtentHeight > TopicPresetScrollViewer.ViewportHeight + 1;
TopicPresetScrollViewer.VerticalScrollBarVisibility = shouldScroll
? ScrollBarVisibility.Auto
: ScrollBarVisibility.Disabled;
TopicPresetScrollViewer.Padding = shouldScroll
? new Thickness(0, 2, 6, 0)
: new Thickness(0, 2, 0, 0);
}), System.Windows.Threading.DispatcherPriority.Loaded);
}
private void ShowCustomPresetDialog(CustomPresetEntry? existing = null)
{
var dialog = new CustomPresetDialog(
existingName: existing?.Label ?? "",
existingDesc: existing?.Description ?? "",
existingPrompt: existing?.SystemPrompt ?? "",
existingColor: existing?.Color ?? "#6366F1",
existingSymbol: existing?.Symbol ?? "\uE713",
existingTab: existing?.Tab ?? _activeTab)
{
Owner = this,
};
if (dialog.ShowDialog() != true)
return;
if (existing != null)
{
existing.Label = dialog.PresetName;
existing.Description = dialog.PresetDescription;
existing.SystemPrompt = dialog.PresetSystemPrompt;
existing.Color = dialog.PresetColor;
existing.Symbol = dialog.PresetSymbol;
existing.Tab = dialog.PresetTab;
}
else
{
_settings.Settings.Llm.CustomPresets.Add(new CustomPresetEntry
{
Label = dialog.PresetName,
Description = dialog.PresetDescription,
SystemPrompt = dialog.PresetSystemPrompt,
Color = dialog.PresetColor,
Symbol = dialog.PresetSymbol,
Tab = dialog.PresetTab,
});
}
_settings.Save();
BuildTopicButtons();
}
private void ShowCustomPresetContextMenu(Border? anchor, Services.TopicPreset preset)
{
if (anchor == null || preset.CustomId == null)
return;
var popup = new Popup
{
PlacementTarget = anchor,
Placement = PlacementMode.Bottom,
StaysOpen = false,
AllowsTransparency = true,
};
var menuBackground = TryFindResource("LauncherBackground") as Brush ?? Brushes.Black;
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var menuBorder = new Border
{
Background = menuBackground,
CornerRadius = new CornerRadius(10),
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
Padding = new Thickness(4),
MinWidth = 120,
Effect = new System.Windows.Media.Effects.DropShadowEffect
{
BlurRadius = 12,
ShadowDepth = 2,
Opacity = 0.3,
Color = Colors.Black,
},
};
var stack = new StackPanel();
var editItem = CreateContextMenuItem("\uE70F", "편집", primaryText);
editItem.MouseLeftButtonDown += (_, _) =>
{
popup.IsOpen = false;
var entry = _settings.Settings.Llm.CustomPresets.FirstOrDefault(item => item.Id == preset.CustomId);
if (entry != null)
ShowCustomPresetDialog(entry);
};
stack.Children.Add(editItem);
var deleteItem = CreateContextMenuItem("\uE74D", "삭제", new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44)));
deleteItem.MouseLeftButtonDown += (_, _) =>
{
popup.IsOpen = false;
var result = CustomMessageBox.Show(
$"'{preset.Label}' 프리셋을 삭제하시겠습니까?",
"프리셋 삭제",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result != MessageBoxResult.Yes)
return;
_settings.Settings.Llm.CustomPresets.RemoveAll(item => item.Id == preset.CustomId);
_settings.Save();
BuildTopicButtons();
};
stack.Children.Add(deleteItem);
menuBorder.Child = stack;
popup.Child = menuBorder;
popup.IsOpen = true;
}
private Border CreateContextMenuItem(string icon, string label, Brush foreground)
{
var item = new Border
{
Background = Brushes.Transparent,
CornerRadius = new CornerRadius(6),
Padding = new Thickness(10, 6, 14, 6),
Cursor = Cursors.Hand,
};
var stack = new StackPanel { Orientation = Orientation.Horizontal };
stack.Children.Add(new TextBlock
{
Text = icon,
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 13,
Foreground = foreground,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 8, 0),
});
stack.Children.Add(new TextBlock
{
Text = label,
FontSize = 13,
Foreground = foreground,
VerticalAlignment = VerticalAlignment.Center,
});
item.Child = stack;
var hoverBackground = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
item.MouseEnter += (sender, _) =>
{
if (sender is Border hovered)
hovered.Background = hoverBackground;
};
item.MouseLeave += (sender, _) =>
{
if (sender is Border hovered)
hovered.Background = Brushes.Transparent;
};
return item;
}
private Border CreateContextMenuItem(string icon, string label, Brush foreground, Brush secondaryForeground)
=> CreateContextMenuItem(icon, label, foreground);
private void SelectTopic(Services.TopicPreset preset)
{
bool hasConversation;
bool hasMessages;
lock (_convLock)
{
hasConversation = _currentConversation != null;
hasMessages = _currentConversation?.Messages.Count > 0;
}
var hasInput = !string.IsNullOrEmpty(InputBox.Text);
if (!hasConversation)
StartNewConversation();
lock (_convLock)
{
if (_currentConversation == null)
return;
var session = ChatSession;
if (session != null)
{
_currentConversation = session.UpdateConversationMetadata(_activeTab, conversation =>
{
conversation.SystemCommand = preset.SystemPrompt;
conversation.Category = preset.Category;
}, _storage);
}
else
{
_currentConversation.SystemCommand = preset.SystemPrompt;
_currentConversation.Category = preset.Category;
}
}
UpdateCategoryLabel();
SaveConversationSettings();
RefreshConversationList();
UpdateSelectedPresetGuide();
if (EmptyState != null)
EmptyState.Visibility = Visibility.Collapsed;
InputBox.Focus();
if (!string.IsNullOrEmpty(preset.Placeholder))
{
_promptCardPlaceholder = preset.Placeholder;
if (!hasMessages && !hasInput)
ShowPlaceholder();
}
if (hasMessages || hasInput)
ShowToast($"프리셋 변경: {preset.Label}");
if (_activeTab == "Cowork")
BuildBottomBar();
}
}