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();
}
}