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(); } }