using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using AxCopilot.Services.Agent; namespace AxCopilot.Views; /// 스킬 갤러리 창. 설치된 모든 스킬을 카드 형태로 표시하고 관리합니다. public partial class SkillGalleryWindow : Window { private string _selectedCategory = "전체"; public SkillGalleryWindow() { InitializeComponent(); Loaded += (_, _) => { BuildCategoryFilter(); RenderSkills(); }; } // ─── 타이틀바 ───────────────────────────────────────────────────────── private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { // 타이틀바 우측 버튼 영역에서는 DragMove 실행하지 않음 var pos = e.GetPosition(this); if (pos.X > ActualWidth - 160) return; // 우측 버튼 영역 보호 if (e.ClickCount == 2) WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; else DragMove(); } private void BtnClose_Click(object sender, MouseButtonEventArgs e) => Close(); // ─── 버튼 ───────────────────────────────────────────────────────────── private void BtnAddSkill_Click(object sender, MouseButtonEventArgs e) { var editor = new SkillEditorWindow { Owner = this }; if (editor.ShowDialog() == true) { BuildCategoryFilter(); RenderSkills(); } } private void BtnImport_Click(object sender, MouseButtonEventArgs e) { var dlg = new Microsoft.Win32.OpenFileDialog { Filter = "스킬 패키지 (*.zip)|*.zip", Title = "가져올 스킬 zip 파일을 선택하세요", }; if (dlg.ShowDialog() != true) return; var count = SkillService.ImportSkills(dlg.FileName); if (count > 0) { CustomMessageBox.Show($"스킬 {count}개를 성공적으로 가져왔습니다.", "스킬 가져오기"); BuildCategoryFilter(); RenderSkills(); } else CustomMessageBox.Show("스킬 가져오기에 실패했습니다.", "스킬 가져오기"); } // ─── 카테고리 필터 ───────────────────────────────────────────────────── private void BuildCategoryFilter() { CategoryFilterBar.Children.Clear(); var skills = SkillService.Skills; var categories = new[] { "전체" } .Concat(skills .Select(s => string.IsNullOrEmpty(s.Requires) ? "내장" : "고급 (런타임)") .Distinct()) .ToList(); // 사용자 스킬이 있으면 추가 var userFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills"); var hasUser = skills.Any(s => s.FilePath.StartsWith(userFolder, StringComparison.OrdinalIgnoreCase)); if (hasUser && !categories.Contains("사용자")) categories.Add("사용자"); foreach (var cat in categories) { var btn = new Border { Tag = cat, CornerRadius = new CornerRadius(8), Padding = new Thickness(14, 5, 14, 5), Margin = new Thickness(0, 0, 6, 0), Background = cat == _selectedCategory ? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)) : TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent, Cursor = Cursors.Hand, }; btn.Child = new TextBlock { Text = cat, FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = cat == _selectedCategory ? Brushes.White : TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, }; btn.MouseLeftButtonUp += (s, _) => { if (s is Border b && b.Tag is string tag) { _selectedCategory = tag; BuildCategoryFilter(); RenderSkills(); } }; CategoryFilterBar.Children.Add(btn); } } // ─── 스킬 목록 렌더링 ────────────────────────────────────────────────── private void RenderSkills() { SkillListPanel.Children.Clear(); var skills = FilterSkills(SkillService.Skills); var userFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills"); foreach (var skill in skills) SkillListPanel.Children.Add(BuildSkillCard(skill, userFolder)); if (skills.Count == 0) SkillListPanel.Children.Add(new TextBlock { Text = "표시할 스킬이 없습니다.", FontSize = 13, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, Margin = new Thickness(0, 20, 0, 0), HorizontalAlignment = HorizontalAlignment.Center, }); var total = SkillService.Skills.Count; var avail = SkillService.Skills.Count(s => s.IsAvailable); GalleryStatus.Text = $"총 {total}개 스킬 | 사용 가능 {avail}개 | 런타임 필요 {total - avail}개"; } private List FilterSkills(IReadOnlyList skills) { var userFolder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "skills"); return _selectedCategory switch { "내장" => skills.Where(s => string.IsNullOrEmpty(s.Requires) && !s.FilePath.StartsWith(userFolder, StringComparison.OrdinalIgnoreCase)).ToList(), "고급 (런타임)" => skills.Where(s => !string.IsNullOrEmpty(s.Requires)).ToList(), "사용자" => skills.Where(s => s.FilePath.StartsWith(userFolder, StringComparison.OrdinalIgnoreCase)).ToList(), _ => skills.ToList(), }; } private Border BuildSkillCard(SkillDefinition skill, string userFolder) { var bgBrush = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent; var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var subBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var isUser = skill.FilePath.StartsWith(userFolder, StringComparison.OrdinalIgnoreCase); var isAdvanced = !string.IsNullOrEmpty(skill.Requires); var card = new Border { Background = bgBrush, CornerRadius = new CornerRadius(10), Padding = new Thickness(14, 10, 14, 10), Margin = new Thickness(0, 0, 0, 6), }; var grid = new Grid(); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) }); grid.ColumnDefinitions.Add(new ColumnDefinition()); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) }); // ── 아이콘 원 ── var iconBorder = new Border { Width = 38, Height = 38, CornerRadius = new CornerRadius(10), Background = new SolidColorBrush(Color.FromArgb(0x30, 0x4B, 0x5E, 0xFC)), Margin = new Thickness(0, 0, 12, 0), VerticalAlignment = VerticalAlignment.Center, }; iconBorder.Child = new TextBlock { Text = skill.Icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 16, Foreground = skill.IsAvailable ? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)) : subBrush, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, }; Grid.SetColumn(iconBorder, 0); // ── 정보 ── var infoPanel = new StackPanel { VerticalAlignment = VerticalAlignment.Center }; // 이름 + 뱃지들 var nameRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 3) }; nameRow.Children.Add(new TextBlock { Text = $"/{skill.Name}", FontSize = 13, FontWeight = FontWeights.SemiBold, FontFamily = new FontFamily("Consolas"), Foreground = skill.IsAvailable ? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)) : subBrush, Opacity = skill.IsAvailable ? 1.0 : 0.6, VerticalAlignment = VerticalAlignment.Center, }); nameRow.Children.Add(new TextBlock { Text = $" {skill.Label}", FontSize = 12.5, Foreground = fgBrush, VerticalAlignment = VerticalAlignment.Center, }); // 소스/유형 뱃지 if (isUser) nameRow.Children.Add(MakeBadge("사용자", "#34D399")); else if (isAdvanced) nameRow.Children.Add(MakeBadge("고급", "#A78BFA")); else nameRow.Children.Add(MakeBadge("내장", "#9CA3AF")); if (skill.IsSample) nameRow.Children.Add(MakeBadge("예제", "#F59E0B")); // 비가용 뱃지 if (!skill.IsAvailable) nameRow.Children.Add(MakeBadge(skill.UnavailableHint, "#F87171")); infoPanel.Children.Add(nameRow); infoPanel.Children.Add(new TextBlock { Text = skill.Description, FontSize = 11.5, Foreground = subBrush, TextWrapping = TextWrapping.Wrap, Opacity = skill.IsAvailable ? 1.0 : 0.6, }); Grid.SetColumn(infoPanel, 1); // ── 액션 버튼들 ── var actions = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(8, 0, 0, 0), }; // 편집 actions.Children.Add(MakeActionBtn("\uE70F", "#3B82F6", isUser ? "편집 (시각적 편집기)" : "편집 (파일 열기)", () => { if (isUser) { var editor = new SkillEditorWindow(skill) { Owner = this }; if (editor.ShowDialog() == true) { SkillService.LoadSkills(); BuildCategoryFilter(); RenderSkills(); } } else { try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(skill.FilePath) { UseShellExecute = true }); } catch (Exception ex) { CustomMessageBox.Show($"파일을 열 수 없습니다: {ex.Message}", "편집"); } } })); // 복제 (사용자 스킬/폴더 스킬만) actions.Children.Add(MakeActionBtn("\uE8C8", "#10B981", "복제", () => { try { var destFolder = Path.Combine(userFolder); if (!Directory.Exists(destFolder)) Directory.CreateDirectory(destFolder); var srcName = Path.GetFileNameWithoutExtension(skill.FilePath); var destPath = Path.Combine(destFolder, $"{srcName}_copy.skill.md"); var counter = 2; while (File.Exists(destPath)) destPath = Path.Combine(destFolder, $"{srcName}_copy{counter++}.skill.md"); File.Copy(skill.FilePath, destPath); SkillService.LoadSkills(); BuildCategoryFilter(); RenderSkills(); } catch (Exception ex) { CustomMessageBox.Show($"복제 실패: {ex.Message}", "복제"); } })); // 내보내기 actions.Children.Add(MakeActionBtn("\uEDE1", "#F59E0B", "내보내기 (.zip)", () => { var folderDlg = new System.Windows.Forms.FolderBrowserDialog { Description = "내보낼 폴더를 선택하세요" }; if (folderDlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return; var result = SkillService.ExportSkill(skill, folderDlg.SelectedPath); if (result != null) CustomMessageBox.Show($"내보내기 완료:\n{result}", "내보내기"); else CustomMessageBox.Show("내보내기에 실패했습니다.", "내보내기"); })); // 삭제 (사용자 스킬만) if (isUser) { actions.Children.Add(MakeActionBtn("\uE74D", "#F87171", "삭제", () => { var confirm = CustomMessageBox.Show( $"스킬 '/{skill.Name}'을 삭제하시겠습니까?", "스킬 삭제", MessageBoxButton.YesNo, MessageBoxImage.Question); if (confirm != MessageBoxResult.Yes) return; try { File.Delete(skill.FilePath); SkillService.LoadSkills(); BuildCategoryFilter(); RenderSkills(); } catch (Exception ex) { CustomMessageBox.Show($"삭제 실패: {ex.Message}", "삭제"); } })); } Grid.SetColumn(actions, 2); grid.Children.Add(iconBorder); grid.Children.Add(infoPanel); grid.Children.Add(actions); card.Child = grid; // 호버 효과 + 카드 클릭 → 상세 보기 card.Cursor = Cursors.Hand; card.MouseEnter += (_, _) => { card.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); }; card.MouseLeave += (_, _) => card.Background = bgBrush; card.MouseLeftButtonUp += (_, e) => { // 액션 버튼 클릭은 무시 (버블링 방지) if (e.OriginalSource is FrameworkElement src) { var parent = src; while (parent != null) { if (parent == actions) return; parent = VisualTreeHelper.GetParent(parent) as FrameworkElement; } } ShowSkillDetail(skill); }; return card; } private Border MakeBadge(string text, string colorHex) { var col = (Color)ColorConverter.ConvertFromString(colorHex); return new Border { Background = new SolidColorBrush(Color.FromArgb(0x25, col.R, col.G, col.B)), CornerRadius = new CornerRadius(4), Padding = new Thickness(5, 1, 5, 1), Margin = new Thickness(6, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center, Child = new TextBlock { Text = text, FontSize = 9.5, FontWeight = FontWeights.SemiBold, Foreground = new SolidColorBrush(col), }, }; } private Border MakeActionBtn(string icon, string colorHex, string tooltip, Action action) { var col = (Color)ColorConverter.ConvertFromString(colorHex); var btn = new Border { Width = 28, Height = 28, CornerRadius = new CornerRadius(6), Background = Brushes.Transparent, Cursor = Cursors.Hand, Margin = new Thickness(2, 0, 0, 0), ToolTip = tooltip, Child = new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 12, Foreground = new SolidColorBrush(col), HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, }, }; btn.MouseEnter += (_, _) => btn.Background = new SolidColorBrush(Color.FromArgb(0x20, col.R, col.G, col.B)); btn.MouseLeave += (_, _) => btn.Background = Brushes.Transparent; btn.MouseLeftButtonUp += (_, _) => action(); return btn; } // ─── 스킬 상세 보기 팝업 ─────────────────────────────────────────────── private void ShowSkillDetail(SkillDefinition skill) { var bgBrush = TryFindResource("LauncherBackground") as Brush ?? Brushes.Black; var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var subBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var itemBg = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent; var borderBr = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var popup = new Window { Title = $"/{skill.Name}", Width = 580, Height = 480, WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent, WindowStartupLocation = WindowStartupLocation.CenterOwner, Owner = this, }; var outerBorder = new Border { Background = bgBrush, CornerRadius = new CornerRadius(12), BorderBrush = borderBr, BorderThickness = new Thickness(1, 1, 1, 1), Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 20, ShadowDepth = 4, Opacity = 0.3, Color = Colors.Black, }, }; var mainGrid = new Grid(); mainGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(44) }); mainGrid.RowDefinitions.Add(new RowDefinition()); // ── 타이틀바 ── var titleBar = new Border { CornerRadius = new CornerRadius(12, 12, 0, 0), Background = itemBg, }; titleBar.MouseLeftButtonDown += (_, e) => { var pos = e.GetPosition(popup); if (pos.X > popup.ActualWidth - 50) return; popup.DragMove(); }; var titleGrid = new Grid { Margin = new Thickness(16, 0, 12, 0) }; var titleLeft = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center }; titleLeft.Children.Add(new TextBlock { Text = skill.Icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 14, Foreground = new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 1, 8, 0), }); titleLeft.Children.Add(new TextBlock { Text = $"/{skill.Name} {skill.Label}", FontSize = 14, FontWeight = FontWeights.SemiBold, Foreground = fgBrush, VerticalAlignment = VerticalAlignment.Center, }); titleGrid.Children.Add(titleLeft); var closeBtn = new Border { Width = 28, Height = 28, CornerRadius = new CornerRadius(6), Cursor = Cursors.Hand, HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center, }; closeBtn.Child = new TextBlock { Text = "\uE8BB", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 10, Foreground = subBrush, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, }; closeBtn.MouseEnter += (s, _) => ((Border)s).Background = new SolidColorBrush(Color.FromArgb(0x33, 0xFF, 0x44, 0x44)); closeBtn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent; closeBtn.MouseLeftButtonUp += (_, _) => popup.Close(); titleGrid.Children.Add(closeBtn); titleBar.Child = titleGrid; Grid.SetRow(titleBar, 0); // ── 본문: 스킬 정보 + 프롬프트 미리보기 ── var body = new ScrollViewer { VerticalScrollBarVisibility = ScrollBarVisibility.Auto, Padding = new Thickness(20, 16, 20, 16), }; var bodyPanel = new StackPanel(); // 메타 정보 var metaGrid = new Grid { Margin = new Thickness(0, 0, 0, 14) }; metaGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(80) }); metaGrid.ColumnDefinitions.Add(new ColumnDefinition()); void AddMetaRow(string label, string value, int row) { if (string.IsNullOrEmpty(value)) return; metaGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); var lb = new TextBlock { Text = label, FontSize = 11.5, Foreground = subBrush, Margin = new Thickness(0, 2, 0, 2), }; Grid.SetRow(lb, row); Grid.SetColumn(lb, 0); metaGrid.Children.Add(lb); var vb = new TextBlock { Text = value, FontSize = 11.5, Foreground = fgBrush, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 2), }; Grid.SetRow(vb, row); Grid.SetColumn(vb, 1); metaGrid.Children.Add(vb); } var metaRow = 0; AddMetaRow("명령어", $"/{skill.Name}", metaRow++); if (skill.IsSample) AddMetaRow("유형", "예제", metaRow++); AddMetaRow("라벨", skill.Label, metaRow++); AddMetaRow("설명", skill.Description, metaRow++); if (!string.IsNullOrEmpty(skill.Requires)) AddMetaRow("런타임", skill.Requires, metaRow++); if (!string.IsNullOrEmpty(skill.AllowedTools)) AddMetaRow("허용 도구", skill.AllowedTools, metaRow++); AddMetaRow("상태", skill.IsAvailable ? "✓ 사용 가능" : $"✗ {skill.UnavailableHint}", metaRow++); AddMetaRow("경로", skill.FilePath, metaRow++); bodyPanel.Children.Add(metaGrid); // 구분선 bodyPanel.Children.Add(new Border { Height = 1, Background = borderBr, Margin = new Thickness(0, 4, 0, 12), }); // 프롬프트 내용 미리보기 bodyPanel.Children.Add(new TextBlock { Text = "시스템 프롬프트 (미리보기)", FontSize = 11, FontWeight = FontWeights.SemiBold, Foreground = subBrush, Margin = new Thickness(0, 0, 0, 6), }); var promptBorder = new Border { Background = itemBg, CornerRadius = new CornerRadius(8), Padding = new Thickness(12, 10, 12, 10), }; var promptText = skill.SystemPrompt; if (promptText.Length > 2000) promptText = promptText[..2000] + "\n\n... (이하 생략)"; promptBorder.Child = new TextBlock { Text = promptText, FontSize = 11.5, FontFamily = new FontFamily("Consolas, Cascadia Code, Segoe UI"), Foreground = fgBrush, TextWrapping = TextWrapping.Wrap, Opacity = 0.85, }; bodyPanel.Children.Add(promptBorder); body.Content = bodyPanel; Grid.SetRow(body, 1); mainGrid.Children.Add(titleBar); mainGrid.Children.Add(body); outerBorder.Child = mainGrid; popup.Content = outerBorder; popup.ShowDialog(); } }