diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index ca01b2c..dbd5232 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4919,3 +4919,5 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - Document update: 2026-04-06 09:44 (KST) - At this point the completed structure-improvement items are: status presentation cataloging, permission/tool-result catalog enrichment, permission UI cleanup, and ask/plan renderer separation. The remaining larger tracks are footer/composer work-bar refinement and enforcing the regression prompt ritual in day-to-day development. - Document update: 2026-04-06 09:58 (KST) - Split Git branch popup assembly and footer-adjacent summary helpers out of `ChatWindow.FooterPresentation.cs` into `ChatWindow.GitBranchPresentation.cs`. The footer presentation partial now focuses on folder-bar state and selected-preset guide synchronization instead of mixed popup rendering. - Document update: 2026-04-06 09:58 (KST) - Rewrote `docs/AX_AGENT_REGRESSION_PROMPTS.md` into a repeatable regression ritual with explicit failure classes (`blank-reply`, `duplicate-banner`, `bad-approval-flow`, `queue-drift`, `restore-drift`, `status-noise`) and required prompt bundles per change area so runtime/transcript work can be validated consistently. +- Document update: 2026-04-06 10:07 (KST) - Split topic preset rendering and selection flow out of `ChatWindow.xaml.cs` into `ChatWindow.TopicPresetPresentation.cs`. Preset card creation, custom preset dialogs/context menus, and `SelectTopic(...)` metadata application now live in a dedicated partial. +- Document update: 2026-04-06 10:07 (KST) - This keeps the main chat window more orchestration-focused and narrows the remaining maintainability work to small follow-up polish rather than any large structural split. diff --git a/docs/claw-code-parity-plan.md b/docs/claw-code-parity-plan.md index 33d178f..5dfd472 100644 --- a/docs/claw-code-parity-plan.md +++ b/docs/claw-code-parity-plan.md @@ -17,6 +17,8 @@ - Updated: 2026-04-06 09:58 (KST) - Continued the maintainability track by splitting Git branch popup and footer-adjacent summary helpers into `ChatWindow.GitBranchPresentation.cs`, leaving `ChatWindow.FooterPresentation.cs` focused on folder bar state and preset-guide sync only. - Formalized the regression ritual in `docs/AX_AGENT_REGRESSION_PROMPTS.md` by adding failure classes (`blank-reply`, `duplicate-banner`, `bad-approval-flow`, `queue-drift`, `restore-drift`, `status-noise`) and required prompt bundles per change area. +- Updated: 2026-04-06 10:07 (KST) +- Continued the maintainability track by moving topic preset rendering, custom preset context menus, and topic-selection application flow into `ChatWindow.TopicPresetPresentation.cs`. This reduces mixed preset UI logic inside `ChatWindow.xaml.cs` and keeps the main window closer to orchestration-only responsibility. ## Preserved History (Summary) - Core loop guards and post-tool verification gates are already partially implemented. diff --git a/src/AxCopilot/Views/ChatWindow.TopicPresetPresentation.cs b/src/AxCopilot/Views/ChatWindow.TopicPresetPresentation.cs new file mode 100644 index 0000000..d8abbf9 --- /dev/null +++ b/src/AxCopilot/Views/ChatWindow.TopicPresetPresentation.cs @@ -0,0 +1,523 @@ +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 +{ + /// 프리셋에서 대화 주제 버튼을 동적으로 생성합니다. + 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(); + } +} diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index 8960b7b..79024ff 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -10110,521 +10110,6 @@ public partial class ChatWindow : Window _toastHideTimer.Start(); } - // ─── 대화 주제 버튼 ────────────────────────────────────────────────── - - /// 프리셋에서 대화 주제 버튼을 동적으로 생성합니다. - private void BuildTopicButtons() - { - TopicButtonPanel.Children.Clear(); - TopicButtonPanel.Visibility = Visibility.Visible; - if (TopicPresetScrollViewer != null) - TopicPresetScrollViewer.Visibility = Visibility.Visible; - - // 탭별 EmptyState 텍스트 - 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 += (s, _) => - { - if (s is Border b) - { - b.Background = hoverBackground; - b.BorderBrush = TryFindResource("AccentColor") as Brush ?? cardBorder; - } - }; - card.MouseLeave += (s, _) => - { - if (s is Border b) - { - b.Background = normalBackground; - b.BorderBrush = cardBorder; - } - }; - } - - foreach (var preset in presets) - { - var capturedPreset = preset; - var btnColor = 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)btnColor).Color) { Opacity = 0.15 }, - HorizontalAlignment = HorizontalAlignment.Center, - Margin = new Thickness(0, 0, 0, 9), - }; - var iconTb = new TextBlock - { - Text = preset.Symbol, - FontFamily = new FontFamily("Segoe MDL2 Assets"), - FontSize = 15, - Foreground = btnColor, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - }; - iconCircle.Child = iconTb; - 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 = btnColor, - 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 += (s, e) => - { - e.Handled = true; - ShowCustomPresetContextMenu(s 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", // Edit 아이콘 - 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 addColor = BrushFromHex("#6366F1"); - 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 }; - - // + 아이콘 - var plusIcon = 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(plusIcon); - - 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(Models.CustomPresetEntry? existing = null) - { - bool isEdit = existing != null; - var dlg = 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 (dlg.ShowDialog() == true) - { - if (isEdit) - { - existing!.Label = dlg.PresetName; - existing.Description = dlg.PresetDescription; - existing.SystemPrompt = dlg.PresetSystemPrompt; - existing.Color = dlg.PresetColor; - existing.Symbol = dlg.PresetSymbol; - existing.Tab = dlg.PresetTab; - } - else - { - _settings.Settings.Llm.CustomPresets.Add(new Models.CustomPresetEntry - { - Label = dlg.PresetName, - Description = dlg.PresetDescription, - SystemPrompt = dlg.PresetSystemPrompt, - Color = dlg.PresetColor, - Symbol = dlg.PresetSymbol, - Tab = dlg.PresetTab, - }); - } - _settings.Save(); - BuildTopicButtons(); - } - } - - /// 커스텀 프리셋 우클릭 컨텍스트 메뉴를 표시합니다. - private void ShowCustomPresetContextMenu(Border? anchor, Services.TopicPreset preset) - { - if (anchor == null || preset.CustomId == null) return; - - var popup = new System.Windows.Controls.Primitives.Popup - { - PlacementTarget = anchor, - Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom, - StaysOpen = false, - AllowsTransparency = true, - }; - - var menuBg = 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 = menuBg, - 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, secondaryText); - editItem.MouseLeftButtonDown += (_, _) => - { - popup.IsOpen = false; - var entry = _settings.Settings.Llm.CustomPresets.FirstOrDefault(c => c.Id == preset.CustomId); - if (entry != null) ShowCustomPresetDialog(entry); - }; - stack.Children.Add(editItem); - - // 삭제 버튼 - var deleteItem = CreateContextMenuItem("\uE74D", "삭제", new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44)), secondaryText); - deleteItem.MouseLeftButtonDown += (_, _) => - { - popup.IsOpen = false; - var result = CustomMessageBox.Show( - $"'{preset.Label}' 프리셋을 삭제하시겠습니까?", - "프리셋 삭제", MessageBoxButton.YesNo, MessageBoxImage.Question); - if (result == MessageBoxResult.Yes) - { - _settings.Settings.Llm.CustomPresets.RemoveAll(c => c.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 fg, Brush secondaryFg) - { - var item = new Border - { - Background = Brushes.Transparent, - CornerRadius = new CornerRadius(6), - Padding = new Thickness(10, 6, 14, 6), - Cursor = Cursors.Hand, - }; - - var sp = new StackPanel { Orientation = Orientation.Horizontal }; - sp.Children.Add(new TextBlock - { - Text = icon, - FontFamily = new FontFamily("Segoe MDL2 Assets"), - FontSize = 13, Foreground = fg, - VerticalAlignment = VerticalAlignment.Center, - Margin = new Thickness(0, 0, 8, 0), - }); - sp.Children.Add(new TextBlock - { - Text = label, FontSize = 13, Foreground = fg, - VerticalAlignment = VerticalAlignment.Center, - }); - item.Child = sp; - - var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent; - item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; }; - item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; - - return item; - } - - /// 대화 주제 선택 — 프리셋 시스템 프롬프트 + 카테고리 적용. - private void SelectTopic(Services.TopicPreset preset) - { - bool hasMessages; - bool hasConversation; - lock (_convLock) - { - hasConversation = _currentConversation != null; - hasMessages = _currentConversation?.Messages.Count > 0; - } - - // 입력란에 텍스트가 있으면 기존 대화를 유지 (입력 내용 보존) - bool hasInput = !string.IsNullOrEmpty(InputBox.Text); - bool keepConversation = hasConversation; - - if (!keepConversation) - { - // 현재 대화가 아예 없는 경우에만 새 대화 시작 - StartNewConversation(); - keepConversation = true; - } - - // 프리셋 적용 (기존 대화에도 프리셋 변경 가능) - lock (_convLock) - { - if (_currentConversation != null) - { - var session = ChatSession; - if (session != null) - { - _currentConversation = session.UpdateConversationMetadata(_activeTab, c => - { - c.SystemCommand = preset.SystemPrompt; - c.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}"); - - // Cowork 탭: 하단 바 갱신 - if (_activeTab == "Cowork") - BuildBottomBar(); - } - - - /// 선택된 디자인 무드 키 (HtmlSkill에서 사용). private string _selectedMood = null!; // Loaded 이벤트에서 초기화 private string _selectedLanguage = "auto"; // Code 탭 개발 언어