diff --git a/README.md b/README.md index 1c959d1..2f150fa 100644 --- a/README.md +++ b/README.md @@ -1188,3 +1188,6 @@ MIT License - timeline 조립 helper를 [ChatWindow.TimelinePresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs) 로 분리했다. `RenderMessages()`가 직접 처리하던 visible 메시지 필터링, execution event 노출 집계, timestamp/order 기반 timeline action 조립을 helper 메서드로 옮겨 메인 렌더 루프를 더 단순화했다. - 업데이트: 2026-04-06 10:44 (KST) - timeline presentation 정리를 이어서 진행했다. [ChatWindow.TimelinePresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.TimelinePresentation.cs) 에 `CreateTimelineLoadMoreCard`, `ToAgentEvent`, `IsCompactionMetaMessage`, `CreateCompactionMetaCard`까지 옮겨 `RenderMessages()` 주변의 timeline helper를 한 파일로 모았다. +- 업데이트: 2026-04-06 10:56 (KST) + - 대화 목록 관리 interaction을 [ChatWindow.ConversationManagementPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.ConversationManagementPresentation.cs) 로 분리했다. 제목 인라인 편집 `EnterTitleEditMode(...)` 와 대화 메뉴 `ShowConversationMenu(...)`가 메인 [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 밖으로 이동해, 고정/이름 변경/카테고리 변경/삭제 같은 목록 관리 UI 책임도 별도 presentation surface에서 다루게 정리했다. + - 이 단계까지 완료된 구조 개선은 상태선/권한/도구 결과 카탈로그화, inline ask/plan 분리, footer/Git/preset/list/message/timeline 분리, 그리고 conversation management 분리까지다. 이제 남은 건 큰 구조 개선이 아니라 개별 surface polish와 후속 UX 고도화 수준이다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 3e85b4f..7e087cd 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4929,3 +4929,5 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - Document update: 2026-04-06 10:36 (KST) - This keeps `RenderMessages()` closer to a simple orchestration loop and reduces mixed responsibilities inside the main chat window file. - Document update: 2026-04-06 10:44 (KST) - Continued timeline presentation cleanup by moving `CreateTimelineLoadMoreCard`, `ToAgentEvent`, `IsCompactionMetaMessage`, and `CreateCompactionMetaCard` into `ChatWindow.TimelinePresentation.cs`. - Document update: 2026-04-06 10:44 (KST) - This consolidates timeline-related helpers in one place and leaves the main chat window file with less transcript-specific rendering logic around `RenderMessages()`. +- Document update: 2026-04-06 10:56 (KST) - Split conversation-management interactions out of `ChatWindow.xaml.cs` into `ChatWindow.ConversationManagementPresentation.cs`. Inline title editing and the conversation action popup (pin/unpin, rename, category change, delete) now live in a dedicated presentation partial. +- Document update: 2026-04-06 10:56 (KST) - With this pass, the remaining large structure-improvement track is effectively complete; follow-up work is now mostly UX polish and surface-level tuning rather than further decomposition of the main chat window orchestration file. diff --git a/src/AxCopilot/Views/ChatWindow.ConversationManagementPresentation.cs b/src/AxCopilot/Views/ChatWindow.ConversationManagementPresentation.cs new file mode 100644 index 0000000..0eea7cd --- /dev/null +++ b/src/AxCopilot/Views/ChatWindow.ConversationManagementPresentation.cs @@ -0,0 +1,433 @@ +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; + } +} diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index e91faf7..a71222a 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -2084,416 +2084,6 @@ public partial class ChatWindow : Window // ─── 대화 목록 ──────────────────────────────────────────────────────── - // ─── 대화 제목 인라인 편집 ──────────────────────────────────────────── - - private void EnterTitleEditMode(TextBlock titleTb, string conversationId, Brush titleColor) - { - try - { - // titleTb가 이미 부모에서 분리된 경우(편집 중) 무시 - 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; - // editBox가 아직 parent에 있는지 확인 후 교체 - 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) - { - 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 Border b && b.Child is Grid g) - { - foreach (UIElement gc in g.Children) - { - if (gc is StackPanel sp && sp.Children.Count > 0 && sp.Children[0] is TextBlock tb) - { - // title과 매칭 - if (conv != null && tb.Text == conv.Title) - { - var titleColor = TryFindResource("PrimaryText") as Brush ?? Brushes.White; - EnterTitleEditMode(tb, conversationId, titleColor); - return; - } - } - } - } - } - })); - - // Cowork/Code 탭: 작업 유형 읽기 전용 표시 - if ((_activeTab == "Cowork" || _activeTab == "Code") && conv != null) - { - var catKey = conv.Category ?? ChatCategory.General; - // ChatCategory 또는 프리셋에서 아이콘/라벨 검색 - 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); - } - - // Chat 탭만 분류 변경 표시 (Cowork/Code 탭은 분류 불필요) - var showCategorySection = _activeTab == "Chat"; - - if (showCategorySection) - { - 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) - { - 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); - } - } // end showCategorySection - - 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; - } // ─── 검색 ────────────────────────────────────────────────────────────