using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; namespace AxCopilot.Views; /// 모델 등록/편집 다이얼로그. 별칭 + 실제 모델명 입력. internal sealed class ModelRegistrationDialog : Window { private readonly TextBox _aliasBox; private readonly TextBox _modelBox; private readonly TextBox _endpointBox; private readonly TextBox _apiKeyBox; private readonly CheckBox _allowInsecureTlsCheck; // IBM/CP4D 인증 필드 private readonly ComboBox _authTypeBox; private readonly ComboBox _executionProfileBox; private readonly StackPanel _cp4dPanel; private readonly TextBox _cp4dUrlBox; private readonly TextBox _cp4dUsernameBox; private readonly PasswordBox _cp4dPasswordBox; public string ModelAlias => _aliasBox.Text.Trim(); public string ModelName => _modelBox.Text.Trim(); public string Endpoint => _endpointBox.Text.Trim(); public string ApiKey => _apiKeyBox.Text.Trim(); public bool AllowInsecureTls => _allowInsecureTlsCheck.IsChecked == true; public string AuthType => (_authTypeBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "bearer"; public string ExecutionProfile => (_executionProfileBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "balanced"; public string Cp4dUrl => _cp4dUrlBox.Text.Trim(); public string Cp4dUsername => _cp4dUsernameBox.Text.Trim(); public string Cp4dPassword => _cp4dPasswordBox.Password.Trim(); public ModelRegistrationDialog(string service, string existingAlias = "", string existingModel = "", string existingEndpoint = "", string existingApiKey = "", bool existingAllowInsecureTls = false, string existingAuthType = "bearer", string existingCp4dUrl = "", string existingCp4dUsername = "", string existingCp4dPassword = "", string existingExecutionProfile = "balanced") { bool isEdit = !string.IsNullOrEmpty(existingAlias); Title = isEdit ? "모델 편집" : "모델 추가"; Width = 420; SizeToContent = SizeToContent.Height; WindowStartupLocation = WindowStartupLocation.CenterOwner; ResizeMode = ResizeMode.NoResize; WindowStyle = WindowStyle.None; AllowsTransparency = true; Background = Brushes.Transparent; var bgBrush = Application.Current.TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E)); var primaryText = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush ?? Brushes.Blue; var itemBg = Application.Current.TryFindResource("ItemBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40)); var borderBrush = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray; // 루트 컨테이너 var root = new Border { Background = bgBrush, CornerRadius = new CornerRadius(16), BorderBrush = borderBrush, BorderThickness = new Thickness(1), Padding = new Thickness(28, 24, 28, 24), Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 24, ShadowDepth = 4, Opacity = 0.3, Color = Colors.Black, }, }; var stack = new StackPanel(); // 헤더 var header = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 20) }; header.Children.Add(new TextBlock { Text = "\uEA86", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 18, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0), }); header.Children.Add(new TextBlock { Text = isEdit ? "모델 편집" : "새 모델 등록", FontSize = 17, FontWeight = FontWeights.Bold, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, }); stack.Children.Add(header); // 서비스 표시 var serviceLabel = service == "vllm" ? "vLLM" : "Ollama"; var serviceBadge = new Border { Background = accentBrush, CornerRadius = new CornerRadius(6), Padding = new Thickness(10, 3, 10, 3), Margin = new Thickness(0, 0, 0, 16), HorizontalAlignment = HorizontalAlignment.Left, Opacity = 0.85, }; serviceBadge.Child = new TextBlock { Text = $"{serviceLabel} 서비스", FontSize = 11, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White, }; stack.Children.Add(serviceBadge); // 별칭 입력 stack.Children.Add(new TextBlock { Text = "별칭 (표시 이름)", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = primaryText, Margin = new Thickness(0, 0, 0, 6), }); stack.Children.Add(new TextBlock { Text = "채팅 화면에서 표시될 이름입니다. 예: 코드 리뷰 전용, 일반 대화", FontSize = 11, Foreground = secondaryText, Margin = new Thickness(0, 0, 0, 8), }); _aliasBox = new TextBox { Text = existingAlias, FontSize = 13, Padding = new Thickness(12, 8, 12, 8), Foreground = primaryText, Background = itemBg, CaretBrush = accentBrush, BorderBrush = borderBrush, BorderThickness = new Thickness(1), }; var aliasBorder = new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _aliasBox }; stack.Children.Add(aliasBorder); // 구분선 stack.Children.Add(new Rectangle { Height = 1, Fill = borderBrush, Margin = new Thickness(0, 16, 0, 16), Opacity = 0.5, }); // 모델명 입력 stack.Children.Add(new TextBlock { Text = "실제 모델명", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = primaryText, Margin = new Thickness(0, 0, 0, 6), }); stack.Children.Add(new TextBlock { Text = "서버에 배포된 실제 모델 ID (예: llama3:8b, qwen2:7b)", FontSize = 11, Foreground = secondaryText, Margin = new Thickness(0, 0, 0, 8), }); _modelBox = new TextBox { Text = existingModel, FontSize = 13, Padding = new Thickness(12, 8, 12, 8), Foreground = primaryText, Background = itemBg, CaretBrush = accentBrush, BorderBrush = borderBrush, BorderThickness = new Thickness(1), }; var modelBorder = new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _modelBox }; stack.Children.Add(modelBorder); stack.Children.Add(new TextBlock { Text = "실행 프로파일", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = primaryText, Margin = new Thickness(0, 12, 0, 6), }); stack.Children.Add(new TextBlock { Text = "모델 성향에 따라 도구 호출 강도, 읽기 속도, 문서 생성 흐름을 조절합니다.", FontSize = 11, Foreground = secondaryText, Margin = new Thickness(0, 0, 0, 8), }); _executionProfileBox = new ComboBox { FontSize = 13, Padding = new Thickness(8, 6, 8, 6), Foreground = primaryText, Background = itemBg, BorderBrush = borderBrush, BorderThickness = new Thickness(1), }; var balancedProfile = new ComboBoxItem { Content = "균형", Tag = "balanced" }; var strictProfile = new ComboBoxItem { Content = "도구 호출 우선", Tag = "tool_call_strict" }; var reasoningProfile = new ComboBoxItem { Content = "추론 우선", Tag = "reasoning_first" }; var readonlyProfile = new ComboBoxItem { Content = "읽기 속도 우선", Tag = "fast_readonly" }; var documentProfile = new ComboBoxItem { Content = "문서 생성 우선", Tag = "document_heavy" }; _executionProfileBox.Items.Add(balancedProfile); _executionProfileBox.Items.Add(strictProfile); _executionProfileBox.Items.Add(reasoningProfile); _executionProfileBox.Items.Add(readonlyProfile); _executionProfileBox.Items.Add(documentProfile); _executionProfileBox.SelectedItem = (existingExecutionProfile ?? "balanced").Trim().ToLowerInvariant() switch { "tool_call_strict" => strictProfile, "reasoning_first" => reasoningProfile, "fast_readonly" => readonlyProfile, "document_heavy" => documentProfile, _ => balancedProfile, }; stack.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _executionProfileBox }); // 구분선 stack.Children.Add(new Rectangle { Height = 1, Fill = borderBrush, Margin = new Thickness(0, 16, 0, 16), Opacity = 0.5, }); // 서버 엔드포인트 입력 stack.Children.Add(new TextBlock { Text = "서버 엔드포인트 (선택)", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = primaryText, Margin = new Thickness(0, 0, 0, 6), }); stack.Children.Add(new TextBlock { Text = "이 모델 전용 서버 주소. 비워두면 기본 엔드포인트를 사용합니다.", FontSize = 11, Foreground = secondaryText, Margin = new Thickness(0, 0, 0, 8), }); _endpointBox = new TextBox { Text = existingEndpoint, FontSize = 13, Padding = new Thickness(12, 8, 12, 8), Foreground = primaryText, Background = itemBg, CaretBrush = accentBrush, BorderBrush = borderBrush, BorderThickness = new Thickness(1), }; stack.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _endpointBox }); var showTlsOption = string.Equals(service, "vllm", StringComparison.OrdinalIgnoreCase); var tlsPanel = new Border { Background = Brushes.Transparent, BorderBrush = borderBrush, BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(8), Padding = new Thickness(10, 8, 10, 8), Margin = new Thickness(0, 10, 0, 0), }; var tlsGrid = new Grid(); tlsGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); tlsGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var tlsLeft = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; tlsLeft.Children.Add(new TextBlock { Text = "SSL 인증서 검증 생략", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = primaryText, }); tlsLeft.Children.Add(new TextBlock { Text = "사내 자체 인증서 환경에서만 사용하세요.", FontSize = 11, Foreground = secondaryText, Margin = new Thickness(0, 2, 0, 0), }); Grid.SetColumn(tlsLeft, 0); tlsGrid.Children.Add(tlsLeft); _allowInsecureTlsCheck = new CheckBox { IsChecked = existingAllowInsecureTls, VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(12, 0, 0, 0), }; _allowInsecureTlsCheck.Style = Application.Current.TryFindResource("ToggleSwitch") as Style ?? _allowInsecureTlsCheck.Style; Grid.SetColumn(_allowInsecureTlsCheck, 1); tlsGrid.Children.Add(_allowInsecureTlsCheck); tlsPanel.Child = tlsGrid; if (showTlsOption) stack.Children.Add(tlsPanel); // ── 인증 방식 선택 ────────────────────────────────────────────────── stack.Children.Add(new TextBlock { Text = "인증 방식", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = primaryText, Margin = new Thickness(0, 10, 0, 6), }); _authTypeBox = new ComboBox { FontSize = 13, Padding = new Thickness(8, 6, 8, 6), Foreground = primaryText, Background = itemBg, BorderBrush = borderBrush, BorderThickness = new Thickness(1), }; var bearerItem = new ComboBoxItem { Content = "Bearer 토큰 (API 키)", Tag = "bearer" }; var ibmIamItem = new ComboBoxItem { Content = "IBM IAM (토큰 교환)", Tag = "ibm_iam" }; var cp4dPasswordItem = new ComboBoxItem { Content = "CP4D (사용자 이름 + 비밀번호)", Tag = "cp4d_password" }; var cp4dApiKeyItem = new ComboBoxItem { Content = "CP4D (사용자 이름 + API 키)", Tag = "cp4d_api_key" }; _authTypeBox.Items.Add(bearerItem); _authTypeBox.Items.Add(ibmIamItem); _authTypeBox.Items.Add(cp4dPasswordItem); _authTypeBox.Items.Add(cp4dApiKeyItem); _authTypeBox.SelectedItem = existingAuthType switch { "cp4d" => cp4dPasswordItem, "cp4d_password" => cp4dPasswordItem, "cp4d_api_key" => cp4dApiKeyItem, "ibm_iam" => ibmIamItem, _ => bearerItem, }; stack.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _authTypeBox }); // ── Bearer 인증: API 키 입력 ──────────────────────────────────────── var apiKeyPanel = new StackPanel(); apiKeyPanel.Children.Add(new TextBlock { Text = "API 키 (선택)", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = primaryText, Margin = new Thickness(0, 10, 0, 6), }); _apiKeyBox = new TextBox { Text = existingApiKey, FontSize = 13, Padding = new Thickness(12, 8, 12, 8), Foreground = primaryText, Background = itemBg, CaretBrush = accentBrush, BorderBrush = borderBrush, BorderThickness = new Thickness(1), }; apiKeyPanel.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _apiKeyBox }); stack.Children.Add(apiKeyPanel); // ── CP4D 인증: URL + 사용자명 + 비밀번호/API 키 ─────────────────────── var initialCp4dAuth = existingAuthType is "cp4d" or "cp4d_password" or "cp4d_api_key"; _cp4dPanel = new StackPanel { Visibility = initialCp4dAuth ? Visibility.Visible : Visibility.Collapsed }; _cp4dPanel.Children.Add(new TextBlock { Text = "CP4D 서버 URL", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = primaryText, Margin = new Thickness(0, 10, 0, 6), }); _cp4dPanel.Children.Add(new TextBlock { Text = "예: https://cpd-host.example.com", FontSize = 11, Foreground = secondaryText, Margin = new Thickness(0, 0, 0, 6), }); _cp4dUrlBox = new TextBox { Text = existingCp4dUrl, FontSize = 13, Padding = new Thickness(12, 8, 12, 8), Foreground = primaryText, Background = itemBg, CaretBrush = accentBrush, BorderBrush = borderBrush, BorderThickness = new Thickness(1), }; _cp4dPanel.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _cp4dUrlBox }); _cp4dPanel.Children.Add(new TextBlock { Text = "사용자 이름", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = primaryText, Margin = new Thickness(0, 10, 0, 6), }); _cp4dUsernameBox = new TextBox { Text = existingCp4dUsername, FontSize = 13, Padding = new Thickness(12, 8, 12, 8), Foreground = primaryText, Background = itemBg, CaretBrush = accentBrush, BorderBrush = borderBrush, BorderThickness = new Thickness(1), }; _cp4dPanel.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _cp4dUsernameBox }); var cp4dSecretLabel = new TextBlock { Text = "비밀번호", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = primaryText, Margin = new Thickness(0, 10, 0, 6), }; _cp4dPanel.Children.Add(cp4dSecretLabel); _cp4dPasswordBox = new PasswordBox { Password = existingCp4dPassword, FontSize = 13, Padding = new Thickness(12, 8, 12, 8), Foreground = primaryText, Background = itemBg, CaretBrush = accentBrush, BorderBrush = borderBrush, BorderThickness = new Thickness(1), }; _cp4dPanel.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _cp4dPasswordBox }); stack.Children.Add(_cp4dPanel); // 인증 방식 전환 시 패널 표시/숨김 void UpdateAuthPanels() { var isCp4d = AuthType is "cp4d" or "cp4d_password" or "cp4d_api_key"; _cp4dPanel.Visibility = isCp4d ? Visibility.Visible : Visibility.Collapsed; apiKeyPanel.Visibility = isCp4d ? Visibility.Collapsed : Visibility.Visible; cp4dSecretLabel.Text = AuthType == "cp4d_api_key" ? "API 키" : "비밀번호"; } _authTypeBox.SelectionChanged += (_, _) => { UpdateAuthPanels(); }; // 초기 상태 설정 UpdateAuthPanels(); // 보안 안내 var securityNote = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 12, 0, 0), }; securityNote.Children.Add(new TextBlock { Text = "\uE72E", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 11, Foreground = new SolidColorBrush(Color.FromRgb(0x38, 0xA1, 0x69)), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), }); securityNote.Children.Add(new TextBlock { Text = "모델명은 AES-256으로 암호화되어 설정 파일에 저장됩니다", FontSize = 10.5, Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, }); stack.Children.Add(securityNote); // 버튼 바 var btnBar = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 24, 0, 0), }; var cancelBtn = new Button { Content = "취소", Width = 80, Padding = new Thickness(0, 8, 0, 8), Margin = new Thickness(0, 0, 10, 0), Background = Brushes.Transparent, Foreground = secondaryText, BorderBrush = borderBrush, BorderThickness = new Thickness(1), Cursor = Cursors.Hand, FontSize = 13, }; cancelBtn.Click += (_, _) => { DialogResult = false; Close(); }; btnBar.Children.Add(cancelBtn); var okBtn = new Button { Content = isEdit ? "저장" : "등록", Width = 80, Padding = new Thickness(0, 8, 0, 8), Background = accentBrush, Foreground = Brushes.White, BorderThickness = new Thickness(0), Cursor = Cursors.Hand, FontSize = 13, FontWeight = FontWeights.SemiBold, }; okBtn.Click += (_, _) => { if (string.IsNullOrWhiteSpace(_aliasBox.Text)) { CustomMessageBox.Show("별칭을 입력하세요.", "입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning); _aliasBox.Focus(); return; } if (string.IsNullOrWhiteSpace(_modelBox.Text)) { CustomMessageBox.Show("모델명을 입력하세요.", "입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning); _modelBox.Focus(); return; } DialogResult = true; Close(); }; btnBar.Children.Add(okBtn); stack.Children.Add(btnBar); root.Child = stack; Content = root; // 키보드 핸들링 KeyDown += (_, ke) => { if (ke.Key == Key.Escape) { DialogResult = false; Close(); } }; Loaded += (_, _) => { _aliasBox.Focus(); _aliasBox.SelectAll(); }; // 드래그 이동 root.MouseLeftButtonDown += (_, me) => { if (me.LeftButton == System.Windows.Input.MouseButtonState.Pressed) try { DragMove(); } catch { } }; } }