AX Agent 대화 목록 관리 프레젠테이션 분리 및 구조 개선 마감\n\n- ChatWindow에서 제목 인라인 편집과 대화 관리 팝업 렌더를 ConversationManagementPresentation partial로 분리\n- 고정, 이름 변경, 카테고리 변경, 삭제 등 대화 목록 관리 상호작용을 메인 창 orchestration 코드 밖으로 이동\n- README와 DEVELOPMENT 문서에 구조 개선 완료 범위와 남은 작업 수준을 2026-04-06 10:56 (KST) 기준으로 반영\n- dotnet build 검증 경고 0, 오류 0 확인
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
This commit is contained in:
@@ -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 메서드로 옮겨 메인 렌더 루프를 더 단순화했다.
|
- 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)
|
- 업데이트: 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를 한 파일로 모았다.
|
- 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 고도화 수준이다.
|
||||||
|
|||||||
@@ -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: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) - 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: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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── 검색 ────────────────────────────────────────────────────────────
|
// ─── 검색 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user