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 System.Windows.Threading;
using AxCopilot.Models;
using AxCopilot.Services;
namespace AxCopilot.Views;
public partial class ChatWindow
{
// ─── 사용자 정보 ────────────────────────────────────────────────────
private void SetupUserInfo()
{
var userName = Environment.UserName;
// AD\, AD/, AD: 접두사 제거
var cleanName = userName;
foreach (var sep in new[] { '\\', '/', ':' })
{
var idx = cleanName.LastIndexOf(sep);
if (idx >= 0) cleanName = cleanName[(idx + 1)..];
}
var initial = cleanName.Length > 0 ? cleanName[..1].ToUpper() : "U";
var pcName = Environment.MachineName;
UserInitialSidebar.Text = initial;
UserInitialIconBar.Text = initial;
UserNameText.Text = cleanName;
UserPcText.Text = pcName;
BtnUserIconBar.ToolTip = $"{cleanName} ({pcName})";
}
// ─── 스크롤 동작 ──────────────────────────────────────────────────
private void MessageScroll_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
// 스크롤 가능 영역이 없으면(콘텐츠가 짧음) 항상 바닥
if (MessageScroll.ScrollableHeight <= 1)
{
_userScrolled = false;
return;
}
// 콘텐츠 크기 변경(ExtentHeightChange > 0)에 의한 스크롤은 무시 — 사용자 조작만 감지
if (Math.Abs(e.ExtentHeightChange) > 0.5)
return;
var atBottom = MessageScroll.VerticalOffset >= MessageScroll.ScrollableHeight - 40;
_userScrolled = !atBottom;
}
private void AutoScrollIfNeeded()
{
if (!_userScrolled)
SmoothScrollToEnd();
}
/// 새 응답 시작 시 강제로 하단 스크롤합니다 (사용자 스크롤 상태 리셋).
private void ForceScrollToEnd()
{
_userScrolled = false;
Dispatcher.InvokeAsync(() => SmoothScrollToEnd(), DispatcherPriority.Background);
}
/// 부드러운 자동 스크롤 — 하단으로 부드럽게 이동합니다.
private void SmoothScrollToEnd()
{
var targetOffset = MessageScroll.ScrollableHeight;
var currentOffset = MessageScroll.VerticalOffset;
var diff = targetOffset - currentOffset;
// 차이가 작으면 즉시 이동 (깜빡임 방지)
if (diff <= 60)
{
MessageScroll.ScrollToEnd();
return;
}
// 부드럽게 스크롤 (DoubleAnimation)
var animation = new DoubleAnimation
{
From = currentOffset,
To = targetOffset,
Duration = TimeSpan.FromMilliseconds(200),
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut },
};
animation.Completed += (_, _) => MessageScroll.ScrollToVerticalOffset(targetOffset);
// ScrollViewer에 직접 애니메이션을 적용할 수 없으므로 타이머 기반으로 보간
var startTime = DateTime.UtcNow;
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(16) }; // ~60fps
EventHandler tickHandler = null!;
tickHandler = (_, _) =>
{
var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds;
var progress = Math.Min(elapsed / 200.0, 1.0);
var eased = 1.0 - Math.Pow(1.0 - progress, 3);
var offset = currentOffset + diff * eased;
MessageScroll.ScrollToVerticalOffset(offset);
if (progress >= 1.0)
{
timer.Stop();
timer.Tick -= tickHandler;
}
};
timer.Tick += tickHandler;
timer.Start();
}
// ─── 대화 제목 인라인 편집 ──────────────────────────────────────────
private void ChatTitle_MouseDown(object sender, MouseButtonEventArgs e)
{
lock (_convLock)
{
if (_currentConversation == null) return;
}
ChatTitle.Visibility = Visibility.Collapsed;
ChatTitleEdit.Text = ChatTitle.Text;
ChatTitleEdit.Visibility = Visibility.Visible;
ChatTitleEdit.Focus();
ChatTitleEdit.SelectAll();
}
private void ChatTitleEdit_LostFocus(object sender, RoutedEventArgs e) => CommitTitleEdit();
private void ChatTitleEdit_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter) { CommitTitleEdit(); e.Handled = true; }
if (e.Key == Key.Escape) { CancelTitleEdit(); e.Handled = true; }
}
private void CommitTitleEdit()
{
var newTitle = ChatTitleEdit.Text.Trim();
ChatTitleEdit.Visibility = Visibility.Collapsed;
ChatTitle.Visibility = Visibility.Visible;
if (string.IsNullOrEmpty(newTitle)) return;
lock (_convLock)
{
if (_currentConversation == null) return;
_currentConversation.Title = newTitle;
}
ChatTitle.Text = newTitle;
try
{
ChatConversation conv;
lock (_convLock) conv = _currentConversation!;
_storage.Save(conv);
}
catch (Exception) { /* 대화 저장 실패 — UI 차단 방지 */ }
RefreshConversationList();
}
private void CancelTitleEdit()
{
ChatTitleEdit.Visibility = Visibility.Collapsed;
ChatTitle.Visibility = Visibility.Visible;
}
// ─── 카테고리 드롭다운 ──────────────────────────────────────────────
private void BtnCategoryDrop_Click(object sender, RoutedEventArgs e)
{
var borderBrush = ThemeResourceHelper.Border(this);
var primaryText = ThemeResourceHelper.Primary(this);
var secondaryText = ThemeResourceHelper.Secondary(this);
var hoverBg = ThemeResourceHelper.HoverBg(this);
var accentBrush = ThemeResourceHelper.Accent(this);
var (popup, stack) = PopupMenuHelper.Create(BtnCategoryDrop, this, PlacementMode.Bottom, minWidth: 180);
popup.VerticalOffset = 4;
Border CreateCatItem(string icon, string text, Brush iconColor, bool isSelected, 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) });
g.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(20) });
var iconTb = new TextBlock
{
Text = icon, FontFamily = ThemeResourceHelper.SegoeMdl2,
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);
if (isSelected)
{
var check = CreateSimpleCheck(accentBrush, 14);
Grid.SetColumn(check, 2);
g.Children.Add(check);
}
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 CreateSep() => new()
{
Height = 1, Background = borderBrush, Opacity = 0.3, Margin = new Thickness(8, 4, 8, 4),
};
// 전체 보기
var allLabel = _activeTab switch
{
"Cowork" => "모든 작업",
"Code" => "모든 작업",
_ => "모든 주제",
};
stack.Children.Add(CreateCatItem("\uE8BD", allLabel, secondaryText,
string.IsNullOrEmpty(_selectedCategory),
() => { _selectedCategory = ""; UpdateCategoryLabel(); RefreshConversationList(); }));
stack.Children.Add(CreateSep());
if (_activeTab == "Cowork" || _activeTab == "Code")
{
// 코워크/코드: 프리셋 카테고리 기반 필터
var presets = Services.PresetService.GetByTabWithCustom(_activeTab, Llm.CustomPresets);
var seen = new HashSet();
foreach (var p in presets)
{
if (p.IsCustom) continue; // 커스텀은 별도 그룹
if (!seen.Add(p.Category)) continue;
var capturedCat = p.Category;
stack.Children.Add(CreateCatItem(p.Symbol, p.Label, BrushFromHex(p.Color),
_selectedCategory == capturedCat,
() => { _selectedCategory = capturedCat; UpdateCategoryLabel(); RefreshConversationList(); }));
}
// 커스텀 프리셋 통합 필터
if (presets.Any(p => p.IsCustom))
{
stack.Children.Add(CreateSep());
stack.Children.Add(CreateCatItem("\uE710", "커스텀 프리셋", secondaryText,
_selectedCategory == "__custom__",
() => { _selectedCategory = "__custom__"; UpdateCategoryLabel(); RefreshConversationList(); }));
}
}
else
{
// Chat: 기존 ChatCategory 기반
foreach (var (key, label, symbol, color) in ChatCategory.All)
{
var capturedKey = key;
stack.Children.Add(CreateCatItem(symbol, label, BrushFromHex(color),
_selectedCategory == capturedKey,
() => { _selectedCategory = capturedKey; UpdateCategoryLabel(); RefreshConversationList(); }));
}
// 커스텀 프리셋 통합 필터 (Chat)
var chatCustom = Llm.CustomPresets.Where(c => c.Tab == "Chat").ToList();
if (chatCustom.Count > 0)
{
stack.Children.Add(CreateSep());
stack.Children.Add(CreateCatItem("\uE710", "커스텀 프리셋", secondaryText,
_selectedCategory == "__custom__",
() => { _selectedCategory = "__custom__"; UpdateCategoryLabel(); RefreshConversationList(); }));
}
}
popup.IsOpen = true;
}
private void UpdateCategoryLabel()
{
if (string.IsNullOrEmpty(_selectedCategory))
{
CategoryLabel.Text = _activeTab switch { "Cowork" or "Code" => "모든 작업", _ => "모든 주제" };
CategoryIcon.Text = "\uE8BD";
}
else if (_selectedCategory == "__custom__")
{
CategoryLabel.Text = "커스텀 프리셋";
CategoryIcon.Text = "\uE710";
}
else
{
// ChatCategory에서 찾기
foreach (var (key, label, symbol, _) in ChatCategory.All)
{
if (key == _selectedCategory)
{
CategoryLabel.Text = label;
CategoryIcon.Text = symbol;
return;
}
}
// 프리셋 카테고리에서 찾기 (Cowork/Code)
var presets = Services.PresetService.GetByTabWithCustom(_activeTab, Llm.CustomPresets);
var match = presets.FirstOrDefault(p => p.Category == _selectedCategory);
if (match != null)
{
CategoryLabel.Text = match.Label;
CategoryIcon.Text = match.Symbol;
}
else
{
CategoryLabel.Text = _selectedCategory;
CategoryIcon.Text = "\uE8BD";
}
}
}
// ─── 창 컨트롤 ──────────────────────────────────────────────────────
// WindowChrome의 CaptionHeight가 드래그를 처리하므로 별도 핸들러 불필요
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
var source = System.Windows.Interop.HwndSource.FromHwnd(
new System.Windows.Interop.WindowInteropHelper(this).Handle);
source?.AddHook(WndProc);
}
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
// WM_GETMINMAXINFO — 최대화 시 작업 표시줄 영역 확보
if (msg == 0x0024)
{
var screen = System.Windows.Forms.Screen.FromHandle(hwnd);
var workArea = screen.WorkingArea;
var monitor = screen.Bounds;
var source = System.Windows.Interop.HwndSource.FromHwnd(hwnd);
// MINMAXINFO: ptReserved(0,4) ptMaxSize(8,12) ptMaxPosition(16,20) ptMinTrackSize(24,28) ptMaxTrackSize(32,36)
System.Runtime.InteropServices.Marshal.WriteInt32(lParam, 8, workArea.Width); // ptMaxSize.cx
System.Runtime.InteropServices.Marshal.WriteInt32(lParam, 12, workArea.Height); // ptMaxSize.cy
System.Runtime.InteropServices.Marshal.WriteInt32(lParam, 16, workArea.Left - monitor.Left); // ptMaxPosition.x
System.Runtime.InteropServices.Marshal.WriteInt32(lParam, 20, workArea.Top - monitor.Top); // ptMaxPosition.y
handled = true;
}
return IntPtr.Zero;
}
private void BtnMinimize_Click(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
private void BtnMaximize_Click(object sender, RoutedEventArgs e)
{
WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
MaximizeIcon.Text = WindowState == WindowState.Maximized ? "\uE923" : "\uE739"; // 복원/최대화 아이콘
}
private void BtnClose_Click(object sender, RoutedEventArgs e) => Close();
}