using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using AxCopilot.Services;
using AxCopilot.Services.Agent;
namespace AxCopilot.Views;
/// 스킬 시각적 편집기. 새 스킬 생성 또는 기존 스킬 편집을 GUI로 지원합니다.
public partial class SkillEditorWindow : Window
{
// ── 아이콘 후보 목록 (Segoe MDL2 Assets 코드포인트) ──
private static readonly string[] IconCandidates =
[
"\uE70F", // Edit
"\uE8A5", // Document
"\uE943", // Code
"\uE74C", // Search
"\uE8B7", // Folder
"\uE896", // People
"\uE713", // Settings
"\uE753", // Cloud
"\uE774", // Camera
"\uE8D6", // Mail
"\uE8F1", // Lightbulb
"\uE7C3", // Data
"\uECA7", // Rocket
"\uE71E", // Chart
"\uE8C8", // Copy
"\uE8F6", // Process
"\uE81E", // Link
"\uEBD2", // AI
"\uE9D9", // Globe
"\uE77B", // Shield
];
private string _selectedIcon = "\uE70F";
private SkillDefinition? _editingSkill;
private readonly ToolRegistry _toolRegistry;
/// 새 스킬 모드로 열기.
public SkillEditorWindow()
{
InitializeComponent();
_toolRegistry = ToolRegistry.CreateDefault();
Loaded += (_, _) => { BuildIconSelector(); BuildToolChecklist(); UpdatePreview(); };
}
/// 편집 모드로 열기.
public SkillEditorWindow(SkillDefinition skill) : this()
{
_editingSkill = skill;
Loaded += (_, _) => LoadSkill(skill);
}
// ─── 타이틀바 ─────────────────────────────────────────────────────────
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// 타이틀바 우측 닫기 버튼 영역에서는 DragMove 실행하지 않음
var pos = e.GetPosition(this);
if (pos.X > ActualWidth - 50) 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 BtnCancel_Click(object sender, MouseButtonEventArgs e) => Close();
// ─── 아이콘 선택기 ────────────────────────────────────────────────────
private void BuildIconSelector()
{
IconSelectorPanel.Children.Clear();
var accentBrush = new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
var subBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
var bgBrush = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent;
foreach (var icon in IconCandidates)
{
var isSelected = icon == _selectedIcon;
var border = new Border
{
Width = 34,
Height = 34,
CornerRadius = new CornerRadius(8),
Margin = new Thickness(0, 0, 4, 4),
Cursor = Cursors.Hand,
Background = isSelected ? accentBrush : bgBrush,
BorderBrush = isSelected ? accentBrush : Brushes.Transparent,
BorderThickness = new Thickness(1.5),
Tag = icon,
};
border.Child = new TextBlock
{
Text = icon,
FontFamily = new FontFamily("Segoe MDL2 Assets"),
FontSize = 14,
Foreground = isSelected ? Brushes.White : subBrush,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
border.MouseLeftButtonUp += (s, _) =>
{
if (s is Border b && b.Tag is string ic)
{
_selectedIcon = ic;
BuildIconSelector();
}
};
// 호버
var capturedIcon = icon;
border.MouseEnter += (s, _) =>
{
if (s is Border b && capturedIcon != _selectedIcon)
b.Background = new SolidColorBrush(Color.FromArgb(0x30, 0x4B, 0x5E, 0xFC));
};
border.MouseLeave += (s, _) =>
{
if (s is Border b && capturedIcon != _selectedIcon)
b.Background = bgBrush;
};
IconSelectorPanel.Children.Add(border);
}
}
// ─── 도구 체크리스트 ──────────────────────────────────────────────────
private void BuildToolChecklist()
{
ToolCheckListPanel.Children.Clear();
var tools = _toolRegistry.All.OrderBy(t => t.Name).ToList();
var fgBrush = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
var subBrush = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
// 기존 스킬 편집 시 활성 도구 파싱
var activeTools = new HashSet(StringComparer.OrdinalIgnoreCase);
if (_editingSkill != null && !string.IsNullOrWhiteSpace(_editingSkill.AllowedTools))
{
foreach (var t in _editingSkill.AllowedTools.Split(',', StringSplitOptions.RemoveEmptyEntries))
activeTools.Add(t.Trim());
}
foreach (var tool in tools)
{
var cb = new CheckBox
{
Tag = tool.Name,
IsChecked = activeTools.Count == 0 || activeTools.Contains(tool.Name),
Margin = new Thickness(0, 0, 0, 4),
Style = TryFindResource("ToggleSwitch") as Style,
};
var row = new StackPanel
{
Orientation = Orientation.Horizontal,
Margin = new Thickness(0, 0, 0, 2),
};
row.Children.Add(cb);
row.Children.Add(new TextBlock
{
Text = tool.Name,
FontSize = 11.5,
FontFamily = new FontFamily("Consolas"),
Foreground = fgBrush,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(6, 0, 0, 0),
});
// 설명 툴팁
if (!string.IsNullOrEmpty(tool.Description))
row.ToolTip = tool.Description;
ToolCheckListPanel.Children.Add(row);
}
}
/// 체크된 도구 이름 목록을 가져옵니다.
private List GetCheckedTools()
{
var result = new List();
foreach (var child in ToolCheckListPanel.Children)
{
if (child is StackPanel row && row.Children.Count > 0 && row.Children[0] is CheckBox cb)
{
if (cb.IsChecked == true && cb.Tag is string name)
result.Add(name);
}
}
return result;
}
// ─── 템플릿 삽입 ──────────────────────────────────────────────────────
private void BtnInsertTemplate_Click(object sender, MouseButtonEventArgs e)
{
if (sender is not FrameworkElement el || el.Tag is not string tag) return;
var template = tag switch
{
"tools" => BuildToolListTemplate(),
"format" => """
## 출력 형식
1. 결과를 사용자에게 한국어로 설명합니다.
2. 코드 블록은 마크다운 형식으로 감쌉니다.
3. 핵심 정보를 먼저 제시하고, 세부 사항은 뒤에 붙입니다.
""",
"steps" => """
## 실행 단계
1. **분석**: 사용자의 요청을 분석합니다.
2. **계획**: 수행할 작업을 계획합니다.
3. **실행**: 도구를 사용하여 작업을 수행합니다.
4. **검증**: 결과를 검증합니다.
5. **보고**: 결과를 사용자에게 보고합니다.
""",
_ => "",
};
if (string.IsNullOrEmpty(template)) return;
var insertPos = TxtInstructions.CaretIndex;
var prefix = insertPos > 0 && TxtInstructions.Text.Length > 0
&& TxtInstructions.Text[insertPos - 1] != '\n' ? "\n\n" : "";
TxtInstructions.Text = TxtInstructions.Text.Insert(insertPos, prefix + template.Trim());
TxtInstructions.CaretIndex = insertPos + prefix.Length + template.Trim().Length;
TxtInstructions.Focus();
}
private string BuildToolListTemplate()
{
var checkedTools = GetCheckedTools();
if (checkedTools.Count == 0)
return "## 사용 가능한 도구\n\n(도구가 선택되지 않았습니다. 우측 패널에서 도구를 선택하세요.)";
var lines = new List { "## 사용 가능한 도구", "" };
foreach (var toolName in checkedTools)
{
var tool = _toolRegistry.All.FirstOrDefault(t => t.Name == toolName);
if (tool != null)
lines.Add($"- **{tool.Name}**: {tool.Description}");
}
return string.Join("\n", lines);
}
// ─── 미리보기 업데이트 ────────────────────────────────────────────────
private void TxtInstructions_TextChanged(object sender, TextChangedEventArgs e) => UpdatePreview();
private void UpdatePreview()
{
if (PreviewTokens == null || PreviewFileName == null) return;
var content = GenerateSkillContent();
var tokens = TokenEstimator.Estimate(content);
PreviewTokens.Text = $"예상 토큰: ~{tokens:N0}자 | {content.Length:N0}자";
var name = string.IsNullOrWhiteSpace(TxtName?.Text) ? "new-skill" : TxtName.Text.Trim();
PreviewFileName.Text = $"{name}.skill.md";
}
// ─── 스킬 콘텐츠 생성 ────────────────────────────────────────────────
private string GenerateSkillContent()
{
var name = TxtName?.Text?.Trim() ?? "new-skill";
var label = TxtLabel?.Text?.Trim() ?? "";
var desc = TxtDescription?.Text?.Trim() ?? "";
var instructions = TxtInstructions?.Text ?? "";
// 런타임 요구사항
var requiresTag = "";
if (CmbRequires?.SelectedItem is ComboBoxItem cbi && cbi.Tag is string req && !string.IsNullOrEmpty(req))
requiresTag = req;
// 도구 목록
var checkedTools = GetCheckedTools();
var allToolCount = _toolRegistry.All.Count;
var allowedToolsStr = checkedTools.Count < allToolCount
? string.Join(", ", checkedTools)
: ""; // 전체 선택이면 비워둠
// YAML 프론트매터 생성
var yaml = new List { "---" };
yaml.Add($"name: {name}");
if (!string.IsNullOrEmpty(label)) yaml.Add($"label: {label}");
if (!string.IsNullOrEmpty(desc)) yaml.Add($"description: {desc}");
yaml.Add($"icon: {_selectedIcon}");
if (!string.IsNullOrEmpty(requiresTag)) yaml.Add($"requires: {requiresTag}");
if (!string.IsNullOrEmpty(allowedToolsStr)) yaml.Add($"allowed-tools: {allowedToolsStr}");
yaml.Add("---");
yaml.Add("");
return string.Join("\n", yaml) + instructions;
}
// ─── 미리보기 버튼 ───────────────────────────────────────────────────
private void BtnPreview_Click(object sender, MouseButtonEventArgs e)
{
var content = GenerateSkillContent();
var previewWin = new Window
{
Title = "스킬 파일 미리보기",
Width = 640,
Height = 520,
WindowStyle = WindowStyle.None,
AllowsTransparency = true,
Background = Brushes.Transparent,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Owner = this,
};
var bgBrush = TryFindResource("LauncherBackground") as Brush ?? Brushes.White;
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 borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
var outerBorder = new Border
{
Background = bgBrush,
CornerRadius = new CornerRadius(12),
BorderBrush = borderBrush,
BorderThickness = new Thickness(1),
Effect = new System.Windows.Media.Effects.DropShadowEffect
{
BlurRadius = 20, ShadowDepth = 4, Opacity = 0.3, Color = Colors.Black,
},
};
var grid = new Grid();
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(44) });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
// 타이틀바
var titleBar = new Border
{
Background = itemBg,
CornerRadius = new CornerRadius(12, 12, 0, 0),
};
titleBar.MouseLeftButtonDown += (_, _) => previewWin.DragMove();
var titleText = new TextBlock
{
Text = "미리보기 — .skill.md",
FontSize = 14,
FontWeight = FontWeights.SemiBold,
Foreground = fgBrush,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(16, 0, 0, 0),
};
titleBar.Child = titleText;
Grid.SetRow(titleBar, 0);
// 콘텐츠
var textBox = new TextBox
{
Text = content,
FontFamily = new FontFamily("Consolas, Cascadia Code, Segoe UI"),
FontSize = 12.5,
IsReadOnly = true,
AcceptsReturn = true,
TextWrapping = TextWrapping.Wrap,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
Background = itemBg,
Foreground = fgBrush,
BorderThickness = new Thickness(0),
Padding = new Thickness(16, 12, 16, 12),
Margin = new Thickness(8, 8, 8, 0),
};
Grid.SetRow(textBox, 1);
// 하단
var bottomBar = new Border
{
Padding = new Thickness(16, 10, 16, 10),
CornerRadius = new CornerRadius(0, 0, 12, 12),
};
var closeBtn = new Border
{
CornerRadius = new CornerRadius(8),
Padding = new Thickness(18, 8, 18, 8),
Background = itemBg,
Cursor = Cursors.Hand,
HorizontalAlignment = HorizontalAlignment.Right,
};
closeBtn.Child = new TextBlock
{
Text = "닫기",
FontSize = 12.5,
Foreground = subBrush,
};
closeBtn.MouseLeftButtonUp += (_, _) => previewWin.Close();
closeBtn.MouseEnter += (s, _) =>
{
if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
};
closeBtn.MouseLeave += (s, _) =>
{
if (s is Border b) b.Background = itemBg;
};
bottomBar.Child = closeBtn;
Grid.SetRow(bottomBar, 2);
grid.Children.Add(titleBar);
grid.Children.Add(textBox);
grid.Children.Add(bottomBar);
outerBorder.Child = grid;
previewWin.Content = outerBorder;
previewWin.ShowDialog();
}
// ─── 저장 ─────────────────────────────────────────────────────────────
private void BtnSave_Click(object sender, MouseButtonEventArgs e)
{
// 유효성 검사
var name = TxtName.Text.Trim();
if (string.IsNullOrWhiteSpace(name))
{
StatusText.Text = "⚠ 이름을 입력하세요.";
StatusText.Foreground = new SolidColorBrush(Color.FromRgb(0xF8, 0x71, 0x71));
TxtName.Focus();
return;
}
// 영문 + 하이픈 + 숫자만 허용
if (!System.Text.RegularExpressions.Regex.IsMatch(name, @"^[a-zA-Z][a-zA-Z0-9\-]*$"))
{
StatusText.Text = "⚠ 이름은 영문으로 시작하며 영문, 숫자, 하이픈만 사용 가능합니다.";
StatusText.Foreground = new SolidColorBrush(Color.FromRgb(0xF8, 0x71, 0x71));
TxtName.Focus();
return;
}
if (string.IsNullOrWhiteSpace(TxtInstructions.Text))
{
StatusText.Text = "⚠ 지시사항을 입력하세요.";
StatusText.Foreground = new SolidColorBrush(Color.FromRgb(0xF8, 0x71, 0x71));
TxtInstructions.Focus();
return;
}
var content = GenerateSkillContent();
// 저장 경로 결정
string savePath;
if (_editingSkill != null)
{
// 편집 모드: 기존 파일 덮어쓰기
savePath = _editingSkill.FilePath;
}
else
{
// 새 스킬: 사용자 폴더에 저장
var userFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AxCopilot", "skills");
if (!Directory.Exists(userFolder))
Directory.CreateDirectory(userFolder);
savePath = Path.Combine(userFolder, $"{name}.skill.md");
// 파일 이름 충돌 시 숫자 추가
if (File.Exists(savePath))
{
var counter = 2;
while (File.Exists(Path.Combine(userFolder, $"{name}_{counter}.skill.md")))
counter++;
savePath = Path.Combine(userFolder, $"{name}_{counter}.skill.md");
}
}
try
{
File.WriteAllText(savePath, content, System.Text.Encoding.UTF8);
SkillService.LoadSkills();
StatusText.Text = $"✓ 저장 완료: {Path.GetFileName(savePath)}";
StatusText.Foreground = new SolidColorBrush(Color.FromRgb(0x34, 0xD3, 0x99));
// 편집 결과 반환
DialogResult = true;
}
catch (Exception ex)
{
CustomMessageBox.Show($"저장 실패: {ex.Message}", "스킬 저장");
}
}
// ─── 편집 모드 로드 ───────────────────────────────────────────────────
private void LoadSkill(SkillDefinition skill)
{
TitleText.Text = "스킬 편집";
TxtName.Text = skill.Name;
TxtLabel.Text = skill.Label;
TxtDescription.Text = skill.Description;
TxtInstructions.Text = skill.SystemPrompt;
// 아이콘 선택
_selectedIcon = IconCandidates.Contains(skill.Icon) ? skill.Icon : IconCandidates[0];
BuildIconSelector();
// 런타임 요구사항
for (int i = 0; i < CmbRequires.Items.Count; i++)
{
if (CmbRequires.Items[i] is ComboBoxItem item
&& item.Tag is string tag
&& string.Equals(tag, skill.Requires, StringComparison.OrdinalIgnoreCase))
{
CmbRequires.SelectedIndex = i;
break;
}
}
// 도구 체크리스트 (BuildToolChecklist에서 이미 _editingSkill 기반으로 설정됨)
UpdatePreview();
}
}