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 EnterTitleEditMode(TextBlock titleTb, string conversationId, Brush titleColor) { try { var parent = titleTb.Parent as StackPanel; if (parent == null) return; var idx = parent.Children.IndexOf(titleTb); if (idx < 0) return; var editBox = new TextBox { Text = titleTb.Text, FontSize = 12.5, Foreground = titleColor, Background = Brushes.Transparent, BorderBrush = TryFindResource("AccentColor") as Brush ?? Brushes.Blue, BorderThickness = new Thickness(0, 0, 0, 1), CaretBrush = titleColor, Padding = new Thickness(0), Margin = new Thickness(0), }; parent.Children.RemoveAt(idx); parent.Children.Insert(idx, editBox); var committed = false; void CommitEdit() { if (committed) return; committed = true; var newTitle = editBox.Text.Trim(); if (string.IsNullOrEmpty(newTitle)) newTitle = titleTb.Text; titleTb.Text = newTitle; try { var currentIdx = parent.Children.IndexOf(editBox); if (currentIdx >= 0) { parent.Children.RemoveAt(currentIdx); parent.Children.Insert(currentIdx, titleTb); } } catch { } var conv = _storage.Load(conversationId); if (conv != null) { conv.Title = newTitle; _storage.Save(conv); lock (_convLock) { if (_currentConversation?.Id == conversationId) { _currentConversation = ChatSession?.UpdateConversationMetadata(_activeTab, c => c.Title = newTitle, _storage) ?? _currentConversation; } } UpdateChatTitle(); } } void CancelEdit() { if (committed) return; committed = true; try { var currentIdx = parent.Children.IndexOf(editBox); if (currentIdx >= 0) { parent.Children.RemoveAt(currentIdx); parent.Children.Insert(currentIdx, titleTb); } } catch { } } editBox.KeyDown += (_, ke) => { if (ke.Key == Key.Enter) { ke.Handled = true; CommitEdit(); } if (ke.Key == Key.Escape) { ke.Handled = true; CancelEdit(); } }; editBox.LostFocus += (_, _) => CommitEdit(); editBox.Focus(); editBox.SelectAll(); } catch (Exception ex) { LogService.Error($"제목 편집 오류: {ex.Message}"); } } private void ShowConversationMenu(string conversationId) { var conv = _storage.Load(conversationId); var isPinned = conv?.Pinned ?? false; var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E)); var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(30, 255, 255, 255)); var popup = new Popup { StaysOpen = false, AllowsTransparency = true, PopupAnimation = PopupAnimation.Fade, Placement = PlacementMode.MousePoint, }; var container = new Border { Background = bgBrush, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(12), Padding = new Thickness(6), MinWidth = 200, Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3, Color = Colors.Black, }, }; var stack = new StackPanel(); Border CreateMenuItem(string icon, string text, Brush iconColor, Action onClick) { var item = new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 7, 10, 7), Margin = new Thickness(0, 1, 0, 1), Cursor = Cursors.Hand, }; var g = new Grid(); g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(24) }); g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); var iconTb = new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = iconColor, VerticalAlignment = VerticalAlignment.Center, }; Grid.SetColumn(iconTb, 0); g.Children.Add(iconTb); var textTb = new TextBlock { Text = text, FontSize = 12.5, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, }; Grid.SetColumn(textTb, 1); g.Children.Add(textTb); item.Child = g; item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; }; item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; item.MouseLeftButtonUp += (_, _) => { popup.IsOpen = false; onClick(); }; return item; } Border CreateSeparator() => new() { Height = 1, Background = borderBrush, Opacity = 0.3, Margin = new Thickness(8, 4, 8, 4), }; stack.Children.Add(CreateMenuItem( isPinned ? "\uE77A" : "\uE718", isPinned ? "고정 해제" : "상단 고정", TryFindResource("AccentColor") as Brush ?? Brushes.Blue, () => { var c = _storage.Load(conversationId); if (c == null) return; c.Pinned = !c.Pinned; _storage.Save(c); lock (_convLock) { if (_currentConversation?.Id == conversationId) { _currentConversation = ChatSession?.UpdateConversationMetadata(_activeTab, current => current.Pinned = c.Pinned, _storage) ?? _currentConversation; } } RefreshConversationList(); })); stack.Children.Add(CreateMenuItem("\uE8AC", "이름 변경", secondaryText, () => { foreach (UIElement child in ConversationPanel.Children) { if (child is not Border b || b.Child is not Grid g) continue; foreach (UIElement gc in g.Children) { if (gc is StackPanel sp && sp.Children.Count > 0 && sp.Children[0] is TextBlock tb) { if (conv != null && tb.Text == conv.Title) { var titleColor = TryFindResource("PrimaryText") as Brush ?? Brushes.White; EnterTitleEditMode(tb, conversationId, titleColor); return; } } } } })); if ((_activeTab == "Cowork" || _activeTab == "Code") && conv != null) { var catKey = conv.Category ?? ChatCategory.General; string catSymbol = "\uE8BD", catLabel = catKey, catColor = "#6B7280"; var chatCat = ChatCategory.All.FirstOrDefault(c => c.Key == catKey); if (chatCat != default && chatCat.Key != ChatCategory.General) { catSymbol = chatCat.Symbol; catLabel = chatCat.Label; catColor = chatCat.Color; } else { var preset = Services.PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets) .FirstOrDefault(p => p.Category == catKey); if (preset != null) { catSymbol = preset.Symbol; catLabel = preset.Label; catColor = preset.Color; } } stack.Children.Add(CreateSeparator()); var infoSp = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(10, 4, 10, 4) }; try { var catBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(catColor)); infoSp.Children.Add(new TextBlock { Text = catSymbol, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = catBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), }); infoSp.Children.Add(new TextBlock { Text = catLabel, FontSize = 12, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, }); } catch { infoSp.Children.Add(new TextBlock { Text = catLabel, FontSize = 12, Foreground = primaryText }); } stack.Children.Add(infoSp); } if (_activeTab == "Chat") { stack.Children.Add(CreateSeparator()); stack.Children.Add(new TextBlock { Text = "분류 변경", FontSize = 10.5, Foreground = secondaryText, Margin = new Thickness(10, 4, 0, 4), FontWeight = FontWeights.SemiBold, }); var currentCategory = conv?.Category ?? ChatCategory.General; var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; foreach (var (key, label, symbol, color) in ChatCategory.All) { var capturedKey = key; var isCurrentCat = capturedKey == currentCategory; var catItem = new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 7, 10, 7), Margin = new Thickness(0, 1, 0, 1), Cursor = Cursors.Hand, }; var catGrid = new Grid(); catGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(24) }); catGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); catGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(20) }); var catIcon = new TextBlock { Text = symbol, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = BrushFromHex(color), VerticalAlignment = VerticalAlignment.Center, }; Grid.SetColumn(catIcon, 0); catGrid.Children.Add(catIcon); var catText = new TextBlock { Text = label, FontSize = 12.5, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, FontWeight = isCurrentCat ? FontWeights.Bold : FontWeights.Normal, }; Grid.SetColumn(catText, 1); catGrid.Children.Add(catText); if (isCurrentCat) { var check = CreateSimpleCheck(accentBrush, 14); Grid.SetColumn(check, 2); catGrid.Children.Add(check); } catItem.Child = catGrid; catItem.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; }; catItem.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; catItem.MouseLeftButtonUp += (_, _) => { popup.IsOpen = false; var c = _storage.Load(conversationId); if (c == null) return; c.Category = capturedKey; var preset = Services.PresetService.GetByCategory(capturedKey); if (preset != null) { c.SystemCommand = preset.SystemPrompt; } _storage.Save(c); lock (_convLock) { if (_currentConversation?.Id == conversationId) { _currentConversation = ChatSession?.UpdateConversationMetadata(_activeTab, current => { current.Category = capturedKey; if (preset != null) { current.SystemCommand = preset.SystemPrompt; } }, _storage) ?? _currentConversation; } } bool isCurrent; lock (_convLock) { isCurrent = _currentConversation?.Id == conversationId; } if (isCurrent && preset != null && !string.IsNullOrEmpty(preset.Placeholder)) { _promptCardPlaceholder = preset.Placeholder; UpdateWatermarkVisibility(); if (string.IsNullOrEmpty(InputBox.Text)) { InputWatermark.Text = preset.Placeholder; InputWatermark.Visibility = Visibility.Visible; } } else if (isCurrent) { ClearPromptCardPlaceholder(); } RefreshConversationList(); }; stack.Children.Add(catItem); } } stack.Children.Add(CreateSeparator()); stack.Children.Add(CreateMenuItem("\uE74D", "이 대화 삭제", Brushes.IndianRed, () => { var result = CustomMessageBox.Show("이 대화를 삭제하시겠습니까?", "대화 삭제", MessageBoxButton.YesNo, MessageBoxImage.Question); if (result != MessageBoxResult.Yes) return; _storage.Delete(conversationId); lock (_convLock) { if (_currentConversation?.Id == conversationId) { _currentConversation = null; MessagePanel.Children.Clear(); EmptyState.Visibility = Visibility.Visible; UpdateChatTitle(); } } RefreshConversationList(); })); container.Child = stack; popup.Child = container; popup.IsOpen = true; } }