[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:
595
src/AxCopilot/Views/ChatWindow.Controls.cs
Normal file
595
src/AxCopilot/Views/ChatWindow.Controls.cs
Normal file
@@ -0,0 +1,595 @@
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>새 응답 시작 시 강제로 하단 스크롤합니다 (사용자 스크롤 상태 리셋).</summary>
|
||||
private void ForceScrollToEnd()
|
||||
{
|
||||
_userScrolled = false;
|
||||
Dispatcher.InvokeAsync(() => SmoothScrollToEnd(), DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
/// <summary>부드러운 자동 스크롤 — 하단으로 부드럽게 이동합니다.</summary>
|
||||
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<string>();
|
||||
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();
|
||||
|
||||
// ─── 탭 전환 ──────────────────────────────────────────────────────────
|
||||
|
||||
private string _activeTab = "Chat";
|
||||
|
||||
private void SaveCurrentTabConversationId()
|
||||
{
|
||||
lock (_convLock)
|
||||
{
|
||||
if (_currentConversation != null && _currentConversation.Messages.Count > 0)
|
||||
{
|
||||
_tabConversationId[_activeTab] = _currentConversation.Id;
|
||||
// 탭 전환 시 현재 대화를 즉시 저장 (스트리밍 중이어도 진행 중인 내용 보존)
|
||||
try { _storage.Save(_currentConversation); } catch (Exception) { /* 대화 저장 실패 — UI 차단 방지 */ }
|
||||
}
|
||||
}
|
||||
// 탭별 마지막 대화 ID를 설정에 영속 저장 (앱 재시작 시 복원용)
|
||||
SaveLastConversations();
|
||||
}
|
||||
|
||||
/// <summary>탭 전환 전 스트리밍 중이면 즉시 중단합니다.</summary>
|
||||
private void StopStreamingIfActive()
|
||||
{
|
||||
if (!_isStreaming) return;
|
||||
// 스트리밍 중단
|
||||
_streamCts?.Cancel();
|
||||
_cursorTimer.Stop();
|
||||
_elapsedTimer.Stop();
|
||||
_typingTimer.Stop();
|
||||
StopRainbowGlow();
|
||||
HideStickyProgress();
|
||||
_activeStreamText = null;
|
||||
_elapsedLabel = null;
|
||||
_cachedStreamContent = "";
|
||||
_isStreaming = false;
|
||||
BtnSend.IsEnabled = true;
|
||||
BtnStop.Visibility = Visibility.Collapsed;
|
||||
BtnPause.Visibility = Visibility.Collapsed;
|
||||
PauseIcon.Text = "\uE769"; // 리셋
|
||||
BtnSend.Visibility = Visibility.Visible;
|
||||
_streamCts?.Dispose();
|
||||
_streamCts = null;
|
||||
SetStatusIdle();
|
||||
}
|
||||
|
||||
private void TabChat_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_activeTab == "Chat") return;
|
||||
StopStreamingIfActive();
|
||||
SaveCurrentTabConversationId();
|
||||
_activeTab = "Chat";
|
||||
_selectedCategory = ""; UpdateCategoryLabel();
|
||||
UpdateTabUI();
|
||||
UpdatePlanModeUI();
|
||||
}
|
||||
|
||||
private void TabCowork_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_activeTab == "Cowork") return;
|
||||
StopStreamingIfActive();
|
||||
SaveCurrentTabConversationId();
|
||||
_activeTab = "Cowork";
|
||||
_selectedCategory = ""; UpdateCategoryLabel();
|
||||
UpdateTabUI();
|
||||
UpdatePlanModeUI();
|
||||
}
|
||||
|
||||
private void TabCode_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_activeTab == "Code") return;
|
||||
StopStreamingIfActive();
|
||||
SaveCurrentTabConversationId();
|
||||
_activeTab = "Code";
|
||||
_selectedCategory = ""; UpdateCategoryLabel();
|
||||
UpdateTabUI();
|
||||
UpdatePlanModeUI();
|
||||
}
|
||||
|
||||
/// <summary>탭별로 마지막으로 활성화된 대화 ID를 기억.</summary>
|
||||
private readonly Dictionary<string, string?> _tabConversationId = new()
|
||||
{
|
||||
["Chat"] = null, ["Cowork"] = null, ["Code"] = null,
|
||||
};
|
||||
|
||||
private void UpdateTabUI()
|
||||
{
|
||||
// 폴더 바는 Cowork/Code 탭에서만 표시
|
||||
if (FolderBar != null)
|
||||
FolderBar.Visibility = _activeTab != "Chat" ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
// 탭별 입력 안내 문구
|
||||
if (InputWatermark != null)
|
||||
{
|
||||
InputWatermark.Text = _activeTab switch
|
||||
{
|
||||
"Cowork" => "에이전트에게 작업을 요청하세요 (파일 읽기/쓰기, 문서 생성...)",
|
||||
"Code" => "코드 관련 작업을 요청하세요...",
|
||||
_ => _promptCardPlaceholder,
|
||||
};
|
||||
}
|
||||
|
||||
// 권한 기본값 적용 (Cowork/Code 탭은 설정의 기본값 사용)
|
||||
ApplyTabDefaultPermission();
|
||||
|
||||
// 포맷/디자인 드롭다운은 Cowork 탭에서만 표시
|
||||
if (_activeTab == "Cowork")
|
||||
{
|
||||
BuildBottomBar();
|
||||
if (Llm.ShowFileBrowser && FileBrowserPanel != null)
|
||||
{
|
||||
FileBrowserPanel.Visibility = Visibility.Visible;
|
||||
BuildFileTree();
|
||||
}
|
||||
}
|
||||
else if (_activeTab == "Code")
|
||||
{
|
||||
// Code 탭: 언어 선택기 + 파일 탐색기
|
||||
BuildCodeBottomBar();
|
||||
if (Llm.ShowFileBrowser && FileBrowserPanel != null)
|
||||
{
|
||||
FileBrowserPanel.Visibility = Visibility.Visible;
|
||||
BuildFileTree();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MoodIconPanel.Children.Clear();
|
||||
if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Collapsed;
|
||||
if (FileBrowserPanel != null) FileBrowserPanel.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
// 탭별 프리셋 버튼 재구성
|
||||
BuildTopicButtons();
|
||||
|
||||
// 현재 대화를 해당 탭 대화로 전환
|
||||
SwitchToTabConversation();
|
||||
|
||||
// Cowork/Code 탭 전환 시 팁 표시
|
||||
ShowRandomTip();
|
||||
}
|
||||
|
||||
private void BtnPlanMode_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
// 3단 순환: off → auto → always → off
|
||||
Llm.PlanMode = Llm.PlanMode switch
|
||||
{
|
||||
"auto" => "always",
|
||||
"always" => "off",
|
||||
_ => "auto"
|
||||
};
|
||||
_settings.Save();
|
||||
UpdatePlanModeUI();
|
||||
}
|
||||
|
||||
private void UpdatePlanModeUI()
|
||||
{
|
||||
var planMode = Llm.PlanMode ?? "off";
|
||||
if (PlanModeValue == null) return;
|
||||
|
||||
PlanModeValue.Text = planMode switch
|
||||
{
|
||||
"auto" => "Auto",
|
||||
"always" => "Always",
|
||||
_ => "Off"
|
||||
};
|
||||
var isActive = planMode != "off";
|
||||
var activeBrush = ThemeResourceHelper.Accent(this);
|
||||
var secondaryBrush = ThemeResourceHelper.Secondary(this);
|
||||
if (PlanModeIcon != null) PlanModeIcon.Foreground = isActive ? activeBrush : secondaryBrush;
|
||||
if (PlanModeLabel != null) PlanModeLabel.Foreground = isActive ? activeBrush : secondaryBrush;
|
||||
if (BtnPlanMode != null)
|
||||
BtnPlanMode.Background = isActive
|
||||
? new SolidColorBrush(Color.FromArgb(0x1A, 0x4B, 0x5E, 0xFC))
|
||||
: Brushes.Transparent;
|
||||
}
|
||||
|
||||
private void SwitchToTabConversation()
|
||||
{
|
||||
// 이전 탭의 대화 저장
|
||||
lock (_convLock)
|
||||
{
|
||||
if (_currentConversation != null && _currentConversation.Messages.Count > 0)
|
||||
{
|
||||
try { _storage.Save(_currentConversation); } catch (Exception) { /* 대화 저장 실패 — UI 차단 방지 */ }
|
||||
}
|
||||
}
|
||||
|
||||
// 현재 탭에 기억된 대화가 있으면 복원
|
||||
var savedId = _tabConversationId.GetValueOrDefault(_activeTab);
|
||||
if (!string.IsNullOrEmpty(savedId))
|
||||
{
|
||||
var conv = _storage.Load(savedId);
|
||||
if (conv != null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(conv.Tab)) conv.Tab = _activeTab;
|
||||
lock (_convLock) _currentConversation = conv;
|
||||
MessagePanel.Children.Clear();
|
||||
foreach (var msg in conv.Messages)
|
||||
AddMessageBubble(msg.Role, msg.Content, animate: false, message: msg);
|
||||
EmptyState.Visibility = conv.Messages.Count > 0 ? Visibility.Collapsed : Visibility.Visible;
|
||||
UpdateChatTitle();
|
||||
RefreshConversationList();
|
||||
UpdateFolderBar();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 기억된 대화가 없으면 새 대화
|
||||
lock (_convLock)
|
||||
{
|
||||
_currentConversation = new ChatConversation { Tab = _activeTab };
|
||||
var workFolder = Llm.WorkFolder;
|
||||
if (!string.IsNullOrEmpty(workFolder) && _activeTab != "Chat")
|
||||
_currentConversation.WorkFolder = workFolder;
|
||||
}
|
||||
MessagePanel.Children.Clear();
|
||||
EmptyState.Visibility = Visibility.Visible;
|
||||
_attachedFiles.Clear();
|
||||
RefreshAttachedFilesUI();
|
||||
UpdateChatTitle();
|
||||
RefreshConversationList();
|
||||
UpdateFolderBar();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user