using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Animation; using AxCopilot.Services; namespace AxCopilot.Views.Controls; /// /// Phase 32-A: 우측 슬라이드인 설정 패널. /// ChatWindow 내부에서 AX Agent 설정을 인라인으로 제어합니다. /// CC의 Claude.ai 설정 패널과 동등한 UX. /// public partial class AgentSettingsPanel : UserControl { private static App? CurrentApp => System.Windows.Application.Current as App; private bool _isOpen; private bool _isLoading = true; // 초기 로드 중 이벤트 발생 방지 /// 설정 패널 열림/닫힘 상태. public bool IsOpen { get => _isOpen; set { if (_isOpen == value) return; _isOpen = value; AnimateSlide(value); } } /// 설정 변경 시 발생하는 이벤트. public event EventHandler? SettingsChanged; /// 닫기 요청 이벤트. public event EventHandler? CloseRequested; public AgentSettingsPanel() { InitializeComponent(); Visibility = Visibility.Collapsed; } /// 현재 설정값으로 UI를 초기화합니다. public void LoadFromSettings(SettingsService settings, string activeTab, Services.Agent.ToolRegistry? toolRegistry = null) { _isLoading = true; var llm = settings.Settings.Llm; TxtCurrentTab.Text = activeTab; TxtServiceName.Text = llm.Service ?? "Ollama"; TxtModelName.Text = llm.Model ?? "unknown"; TxtApiEndpoint.Text = llm.OllamaEndpoint ?? ""; SliderMaxIterations.Value = llm.MaxAgentIterations > 0 ? llm.MaxAgentIterations : 25; TxtMaxIterations.Text = ((int)SliderMaxIterations.Value).ToString(); SliderMaxRetry.Value = llm.MaxRetryOnError > 0 ? llm.MaxRetryOnError : 3; TxtMaxRetry.Text = ((int)SliderMaxRetry.Value).ToString(); ChkParallelTools.IsChecked = llm.EnableParallelTools; SliderAutoCompact.Value = llm.AutoCompactThreshold > 0 ? llm.AutoCompactThreshold : 80; TxtAutoCompact.Text = ((int)SliderAutoCompact.Value).ToString(); ChkDevMode.IsChecked = llm.DevMode; // 탭별 설정 표시 PanelCoworkSettings.Visibility = activeTab == "Cowork" ? Visibility.Visible : Visibility.Collapsed; PanelCodeSettings.Visibility = activeTab == "Code" ? Visibility.Visible : Visibility.Collapsed; if (activeTab == "Cowork") { ChkCoworkVerify.IsChecked = llm.EnableCoworkVerification; } else if (activeTab == "Code") { ChkCodeLsp.IsChecked = llm.Code?.EnableLsp ?? true; ChkCodeVerify.IsChecked = llm.Code?.EnableCodeVerification ?? false; } // 도구 토글 동적 생성 BuildToolToggles(settings, toolRegistry); _isLoading = false; } /// 활성 탭 변경 시 UI를 업데이트합니다. public void UpdateActiveTab(string tab) { TxtCurrentTab.Text = tab; PanelCoworkSettings.Visibility = tab == "Cowork" ? Visibility.Visible : Visibility.Collapsed; PanelCodeSettings.Visibility = tab == "Code" ? Visibility.Visible : Visibility.Collapsed; } private void BuildToolToggles(SettingsService settings, Services.Agent.ToolRegistry? externalRegistry = null) { PanelToolToggles.Children.Clear(); // Phase 17-UI: 외부에서 전달된 레지스트리 우선, 없으면 독립 생성 var registry = externalRegistry ?? Services.Agent.ToolRegistry.CreateDefault(); if (registry == null) return; var disabled = new HashSet( settings.Settings.Llm.DisabledTools ?? new List(), StringComparer.OrdinalIgnoreCase); foreach (var tool in registry.All.OrderBy(t => t.Name)) { var grid = new Grid { Margin = new Thickness(0, 0, 0, 4) }; grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var label = new TextBlock { Text = tool.Name, FontSize = 11, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, VerticalAlignment = VerticalAlignment.Center, }; Grid.SetColumn(label, 0); var toggle = new CheckBox { IsChecked = !disabled.Contains(tool.Name), Tag = tool.Name, VerticalAlignment = VerticalAlignment.Center, }; if (TryFindResource("ToggleSwitch") is Style toggleStyle) toggle.Style = toggleStyle; toggle.Checked += ToolToggle_Changed; toggle.Unchecked += ToolToggle_Changed; Grid.SetColumn(toggle, 1); grid.Children.Add(label); grid.Children.Add(toggle); PanelToolToggles.Children.Add(grid); } } #region ── 이벤트 핸들러 ── private void BtnClose_Click(object sender, System.Windows.Input.MouseButtonEventArgs e) { IsOpen = false; CloseRequested?.Invoke(this, EventArgs.Empty); } private void BtnServiceSelector_Click(object sender, System.Windows.Input.MouseButtonEventArgs e) { // TODO: 서비스 선택 팝업 (Ollama/vLLM/Gemini/Claude) } private void BtnModelSelector_Click(object sender, System.Windows.Input.MouseButtonEventArgs e) { // TODO: 모델 선택 팝업 — ChatWindow의 기존 모델 선택 로직 재사용 } private void TxtApiEndpoint_LostFocus(object sender, RoutedEventArgs e) { if (_isLoading) return; SaveSetting(s => s.Llm.OllamaEndpoint = TxtApiEndpoint.Text); } private void SliderMaxIterations_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) { if (_isLoading || TxtMaxIterations == null) return; var val = (int)SliderMaxIterations.Value; TxtMaxIterations.Text = val.ToString(); SaveSetting(s => s.Llm.MaxAgentIterations = val); } private void SliderMaxRetry_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) { if (_isLoading || TxtMaxRetry == null) return; var val = (int)SliderMaxRetry.Value; TxtMaxRetry.Text = val.ToString(); SaveSetting(s => s.Llm.MaxRetryOnError = val); } private void ChkParallelTools_Changed(object sender, RoutedEventArgs e) { if (_isLoading) return; SaveSetting(s => s.Llm.EnableParallelTools = ChkParallelTools.IsChecked == true); } private void ChkCoworkVerify_Changed(object sender, RoutedEventArgs e) { if (_isLoading) return; SaveSetting(s => s.Llm.EnableCoworkVerification = ChkCoworkVerify.IsChecked == true); } private void ChkCodeLsp_Changed(object sender, RoutedEventArgs e) { if (_isLoading) return; SaveSetting(s => { if (s.Llm.Code != null) s.Llm.Code.EnableLsp = ChkCodeLsp.IsChecked == true; }); } private void ChkCodeVerify_Changed(object sender, RoutedEventArgs e) { if (_isLoading) return; SaveSetting(s => { if (s.Llm.Code != null) s.Llm.Code.EnableCodeVerification = ChkCodeVerify.IsChecked == true; }); } private void SliderAutoCompact_ValueChanged(object sender, RoutedPropertyChangedEventArgs e) { if (_isLoading || TxtAutoCompact == null) return; var val = (int)SliderAutoCompact.Value; TxtAutoCompact.Text = val.ToString(); SaveSetting(s => s.Llm.AutoCompactThreshold = val); } private void ChkProjectRules_Changed(object sender, RoutedEventArgs e) { if (_isLoading) return; // 프로젝트 규칙 활성/비활성 — 현재 세션에만 영향 } private void ChkDevMode_Changed(object sender, RoutedEventArgs e) { if (_isLoading) return; SaveSetting(s => s.Llm.DevMode = ChkDevMode.IsChecked == true); } private void ToolToggle_Changed(object sender, RoutedEventArgs e) { if (_isLoading || sender is not CheckBox chk || chk.Tag is not string toolName) return; SaveSetting(s => { s.Llm.DisabledTools ??= new List(); if (chk.IsChecked == true) s.Llm.DisabledTools.Remove(toolName); else if (!s.Llm.DisabledTools.Contains(toolName)) s.Llm.DisabledTools.Add(toolName); }); } #endregion #region ── 설정 저장 & 애니메이션 ── private void SaveSetting(Action apply) { var svc = CurrentApp?.SettingsService; if (svc == null) return; apply(svc.Settings); svc.Save(); SettingsChanged?.Invoke(this, EventArgs.Empty); } private void AnimateSlide(bool open) { if (open) { Visibility = Visibility.Visible; var anim = new DoubleAnimation(300, 0, TimeSpan.FromMilliseconds(200)) { EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } }; SlideTransform.BeginAnimation(TranslateTransform.XProperty, anim); } else { var anim = new DoubleAnimation(0, 300, TimeSpan.FromMilliseconds(150)) { EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseIn } }; anim.Completed += (_, _) => { if (!_isOpen) Visibility = Visibility.Collapsed; }; SlideTransform.BeginAnimation(TranslateTransform.XProperty, anim); } } #endregion }