Initial commit to new repository
This commit is contained in:
635
src/AxCopilot/Views/SkillGalleryWindow.xaml.cs
Normal file
635
src/AxCopilot/Views/SkillGalleryWindow.xaml.cs
Normal file
@@ -0,0 +1,635 @@
|
||||
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;
|
||||
|
||||
/// <summary>스킬 갤러리 창. 설치된 모든 스킬을 카드 형태로 표시하고 관리합니다.</summary>
|
||||
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<SkillDefinition> FilterSkills(IReadOnlyList<SkillDefinition> 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user