[Phase 40] ChatWindow 2차 파셜 클래스 분할 (94.5% 감소)
4,767줄 ChatWindow.xaml.cs를 7개 파셜 파일로 추가 분할 메인 파일: 4,767줄 → 262줄 (94.5% 감소) 전체 ChatWindow 파셜 파일: 15개 - ChatWindow.Controls.cs (595줄): 사용자정보, 스크롤, 제목편집, 탭전환 - ChatWindow.WorkFolder.cs (359줄): 작업폴더, 폴더 설정 - ChatWindow.PermissionMenu.cs (498줄): 권한, 파일첨부, 사이드바 - ChatWindow.ConversationList.cs (747줄): 대화목록, 제목편집, 검색 - ChatWindow.Sending.cs (720줄): 전송, 편집모드, 타이머 - ChatWindow.HelpCommands.cs (157줄): /help 도움말 - ChatWindow.ResponseHandling.cs (1,494줄): 응답재생성, 스트리밍, 토스트 - 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
747
src/AxCopilot/Views/ChatWindow.ConversationList.cs
Normal file
747
src/AxCopilot/Views/ChatWindow.ConversationList.cs
Normal file
@@ -0,0 +1,747 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
// ─── 대화 목록 ────────────────────────────────────────────────────────
|
||||
|
||||
public void RefreshConversationList()
|
||||
{
|
||||
var metas = _storage.LoadAllMeta();
|
||||
// 프리셋 카테고리 → 아이콘/색상 매핑 (ChatCategory에 없는 코워크/코드 카테고리 지원)
|
||||
var allPresets = Services.PresetService.GetByTabWithCustom("Cowork", Llm.CustomPresets)
|
||||
.Concat(Services.PresetService.GetByTabWithCustom("Code", Llm.CustomPresets))
|
||||
.Concat(Services.PresetService.GetByTabWithCustom("Chat", Llm.CustomPresets));
|
||||
var presetMap = new Dictionary<string, (string Symbol, string Color)>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var p in allPresets)
|
||||
presetMap.TryAdd(p.Category, (p.Symbol, p.Color));
|
||||
|
||||
var items = metas.Select(c =>
|
||||
{
|
||||
var symbol = ChatCategory.GetSymbol(c.Category);
|
||||
var color = ChatCategory.GetColor(c.Category);
|
||||
// ChatCategory 기본값이면 프리셋에서 검색
|
||||
if (symbol == "\uE8BD" && color == "#6B7280" && c.Category != ChatCategory.General)
|
||||
{
|
||||
if (presetMap.TryGetValue(c.Category, out var pm))
|
||||
{
|
||||
symbol = pm.Symbol;
|
||||
color = pm.Color;
|
||||
}
|
||||
}
|
||||
return new ConversationMeta
|
||||
{
|
||||
Id = c.Id,
|
||||
Title = c.Title,
|
||||
Pinned = c.Pinned,
|
||||
Category = c.Category,
|
||||
Symbol = symbol,
|
||||
ColorHex = color,
|
||||
Tab = string.IsNullOrEmpty(c.Tab) ? "Chat" : c.Tab,
|
||||
UpdatedAtText = FormatDate(c.UpdatedAt),
|
||||
UpdatedAt = c.UpdatedAt,
|
||||
Preview = c.Preview ?? "",
|
||||
ParentId = c.ParentId,
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
// 탭 필터 — 현재 활성 탭의 대화만 표시
|
||||
items = items.Where(i => i.Tab == _activeTab).ToList();
|
||||
|
||||
// 카테고리 필터 적용
|
||||
if (_selectedCategory == "__custom__")
|
||||
{
|
||||
// 커스텀 프리셋으로 만든 대화만 표시
|
||||
var customCats = Llm.CustomPresets
|
||||
.Select(c => $"custom_{c.Id}").ToHashSet();
|
||||
items = items.Where(i => customCats.Contains(i.Category)).ToList();
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(_selectedCategory))
|
||||
items = items.Where(i => i.Category == _selectedCategory).ToList();
|
||||
|
||||
// 검색 필터 (제목 + 내용 미리보기)
|
||||
var search = SearchBox?.Text?.Trim() ?? "";
|
||||
if (!string.IsNullOrEmpty(search))
|
||||
items = items.Where(i =>
|
||||
i.Title.Contains(search, StringComparison.OrdinalIgnoreCase) ||
|
||||
i.Preview.Contains(search, StringComparison.OrdinalIgnoreCase)
|
||||
).ToList();
|
||||
|
||||
RenderConversationList(items);
|
||||
}
|
||||
|
||||
private const int ConversationPageSize = 50;
|
||||
private List<ConversationMeta>? _pendingConversations;
|
||||
|
||||
private void RenderConversationList(List<ConversationMeta> items)
|
||||
{
|
||||
ConversationPanel.Children.Clear();
|
||||
_pendingConversations = null;
|
||||
|
||||
if (items.Count == 0)
|
||||
{
|
||||
var empty = new TextBlock
|
||||
{
|
||||
Text = "대화가 없습니다",
|
||||
FontSize = 12,
|
||||
Foreground = (ThemeResourceHelper.Secondary(this)),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Margin = new Thickness(0, 20, 0, 0)
|
||||
};
|
||||
ConversationPanel.Children.Add(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
// 오늘 / 이전 그룹 분리
|
||||
var today = DateTime.Today;
|
||||
var todayItems = items.Where(i => i.UpdatedAt.Date == today).ToList();
|
||||
var olderItems = items.Where(i => i.UpdatedAt.Date < today).ToList();
|
||||
|
||||
var allOrdered = new List<(string Group, ConversationMeta Item)>();
|
||||
foreach (var item in todayItems) allOrdered.Add(("오늘", item));
|
||||
foreach (var item in olderItems) allOrdered.Add(("이전", item));
|
||||
|
||||
// 첫 페이지만 렌더링
|
||||
var firstPage = allOrdered.Take(ConversationPageSize).ToList();
|
||||
string? lastGroup = null;
|
||||
foreach (var (group, item) in firstPage)
|
||||
{
|
||||
if (group != lastGroup) { AddGroupHeader(group); lastGroup = group; }
|
||||
AddConversationItem(item);
|
||||
}
|
||||
|
||||
// 나머지가 있으면 "더 보기" 버튼
|
||||
if (allOrdered.Count > ConversationPageSize)
|
||||
{
|
||||
_pendingConversations = items;
|
||||
AddLoadMoreButton(allOrdered.Count - ConversationPageSize);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddLoadMoreButton(int remaining)
|
||||
{
|
||||
var secondaryText = ThemeResourceHelper.Secondary(this);
|
||||
var accentBrush = ThemeResourceHelper.Accent(this);
|
||||
var btn = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Cursor = Cursors.Hand,
|
||||
Padding = new Thickness(8, 10, 8, 10),
|
||||
Margin = new Thickness(6, 4, 6, 4),
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
};
|
||||
var sp = new StackPanel { HorizontalAlignment = HorizontalAlignment.Center };
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"더 보기 ({remaining}개 남음)",
|
||||
FontSize = 12,
|
||||
Foreground = accentBrush,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
btn.Child = sp;
|
||||
btn.MouseEnter += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x12, 0xFF, 0xFF, 0xFF)); };
|
||||
btn.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||||
btn.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
// 전체 목록 렌더링
|
||||
if (_pendingConversations != null)
|
||||
{
|
||||
var all = _pendingConversations;
|
||||
_pendingConversations = null;
|
||||
ConversationPanel.Children.Clear();
|
||||
|
||||
var today = DateTime.Today;
|
||||
var todayItems = all.Where(i => i.UpdatedAt.Date == today).ToList();
|
||||
var olderItems = all.Where(i => i.UpdatedAt.Date < today).ToList();
|
||||
|
||||
if (todayItems.Count > 0)
|
||||
{
|
||||
AddGroupHeader("오늘");
|
||||
foreach (var item in todayItems) AddConversationItem(item);
|
||||
}
|
||||
if (olderItems.Count > 0)
|
||||
{
|
||||
AddGroupHeader("이전");
|
||||
foreach (var item in olderItems) AddConversationItem(item);
|
||||
}
|
||||
}
|
||||
};
|
||||
ConversationPanel.Children.Add(btn);
|
||||
}
|
||||
|
||||
private void AddGroupHeader(string text)
|
||||
{
|
||||
var header = new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
FontSize = 11,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = (ThemeResourceHelper.Secondary(this)),
|
||||
Margin = new Thickness(8, 12, 0, 4)
|
||||
};
|
||||
ConversationPanel.Children.Add(header);
|
||||
}
|
||||
|
||||
private void AddConversationItem(ConversationMeta item)
|
||||
{
|
||||
var isSelected = false;
|
||||
lock (_convLock)
|
||||
isSelected = _currentConversation?.Id == item.Id;
|
||||
|
||||
var isBranch = !string.IsNullOrEmpty(item.ParentId);
|
||||
var border = new Border
|
||||
{
|
||||
Background = isSelected
|
||||
? new SolidColorBrush(Color.FromArgb(0x30, 0x4B, 0x5E, 0xFC))
|
||||
: Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(10, 8, 10, 8),
|
||||
Margin = isBranch ? new Thickness(16, 1, 0, 1) : new Thickness(0, 1, 0, 1),
|
||||
Cursor = Cursors.Hand
|
||||
};
|
||||
|
||||
var grid = new Grid();
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(28) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
// 카테고리 아이콘 (고정 시 핀 아이콘, 그 외 카테고리 색상)
|
||||
Brush iconBrush;
|
||||
if (item.Pinned)
|
||||
iconBrush = Brushes.Orange;
|
||||
else
|
||||
{
|
||||
try { iconBrush = ThemeResourceHelper.HexBrush(item.ColorHex); }
|
||||
catch (Exception) { iconBrush = ThemeResourceHelper.Accent(this); }
|
||||
}
|
||||
var iconText = item.Pinned ? "\uE718" : !string.IsNullOrEmpty(item.ParentId) ? "\uE8A5" : item.Symbol;
|
||||
if (!string.IsNullOrEmpty(item.ParentId)) iconBrush = new SolidColorBrush(Color.FromRgb(0x8B, 0x5C, 0xF6)); // 분기: 보라색
|
||||
var icon = new TextBlock
|
||||
{
|
||||
Text = iconText,
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 13,
|
||||
Foreground = iconBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
};
|
||||
Grid.SetColumn(icon, 0);
|
||||
grid.Children.Add(icon);
|
||||
|
||||
// 제목 + 날짜 (선택 시 약간 밝게)
|
||||
var titleColor = ThemeResourceHelper.Primary(this);
|
||||
var dateColor = ThemeResourceHelper.HintFg(this);
|
||||
|
||||
var stack = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
|
||||
var title = new TextBlock
|
||||
{
|
||||
Text = item.Title,
|
||||
FontSize = 12.5,
|
||||
Foreground = titleColor,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis
|
||||
};
|
||||
var date = new TextBlock
|
||||
{
|
||||
Text = item.UpdatedAtText,
|
||||
FontSize = 10,
|
||||
Foreground = dateColor,
|
||||
Margin = new Thickness(0, 2, 0, 0)
|
||||
};
|
||||
stack.Children.Add(title);
|
||||
stack.Children.Add(date);
|
||||
Grid.SetColumn(stack, 1);
|
||||
grid.Children.Add(stack);
|
||||
|
||||
// 카테고리 변경 버튼 (호버 시 표시)
|
||||
var catBtn = new Button
|
||||
{
|
||||
Content = new TextBlock
|
||||
{
|
||||
Text = "\uE70F", // Edit
|
||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
FontSize = 10,
|
||||
Foreground = (ThemeResourceHelper.Secondary(this))
|
||||
},
|
||||
Background = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
Cursor = Cursors.Hand,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Visibility = Visibility.Collapsed,
|
||||
Padding = new Thickness(4),
|
||||
ToolTip = _activeTab == "Cowork" ? "작업 유형" : "대화 주제 변경"
|
||||
};
|
||||
var capturedId = item.Id;
|
||||
catBtn.Click += (_, _) => ShowConversationMenu(capturedId);
|
||||
Grid.SetColumn(catBtn, 2);
|
||||
grid.Children.Add(catBtn);
|
||||
|
||||
// 선택 시 좌측 액센트 바
|
||||
if (isSelected)
|
||||
{
|
||||
border.BorderBrush = ThemeResourceHelper.Accent(this);
|
||||
border.BorderThickness = new Thickness(2, 0, 0, 0);
|
||||
}
|
||||
|
||||
border.Child = grid;
|
||||
|
||||
// 호버 이벤트 — 배경 + 미세 확대
|
||||
border.RenderTransformOrigin = new Point(0.5, 0.5);
|
||||
border.RenderTransform = new ScaleTransform(1, 1);
|
||||
var selectedBg = new SolidColorBrush(Color.FromArgb(0x30, 0x4B, 0x5E, 0xFC));
|
||||
border.MouseEnter += (_, _) =>
|
||||
{
|
||||
if (!isSelected)
|
||||
border.Background = new SolidColorBrush(Color.FromArgb(0x15, 0xFF, 0xFF, 0xFF));
|
||||
catBtn.Visibility = Visibility.Visible;
|
||||
var st = border.RenderTransform as ScaleTransform;
|
||||
st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
|
||||
st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
|
||||
};
|
||||
border.MouseLeave += (_, _) =>
|
||||
{
|
||||
if (!isSelected)
|
||||
border.Background = Brushes.Transparent;
|
||||
catBtn.Visibility = Visibility.Collapsed;
|
||||
var st = border.RenderTransform as ScaleTransform;
|
||||
st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
|
||||
st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
|
||||
};
|
||||
|
||||
// 클릭 — 이미 선택된 대화면 제목 편집, 아니면 대화 전환
|
||||
border.MouseLeftButtonDown += (_, _) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (isSelected)
|
||||
{
|
||||
// 이미 선택된 대화 → 제목 편집 모드
|
||||
EnterTitleEditMode(title, item.Id, titleColor);
|
||||
return;
|
||||
}
|
||||
// 스트리밍 중이면 취소
|
||||
if (_isStreaming)
|
||||
{
|
||||
_streamCts?.Cancel();
|
||||
_cursorTimer.Stop();
|
||||
_typingTimer.Stop();
|
||||
_elapsedTimer.Stop();
|
||||
_activeStreamText = null;
|
||||
_elapsedLabel = null;
|
||||
_isStreaming = false;
|
||||
}
|
||||
var conv = _storage.Load(item.Id);
|
||||
if (conv != null)
|
||||
{
|
||||
// Tab 보정
|
||||
if (string.IsNullOrEmpty(conv.Tab)) conv.Tab = _activeTab;
|
||||
lock (_convLock) _currentConversation = conv;
|
||||
_tabConversationId[_activeTab] = conv.Id;
|
||||
UpdateChatTitle();
|
||||
RenderMessages();
|
||||
RefreshConversationList();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Error($"대화 전환 오류: {ex.Message}");
|
||||
}
|
||||
};
|
||||
|
||||
// 우클릭 → 대화 관리 메뉴 바로 표시
|
||||
border.MouseRightButtonUp += (_, me) =>
|
||||
{
|
||||
me.Handled = true;
|
||||
// 선택되지 않은 대화를 우클릭하면 먼저 선택
|
||||
if (!isSelected)
|
||||
{
|
||||
var conv = _storage.Load(item.Id);
|
||||
if (conv != null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(conv.Tab)) conv.Tab = _activeTab;
|
||||
lock (_convLock) _currentConversation = conv;
|
||||
_tabConversationId[_activeTab] = conv.Id;
|
||||
UpdateChatTitle();
|
||||
RenderMessages();
|
||||
}
|
||||
}
|
||||
// Dispatcher로 지연 호출 — 마우스 이벤트 완료 후 Popup 열기
|
||||
Dispatcher.BeginInvoke(new Action(() => ShowConversationMenu(item.Id)), System.Windows.Threading.DispatcherPriority.Input);
|
||||
};
|
||||
|
||||
ConversationPanel.Children.Add(border);
|
||||
}
|
||||
|
||||
// ─── 대화 제목 인라인 편집 ────────────────────────────────────────────
|
||||
|
||||
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 = ThemeResourceHelper.Accent(this),
|
||||
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 (Exception) { /* 부모가 이미 해제된 경우 무시 */ }
|
||||
|
||||
var conv = _storage.Load(conversationId);
|
||||
if (conv != null)
|
||||
{
|
||||
conv.Title = newTitle;
|
||||
_storage.Save(conv);
|
||||
lock (_convLock)
|
||||
{
|
||||
if (_currentConversation?.Id == conversationId)
|
||||
_currentConversation.Title = newTitle;
|
||||
}
|
||||
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 (Exception) { /* 부모가 이미 해제된 경우 무시 */ }
|
||||
}
|
||||
|
||||
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 primaryText = ThemeResourceHelper.Primary(this);
|
||||
var secondaryText = ThemeResourceHelper.Secondary(this);
|
||||
var hoverBg = ThemeResourceHelper.HoverBg(this);
|
||||
|
||||
var (popup, stack) = PopupMenuHelper.Create(this, this, PlacementMode.MousePoint, minWidth: 200);
|
||||
|
||||
// 메뉴 항목 헬퍼 — PopupMenuHelper.MenuItem 래핑 (아이콘 색상 개별 지정)
|
||||
Border CreateMenuItem(string icon, string text, Brush iconColor, Action onClick)
|
||||
=> PopupMenuHelper.MenuItem(text, primaryText, hoverBg,
|
||||
() => { popup.IsOpen = false; onClick(); },
|
||||
icon: icon, iconColor: iconColor, fontSize: 12.5);
|
||||
|
||||
Border CreateSeparator() => PopupMenuHelper.Separator();
|
||||
|
||||
// 고정/해제
|
||||
stack.Children.Add(CreateMenuItem(
|
||||
isPinned ? "\uE77A" : "\uE718",
|
||||
isPinned ? "고정 해제" : "상단 고정",
|
||||
ThemeResourceHelper.Accent(this),
|
||||
() =>
|
||||
{
|
||||
var c = _storage.Load(conversationId);
|
||||
if (c != null)
|
||||
{
|
||||
c.Pinned = !c.Pinned;
|
||||
_storage.Save(c);
|
||||
lock (_convLock) { if (_currentConversation?.Id == conversationId) _currentConversation.Pinned = c.Pinned; }
|
||||
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 = ThemeResourceHelper.Primary(this);
|
||||
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, 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 = ThemeResourceHelper.HexBrush(catColor);
|
||||
infoSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = catSymbol, FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||
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 (Exception)
|
||||
{
|
||||
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 = ThemeResourceHelper.Accent(this);
|
||||
|
||||
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 = ThemeResourceHelper.SegoeMdl2,
|
||||
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.Category = capturedKey;
|
||||
if (preset != null)
|
||||
_currentConversation.SystemCommand = preset.SystemPrompt;
|
||||
}
|
||||
}
|
||||
// 현재 대화의 카테고리가 변경되면 입력 안내 문구도 갱신
|
||||
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();
|
||||
}));
|
||||
|
||||
popup.IsOpen = true;
|
||||
}
|
||||
|
||||
// ─── 검색 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void SearchBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
RefreshConversationList();
|
||||
}
|
||||
|
||||
private static string FormatDate(DateTime dt)
|
||||
{
|
||||
var diff = DateTime.Now - dt;
|
||||
if (diff.TotalMinutes < 1) return "방금 전";
|
||||
if (diff.TotalHours < 1) return $"{(int)diff.TotalMinutes}분 전";
|
||||
if (diff.TotalDays < 1) return $"{(int)diff.TotalHours}시간 전";
|
||||
if (diff.TotalDays < 7) return $"{(int)diff.TotalDays}일 전";
|
||||
return dt.ToString("MM/dd");
|
||||
}
|
||||
|
||||
private void UpdateChatTitle()
|
||||
{
|
||||
lock (_convLock)
|
||||
{
|
||||
ChatTitle.Text = _currentConversation?.Title ?? "";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user