diff --git a/src/AxCopilot/Views/PlanViewerWindow.EditButtons.cs b/src/AxCopilot/Views/PlanViewerWindow.EditButtons.cs new file mode 100644 index 0000000..d554027 --- /dev/null +++ b/src/AxCopilot/Views/PlanViewerWindow.EditButtons.cs @@ -0,0 +1,200 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace AxCopilot.Views; + +internal sealed partial class PlanViewerWindow +{ + // ════════════════════════════════════════════════════════════ + // 버튼 빌더 + 유틸리티 + // ════════════════════════════════════════════════════════════ + + private void BuildApprovalButtons() + { + _btnPanel.Children.Clear(); + var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush + ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); + + var approveBtn = CreateActionButton("\uE73E", "승인", accentBrush, Brushes.White, true); + approveBtn.MouseLeftButtonUp += (_, _) => + { + _tcs?.TrySetResult(null); + SwitchToExecutionMode(); + }; + _btnPanel.Children.Add(approveBtn); + + var editBtn = CreateActionButton("\uE70F", "수정 요청", accentBrush, accentBrush, false); + editBtn.MouseLeftButtonUp += (_, _) => ShowEditInput(); + _btnPanel.Children.Add(editBtn); + + var reconfirmBtn = CreateActionButton("\uE72C", "재확인", + Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, + Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White, false); + reconfirmBtn.MouseLeftButtonUp += (_, _) => + _tcs?.TrySetResult("계획을 다시 검토하고 더 구체적으로 수정해주세요."); + _btnPanel.Children.Add(reconfirmBtn); + + var cancelBrush = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)); + var cancelBtn = CreateActionButton("\uE711", "취소", cancelBrush, cancelBrush, false); + cancelBtn.MouseLeftButtonUp += (_, _) => { _tcs?.TrySetResult("취소"); Hide(); }; + _btnPanel.Children.Add(cancelBtn); + } + + private void BuildExecutionButtons() + { + _btnPanel.Children.Clear(); + var secondaryText = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; + var hideBtn = CreateActionButton("\uE921", "숨기기", secondaryText, + Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White, false); + hideBtn.MouseLeftButtonUp += (_, _) => Hide(); + _btnPanel.Children.Add(hideBtn); + } + + private void BuildCloseButton() + { + _btnPanel.Children.Clear(); + var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush + ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); + var closeBtn = CreateActionButton("\uE73E", "닫기", accentBrush, Brushes.White, true); + closeBtn.MouseLeftButtonUp += (_, _) => Hide(); + _btnPanel.Children.Add(closeBtn); + } + + private void ShowEditInput() + { + var editPanel = new Border + { + Margin = new Thickness(20, 0, 20, 12), + Padding = new Thickness(12, 8, 12, 8), + CornerRadius = new CornerRadius(10), + Background = Application.Current.TryFindResource("ItemBackground") as Brush + ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40)), + }; + var editStack = new StackPanel(); + editStack.Children.Add(new TextBlock + { + Text = "수정 사항을 입력하세요:", + FontSize = 11.5, + Foreground = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, + Margin = new Thickness(0, 0, 0, 6), + }); + var textBox = new TextBox + { + MinHeight = 44, + MaxHeight = 120, + AcceptsReturn = true, + TextWrapping = TextWrapping.Wrap, + FontSize = 13, + Background = Application.Current.TryFindResource("LauncherBackground") as Brush + ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E)), + Foreground = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White, + CaretBrush = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White, + BorderBrush = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray, + BorderThickness = new Thickness(1), + Padding = new Thickness(10, 8, 10, 8), + }; + editStack.Children.Add(textBox); + + var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush + ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); + var sendBtn = new Border + { + Background = accentBrush, + CornerRadius = new CornerRadius(8), + Padding = new Thickness(14, 6, 14, 6), + Margin = new Thickness(0, 8, 0, 0), + Cursor = Cursors.Hand, + HorizontalAlignment = HorizontalAlignment.Right, + Child = new TextBlock + { + Text = "전송", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White, + }, + }; + sendBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85; + sendBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0; + sendBtn.MouseLeftButtonUp += (_, _) => + { + var feedback = textBox.Text.Trim(); + if (string.IsNullOrEmpty(feedback)) return; + _tcs?.TrySetResult(feedback); + }; + editStack.Children.Add(sendBtn); + editPanel.Child = editStack; + + if (_btnPanel.Parent is Grid parentGrid) + { + for (int i = parentGrid.Children.Count - 1; i >= 0; i--) + { + if (parentGrid.Children[i] is Border b && b.Tag?.ToString() == "EditPanel") + parentGrid.Children.RemoveAt(i); + } + editPanel.Tag = "EditPanel"; + Grid.SetRow(editPanel, 4); // row 4 = 하단 버튼 행 (toolBar 추가로 1 증가) + parentGrid.Children.Add(editPanel); + _btnPanel.Margin = new Thickness(20, 0, 20, 16); + textBox.Focus(); + } + } + + // ════════════════════════════════════════════════════════════ + // 공통 버튼 팩토리 + // ════════════════════════════════════════════════════════════ + + private static Border CreateMiniButton(string icon, Brush fg, Brush hoverBg) + { + var btn = new Border + { + Width = 24, Height = 24, + CornerRadius = new CornerRadius(6), + Background = Brushes.Transparent, + Cursor = Cursors.Hand, + Margin = new Thickness(1, 0, 1, 0), + Child = new TextBlock + { + Text = icon, FontFamily = ThemeResourceHelper.SegoeMdl2, + FontSize = 10, Foreground = fg, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + }, + }; + btn.MouseEnter += (s, _) => ((Border)s).Background = hoverBg; + btn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent; + return btn; + } + + private static Border CreateActionButton(string icon, string text, Brush borderColor, + Brush textColor, bool filled) + { + var color = ((SolidColorBrush)borderColor).Color; + var btn = new Border + { + CornerRadius = new CornerRadius(12), + Padding = new Thickness(16, 8, 16, 8), + Margin = new Thickness(4, 0, 4, 0), + Cursor = Cursors.Hand, + Background = filled ? borderColor + : new SolidColorBrush(Color.FromArgb(0x18, color.R, color.G, color.B)), + BorderBrush = filled ? Brushes.Transparent + : new SolidColorBrush(Color.FromArgb(0x80, color.R, color.G, color.B)), + BorderThickness = new Thickness(filled ? 0 : 1.2), + }; + var sp = new StackPanel { Orientation = Orientation.Horizontal }; + sp.Children.Add(new TextBlock + { + Text = icon, FontFamily = ThemeResourceHelper.SegoeMdl2, + FontSize = 12, Foreground = filled ? Brushes.White : textColor, + VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), + }); + sp.Children.Add(new TextBlock + { + Text = text, FontSize = 12.5, FontWeight = FontWeights.SemiBold, + Foreground = filled ? Brushes.White : textColor, + }); + btn.Child = sp; + btn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85; + btn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0; + return btn; + } +} diff --git a/src/AxCopilot/Views/PlanViewerWindow.StepRenderer.cs b/src/AxCopilot/Views/PlanViewerWindow.StepRenderer.cs index b5a52dc..1ba6d85 100644 --- a/src/AxCopilot/Views/PlanViewerWindow.StepRenderer.cs +++ b/src/AxCopilot/Views/PlanViewerWindow.StepRenderer.cs @@ -422,195 +422,4 @@ internal sealed partial class PlanViewerWindow textBox.Focus(); textBox.SelectAll(); } - - // ════════════════════════════════════════════════════════════ - // 하단 버튼 빌드 - // ════════════════════════════════════════════════════════════ - - private void BuildApprovalButtons() - { - _btnPanel.Children.Clear(); - var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush - ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); - - var approveBtn = CreateActionButton("\uE73E", "승인", accentBrush, Brushes.White, true); - approveBtn.MouseLeftButtonUp += (_, _) => - { - _tcs?.TrySetResult(null); - SwitchToExecutionMode(); - }; - _btnPanel.Children.Add(approveBtn); - - var editBtn = CreateActionButton("\uE70F", "수정 요청", accentBrush, accentBrush, false); - editBtn.MouseLeftButtonUp += (_, _) => ShowEditInput(); - _btnPanel.Children.Add(editBtn); - - var reconfirmBtn = CreateActionButton("\uE72C", "재확인", - Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, - Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White, false); - reconfirmBtn.MouseLeftButtonUp += (_, _) => - _tcs?.TrySetResult("계획을 다시 검토하고 더 구체적으로 수정해주세요."); - _btnPanel.Children.Add(reconfirmBtn); - - var cancelBrush = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)); - var cancelBtn = CreateActionButton("\uE711", "취소", cancelBrush, cancelBrush, false); - cancelBtn.MouseLeftButtonUp += (_, _) => { _tcs?.TrySetResult("취소"); Hide(); }; - _btnPanel.Children.Add(cancelBtn); - } - - private void BuildExecutionButtons() - { - _btnPanel.Children.Clear(); - var secondaryText = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; - var hideBtn = CreateActionButton("\uE921", "숨기기", secondaryText, - Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White, false); - hideBtn.MouseLeftButtonUp += (_, _) => Hide(); - _btnPanel.Children.Add(hideBtn); - } - - private void BuildCloseButton() - { - _btnPanel.Children.Clear(); - var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush - ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); - var closeBtn = CreateActionButton("\uE73E", "닫기", accentBrush, Brushes.White, true); - closeBtn.MouseLeftButtonUp += (_, _) => Hide(); - _btnPanel.Children.Add(closeBtn); - } - - private void ShowEditInput() - { - var editPanel = new Border - { - Margin = new Thickness(20, 0, 20, 12), - Padding = new Thickness(12, 8, 12, 8), - CornerRadius = new CornerRadius(10), - Background = Application.Current.TryFindResource("ItemBackground") as Brush - ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40)), - }; - var editStack = new StackPanel(); - editStack.Children.Add(new TextBlock - { - Text = "수정 사항을 입력하세요:", - FontSize = 11.5, - Foreground = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, - Margin = new Thickness(0, 0, 0, 6), - }); - var textBox = new TextBox - { - MinHeight = 44, - MaxHeight = 120, - AcceptsReturn = true, - TextWrapping = TextWrapping.Wrap, - FontSize = 13, - Background = Application.Current.TryFindResource("LauncherBackground") as Brush - ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E)), - Foreground = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White, - CaretBrush = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White, - BorderBrush = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray, - BorderThickness = new Thickness(1), - Padding = new Thickness(10, 8, 10, 8), - }; - editStack.Children.Add(textBox); - - var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush - ?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); - var sendBtn = new Border - { - Background = accentBrush, - CornerRadius = new CornerRadius(8), - Padding = new Thickness(14, 6, 14, 6), - Margin = new Thickness(0, 8, 0, 0), - Cursor = Cursors.Hand, - HorizontalAlignment = HorizontalAlignment.Right, - Child = new TextBlock - { - Text = "전송", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White, - }, - }; - sendBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85; - sendBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0; - sendBtn.MouseLeftButtonUp += (_, _) => - { - var feedback = textBox.Text.Trim(); - if (string.IsNullOrEmpty(feedback)) return; - _tcs?.TrySetResult(feedback); - }; - editStack.Children.Add(sendBtn); - editPanel.Child = editStack; - - if (_btnPanel.Parent is Grid parentGrid) - { - for (int i = parentGrid.Children.Count - 1; i >= 0; i--) - { - if (parentGrid.Children[i] is Border b && b.Tag?.ToString() == "EditPanel") - parentGrid.Children.RemoveAt(i); - } - editPanel.Tag = "EditPanel"; - Grid.SetRow(editPanel, 4); // row 4 = 하단 버튼 행 (toolBar 추가로 1 증가) - parentGrid.Children.Add(editPanel); - _btnPanel.Margin = new Thickness(20, 0, 20, 16); - textBox.Focus(); - } - } - - // ════════════════════════════════════════════════════════════ - // 공통 버튼 팩토리 - // ════════════════════════════════════════════════════════════ - - private static Border CreateMiniButton(string icon, Brush fg, Brush hoverBg) - { - var btn = new Border - { - Width = 24, Height = 24, - CornerRadius = new CornerRadius(6), - Background = Brushes.Transparent, - Cursor = Cursors.Hand, - Margin = new Thickness(1, 0, 1, 0), - Child = new TextBlock - { - Text = icon, FontFamily = ThemeResourceHelper.SegoeMdl2, - FontSize = 10, Foreground = fg, - HorizontalAlignment = HorizontalAlignment.Center, - VerticalAlignment = VerticalAlignment.Center, - }, - }; - btn.MouseEnter += (s, _) => ((Border)s).Background = hoverBg; - btn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent; - return btn; - } - - private static Border CreateActionButton(string icon, string text, Brush borderColor, - Brush textColor, bool filled) - { - var color = ((SolidColorBrush)borderColor).Color; - var btn = new Border - { - CornerRadius = new CornerRadius(12), - Padding = new Thickness(16, 8, 16, 8), - Margin = new Thickness(4, 0, 4, 0), - Cursor = Cursors.Hand, - Background = filled ? borderColor - : new SolidColorBrush(Color.FromArgb(0x18, color.R, color.G, color.B)), - BorderBrush = filled ? Brushes.Transparent - : new SolidColorBrush(Color.FromArgb(0x80, color.R, color.G, color.B)), - BorderThickness = new Thickness(filled ? 0 : 1.2), - }; - var sp = new StackPanel { Orientation = Orientation.Horizontal }; - sp.Children.Add(new TextBlock - { - Text = icon, FontFamily = ThemeResourceHelper.SegoeMdl2, - FontSize = 12, Foreground = filled ? Brushes.White : textColor, - VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), - }); - sp.Children.Add(new TextBlock - { - Text = text, FontSize = 12.5, FontWeight = FontWeights.SemiBold, - Foreground = filled ? Brushes.White : textColor, - }); - btn.Child = sp; - btn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85; - btn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0; - return btn; - } } diff --git a/src/AxCopilot/Views/SettingsWindow.AgentConfig.cs b/src/AxCopilot/Views/SettingsWindow.AgentConfig.cs index fc0dd9d..c750fbc 100644 --- a/src/AxCopilot/Views/SettingsWindow.AgentConfig.cs +++ b/src/AxCopilot/Views/SettingsWindow.AgentConfig.cs @@ -300,309 +300,4 @@ public partial class SettingsWindow if (result == MessageBoxResult.Yes) _vm.PromptTemplates.Remove(row); } - - // ─── AI 기능 활성화 토글 ──────────────────────────────────────────────── - - /// AI 기능 토글 상태를 UI와 설정에 반영합니다. - private void ApplyAiEnabledState(bool enabled, bool init = false) - { - // 토글 스위치 체크 상태 동기화 (init 시에는 이벤트 억제) - if (AiEnabledToggle != null && AiEnabledToggle.IsChecked != enabled) - { - AiEnabledToggle.IsChecked = enabled; - } - // AX Agent 탭 가시성 - if (AgentTabItem != null) - AgentTabItem.Visibility = enabled ? Visibility.Visible : Visibility.Collapsed; - } - - private void AiEnabled_Changed(object sender, RoutedEventArgs e) - { - if (!IsLoaded) return; - var tryEnable = AiEnabledToggle?.IsChecked == true; - - // 비활성화는 즉시 적용 (비밀번호 불필요) - if (!tryEnable) - { - var app2 = CurrentApp; - if (app2?.SettingsService?.Settings != null) - { - app2.SettingsService.Settings.AiEnabled = false; - app2.SettingsService.Save(); - } - ApplyAiEnabledState(false); - return; - } - - // 이미 활성화된 상태에서 설정 창이 열릴 때 토글 복원으로 인한 이벤트 → 비밀번호 불필요 - var currentApp = CurrentApp; - if (currentApp?.SettingsService?.Settings.AiEnabled == true) return; - - // 새로 활성화하는 경우에만 비밀번호 확인 - var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); - var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White; - var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; - var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x40, 0x40, 0x60)); - var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40)); - - var dlg = new Window - { - Title = "AI 기능 활성화 — 비밀번호 확인", - Width = 340, SizeToContent = SizeToContent.Height, - WindowStartupLocation = WindowStartupLocation.CenterOwner, - Owner = this, ResizeMode = ResizeMode.NoResize, - WindowStyle = WindowStyle.None, AllowsTransparency = true, - Background = Brushes.Transparent, - }; - var border = new Border - { - Background = bgBrush, CornerRadius = new CornerRadius(12), - BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(20), - }; - var stack = new StackPanel(); - stack.Children.Add(new TextBlock - { - Text = "\U0001f512 AI 기능 활성화", - FontSize = 15, FontWeight = FontWeights.SemiBold, - Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 12), - }); - stack.Children.Add(new TextBlock - { - Text = "비밀번호를 입력하세요:", - FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 6), - }); - var pwBox = new PasswordBox - { - FontSize = 14, Padding = new Thickness(8, 6, 8, 6), - Background = itemBg, Foreground = fgBrush, BorderBrush = borderBrush, PasswordChar = '*', - }; - stack.Children.Add(pwBox); - - var btnRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0) }; - var cancelBtn = new Button { Content = "취소", Padding = new Thickness(16, 6, 16, 6), Margin = new Thickness(0, 0, 8, 0) }; - cancelBtn.Click += (_, _) => { dlg.DialogResult = false; }; - btnRow.Children.Add(cancelBtn); - var okBtn = new Button { Content = "확인", Padding = new Thickness(16, 6, 16, 6), IsDefault = true }; - okBtn.Click += (_, _) => - { - if (pwBox.Password == SettingsPassword) - dlg.DialogResult = true; - else - { - pwBox.Clear(); - pwBox.Focus(); - } - }; - btnRow.Children.Add(okBtn); - stack.Children.Add(btnRow); - border.Child = stack; - dlg.Content = border; - dlg.Loaded += (_, _) => pwBox.Focus(); - - if (dlg.ShowDialog() == true) - { - var app = CurrentApp; - if (app?.SettingsService?.Settings != null) - { - app.SettingsService.Settings.AiEnabled = true; - app.SettingsService.Save(); - } - ApplyAiEnabledState(true); - } - else - { - // 취소/실패 — 토글 원상복구 - if (AiEnabledToggle != null) AiEnabledToggle.IsChecked = false; - } - } - - // ─── 사내/사외 모드 토글 ───────────────────────────────────────────────────── - - private const string SettingsPassword = "axgo123!"; - - private void NetworkMode_Changed(object sender, RoutedEventArgs e) - { - if (!IsLoaded) return; - var tryInternalMode = InternalModeToggle?.IsChecked == true; // true = 사내(차단), false = 사외(허용) - - // 사내 모드로 전환(차단 강화)은 비밀번호 불필요 - if (tryInternalMode) - { - var app2 = CurrentApp; - if (app2?.SettingsService?.Settings != null) - { - app2.SettingsService.Settings.InternalModeEnabled = true; - app2.SettingsService.Save(); - } - return; - } - - // 이미 사외 모드인 경우 토글 복원으로 인한 이벤트 → 비밀번호 불필요 - var currentApp = CurrentApp; - if (currentApp?.SettingsService?.Settings.InternalModeEnabled == false) return; - - // 사외 모드 활성화(외부 허용)는 비밀번호 확인 필요 - var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); - var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White; - var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; - var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x40, 0x40, 0x60)); - var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40)); - - var dlg = new Window - { - Title = "사외 모드 활성화 — 비밀번호 확인", - Width = 340, SizeToContent = SizeToContent.Height, - WindowStartupLocation = WindowStartupLocation.CenterOwner, - Owner = this, ResizeMode = ResizeMode.NoResize, - WindowStyle = WindowStyle.None, AllowsTransparency = true, - Background = Brushes.Transparent, - }; - var border = new Border - { - Background = bgBrush, CornerRadius = new CornerRadius(12), - BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(20), - }; - var stack = new StackPanel(); - stack.Children.Add(new TextBlock - { - Text = "🌐 사외 모드 활성화", - FontSize = 15, FontWeight = FontWeights.SemiBold, - Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 8), - }); - stack.Children.Add(new TextBlock - { - Text = "사외 모드에서는 인터넷 검색과 외부 HTTP 접속이 허용됩니다.\n비밀번호를 입력하세요:", - FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 10), - TextWrapping = TextWrapping.Wrap, - }); - var pwBox = new PasswordBox - { - FontSize = 14, Padding = new Thickness(8, 6, 8, 6), - Background = itemBg, Foreground = fgBrush, BorderBrush = borderBrush, PasswordChar = '*', - }; - stack.Children.Add(pwBox); - - var btnRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0) }; - var cancelBtn = new Button { Content = "취소", Padding = new Thickness(16, 6, 16, 6), Margin = new Thickness(0, 0, 8, 0) }; - cancelBtn.Click += (_, _) => { dlg.DialogResult = false; }; - btnRow.Children.Add(cancelBtn); - var okBtn = new Button { Content = "확인", Padding = new Thickness(16, 6, 16, 6), IsDefault = true }; - okBtn.Click += (_, _) => - { - if (pwBox.Password == SettingsPassword) - dlg.DialogResult = true; - else - { - pwBox.Clear(); - pwBox.Focus(); - } - }; - btnRow.Children.Add(okBtn); - stack.Children.Add(btnRow); - border.Child = stack; - dlg.Content = border; - dlg.Loaded += (_, _) => pwBox.Focus(); - - if (dlg.ShowDialog() == true) - { - var app = CurrentApp; - if (app?.SettingsService?.Settings != null) - { - app.SettingsService.Settings.InternalModeEnabled = false; - app.SettingsService.Save(); - } - } - else - { - // 취소/실패 — 토글 원상복구 (사내 모드 유지) - if (InternalModeToggle != null) InternalModeToggle.IsChecked = true; - } - } - - private void StepApprovalCheckBox_Checked(object sender, RoutedEventArgs e) - { - if (sender is not CheckBox cb || !cb.IsChecked.GetValueOrDefault()) return; - // 설정 창 로드 중 바인딩에 의한 자동 Checked 이벤트 무시 (이미 활성화된 상태 복원) - if (!IsLoaded) return; - - // 테마 리소스 조회 - var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); - var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White; - var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; - var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x40, 0x40, 0x60)); - var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40)); - - var dlg = new Window - { - Title = "스텝 바이 스텝 승인 — 비밀번호 확인", - Width = 340, SizeToContent = SizeToContent.Height, - WindowStartupLocation = WindowStartupLocation.CenterOwner, - Owner = this, ResizeMode = ResizeMode.NoResize, - WindowStyle = WindowStyle.None, AllowsTransparency = true, - Background = Brushes.Transparent, - }; - var border = new Border - { - Background = bgBrush, CornerRadius = new CornerRadius(12), - BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(20), - }; - var stack = new StackPanel(); - stack.Children.Add(new TextBlock - { - Text = "\U0001f50d 스텝 바이 스텝 승인 활성화", - FontSize = 15, FontWeight = FontWeights.SemiBold, - Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 12), - }); - stack.Children.Add(new TextBlock - { - Text = "개발자 비밀번호를 입력하세요:", - FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 6), - }); - var pwBox = new PasswordBox - { - FontSize = 14, Padding = new Thickness(8, 6, 8, 6), - Background = itemBg, Foreground = fgBrush, BorderBrush = borderBrush, PasswordChar = '*', - }; - stack.Children.Add(pwBox); - - var btnRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0) }; - var cancelBtn = new Button { Content = "취소", Padding = new Thickness(16, 6, 16, 6), Margin = new Thickness(0, 0, 8, 0) }; - cancelBtn.Click += (_, _) => { dlg.DialogResult = false; }; - btnRow.Children.Add(cancelBtn); - var okBtn = new Button { Content = "확인", Padding = new Thickness(16, 6, 16, 6), IsDefault = true }; - okBtn.Click += (_, _) => - { - if (pwBox.Password == "mouse12#") - dlg.DialogResult = true; - else - { - pwBox.Clear(); - pwBox.Focus(); - } - }; - btnRow.Children.Add(okBtn); - stack.Children.Add(btnRow); - border.Child = stack; - dlg.Content = border; - dlg.Loaded += (_, _) => pwBox.Focus(); - - if (dlg.ShowDialog() != true) - { - cb.IsChecked = false; - } - } - - private void BtnClearMemory_Click(object sender, RoutedEventArgs e) - { - var result = CustomMessageBox.Show( - "에이전트 메모리를 초기화하면 학습된 모든 규칙과 선호도가 삭제됩니다.\n계속하시겠습니까?", - "에이전트 메모리 초기화", - MessageBoxButton.YesNo, - MessageBoxImage.Warning); - if (result != MessageBoxResult.Yes) return; - - var app = CurrentApp; - app?.MemoryService?.Clear(); - CustomMessageBox.Show("에이전트 메모리가 초기화되었습니다.", "완료", MessageBoxButton.OK, MessageBoxImage.Information); - } } diff --git a/src/AxCopilot/Views/SettingsWindow.AgentHooks.cs b/src/AxCopilot/Views/SettingsWindow.AgentHooks.cs index 5cec6c1..0346728 100644 --- a/src/AxCopilot/Views/SettingsWindow.AgentHooks.cs +++ b/src/AxCopilot/Views/SettingsWindow.AgentHooks.cs @@ -331,275 +331,4 @@ public partial class SettingsWindow HookListPanel.Children.Add(card); } } - - // ─── MCP 서버 관리 ───────────────────────────────────────────────── - private void BtnAddMcpServer_Click(object sender, RoutedEventArgs e) - { - var dlg = new InputDialog("MCP 서버 추가", "서버 이름:", placeholder: "예: my-mcp-server"); - dlg.Owner = this; - if (dlg.ShowDialog() != true || string.IsNullOrWhiteSpace(dlg.ResponseText)) return; - - var name = dlg.ResponseText.Trim(); - var cmdDlg = new InputDialog("MCP 서버 추가", "실행 명령:", placeholder: "예: npx -y @anthropic/mcp-server"); - cmdDlg.Owner = this; - if (cmdDlg.ShowDialog() != true || string.IsNullOrWhiteSpace(cmdDlg.ResponseText)) return; - - var entry = new Models.McpServerEntry { Name = name, Command = cmdDlg.ResponseText.Trim(), Enabled = true }; - _vm.Service.Settings.Llm.McpServers.Add(entry); - BuildMcpServerCards(); - } - - private void BuildMcpServerCards() - { - if (McpServerListPanel == null) return; - McpServerListPanel.Children.Clear(); - - var servers = _vm.Service.Settings.Llm.McpServers; - var primaryText = TryFindResource("PrimaryText") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.Black; - var secondaryText = TryFindResource("SecondaryText") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.Gray; - var accentBrush = TryFindResource("AccentColor") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.Blue; - - for (int i = 0; i < servers.Count; i++) - { - var srv = servers[i]; - var idx = i; - - var card = new Border - { - Background = TryFindResource("ItemBackground") as System.Windows.Media.Brush, - CornerRadius = new CornerRadius(10), - Padding = new Thickness(14, 10, 14, 10), - Margin = new Thickness(0, 4, 0, 0), - BorderBrush = TryFindResource("BorderColor") as System.Windows.Media.Brush, - BorderThickness = new Thickness(1), - }; - - var grid = new Grid(); - grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); - grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); - - var info = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; - info.Children.Add(new TextBlock - { - Text = srv.Name, FontSize = 13.5, FontWeight = FontWeights.SemiBold, Foreground = primaryText, - }); - var detailSp = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 0) }; - detailSp.Children.Add(new Border - { - Background = srv.Enabled ? accentBrush : System.Windows.Media.Brushes.Gray, - CornerRadius = new CornerRadius(4), Padding = new Thickness(6, 1, 6, 1), Margin = new Thickness(0, 0, 8, 0), Opacity = 0.8, - Child = new TextBlock { Text = srv.Enabled ? "활성" : "비활성", FontSize = 10, Foreground = System.Windows.Media.Brushes.White, FontWeight = FontWeights.SemiBold }, - }); - detailSp.Children.Add(new TextBlock - { - Text = $"{srv.Command} {string.Join(" ", srv.Args)}", FontSize = 11, - Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, - MaxWidth = 300, TextTrimming = TextTrimming.CharacterEllipsis, - }); - info.Children.Add(detailSp); - Grid.SetColumn(info, 0); - grid.Children.Add(info); - - var btnPanel = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center }; - - // 활성/비활성 토글 - var toggleBtn = new Button - { - Content = srv.Enabled ? "\uE73E" : "\uE711", - FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), - FontSize = 12, ToolTip = srv.Enabled ? "비활성화" : "활성화", - Background = System.Windows.Media.Brushes.Transparent, BorderThickness = new Thickness(0), - Foreground = srv.Enabled ? accentBrush : System.Windows.Media.Brushes.Gray, - Padding = new Thickness(6, 4, 6, 4), Cursor = Cursors.Hand, - }; - toggleBtn.Click += (_, _) => { servers[idx].Enabled = !servers[idx].Enabled; BuildMcpServerCards(); }; - btnPanel.Children.Add(toggleBtn); - - // 삭제 - var delBtn = new Button - { - Content = "\uE74D", FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), - FontSize = 12, ToolTip = "삭제", - Background = System.Windows.Media.Brushes.Transparent, BorderThickness = new Thickness(0), - Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0xDD, 0x44, 0x44)), - Padding = new Thickness(6, 4, 6, 4), Cursor = Cursors.Hand, - }; - delBtn.Click += (_, _) => { servers.RemoveAt(idx); BuildMcpServerCards(); }; - btnPanel.Children.Add(delBtn); - - Grid.SetColumn(btnPanel, 1); - grid.Children.Add(btnPanel); - card.Child = grid; - McpServerListPanel.Children.Add(card); - } - } - - // ─── 감사 로그 폴더 열기 ──────────────────────────────────────────── - private void BtnOpenAuditLog_Click(object sender, RoutedEventArgs e) - { - try { System.Diagnostics.Process.Start("explorer.exe", Services.AuditLogService.GetAuditFolder()); } catch (Exception) { } - } - - // ─── 폴백/MCP 텍스트 박스 로드/저장 ─────────────────────────────────── - private void BuildFallbackModelsPanel() - { - if (FallbackModelsPanel == null) return; - FallbackModelsPanel.Children.Clear(); - - var llm = _vm.Service.Settings.Llm; - var fallbacks = llm.FallbackModels; - var toggleStyle = TryFindResource("ToggleSwitch") as Style; - - // 서비스별로 모델 수집 (순서 고정: Ollama → vLLM → Gemini → Claude) - var sections = new (string Service, string Label, string Color, List Models)[] - { - ("ollama", "Ollama", "#107C10", new()), - ("vllm", "vLLM", "#0078D4", new()), - ("gemini", "Gemini", "#4285F4", new()), - ("claude", "Claude", "#8B5CF6", new()), - }; - - // RegisteredModels → ViewModel과 AppSettings 양쪽에서 수집 (저장 전에도 반영) - // 1) ViewModel의 RegisteredModels (UI에서 방금 추가한 것 포함) - foreach (var row in _vm.RegisteredModels) - { - var svc = (row.Service ?? "").ToLowerInvariant(); - var modelName = !string.IsNullOrEmpty(row.Alias) ? row.Alias : row.EncryptedModelName; - var section = sections.FirstOrDefault(s => s.Service == svc); - if (section.Models != null && !string.IsNullOrEmpty(modelName) && !section.Models.Contains(modelName)) - section.Models.Add(modelName); - } - // 2) AppSettings의 RegisteredModels (기존 저장된 것 — ViewModel에 없는 경우 보완) - foreach (var m in llm.RegisteredModels) - { - var svc = (m.Service ?? "").ToLowerInvariant(); - var modelName = !string.IsNullOrEmpty(m.Alias) ? m.Alias : m.EncryptedModelName; - var section = sections.FirstOrDefault(s => s.Service == svc); - if (section.Models != null && !string.IsNullOrEmpty(modelName) && !section.Models.Contains(modelName)) - section.Models.Add(modelName); - } - - // 현재 활성 모델 추가 (중복 제거) - if (!string.IsNullOrEmpty(llm.OllamaModel) && !sections[0].Models.Contains(llm.OllamaModel)) - sections[0].Models.Add(llm.OllamaModel); - if (!string.IsNullOrEmpty(llm.VllmModel) && !sections[1].Models.Contains(llm.VllmModel)) - sections[1].Models.Add(llm.VllmModel); - - // Gemini/Claude 고정 모델 목록 - foreach (var gm in new[] { "gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash" }) - if (!sections[2].Models.Contains(gm)) sections[2].Models.Add(gm); - foreach (var cm in new[] { "claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5", "claude-sonnet-4-5" }) - if (!sections[3].Models.Contains(cm)) sections[3].Models.Add(cm); - - // 렌더링 — 모델이 없는 섹션도 헤더는 표시 - foreach (var (service, svcLabel, svcColor, models) in sections) - { - FallbackModelsPanel.Children.Add(new TextBlock - { - Text = svcLabel, - FontSize = 11, FontWeight = FontWeights.SemiBold, - Foreground = BrushFromHex(svcColor), - Margin = new Thickness(0, 8, 0, 4), - }); - - if (models.Count == 0) - { - FallbackModelsPanel.Children.Add(new TextBlock - { - Text = "등록된 모델 없음", - FontSize = 11, Foreground = Brushes.Gray, FontStyle = FontStyles.Italic, - Margin = new Thickness(8, 2, 0, 4), - }); - continue; - } - - foreach (var modelName in models) - { - var fullKey = $"{service}:{modelName}"; - - var row = new Grid { Margin = new Thickness(8, 2, 0, 2) }; - row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); - row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); - - var label = new TextBlock - { - Text = modelName, FontSize = 12, FontFamily = ThemeResourceHelper.ConsolasCourierNew, - VerticalAlignment = VerticalAlignment.Center, - Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, - }; - Grid.SetColumn(label, 0); - row.Children.Add(label); - - var captured = fullKey; - var cb = new CheckBox - { - IsChecked = fallbacks.Contains(fullKey, StringComparer.OrdinalIgnoreCase), - HorizontalAlignment = HorizontalAlignment.Right, - VerticalAlignment = VerticalAlignment.Center, - }; - if (toggleStyle != null) cb.Style = toggleStyle; - cb.Checked += (_, _) => - { - if (!fallbacks.Contains(captured)) fallbacks.Add(captured); - FallbackModelsBox.Text = string.Join("\n", fallbacks); - }; - cb.Unchecked += (_, _) => - { - fallbacks.RemoveAll(x => x.Equals(captured, StringComparison.OrdinalIgnoreCase)); - FallbackModelsBox.Text = string.Join("\n", fallbacks); - }; - Grid.SetColumn(cb, 1); - row.Children.Add(cb); - - FallbackModelsPanel.Children.Add(row); - } - } - } - - private void LoadAdvancedSettings() - { - var llm = _vm.Service.Settings.Llm; - if (FallbackModelsBox != null) - FallbackModelsBox.Text = string.Join("\n", llm.FallbackModels); - BuildFallbackModelsPanel(); - if (McpServersBox != null) - { - try - { - var json = System.Text.Json.JsonSerializer.Serialize(llm.McpServers, - new System.Text.Json.JsonSerializerOptions { WriteIndented = true, - Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); - McpServersBox.Text = json; - } - catch (Exception) { McpServersBox.Text = "[]"; } - } - BuildMcpServerCards(); - BuildHookCards(); - } - - private void SaveAdvancedSettings() - { - var llm = _vm.Service.Settings.Llm; - if (FallbackModelsBox != null) - { - llm.FallbackModels = FallbackModelsBox.Text - .Split('\n', StringSplitOptions.RemoveEmptyEntries) - .Select(s => s.Trim()) - .Where(s => s.Length > 0) - .ToList(); - } - if (McpServersBox != null && !string.IsNullOrWhiteSpace(McpServersBox.Text)) - { - try - { - llm.McpServers = System.Text.Json.JsonSerializer.Deserialize>( - McpServersBox.Text) ?? new(); - } - catch (Exception) { /* JSON 파싱 실패 시 기존 유지 */ } - } - - // 도구 비활성 목록 저장 - if (_toolCardsLoaded) - llm.DisabledTools = _disabledTools.ToList(); - } } diff --git a/src/AxCopilot/Views/SettingsWindow.AiToggle.cs b/src/AxCopilot/Views/SettingsWindow.AiToggle.cs new file mode 100644 index 0000000..ecdea59 --- /dev/null +++ b/src/AxCopilot/Views/SettingsWindow.AiToggle.cs @@ -0,0 +1,316 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using AxCopilot.Services; +using AxCopilot.ViewModels; + +namespace AxCopilot.Views; + +public partial class SettingsWindow +{ + // ─── AI 기능 활성화 토글 + 네트워크 모드 ──────────────────────────────────── + + /// AI 기능 토글 상태를 UI와 설정에 반영합니다. + private void ApplyAiEnabledState(bool enabled, bool init = false) + { + // 토글 스위치 체크 상태 동기화 (init 시에는 이벤트 억제) + if (AiEnabledToggle != null && AiEnabledToggle.IsChecked != enabled) + { + AiEnabledToggle.IsChecked = enabled; + } + // AX Agent 탭 가시성 + if (AgentTabItem != null) + AgentTabItem.Visibility = enabled ? Visibility.Visible : Visibility.Collapsed; + } + + private void AiEnabled_Changed(object sender, RoutedEventArgs e) + { + if (!IsLoaded) return; + var tryEnable = AiEnabledToggle?.IsChecked == true; + + // 비활성화는 즉시 적용 (비밀번호 불필요) + if (!tryEnable) + { + var app2 = CurrentApp; + if (app2?.SettingsService?.Settings != null) + { + app2.SettingsService.Settings.AiEnabled = false; + app2.SettingsService.Save(); + } + ApplyAiEnabledState(false); + return; + } + + // 이미 활성화된 상태에서 설정 창이 열릴 때 토글 복원으로 인한 이벤트 → 비밀번호 불필요 + var currentApp = CurrentApp; + if (currentApp?.SettingsService?.Settings.AiEnabled == true) return; + + // 새로 활성화하는 경우에만 비밀번호 확인 + var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); + var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White; + var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; + var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x40, 0x40, 0x60)); + var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40)); + + var dlg = new Window + { + Title = "AI 기능 활성화 — 비밀번호 확인", + Width = 340, SizeToContent = SizeToContent.Height, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + Owner = this, ResizeMode = ResizeMode.NoResize, + WindowStyle = WindowStyle.None, AllowsTransparency = true, + Background = Brushes.Transparent, + }; + var border = new Border + { + Background = bgBrush, CornerRadius = new CornerRadius(12), + BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(20), + }; + var stack = new StackPanel(); + stack.Children.Add(new TextBlock + { + Text = "\U0001f512 AI 기능 활성화", + FontSize = 15, FontWeight = FontWeights.SemiBold, + Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 12), + }); + stack.Children.Add(new TextBlock + { + Text = "비밀번호를 입력하세요:", + FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 6), + }); + var pwBox = new PasswordBox + { + FontSize = 14, Padding = new Thickness(8, 6, 8, 6), + Background = itemBg, Foreground = fgBrush, BorderBrush = borderBrush, PasswordChar = '*', + }; + stack.Children.Add(pwBox); + + var btnRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0) }; + var cancelBtn = new Button { Content = "취소", Padding = new Thickness(16, 6, 16, 6), Margin = new Thickness(0, 0, 8, 0) }; + cancelBtn.Click += (_, _) => { dlg.DialogResult = false; }; + btnRow.Children.Add(cancelBtn); + var okBtn = new Button { Content = "확인", Padding = new Thickness(16, 6, 16, 6), IsDefault = true }; + okBtn.Click += (_, _) => + { + if (pwBox.Password == SettingsPassword) + dlg.DialogResult = true; + else + { + pwBox.Clear(); + pwBox.Focus(); + } + }; + btnRow.Children.Add(okBtn); + stack.Children.Add(btnRow); + border.Child = stack; + dlg.Content = border; + dlg.Loaded += (_, _) => pwBox.Focus(); + + if (dlg.ShowDialog() == true) + { + var app = CurrentApp; + if (app?.SettingsService?.Settings != null) + { + app.SettingsService.Settings.AiEnabled = true; + app.SettingsService.Save(); + } + ApplyAiEnabledState(true); + } + else + { + // 취소/실패 — 토글 원상복구 + if (AiEnabledToggle != null) AiEnabledToggle.IsChecked = false; + } + } + + // ─── 사내/사외 모드 토글 ───────────────────────────────────────────────────── + + private const string SettingsPassword = "axgo123!"; + + private void NetworkMode_Changed(object sender, RoutedEventArgs e) + { + if (!IsLoaded) return; + var tryInternalMode = InternalModeToggle?.IsChecked == true; // true = 사내(차단), false = 사외(허용) + + // 사내 모드로 전환(차단 강화)은 비밀번호 불필요 + if (tryInternalMode) + { + var app2 = CurrentApp; + if (app2?.SettingsService?.Settings != null) + { + app2.SettingsService.Settings.InternalModeEnabled = true; + app2.SettingsService.Save(); + } + return; + } + + // 이미 사외 모드인 경우 토글 복원으로 인한 이벤트 → 비밀번호 불필요 + var currentApp = CurrentApp; + if (currentApp?.SettingsService?.Settings.InternalModeEnabled == false) return; + + // 사외 모드 활성화(외부 허용)는 비밀번호 확인 필요 + var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); + var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White; + var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; + var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x40, 0x40, 0x60)); + var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40)); + + var dlg = new Window + { + Title = "사외 모드 활성화 — 비밀번호 확인", + Width = 340, SizeToContent = SizeToContent.Height, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + Owner = this, ResizeMode = ResizeMode.NoResize, + WindowStyle = WindowStyle.None, AllowsTransparency = true, + Background = Brushes.Transparent, + }; + var border = new Border + { + Background = bgBrush, CornerRadius = new CornerRadius(12), + BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(20), + }; + var stack = new StackPanel(); + stack.Children.Add(new TextBlock + { + Text = "🌐 사외 모드 활성화", + FontSize = 15, FontWeight = FontWeights.SemiBold, + Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 8), + }); + stack.Children.Add(new TextBlock + { + Text = "사외 모드에서는 인터넷 검색과 외부 HTTP 접속이 허용됩니다.\n비밀번호를 입력하세요:", + FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 10), + TextWrapping = TextWrapping.Wrap, + }); + var pwBox = new PasswordBox + { + FontSize = 14, Padding = new Thickness(8, 6, 8, 6), + Background = itemBg, Foreground = fgBrush, BorderBrush = borderBrush, PasswordChar = '*', + }; + stack.Children.Add(pwBox); + + var btnRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0) }; + var cancelBtn = new Button { Content = "취소", Padding = new Thickness(16, 6, 16, 6), Margin = new Thickness(0, 0, 8, 0) }; + cancelBtn.Click += (_, _) => { dlg.DialogResult = false; }; + btnRow.Children.Add(cancelBtn); + var okBtn = new Button { Content = "확인", Padding = new Thickness(16, 6, 16, 6), IsDefault = true }; + okBtn.Click += (_, _) => + { + if (pwBox.Password == SettingsPassword) + dlg.DialogResult = true; + else + { + pwBox.Clear(); + pwBox.Focus(); + } + }; + btnRow.Children.Add(okBtn); + stack.Children.Add(btnRow); + border.Child = stack; + dlg.Content = border; + dlg.Loaded += (_, _) => pwBox.Focus(); + + if (dlg.ShowDialog() == true) + { + var app = CurrentApp; + if (app?.SettingsService?.Settings != null) + { + app.SettingsService.Settings.InternalModeEnabled = false; + app.SettingsService.Save(); + } + } + else + { + // 취소/실패 — 토글 원상복구 (사내 모드 유지) + if (InternalModeToggle != null) InternalModeToggle.IsChecked = true; + } + } + + private void StepApprovalCheckBox_Checked(object sender, RoutedEventArgs e) + { + if (sender is not CheckBox cb || !cb.IsChecked.GetValueOrDefault()) return; + // 설정 창 로드 중 바인딩에 의한 자동 Checked 이벤트 무시 (이미 활성화된 상태 복원) + if (!IsLoaded) return; + + // 테마 리소스 조회 + var bgBrush = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); + var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White; + var subFgBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; + var borderBrush = TryFindResource("BorderColor") as Brush ?? new SolidColorBrush(Color.FromRgb(0x40, 0x40, 0x60)); + var itemBg = TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40)); + + var dlg = new Window + { + Title = "스텝 바이 스텝 승인 — 비밀번호 확인", + Width = 340, SizeToContent = SizeToContent.Height, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + Owner = this, ResizeMode = ResizeMode.NoResize, + WindowStyle = WindowStyle.None, AllowsTransparency = true, + Background = Brushes.Transparent, + }; + var border = new Border + { + Background = bgBrush, CornerRadius = new CornerRadius(12), + BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(20), + }; + var stack = new StackPanel(); + stack.Children.Add(new TextBlock + { + Text = "\U0001f50d 스텝 바이 스텝 승인 활성화", + FontSize = 15, FontWeight = FontWeights.SemiBold, + Foreground = fgBrush, Margin = new Thickness(0, 0, 0, 12), + }); + stack.Children.Add(new TextBlock + { + Text = "개발자 비밀번호를 입력하세요:", + FontSize = 12, Foreground = subFgBrush, Margin = new Thickness(0, 0, 0, 6), + }); + var pwBox = new PasswordBox + { + FontSize = 14, Padding = new Thickness(8, 6, 8, 6), + Background = itemBg, Foreground = fgBrush, BorderBrush = borderBrush, PasswordChar = '*', + }; + stack.Children.Add(pwBox); + + var btnRow = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 16, 0, 0) }; + var cancelBtn = new Button { Content = "취소", Padding = new Thickness(16, 6, 16, 6), Margin = new Thickness(0, 0, 8, 0) }; + cancelBtn.Click += (_, _) => { dlg.DialogResult = false; }; + btnRow.Children.Add(cancelBtn); + var okBtn = new Button { Content = "확인", Padding = new Thickness(16, 6, 16, 6), IsDefault = true }; + okBtn.Click += (_, _) => + { + if (pwBox.Password == "mouse12#") + dlg.DialogResult = true; + else + { + pwBox.Clear(); + pwBox.Focus(); + } + }; + btnRow.Children.Add(okBtn); + stack.Children.Add(btnRow); + border.Child = stack; + dlg.Content = border; + dlg.Loaded += (_, _) => pwBox.Focus(); + + if (dlg.ShowDialog() != true) + { + cb.IsChecked = false; + } + } + + private void BtnClearMemory_Click(object sender, RoutedEventArgs e) + { + var result = CustomMessageBox.Show( + "에이전트 메모리를 초기화하면 학습된 모든 규칙과 선호도가 삭제됩니다.\n계속하시겠습니까?", + "에이전트 메모리 초기화", + MessageBoxButton.YesNo, + MessageBoxImage.Warning); + if (result != MessageBoxResult.Yes) return; + + var app = CurrentApp; + app?.MemoryService?.Clear(); + CustomMessageBox.Show("에이전트 메모리가 초기화되었습니다.", "완료", MessageBoxButton.OK, MessageBoxImage.Information); + } +} diff --git a/src/AxCopilot/Views/SettingsWindow.McpAdvanced.cs b/src/AxCopilot/Views/SettingsWindow.McpAdvanced.cs new file mode 100644 index 0000000..4335135 --- /dev/null +++ b/src/AxCopilot/Views/SettingsWindow.McpAdvanced.cs @@ -0,0 +1,284 @@ +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; + +namespace AxCopilot.Views; + +public partial class SettingsWindow +{ + // ─── MCP 서버 관리 + 고급 설정 ────────────────────────────────────────── + + // ─── MCP 서버 관리 ───────────────────────────────────────────────── + private void BtnAddMcpServer_Click(object sender, RoutedEventArgs e) + { + var dlg = new InputDialog("MCP 서버 추가", "서버 이름:", placeholder: "예: my-mcp-server"); + dlg.Owner = this; + if (dlg.ShowDialog() != true || string.IsNullOrWhiteSpace(dlg.ResponseText)) return; + + var name = dlg.ResponseText.Trim(); + var cmdDlg = new InputDialog("MCP 서버 추가", "실행 명령:", placeholder: "예: npx -y @anthropic/mcp-server"); + cmdDlg.Owner = this; + if (cmdDlg.ShowDialog() != true || string.IsNullOrWhiteSpace(cmdDlg.ResponseText)) return; + + var entry = new Models.McpServerEntry { Name = name, Command = cmdDlg.ResponseText.Trim(), Enabled = true }; + _vm.Service.Settings.Llm.McpServers.Add(entry); + BuildMcpServerCards(); + } + + private void BuildMcpServerCards() + { + if (McpServerListPanel == null) return; + McpServerListPanel.Children.Clear(); + + var servers = _vm.Service.Settings.Llm.McpServers; + var primaryText = TryFindResource("PrimaryText") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.Black; + var secondaryText = TryFindResource("SecondaryText") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.Gray; + var accentBrush = TryFindResource("AccentColor") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.Blue; + + for (int i = 0; i < servers.Count; i++) + { + var srv = servers[i]; + var idx = i; + + var card = new Border + { + Background = TryFindResource("ItemBackground") as System.Windows.Media.Brush, + CornerRadius = new CornerRadius(10), + Padding = new Thickness(14, 10, 14, 10), + Margin = new Thickness(0, 4, 0, 0), + BorderBrush = TryFindResource("BorderColor") as System.Windows.Media.Brush, + BorderThickness = new Thickness(1), + }; + + var grid = new Grid(); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + var info = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; + info.Children.Add(new TextBlock + { + Text = srv.Name, FontSize = 13.5, FontWeight = FontWeights.SemiBold, Foreground = primaryText, + }); + var detailSp = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 0) }; + detailSp.Children.Add(new Border + { + Background = srv.Enabled ? accentBrush : System.Windows.Media.Brushes.Gray, + CornerRadius = new CornerRadius(4), Padding = new Thickness(6, 1, 6, 1), Margin = new Thickness(0, 0, 8, 0), Opacity = 0.8, + Child = new TextBlock { Text = srv.Enabled ? "활성" : "비활성", FontSize = 10, Foreground = System.Windows.Media.Brushes.White, FontWeight = FontWeights.SemiBold }, + }); + detailSp.Children.Add(new TextBlock + { + Text = $"{srv.Command} {string.Join(" ", srv.Args)}", FontSize = 11, + Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, + MaxWidth = 300, TextTrimming = TextTrimming.CharacterEllipsis, + }); + info.Children.Add(detailSp); + Grid.SetColumn(info, 0); + grid.Children.Add(info); + + var btnPanel = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center }; + + // 활성/비활성 토글 + var toggleBtn = new Button + { + Content = srv.Enabled ? "\uE73E" : "\uE711", + FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 12, ToolTip = srv.Enabled ? "비활성화" : "활성화", + Background = System.Windows.Media.Brushes.Transparent, BorderThickness = new Thickness(0), + Foreground = srv.Enabled ? accentBrush : System.Windows.Media.Brushes.Gray, + Padding = new Thickness(6, 4, 6, 4), Cursor = Cursors.Hand, + }; + toggleBtn.Click += (_, _) => { servers[idx].Enabled = !servers[idx].Enabled; BuildMcpServerCards(); }; + btnPanel.Children.Add(toggleBtn); + + // 삭제 + var delBtn = new Button + { + Content = "\uE74D", FontFamily = new System.Windows.Media.FontFamily("Segoe MDL2 Assets"), + FontSize = 12, ToolTip = "삭제", + Background = System.Windows.Media.Brushes.Transparent, BorderThickness = new Thickness(0), + Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0xDD, 0x44, 0x44)), + Padding = new Thickness(6, 4, 6, 4), Cursor = Cursors.Hand, + }; + delBtn.Click += (_, _) => { servers.RemoveAt(idx); BuildMcpServerCards(); }; + btnPanel.Children.Add(delBtn); + + Grid.SetColumn(btnPanel, 1); + grid.Children.Add(btnPanel); + card.Child = grid; + McpServerListPanel.Children.Add(card); + } + } + + // ─── 감사 로그 폴더 열기 ──────────────────────────────────────────── + private void BtnOpenAuditLog_Click(object sender, RoutedEventArgs e) + { + try { System.Diagnostics.Process.Start("explorer.exe", Services.AuditLogService.GetAuditFolder()); } catch (Exception) { } + } + + // ─── 폴백/MCP 텍스트 박스 로드/저장 ─────────────────────────────────── + private void BuildFallbackModelsPanel() + { + if (FallbackModelsPanel == null) return; + FallbackModelsPanel.Children.Clear(); + + var llm = _vm.Service.Settings.Llm; + var fallbacks = llm.FallbackModels; + var toggleStyle = TryFindResource("ToggleSwitch") as Style; + + // 서비스별로 모델 수집 (순서 고정: Ollama → vLLM → Gemini → Claude) + var sections = new (string Service, string Label, string Color, List Models)[] + { + ("ollama", "Ollama", "#107C10", new()), + ("vllm", "vLLM", "#0078D4", new()), + ("gemini", "Gemini", "#4285F4", new()), + ("claude", "Claude", "#8B5CF6", new()), + }; + + // RegisteredModels → ViewModel과 AppSettings 양쪽에서 수집 (저장 전에도 반영) + // 1) ViewModel의 RegisteredModels (UI에서 방금 추가한 것 포함) + foreach (var row in _vm.RegisteredModels) + { + var svc = (row.Service ?? "").ToLowerInvariant(); + var modelName = !string.IsNullOrEmpty(row.Alias) ? row.Alias : row.EncryptedModelName; + var section = sections.FirstOrDefault(s => s.Service == svc); + if (section.Models != null && !string.IsNullOrEmpty(modelName) && !section.Models.Contains(modelName)) + section.Models.Add(modelName); + } + // 2) AppSettings의 RegisteredModels (기존 저장된 것 — ViewModel에 없는 경우 보완) + foreach (var m in llm.RegisteredModels) + { + var svc = (m.Service ?? "").ToLowerInvariant(); + var modelName = !string.IsNullOrEmpty(m.Alias) ? m.Alias : m.EncryptedModelName; + var section = sections.FirstOrDefault(s => s.Service == svc); + if (section.Models != null && !string.IsNullOrEmpty(modelName) && !section.Models.Contains(modelName)) + section.Models.Add(modelName); + } + + // 현재 활성 모델 추가 (중복 제거) + if (!string.IsNullOrEmpty(llm.OllamaModel) && !sections[0].Models.Contains(llm.OllamaModel)) + sections[0].Models.Add(llm.OllamaModel); + if (!string.IsNullOrEmpty(llm.VllmModel) && !sections[1].Models.Contains(llm.VllmModel)) + sections[1].Models.Add(llm.VllmModel); + + // Gemini/Claude 고정 모델 목록 + foreach (var gm in new[] { "gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash" }) + if (!sections[2].Models.Contains(gm)) sections[2].Models.Add(gm); + foreach (var cm in new[] { "claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5", "claude-sonnet-4-5" }) + if (!sections[3].Models.Contains(cm)) sections[3].Models.Add(cm); + + // 렌더링 — 모델이 없는 섹션도 헤더는 표시 + foreach (var (service, svcLabel, svcColor, models) in sections) + { + FallbackModelsPanel.Children.Add(new TextBlock + { + Text = svcLabel, + FontSize = 11, FontWeight = FontWeights.SemiBold, + Foreground = BrushFromHex(svcColor), + Margin = new Thickness(0, 8, 0, 4), + }); + + if (models.Count == 0) + { + FallbackModelsPanel.Children.Add(new TextBlock + { + Text = "등록된 모델 없음", + FontSize = 11, Foreground = Brushes.Gray, FontStyle = FontStyles.Italic, + Margin = new Thickness(8, 2, 0, 4), + }); + continue; + } + + foreach (var modelName in models) + { + var fullKey = $"{service}:{modelName}"; + + var row = new Grid { Margin = new Thickness(8, 2, 0, 2) }; + row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); + row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); + + var label = new TextBlock + { + Text = modelName, FontSize = 12, FontFamily = ThemeResourceHelper.ConsolasCourierNew, + VerticalAlignment = VerticalAlignment.Center, + Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, + }; + Grid.SetColumn(label, 0); + row.Children.Add(label); + + var captured = fullKey; + var cb = new CheckBox + { + IsChecked = fallbacks.Contains(fullKey, StringComparer.OrdinalIgnoreCase), + HorizontalAlignment = HorizontalAlignment.Right, + VerticalAlignment = VerticalAlignment.Center, + }; + if (toggleStyle != null) cb.Style = toggleStyle; + cb.Checked += (_, _) => + { + if (!fallbacks.Contains(captured)) fallbacks.Add(captured); + FallbackModelsBox.Text = string.Join("\n", fallbacks); + }; + cb.Unchecked += (_, _) => + { + fallbacks.RemoveAll(x => x.Equals(captured, StringComparison.OrdinalIgnoreCase)); + FallbackModelsBox.Text = string.Join("\n", fallbacks); + }; + Grid.SetColumn(cb, 1); + row.Children.Add(cb); + + FallbackModelsPanel.Children.Add(row); + } + } + } + + private void LoadAdvancedSettings() + { + var llm = _vm.Service.Settings.Llm; + if (FallbackModelsBox != null) + FallbackModelsBox.Text = string.Join("\n", llm.FallbackModels); + BuildFallbackModelsPanel(); + if (McpServersBox != null) + { + try + { + var json = System.Text.Json.JsonSerializer.Serialize(llm.McpServers, + new System.Text.Json.JsonSerializerOptions { WriteIndented = true, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); + McpServersBox.Text = json; + } + catch (Exception) { McpServersBox.Text = "[]"; } + } + BuildMcpServerCards(); + BuildHookCards(); + } + + private void SaveAdvancedSettings() + { + var llm = _vm.Service.Settings.Llm; + if (FallbackModelsBox != null) + { + llm.FallbackModels = FallbackModelsBox.Text + .Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .Where(s => s.Length > 0) + .ToList(); + } + if (McpServersBox != null && !string.IsNullOrWhiteSpace(McpServersBox.Text)) + { + try + { + llm.McpServers = System.Text.Json.JsonSerializer.Deserialize>( + McpServersBox.Text) ?? new(); + } + catch (Exception) { /* JSON 파싱 실패 시 기존 유지 */ } + } + + // 도구 비활성 목록 저장 + if (_toolCardsLoaded) + llm.DisabledTools = _disabledTools.ToList(); + } +}