[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:
2026-04-03 19:21:52 +09:00
parent 0c997f0149
commit 6448451d78
9 changed files with 4592 additions and 4506 deletions

View 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 ?? "";
}
}
}