Initial commit to new repository
This commit is contained in:
310
src/AxCopilot/Views/AboutWindow.xaml
Normal file
310
src/AxCopilot/Views/AboutWindow.xaml
Normal file
@@ -0,0 +1,310 @@
|
||||
<Window x:Class="AxCopilot.Views.AboutWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="AX Copilot — 정보"
|
||||
Width="460" SizeToContent="Height"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
UseLayoutRounding="True"
|
||||
SnapsToDevicePixels="True"
|
||||
Background="Transparent"
|
||||
ResizeMode="NoResize"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ShowInTaskbar="False"
|
||||
Topmost="True"
|
||||
MouseDown="Window_MouseDown">
|
||||
|
||||
<Window.Resources>
|
||||
<Style x:Key="LinkButton" TargetType="Button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<TextBlock x:Name="Tb"
|
||||
Text="{TemplateBinding Content}"
|
||||
FontSize="12"
|
||||
Foreground="#7B8FFF"
|
||||
TextDecorations="Underline"/>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Tb" Property="Foreground" Value="#4B5EFC"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="20">
|
||||
<Border CornerRadius="20" Background="White">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="Black" BlurRadius="22" ShadowDepth="4" Opacity="0.24"/>
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
<Border Background="White" CornerRadius="20" ClipToBounds="True">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<!-- 상단 헤더 그라데이션 -->
|
||||
<RowDefinition Height="210"/>
|
||||
<!-- 본문 -->
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ══ 헤더 ══ -->
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.Background>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Color="#1A1B2E" Offset="0"/>
|
||||
<GradientStop Color="#2B2D5B" Offset="0.5"/>
|
||||
<GradientStop Color="#3B4ECC" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
</Grid.Background>
|
||||
|
||||
<!-- 닫기 버튼 -->
|
||||
<Button HorizontalAlignment="Right" VerticalAlignment="Top"
|
||||
Margin="0,14,16,0"
|
||||
Width="28" Height="28"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
Cursor="Hand" Click="Close_Click">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" CornerRadius="14" Background="Transparent">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="11" Foreground="#88AAFFEE"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#22FFFFFF"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
|
||||
<!-- 앱 아이콘 (항상 표시) -->
|
||||
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0,10,0,0">
|
||||
|
||||
<Border Width="88" Height="88" CornerRadius="44"
|
||||
HorizontalAlignment="Center"
|
||||
BorderBrush="#33FFFFFF" BorderThickness="2">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Color="#2E3060" Offset="0"/>
|
||||
<GradientStop Color="#3B4ECC" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
<Grid>
|
||||
<!-- 다이아몬드 픽셀 아이콘 (벡터, 해상도 독립) -->
|
||||
<Canvas x:Name="DiamondIconCanvas"
|
||||
Width="48" Height="48"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Canvas.RenderTransform>
|
||||
<RotateTransform Angle="45" CenterX="24" CenterY="24"/>
|
||||
</Canvas.RenderTransform>
|
||||
<!-- 좌상→상: Blue -->
|
||||
<Rectangle Canvas.Left="1" Canvas.Top="1"
|
||||
Width="21" Height="21"
|
||||
RadiusX="3" RadiusY="3"
|
||||
Fill="#4488FF"/>
|
||||
<!-- 우상→우: Green -->
|
||||
<Rectangle Canvas.Left="26" Canvas.Top="1"
|
||||
Width="21" Height="21"
|
||||
RadiusX="3" RadiusY="3"
|
||||
Fill="#44DD66"/>
|
||||
<!-- 좌하→좌: Green -->
|
||||
<Rectangle Canvas.Left="1" Canvas.Top="26"
|
||||
Width="21" Height="21"
|
||||
RadiusX="3" RadiusY="3"
|
||||
Fill="#44DD66"/>
|
||||
<!-- 우하→하: Red -->
|
||||
<Rectangle Canvas.Left="26" Canvas.Top="26"
|
||||
Width="21" Height="21"
|
||||
RadiusX="3" RadiusY="3"
|
||||
Fill="#FF4466"/>
|
||||
</Canvas>
|
||||
<!-- 비트맵 아이콘 폴백 (숨김) -->
|
||||
<Image x:Name="AppIconImage" Visibility="Collapsed"
|
||||
Stretch="UniformToFill"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||
<TextBlock x:Name="FallbackIcon" Visibility="Collapsed"
|
||||
Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="38" Foreground="#88FFFFFF"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
<!-- 마스코트 이미지 (숨겨진 상태 — 사용자 클릭 시 오버레이용으로만 사용) -->
|
||||
<Image x:Name="MascotImage" Visibility="Collapsed"/>
|
||||
|
||||
<TextBlock x:Name="AppNameText"
|
||||
Text="AX Copilot"
|
||||
FontSize="20" FontWeight="Bold"
|
||||
Foreground="White"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,14,0,3"/>
|
||||
<TextBlock x:Name="VersionText"
|
||||
Text="v1.0.0"
|
||||
FontSize="12" Foreground="#88AACCFF"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- ══ 마스코트 오버레이 (클릭 시 표시) ══ -->
|
||||
<Grid x:Name="MascotOverlay"
|
||||
Grid.Row="0" Grid.RowSpan="2"
|
||||
Visibility="Collapsed"
|
||||
Panel.ZIndex="10">
|
||||
<!-- 어두운 배경 (클릭 시 닫기) -->
|
||||
<Rectangle Fill="#CC101025"
|
||||
MouseDown="HideMascot_Click"/>
|
||||
<!-- 마스코트 이미지 -->
|
||||
<Border Width="240" Height="240"
|
||||
CornerRadius="20"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
ClipToBounds="True"
|
||||
BorderBrush="#33FFFFFF"
|
||||
BorderThickness="1.5">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="Black" BlurRadius="40"
|
||||
ShadowDepth="0" Opacity="0.7"/>
|
||||
</Border.Effect>
|
||||
<Image x:Name="MascotOverlayImage"
|
||||
Stretch="UniformToFill"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||
</Border>
|
||||
<!-- 닫기 힌트 -->
|
||||
<TextBlock Text="클릭하여 닫기"
|
||||
FontSize="11" Foreground="#88AACCFF"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,270,0,0"
|
||||
IsHitTestVisible="False"/>
|
||||
</Grid>
|
||||
|
||||
<!-- ══ 본문 ══ -->
|
||||
<StackPanel Grid.Row="1" Margin="32,24,32,20">
|
||||
|
||||
<!-- 구분선 -->
|
||||
<Rectangle Height="1" Fill="#F0F0F8" Margin="0,0,0,20"/>
|
||||
|
||||
<!-- 개발 목적 -->
|
||||
<Border Background="#F7F8FF" CornerRadius="12" Padding="16,13" Margin="0,0,0,20">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="12" Foreground="#4B5EFC"
|
||||
VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="개발 목적"
|
||||
FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="#4B5EFC"/>
|
||||
</StackPanel>
|
||||
<TextBlock x:Name="PurposeText"
|
||||
Text="업무 편의성 증가 및 시스템과 직관적인 연결을 위해 제작"
|
||||
FontSize="12" Foreground="#444466"
|
||||
TextWrapping="Wrap" LineHeight="18"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 개발자 정보 -->
|
||||
<StackPanel Margin="0,0,0,16">
|
||||
<TextBlock Text="개발자 정보"
|
||||
FontSize="10.5" FontWeight="SemiBold"
|
||||
Foreground="#AAAACC" Margin="0,0,0,10"/>
|
||||
|
||||
<!-- 조직 (클릭 시 마스코트 팝업) -->
|
||||
<Grid Margin="0,0,0,10" Cursor="Hand" Background="Transparent"
|
||||
MouseDown="ShowMascot_Click">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="28"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Width="24" Height="24" CornerRadius="6"
|
||||
Background="#EEF0FF" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="12" Foreground="#4B5EFC"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" Margin="10,0,0,0" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="CompanyNameText"
|
||||
Text="AX연구소 AI팀"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="#1A1B2E"/>
|
||||
<TextBlock Text="백승재 · SW Architect"
|
||||
FontSize="11" Foreground="#7777AA" Margin="0,2,0,0"/>
|
||||
</StackPanel>
|
||||
<!-- 클릭 힌트 아이콘 -->
|
||||
<TextBlock Grid.Column="2" Text=""
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="9"
|
||||
Foreground="#CCCCDD" VerticalAlignment="Center"
|
||||
Margin="0,0,2,0"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 블로그 -->
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="28"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Width="24" Height="24" CornerRadius="6"
|
||||
Background="#EEF0FF" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="12" Foreground="#4B5EFC"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" Margin="10,0,0,0" VerticalAlignment="Center">
|
||||
<TextBlock Text="블로그" FontSize="11" Foreground="#9999BB" Margin="0,0,0,2"/>
|
||||
<Button x:Name="BlogLinkBtn"
|
||||
Content="www.swarchitect.net"
|
||||
Style="{StaticResource LinkButton}"
|
||||
Click="Blog_Click"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 기여자 -->
|
||||
<Grid Margin="0,12,0,0" x:Name="ContributorsGrid" Visibility="Collapsed">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="28"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Width="24" Height="24" CornerRadius="6"
|
||||
Background="#EEF0FF" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="12" Foreground="#4B5EFC"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" Margin="10,0,0,0" VerticalAlignment="Center">
|
||||
<TextBlock Text="기여자" FontSize="11" Foreground="#9999BB" Margin="0,0,0,2"/>
|
||||
<TextBlock x:Name="ContributorsText"
|
||||
FontSize="12" Foreground="#444466"
|
||||
TextWrapping="Wrap" LineHeight="18"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 구분선 -->
|
||||
<Rectangle Height="1" Fill="#F0F0F8" Margin="0,0,0,14"/>
|
||||
|
||||
<!-- 버전/빌드 정보 -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="10" Foreground="#CCCCDD"
|
||||
VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock x:Name="BuildInfoText"
|
||||
FontSize="10.5" Foreground="#BBBBCC"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
195
src/AxCopilot/Views/AboutWindow.xaml.cs
Normal file
195
src/AxCopilot/Views/AboutWindow.xaml.cs
Normal file
@@ -0,0 +1,195 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class AboutWindow : Window
|
||||
{
|
||||
public AboutWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += OnLoaded;
|
||||
KeyDown += (_, e) => { if (e.Key == Key.Escape) Close(); };
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 버전 정보
|
||||
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
VersionText.Text = version != null ? $"v{version.Major}.{version.Minor}.{version.Build}" : "v1.0";
|
||||
BuildInfoText.Text = $"AX Copilot \u00b7 Commander + Agent \u00b7 \u00a9 2026";
|
||||
|
||||
// about.json에서 모든 텍스트 로드 (없으면 기본값 유지)
|
||||
TryLoadBranding(version);
|
||||
|
||||
// 상단: 앱 아이콘 로드
|
||||
TryLoadAppIcon();
|
||||
// 마스코트: 숨겨진 상태로 로드 (사용자 클릭 시 오버레이용)
|
||||
TryLoadMascot();
|
||||
}
|
||||
|
||||
private void TryLoadBranding(Version? version)
|
||||
{
|
||||
try
|
||||
{
|
||||
var uri = new Uri("pack://application:,,,/Assets/about.json");
|
||||
var info = System.Windows.Application.GetResourceStream(uri);
|
||||
if (info == null) return;
|
||||
|
||||
using var reader = new StreamReader(info.Stream);
|
||||
using var doc = JsonDocument.Parse(reader.ReadToEnd());
|
||||
var root = doc.RootElement;
|
||||
|
||||
string Get(string key) =>
|
||||
root.TryGetProperty(key, out var v) ? v.GetString() ?? "" : "";
|
||||
|
||||
var appName = Get("appName");
|
||||
var company = Get("companyName");
|
||||
var author = Get("authorName");
|
||||
var title = Get("authorTitle");
|
||||
var purpose = Get("purpose");
|
||||
var copyright = Get("copyright");
|
||||
var blogUrl = Get("blogUrl");
|
||||
|
||||
// 앱 이름 (헤더)
|
||||
if (!string.IsNullOrWhiteSpace(appName))
|
||||
AppNameText.Text = appName;
|
||||
|
||||
// 회사명 (조직명만 표시 — 이름/직급은 하단에 별도 표시)
|
||||
if (!string.IsNullOrWhiteSpace(company))
|
||||
CompanyNameText.Text = company;
|
||||
|
||||
// 목적
|
||||
if (!string.IsNullOrWhiteSpace(purpose))
|
||||
PurposeText.Text = purpose;
|
||||
|
||||
// 저작권 + 빌드 정보
|
||||
if (!string.IsNullOrWhiteSpace(copyright))
|
||||
{
|
||||
var ver = version != null ? $"v{version.Major}.{version.Minor}.{version.Build}" : "v1.0";
|
||||
BuildInfoText.Text = $"{appName} \u00b7 {ver} \u00b7 Commander + Agent \u00b7 {copyright}";
|
||||
}
|
||||
|
||||
// 블로그 URL
|
||||
if (!string.IsNullOrWhiteSpace(blogUrl) && BlogLinkBtn != null)
|
||||
BlogLinkBtn.Content = blogUrl;
|
||||
|
||||
// 기여자 목록
|
||||
var contributors = Get("contributors");
|
||||
if (!string.IsNullOrWhiteSpace(contributors))
|
||||
{
|
||||
ContributorsText.Text = contributors;
|
||||
ContributorsGrid.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
catch { /* 브랜딩 로드 실패 시 기본값 유지 */ }
|
||||
}
|
||||
|
||||
private void TryLoadAppIcon()
|
||||
{
|
||||
try
|
||||
{
|
||||
var uri = new Uri("pack://application:,,,/Assets/icon.ico");
|
||||
var stream = System.Windows.Application.GetResourceStream(uri)?.Stream;
|
||||
if (stream != null)
|
||||
{
|
||||
var bmp = new BitmapImage();
|
||||
bmp.BeginInit();
|
||||
bmp.StreamSource = stream;
|
||||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bmp.DecodePixelWidth = 176;
|
||||
bmp.EndInit();
|
||||
bmp.Freeze();
|
||||
|
||||
AppIconImage.Source = bmp;
|
||||
FallbackIcon.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private void TryLoadMascot()
|
||||
{
|
||||
// 내장 리소스에서 마스코트 로드 (오버레이용, 상단에는 표시 안 함)
|
||||
foreach (var name in new[] { "mascot.png", "mascot.jpg", "mascot.webp" })
|
||||
{
|
||||
try
|
||||
{
|
||||
var uri = new Uri($"pack://application:,,,/Assets/{name}");
|
||||
var stream = System.Windows.Application.GetResourceStream(uri)?.Stream;
|
||||
if (stream == null) continue;
|
||||
|
||||
var bmp = new BitmapImage();
|
||||
bmp.BeginInit();
|
||||
bmp.StreamSource = stream;
|
||||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bmp.DecodePixelWidth = 400;
|
||||
bmp.EndInit();
|
||||
bmp.Freeze();
|
||||
|
||||
MascotImage.Source = bmp;
|
||||
return;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// 파일 시스템 폴백
|
||||
var exeDir = Path.GetDirectoryName(Environment.ProcessPath) ?? AppContext.BaseDirectory;
|
||||
foreach (var name in new[] { "mascot.png", "mascot.jpg", "mascot.webp" })
|
||||
{
|
||||
var path = Path.Combine(exeDir, "Assets", name);
|
||||
if (!File.Exists(path)) continue;
|
||||
try
|
||||
{
|
||||
var bmp = new BitmapImage();
|
||||
bmp.BeginInit();
|
||||
bmp.UriSource = new Uri(path, UriKind.Absolute);
|
||||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bmp.DecodePixelWidth = 400;
|
||||
bmp.EndInit();
|
||||
|
||||
MascotImage.Source = bmp;
|
||||
return;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowMascot_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (MascotImage.Source == null) return;
|
||||
MascotOverlayImage.Source = MascotImage.Source;
|
||||
MascotOverlay.Visibility = Visibility.Visible;
|
||||
e.Handled = true; // DragMove 방지
|
||||
}
|
||||
|
||||
private void HideMascot_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
MascotOverlay.Visibility = Visibility.Collapsed;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
// 창 드래그 이동
|
||||
private void Window_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ChangedButton == MouseButton.Left && e.LeftButton == MouseButtonState.Pressed)
|
||||
try { DragMove(); } catch { }
|
||||
}
|
||||
|
||||
private void Close_Click(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private void Blog_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo("https://www.swarchitect.net")
|
||||
{ UseShellExecute = true });
|
||||
}
|
||||
catch { /* 브라우저 열기 실패 무시 */ }
|
||||
}
|
||||
}
|
||||
145
src/AxCopilot/Views/AgentStatsDashboardWindow.xaml
Normal file
145
src/AxCopilot/Views/AgentStatsDashboardWindow.xaml
Normal file
@@ -0,0 +1,145 @@
|
||||
<Window x:Class="AxCopilot.Views.AgentStatsDashboardWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="에이전트 실행 통계"
|
||||
Width="780" Height="580"
|
||||
WindowStyle="None" AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="CanResizeWithGrip">
|
||||
<Border Background="{DynamicResource LauncherBackground}" CornerRadius="12"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="20" ShadowDepth="4" Opacity="0.3" Color="Black"/>
|
||||
</Border.Effect>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="44"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ── 타이틀바 ── -->
|
||||
<Border Grid.Row="0" CornerRadius="12,12,0,0" Background="{DynamicResource ItemBackground}"
|
||||
MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
|
||||
<Grid>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="16,0,0,0">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="14"
|
||||
Foreground="#A78BFA" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="에이전트 실행 통계" FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,12,0">
|
||||
<Border x:Name="BtnClose" Width="28" Height="28" CornerRadius="6" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnClose_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#33FF4444"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── 기간 필터 탭 ── -->
|
||||
<Border Grid.Row="1" Padding="16,10,16,0">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Border x:Name="FilterToday" Tag="1" CornerRadius="8" Padding="14,5" Margin="0,0,6,0"
|
||||
Background="#4B5EFC" Cursor="Hand" MouseLeftButtonUp="Filter_Click">
|
||||
<TextBlock Text="오늘" FontSize="12" FontWeight="SemiBold" Foreground="White"/>
|
||||
</Border>
|
||||
<Border x:Name="Filter7d" Tag="7" CornerRadius="8" Padding="14,5" Margin="0,0,6,0"
|
||||
Background="{DynamicResource ItemBackground}" Cursor="Hand" MouseLeftButtonUp="Filter_Click">
|
||||
<TextBlock Text="7일" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
<Border x:Name="Filter30d" Tag="30" CornerRadius="8" Padding="14,5" Margin="0,0,6,0"
|
||||
Background="{DynamicResource ItemBackground}" Cursor="Hand" MouseLeftButtonUp="Filter_Click">
|
||||
<TextBlock Text="30일" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
<Border x:Name="FilterAll" Tag="0" CornerRadius="8" Padding="14,5"
|
||||
Background="{DynamicResource ItemBackground}" Cursor="Hand" MouseLeftButtonUp="Filter_Click">
|
||||
<TextBlock Text="전체" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
<Border Width="1" Background="{DynamicResource BorderColor}" Margin="14,2" VerticalAlignment="Stretch"/>
|
||||
<Border CornerRadius="8" Padding="14,5" Background="{DynamicResource ItemBackground}"
|
||||
Cursor="Hand" MouseLeftButtonUp="BtnClearStats_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="11"
|
||||
Foreground="#F87171" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock Text="초기화" FontSize="12" Foreground="#F87171"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── 메인 콘텐츠 ── -->
|
||||
<ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto" Padding="16,10,16,0">
|
||||
<StackPanel>
|
||||
<!-- 요약 카드 4종 -->
|
||||
<UniformGrid x:Name="SummaryCards" Columns="4" Margin="0,0,0,12"/>
|
||||
|
||||
<!-- 일별 활동 차트 -->
|
||||
<Border Background="{DynamicResource ItemBackground}" CornerRadius="10"
|
||||
Padding="16,12" Margin="0,0,0,10">
|
||||
<StackPanel>
|
||||
<TextBlock x:Name="DailyChartTitle" Text="일별 실행 횟수"
|
||||
FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,10"/>
|
||||
<Canvas x:Name="DailyBarChart" Height="100" ClipToBounds="True"/>
|
||||
<Canvas x:Name="DailyBarLabels" Height="18" ClipToBounds="True" Margin="0,2,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 도구 사용 빈도 TOP 10 + 모델 분포 -->
|
||||
<Grid Margin="0,0,0,10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="200"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 도구 TOP 10 -->
|
||||
<Border Grid.Column="0" Background="{DynamicResource ItemBackground}"
|
||||
CornerRadius="10" Padding="16,12" Margin="0,0,8,0">
|
||||
<StackPanel>
|
||||
<TextBlock Text="도구 사용 TOP 10" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,10"/>
|
||||
<StackPanel x:Name="ToolFreqPanel"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 모델 / 탭 분포 -->
|
||||
<Border Grid.Column="1" Background="{DynamicResource ItemBackground}"
|
||||
CornerRadius="10" Padding="16,12">
|
||||
<StackPanel>
|
||||
<TextBlock Text="탭별 실행" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,10"/>
|
||||
<StackPanel x:Name="TabBreakPanel"/>
|
||||
<TextBlock Text="모델별" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,10,0,8"/>
|
||||
<StackPanel x:Name="ModelBreakPanel"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ── 상태바 ── -->
|
||||
<Border Grid.Row="3" BorderBrush="{DynamicResource BorderColor}" BorderThickness="0,1,0,0"
|
||||
Padding="16,8" CornerRadius="0,0,12,12">
|
||||
<TextBlock x:Name="StatusText" FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
Text="데이터를 불러오는 중..."/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
398
src/AxCopilot/Views/AgentStatsDashboardWindow.xaml.cs
Normal file
398
src/AxCopilot/Views/AgentStatsDashboardWindow.xaml.cs
Normal file
@@ -0,0 +1,398 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>에이전트 실행 통계 대시보드 창.</summary>
|
||||
public partial class AgentStatsDashboardWindow : Window
|
||||
{
|
||||
private int _currentDays = 1; // 현재 필터 (오늘=1)
|
||||
|
||||
public AgentStatsDashboardWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += (_, _) => LoadStats(_currentDays);
|
||||
}
|
||||
|
||||
// ─── 이벤트 핸들러 ────────────────────────────────────────────────────
|
||||
|
||||
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 Filter_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (sender is not Border b || b.Tag is not string tagStr) return;
|
||||
if (!int.TryParse(tagStr, out var days)) return;
|
||||
|
||||
_currentDays = days;
|
||||
UpdateFilterButtons(b);
|
||||
LoadStats(days);
|
||||
}
|
||||
|
||||
private void BtnClearStats_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var result = CustomMessageBox.Show(
|
||||
"모든 통계 데이터를 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.",
|
||||
"통계 초기화",
|
||||
MessageBoxButton.YesNo, MessageBoxImage.Question);
|
||||
if (result != MessageBoxResult.Yes) return;
|
||||
|
||||
AgentStatsService.Clear();
|
||||
LoadStats(_currentDays);
|
||||
}
|
||||
|
||||
// ─── 통계 로드/렌더링 ─────────────────────────────────────────────────
|
||||
|
||||
private void LoadStats(int days)
|
||||
{
|
||||
var summary = AgentStatsService.Aggregate(days);
|
||||
RenderSummaryCards(summary);
|
||||
RenderDailyChart(summary, days);
|
||||
RenderToolFreq(summary);
|
||||
RenderBreakdowns(summary);
|
||||
UpdateStatus(summary, days);
|
||||
}
|
||||
|
||||
private void RenderSummaryCards(AgentStatsService.AgentStatsSummary s)
|
||||
{
|
||||
SummaryCards.Children.Clear();
|
||||
SummaryCards.Children.Add(MakeSummaryCard("\uE9D9", "총 실행", s.TotalSessions.ToString("N0"), "#A78BFA"));
|
||||
SummaryCards.Children.Add(MakeSummaryCard("\uE8A7", "도구 호출", s.TotalToolCalls.ToString("N0"), "#3B82F6"));
|
||||
SummaryCards.Children.Add(MakeSummaryCard("\uE7C8", "총 토큰", FormatTokens(s.TotalTokens), "#10B981"));
|
||||
SummaryCards.Children.Add(MakeSummaryCard("\uE73E", "재시도 품질",
|
||||
$"{s.RetryQualityRate * 100:F0}%",
|
||||
"#F59E0B"));
|
||||
}
|
||||
|
||||
private Border MakeSummaryCard(string icon, string label, string value, string colorHex)
|
||||
{
|
||||
var bg = TryFindResource("ItemBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2A, 0x40));
|
||||
var fg = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var sub = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var col = (Color)ColorConverter.ConvertFromString(colorHex);
|
||||
|
||||
var card = new Border
|
||||
{
|
||||
Background = bg,
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(14, 12, 14, 12),
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
};
|
||||
var sp = new StackPanel();
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 16,
|
||||
Foreground = new SolidColorBrush(col),
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
});
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = value,
|
||||
FontSize = 20,
|
||||
FontWeight = FontWeights.Bold,
|
||||
Foreground = fg,
|
||||
});
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 11,
|
||||
Foreground = sub,
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
});
|
||||
card.Child = sp;
|
||||
return card;
|
||||
}
|
||||
|
||||
private void RenderDailyChart(AgentStatsService.AgentStatsSummary s, int days)
|
||||
{
|
||||
DailyBarChart.Children.Clear();
|
||||
DailyBarLabels.Children.Clear();
|
||||
|
||||
// 최근 N일 날짜 범위 구성
|
||||
var endDate = DateTime.Today;
|
||||
int chartDays = days == 1 ? 1 : days == 0 ? 30 : days;
|
||||
var dates = Enumerable.Range(0, chartDays)
|
||||
.Select(i => endDate.AddDays(-(chartDays - 1 - i)))
|
||||
.ToList();
|
||||
|
||||
var maxVal = dates
|
||||
.Select(d => s.DailySessions.GetValueOrDefault(d.ToString("yyyy-MM-dd"), 0))
|
||||
.DefaultIfEmpty(0).Max();
|
||||
if (maxVal == 0) maxVal = 1;
|
||||
|
||||
var barColor = (Color)ColorConverter.ConvertFromString("#4B5EFC");
|
||||
var chartW = 748.0; // 근사값 (Canvas는 레이아웃 후 ActualWidth 확정)
|
||||
var barGap = 2.0;
|
||||
var barW = Math.Max(4, (chartW - barGap * dates.Count) / dates.Count);
|
||||
var chartH = 100.0;
|
||||
|
||||
for (int i = 0; i < dates.Count; i++)
|
||||
{
|
||||
var dateKey = dates[i].ToString("yyyy-MM-dd");
|
||||
var val = s.DailySessions.GetValueOrDefault(dateKey, 0);
|
||||
var barH = Math.Max(2, val / (double)maxVal * (chartH - 4));
|
||||
var x = i * (barW + barGap);
|
||||
|
||||
var rect = new Rectangle
|
||||
{
|
||||
Width = barW,
|
||||
Height = barH,
|
||||
Fill = new SolidColorBrush(Color.FromArgb(0xCC,
|
||||
barColor.R, barColor.G, barColor.B)),
|
||||
RadiusX = 3,
|
||||
RadiusY = 3,
|
||||
ToolTip = $"{dateKey}: {val}회",
|
||||
};
|
||||
Canvas.SetLeft(rect, x);
|
||||
Canvas.SetTop(rect, chartH - barH);
|
||||
DailyBarChart.Children.Add(rect);
|
||||
|
||||
// 레이블: 7일 이하면 모두 표시, 그 이상이면 월요일만
|
||||
if (dates.Count <= 7 || dates[i].DayOfWeek == DayOfWeek.Monday)
|
||||
{
|
||||
var lbl = new TextBlock
|
||||
{
|
||||
Text = dates[i].ToString("MM/dd"),
|
||||
FontSize = 9,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
};
|
||||
Canvas.SetLeft(lbl, x);
|
||||
DailyBarLabels.Children.Add(lbl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderToolFreq(AgentStatsService.AgentStatsSummary s)
|
||||
{
|
||||
ToolFreqPanel.Children.Clear();
|
||||
if (s.ToolFrequency.Count == 0)
|
||||
{
|
||||
ToolFreqPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "데이터 없음",
|
||||
FontSize = 12,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var maxVal = s.ToolFrequency.Values.Max();
|
||||
var barColor = (Color)ColorConverter.ConvertFromString("#3B82F6");
|
||||
|
||||
foreach (var (tool, count) in s.ToolFrequency)
|
||||
{
|
||||
var ratio = (double)count / maxVal;
|
||||
var row = new Grid { Margin = new Thickness(0, 2, 0, 2) };
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(120) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition());
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(36) });
|
||||
|
||||
var nameTb = new TextBlock
|
||||
{
|
||||
Text = tool,
|
||||
FontSize = 11,
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
Grid.SetColumn(nameTb, 0);
|
||||
|
||||
var barBg = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x20, 0x3B, 0x82, 0xF6)),
|
||||
CornerRadius = new CornerRadius(3),
|
||||
Height = 10,
|
||||
Margin = new Thickness(6, 0, 4, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
var barFg = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(0xCC, barColor.R, barColor.G, barColor.B)),
|
||||
CornerRadius = new CornerRadius(3),
|
||||
Height = 10,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
};
|
||||
barFg.Width = ratio * 150; // 최대 150px
|
||||
barBg.Child = barFg;
|
||||
Grid.SetColumn(barBg, 1);
|
||||
|
||||
var countTb = new TextBlock
|
||||
{
|
||||
Text = count.ToString(),
|
||||
FontSize = 11,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
Grid.SetColumn(countTb, 2);
|
||||
|
||||
row.Children.Add(nameTb);
|
||||
row.Children.Add(barBg);
|
||||
row.Children.Add(countTb);
|
||||
ToolFreqPanel.Children.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderBreakdowns(AgentStatsService.AgentStatsSummary s)
|
||||
{
|
||||
// 탭 분포
|
||||
TabBreakPanel.Children.Clear();
|
||||
var tabColors = new Dictionary<string, string>
|
||||
{
|
||||
["Cowork"] = "#A78BFA",
|
||||
["Code"] = "#34D399",
|
||||
["Chat"] = "#3B82F6",
|
||||
};
|
||||
foreach (var (tab, count) in s.TabBreakdown.OrderByDescending(kv => kv.Value))
|
||||
{
|
||||
var c = tabColors.GetValueOrDefault(tab, "#9CA3AF");
|
||||
TabBreakPanel.Children.Add(MakeBreakRow(tab, count, c,
|
||||
s.TabBreakdown.Values.DefaultIfEmpty(1).Max()));
|
||||
}
|
||||
if (s.TabBreakdown.Count == 0)
|
||||
TabBreakPanel.Children.Add(MakeEmptyText());
|
||||
|
||||
// 모델 분포
|
||||
ModelBreakPanel.Children.Clear();
|
||||
foreach (var (model, count) in s.ModelBreakdown.OrderByDescending(kv => kv.Value))
|
||||
{
|
||||
var shortName = model.Length > 18 ? model[..18] + "…" : model;
|
||||
ModelBreakPanel.Children.Add(MakeBreakRow(shortName, count, "#F59E0B",
|
||||
s.ModelBreakdown.Values.DefaultIfEmpty(1).Max()));
|
||||
}
|
||||
if (s.ModelBreakdown.Count == 0)
|
||||
ModelBreakPanel.Children.Add(MakeEmptyText());
|
||||
}
|
||||
|
||||
private UIElement MakeBreakRow(string label, int count, string colorHex, int maxVal)
|
||||
{
|
||||
var ratio = maxVal > 0 ? (double)count / maxVal : 0;
|
||||
var col = (Color)ColorConverter.ConvertFromString(colorHex);
|
||||
|
||||
var row = new StackPanel { Margin = new Thickness(0, 2, 0, 2) };
|
||||
var header = new Grid();
|
||||
header.ColumnDefinitions.Add(new ColumnDefinition());
|
||||
header.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) });
|
||||
|
||||
var lbl = new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 11,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||||
};
|
||||
Grid.SetColumn(lbl, 0);
|
||||
|
||||
var cnt = new TextBlock
|
||||
{
|
||||
Text = count.ToString(),
|
||||
FontSize = 11,
|
||||
FontWeight= FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
};
|
||||
Grid.SetColumn(cnt, 1);
|
||||
header.Children.Add(lbl);
|
||||
header.Children.Add(cnt);
|
||||
row.Children.Add(header);
|
||||
|
||||
// 바 차트
|
||||
var barBg = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x20, col.R, col.G, col.B)),
|
||||
CornerRadius = new CornerRadius(3),
|
||||
Height = 6,
|
||||
Margin = new Thickness(0, 3, 0, 0),
|
||||
};
|
||||
var barFg = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(0xCC, col.R, col.G, col.B)),
|
||||
CornerRadius = new CornerRadius(3),
|
||||
Height = 6,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
};
|
||||
barFg.Width = ratio * 160; // 최대 160px
|
||||
barBg.Child = barFg;
|
||||
row.Children.Add(barBg);
|
||||
return row;
|
||||
}
|
||||
|
||||
private TextBlock MakeEmptyText() => new()
|
||||
{
|
||||
Text = "데이터 없음",
|
||||
FontSize = 11,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
};
|
||||
|
||||
private void UpdateFilterButtons(Border active)
|
||||
{
|
||||
foreach (var btn in new[] { FilterToday, Filter7d, Filter30d, FilterAll })
|
||||
{
|
||||
var isActive = btn == active;
|
||||
btn.Background = isActive
|
||||
? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC))
|
||||
: TryFindResource("ItemBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x30, 0x30, 0x45));
|
||||
if (btn.Child is TextBlock tb)
|
||||
{
|
||||
tb.Foreground = isActive
|
||||
? Brushes.White
|
||||
: TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStatus(AgentStatsService.AgentStatsSummary s, int days)
|
||||
{
|
||||
var period = days switch
|
||||
{
|
||||
1 => "오늘",
|
||||
7 => "최근 7일",
|
||||
30 => "최근 30일",
|
||||
_ => "전체 기간",
|
||||
};
|
||||
var fileSize = AgentStatsService.GetFileSize();
|
||||
var taskQuality = string.Join(", ",
|
||||
s.RetryQualityByTaskType
|
||||
.OrderByDescending(kv => kv.Value)
|
||||
.ThenBy(kv => kv.Key, StringComparer.Ordinal)
|
||||
.Take(3)
|
||||
.Select(kv => $"{kv.Key} {kv.Value * 100:F0}%"));
|
||||
var taskQualityText = string.IsNullOrWhiteSpace(taskQuality)
|
||||
? "task 품질 데이터 없음"
|
||||
: $"task 품질 {taskQuality}";
|
||||
StatusText.Text =
|
||||
$"{period} 기준 | 세션 {s.TotalSessions}회, 도구 호출 {s.TotalToolCalls}회, " +
|
||||
$"토큰 {FormatTokens(s.TotalTokens)} | 재시도 복구 {s.TotalRecoveredAfterFailure}, 반복차단 {s.TotalRepeatedFailureBlocked} | {taskQualityText} | 저장 파일 {FormatFileSize(fileSize)}";
|
||||
}
|
||||
|
||||
private static string FormatTokens(int t) =>
|
||||
t >= 1_000_000 ? $"{t / 1_000_000.0:F1}M"
|
||||
: t >= 1_000 ? $"{t / 1_000.0:F1}K"
|
||||
: t.ToString();
|
||||
|
||||
private static string FormatFileSize(long bytes) =>
|
||||
bytes >= 1024 * 1024 ? $"{bytes / (1024.0 * 1024):F1}MB"
|
||||
: bytes >= 1024 ? $"{bytes / 1024.0:F1}KB"
|
||||
: $"{bytes}B";
|
||||
}
|
||||
1548
src/AxCopilot/Views/ChatWindow.xaml
Normal file
1548
src/AxCopilot/Views/ChatWindow.xaml
Normal file
File diff suppressed because it is too large
Load Diff
14042
src/AxCopilot/Views/ChatWindow.xaml.cs
Normal file
14042
src/AxCopilot/Views/ChatWindow.xaml.cs
Normal file
File diff suppressed because it is too large
Load Diff
11207
src/AxCopilot/Views/ChatWindow.xaml.cs.bak-broken
Normal file
11207
src/AxCopilot/Views/ChatWindow.xaml.cs.bak-broken
Normal file
File diff suppressed because it is too large
Load Diff
10800
src/AxCopilot/Views/ChatWindow.xaml.cs.broken
Normal file
10800
src/AxCopilot/Views/ChatWindow.xaml.cs.broken
Normal file
File diff suppressed because it is too large
Load Diff
12693
src/AxCopilot/Views/ChatWindow.xaml.cs.pre-restore
Normal file
12693
src/AxCopilot/Views/ChatWindow.xaml.cs.pre-restore
Normal file
File diff suppressed because it is too large
Load Diff
55
src/AxCopilot/Views/ColorPickResultWindow.xaml
Normal file
55
src/AxCopilot/Views/ColorPickResultWindow.xaml
Normal file
@@ -0,0 +1,55 @@
|
||||
<Window x:Class="AxCopilot.Views.ColorPickResultWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
UseLayoutRounding="True"
|
||||
SnapsToDevicePixels="True"
|
||||
Background="Transparent"
|
||||
Topmost="True"
|
||||
ShowInTaskbar="False"
|
||||
ResizeMode="NoResize"
|
||||
SizeToContent="WidthAndHeight"
|
||||
WindowStartupLocation="Manual">
|
||||
|
||||
<Border CornerRadius="14"
|
||||
Padding="20,16"
|
||||
Margin="16">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="#1A1B2E" Opacity="0.92"/>
|
||||
</Border.Background>
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="Black" BlurRadius="22" ShadowDepth="4" Opacity="0.28"/>
|
||||
</Border.Effect>
|
||||
|
||||
<StackPanel>
|
||||
<!-- 색상 미리보기 원 -->
|
||||
<Ellipse x:Name="ColorPreview"
|
||||
Width="56" Height="56"
|
||||
Stroke="#44FFFFFF" StrokeThickness="2"
|
||||
HorizontalAlignment="Center"/>
|
||||
|
||||
<!-- HEX 코드 -->
|
||||
<TextBlock x:Name="HexText"
|
||||
FontSize="22" FontWeight="Bold"
|
||||
FontFamily="Consolas"
|
||||
Foreground="White"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,10,0,2"/>
|
||||
|
||||
<!-- RGB 값 -->
|
||||
<TextBlock x:Name="RgbText"
|
||||
FontSize="12"
|
||||
FontFamily="Consolas"
|
||||
Foreground="#88AACCFF"
|
||||
HorizontalAlignment="Center"/>
|
||||
|
||||
<!-- 힌트 -->
|
||||
<TextBlock Text="HEX 코드가 클립보드에 복사되었습니다"
|
||||
FontSize="10"
|
||||
Foreground="#55FFFFFF"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,8,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Window>
|
||||
57
src/AxCopilot/Views/ColorPickResultWindow.xaml.cs
Normal file
57
src/AxCopilot/Views/ColorPickResultWindow.xaml.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 스포이드 색상 추출 결과를 반투명 창으로 5초간 표시합니다.
|
||||
/// 클릭 위치 근처에 나타나며, HEX 코드를 클립보드에 복사합니다.
|
||||
/// </summary>
|
||||
public partial class ColorPickResultWindow : Window
|
||||
{
|
||||
private readonly DispatcherTimer _timer;
|
||||
|
||||
public ColorPickResultWindow(System.Drawing.Color color, double screenX, double screenY)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// 색상 표시
|
||||
var wpfColor = Color.FromRgb(color.R, color.G, color.B);
|
||||
ColorPreview.Fill = new SolidColorBrush(wpfColor);
|
||||
HexText.Text = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
|
||||
RgbText.Text = $"RGB({color.R}, {color.G}, {color.B})";
|
||||
|
||||
// 클립보드에 HEX 코드 복사
|
||||
try { Clipboard.SetText(HexText.Text); }
|
||||
catch { }
|
||||
|
||||
// 위치: 클릭 지점 오른쪽 아래
|
||||
var area = SystemParameters.WorkArea;
|
||||
Left = Math.Min(screenX + 20, area.Right - 200);
|
||||
Top = Math.Min(screenY + 20, area.Bottom - 160);
|
||||
|
||||
// 5초 타이머
|
||||
_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(5) };
|
||||
_timer.Tick += (_, _) => Close();
|
||||
_timer.Start();
|
||||
|
||||
// 클릭으로 닫기
|
||||
MouseDown += (_, _) => Close();
|
||||
|
||||
// 등장 애니메이션
|
||||
Opacity = 0;
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
var anim = new System.Windows.Media.Animation.DoubleAnimation(0, 1,
|
||||
TimeSpan.FromMilliseconds(200));
|
||||
BeginAnimation(OpacityProperty, anim);
|
||||
};
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
_timer.Stop();
|
||||
base.OnClosed(e);
|
||||
}
|
||||
}
|
||||
48
src/AxCopilot/Views/CommandPaletteWindow.xaml
Normal file
48
src/AxCopilot/Views/CommandPaletteWindow.xaml
Normal file
@@ -0,0 +1,48 @@
|
||||
<Window x:Class="AxCopilot.Views.CommandPaletteWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="Command Palette"
|
||||
Width="520" SizeToContent="Height"
|
||||
MaxHeight="480"
|
||||
WindowStyle="None" AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ShowInTaskbar="False"
|
||||
Topmost="True"
|
||||
PreviewKeyDown="Window_PreviewKeyDown"
|
||||
Deactivated="Window_Deactivated">
|
||||
|
||||
<Border Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
CornerRadius="14" Padding="8" Margin="16">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="24" ShadowDepth="6" Opacity="0.35"
|
||||
Color="Black" Direction="270"/>
|
||||
</Border.Effect>
|
||||
<StackPanel>
|
||||
<!-- 검색 입력 -->
|
||||
<Border Background="{DynamicResource ItemBackground}" CornerRadius="10"
|
||||
Padding="10,8" Margin="4,4,4,8">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="14"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBox x:Name="SearchBox" Grid.Column="1"
|
||||
FontSize="14" BorderThickness="0" Background="Transparent"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
CaretBrush="{DynamicResource AccentColor}"
|
||||
TextChanged="SearchBox_TextChanged"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 결과 목록 -->
|
||||
<ScrollViewer MaxHeight="360" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel x:Name="ResultPanel" Margin="4,0,4,4"/>
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Window>
|
||||
147
src/AxCopilot/Views/CommandPaletteWindow.xaml.cs
Normal file
147
src/AxCopilot/Views/CommandPaletteWindow.xaml.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// Ctrl+Shift+P로 호출되는 커맨드 팔레트.
|
||||
/// 모든 기능에 빠르게 접근할 수 있는 통합 명령 검색 창입니다.
|
||||
/// </summary>
|
||||
public partial class CommandPaletteWindow : Window
|
||||
{
|
||||
private readonly Action<string>? _onExecute;
|
||||
private readonly List<CommandEntry> _commands = new();
|
||||
// 향후 키보드 탐색용
|
||||
// private int _selectedIndex = -1;
|
||||
|
||||
public string? SelectedCommand { get; private set; }
|
||||
|
||||
public CommandPaletteWindow(Action<string>? onExecute = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
_onExecute = onExecute;
|
||||
RegisterCommands();
|
||||
RenderItems("");
|
||||
Loaded += (_, _) => { SearchBox.Focus(); };
|
||||
}
|
||||
|
||||
private void RegisterCommands()
|
||||
{
|
||||
// 탭 전환
|
||||
_commands.Add(new("Chat 탭으로 전환", "탭 전환", "\uE8BD", "tab:chat"));
|
||||
_commands.Add(new("Cowork 탭으로 전환", "탭 전환", "\uE8BD", "tab:cowork"));
|
||||
_commands.Add(new("Code 탭으로 전환", "탭 전환", "\uE8BD", "tab:code"));
|
||||
|
||||
// 대화 관리
|
||||
_commands.Add(new("새 대화 시작", "대화", "\uE710", "new_conversation"));
|
||||
_commands.Add(new("대화 검색", "대화", "\uE721", "search_conversation"));
|
||||
|
||||
// 모델 변경
|
||||
_commands.Add(new("모델 변경", "설정", "\uEA86", "change_model"));
|
||||
|
||||
// 프리셋
|
||||
_commands.Add(new("프리셋 선택", "프리셋", "\uE71D", "select_preset"));
|
||||
|
||||
// 설정
|
||||
_commands.Add(new("설정 열기", "앱", "\uE713", "open_settings"));
|
||||
_commands.Add(new("테마 변경", "앱", "\uE790", "change_theme"));
|
||||
_commands.Add(new("통계 보기", "앱", "\uE9F9", "open_statistics"));
|
||||
|
||||
// 파일
|
||||
_commands.Add(new("작업 폴더 변경", "파일", "\uED25", "change_folder"));
|
||||
_commands.Add(new("파일 탐색기 열기", "파일", "\uE8B7", "open_file_explorer"));
|
||||
|
||||
// 에이전트
|
||||
_commands.Add(new("개발자 모드 토글", "에이전트", "\uE71C", "toggle_devmode"));
|
||||
_commands.Add(new("감사 로그 보기", "에이전트", "\uE9D9", "open_audit_log"));
|
||||
|
||||
// 도구
|
||||
_commands.Add(new("클립보드에서 붙여넣기", "도구", "\uE77F", "paste_clipboard"));
|
||||
_commands.Add(new("대화 내보내기", "도구", "\uE78C", "export_conversation"));
|
||||
}
|
||||
|
||||
private void RenderItems(string query)
|
||||
{
|
||||
ResultPanel.Children.Clear();
|
||||
|
||||
var filtered = string.IsNullOrWhiteSpace(query)
|
||||
? _commands
|
||||
: _commands.Where(c =>
|
||||
c.Label.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||
c.Category.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||
(Core.FuzzyEngine.IsChosung(query) && Core.FuzzyEngine.ContainsChosung(c.Label, query))).ToList();
|
||||
|
||||
for (int i = 0; i < filtered.Count; i++)
|
||||
{
|
||||
var cmd = filtered[i];
|
||||
var idx = i;
|
||||
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = cmd.Icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 14, Foreground = FindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue,
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0),
|
||||
});
|
||||
var textSp = new StackPanel();
|
||||
textSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = cmd.Label, FontSize = 13,
|
||||
Foreground = FindResource("PrimaryText") as Brush ?? Brushes.White,
|
||||
});
|
||||
textSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = cmd.Category, FontSize = 10.5,
|
||||
Foreground = FindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
});
|
||||
sp.Children.Add(textSp);
|
||||
|
||||
var item = new Border
|
||||
{
|
||||
Child = sp, Background = Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(8), Cursor = Cursors.Hand,
|
||||
Padding = new Thickness(10, 8, 14, 8), Margin = new Thickness(0, 1, 0, 1),
|
||||
};
|
||||
|
||||
var hoverBg = FindResource("HintBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; };
|
||||
item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||||
item.MouseLeftButtonUp += (_, _) => ExecuteCommand(cmd.CommandId);
|
||||
|
||||
ResultPanel.Children.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteCommand(string commandId)
|
||||
{
|
||||
SelectedCommand = commandId;
|
||||
_onExecute?.Invoke(commandId);
|
||||
Close();
|
||||
}
|
||||
|
||||
private void SearchBox_TextChanged(object sender, TextChangedEventArgs e)
|
||||
{
|
||||
RenderItems(SearchBox.Text);
|
||||
}
|
||||
|
||||
private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape) { Close(); e.Handled = true; }
|
||||
else if (e.Key == Key.Enter && ResultPanel.Children.Count > 0)
|
||||
{
|
||||
// 첫 번째 항목 실행
|
||||
var filtered = string.IsNullOrWhiteSpace(SearchBox.Text)
|
||||
? _commands : _commands.Where(c =>
|
||||
c.Label.Contains(SearchBox.Text, StringComparison.OrdinalIgnoreCase) ||
|
||||
c.Category.Contains(SearchBox.Text, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
if (filtered.Count > 0) ExecuteCommand(filtered[0].CommandId);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void Window_Deactivated(object sender, EventArgs e) => Close();
|
||||
|
||||
private record CommandEntry(string Label, string Category, string Icon, string CommandId);
|
||||
}
|
||||
350
src/AxCopilot/Views/CustomMessageBox.cs
Normal file
350
src/AxCopilot/Views/CustomMessageBox.cs
Normal file
@@ -0,0 +1,350 @@
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Effects;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 기본 MessageBox를 대체하는 커스텀 다이얼로그.
|
||||
/// 테마 리소스를 사용하여 앱 디자인과 일관된 모습을 제공합니다.
|
||||
/// </summary>
|
||||
internal sealed class CustomMessageBox : Window
|
||||
{
|
||||
private MessageBoxResult _result = MessageBoxResult.None;
|
||||
|
||||
private CustomMessageBox(string message, string title, MessageBoxButton buttons, MessageBoxImage icon)
|
||||
{
|
||||
Title = title;
|
||||
Width = 400;
|
||||
MinWidth = 320;
|
||||
MaxWidth = 520;
|
||||
SizeToContent = SizeToContent.Height;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||
ResizeMode = ResizeMode.NoResize;
|
||||
WindowStyle = WindowStyle.None;
|
||||
AllowsTransparency = true;
|
||||
Background = Brushes.Transparent;
|
||||
Topmost = true; // 다른 창 위에 표시
|
||||
|
||||
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
|
||||
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
||||
var borderBrush = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var itemBg = Application.Current.TryFindResource("ItemBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40));
|
||||
|
||||
// 루트 컨테이너
|
||||
var root = new Border
|
||||
{
|
||||
Background = bgBrush,
|
||||
CornerRadius = new CornerRadius(16),
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(28, 24, 28, 20),
|
||||
Effect = new DropShadowEffect
|
||||
{
|
||||
BlurRadius = 24, ShadowDepth = 4, Opacity = 0.3, Color = Colors.Black,
|
||||
},
|
||||
};
|
||||
|
||||
var stack = new StackPanel();
|
||||
|
||||
// 타이틀 바 (드래그 가능)
|
||||
var titleBar = new Grid { Margin = new Thickness(0, 0, 0, 16) };
|
||||
titleBar.MouseLeftButtonDown += (_, _) => { try { DragMove(); } catch { } };
|
||||
|
||||
// 아이콘 + 제목
|
||||
var titlePanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
|
||||
var (iconText, iconColor) = GetIconInfo(icon);
|
||||
if (!string.IsNullOrEmpty(iconText))
|
||||
{
|
||||
titlePanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = iconText,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 18,
|
||||
Foreground = iconColor,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
});
|
||||
}
|
||||
|
||||
titlePanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = title,
|
||||
FontSize = 15,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
titleBar.Children.Add(titlePanel);
|
||||
|
||||
// 닫기 버튼
|
||||
var closeBtn = new Button
|
||||
{
|
||||
Content = "\uE8BB",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 10,
|
||||
Foreground = secondaryText,
|
||||
Background = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
Padding = new Thickness(6),
|
||||
Cursor = Cursors.Hand,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
closeBtn.Click += (_, _) => { _result = MessageBoxResult.Cancel; Close(); };
|
||||
titleBar.Children.Add(closeBtn);
|
||||
stack.Children.Add(titleBar);
|
||||
|
||||
// 메시지 본문
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = message,
|
||||
FontSize = 13,
|
||||
Foreground = primaryText,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
LineHeight = 20,
|
||||
Margin = new Thickness(0, 0, 0, 24),
|
||||
});
|
||||
|
||||
// 버튼 영역
|
||||
var btnPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
};
|
||||
|
||||
switch (buttons)
|
||||
{
|
||||
case MessageBoxButton.OK:
|
||||
btnPanel.Children.Add(CreateButton("확인", accentBrush, true, MessageBoxResult.OK));
|
||||
break;
|
||||
case MessageBoxButton.OKCancel:
|
||||
btnPanel.Children.Add(CreateButton("취소", itemBg, false, MessageBoxResult.Cancel));
|
||||
btnPanel.Children.Add(CreateButton("확인", accentBrush, true, MessageBoxResult.OK));
|
||||
break;
|
||||
case MessageBoxButton.YesNo:
|
||||
btnPanel.Children.Add(CreateButton("아니오", itemBg, false, MessageBoxResult.No));
|
||||
btnPanel.Children.Add(CreateButton("예", accentBrush, true, MessageBoxResult.Yes));
|
||||
break;
|
||||
case MessageBoxButton.YesNoCancel:
|
||||
btnPanel.Children.Add(CreateButton("취소", itemBg, false, MessageBoxResult.Cancel));
|
||||
btnPanel.Children.Add(CreateButton("아니오", itemBg, false, MessageBoxResult.No));
|
||||
btnPanel.Children.Add(CreateButton("예", accentBrush, true, MessageBoxResult.Yes));
|
||||
break;
|
||||
}
|
||||
|
||||
stack.Children.Add(btnPanel);
|
||||
root.Child = stack;
|
||||
Content = root;
|
||||
|
||||
// ESC로 닫기
|
||||
KeyDown += (_, e) =>
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
_result = buttons == MessageBoxButton.YesNo ? MessageBoxResult.No : MessageBoxResult.Cancel;
|
||||
Close();
|
||||
}
|
||||
else if (e.Key == Key.Enter)
|
||||
{
|
||||
_result = buttons switch
|
||||
{
|
||||
MessageBoxButton.YesNo or MessageBoxButton.YesNoCancel => MessageBoxResult.Yes,
|
||||
_ => MessageBoxResult.OK,
|
||||
};
|
||||
Close();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Button CreateButton(string text, Brush bg, bool isPrimary, MessageBoxResult result)
|
||||
{
|
||||
var btn = new Button
|
||||
{
|
||||
Content = text,
|
||||
FontSize = 12.5,
|
||||
FontWeight = isPrimary ? FontWeights.SemiBold : FontWeights.Normal,
|
||||
Foreground = isPrimary ? Brushes.White : (Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White),
|
||||
Background = bg,
|
||||
BorderThickness = new Thickness(0),
|
||||
Padding = new Thickness(20, 8, 20, 8),
|
||||
Margin = new Thickness(6, 0, 0, 0),
|
||||
Cursor = Cursors.Hand,
|
||||
MinWidth = 80,
|
||||
};
|
||||
|
||||
// 라운드 코너 템플릿
|
||||
var template = new ControlTemplate(typeof(Button));
|
||||
var border = new FrameworkElementFactory(typeof(Border));
|
||||
border.SetValue(Border.BackgroundProperty, bg);
|
||||
border.SetValue(Border.CornerRadiusProperty, new CornerRadius(10));
|
||||
border.SetValue(Border.PaddingProperty, new Thickness(20, 8, 20, 8));
|
||||
var cp = new FrameworkElementFactory(typeof(ContentPresenter));
|
||||
cp.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Center);
|
||||
cp.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
|
||||
border.AppendChild(cp);
|
||||
template.VisualTree = border;
|
||||
btn.Template = template;
|
||||
|
||||
btn.Click += (_, _) => { _result = result; Close(); };
|
||||
return btn;
|
||||
}
|
||||
|
||||
private static (string icon, Brush color) GetIconInfo(MessageBoxImage image) => image switch
|
||||
{
|
||||
MessageBoxImage.Error => ("\uEA39", new SolidColorBrush(Color.FromRgb(0xE5, 0x3E, 0x3E))),
|
||||
MessageBoxImage.Warning => ("\uE7BA", new SolidColorBrush(Color.FromRgb(0xDD, 0x6B, 0x20))),
|
||||
MessageBoxImage.Information => ("\uE946", new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC))),
|
||||
MessageBoxImage.Question => ("\uE9CE", new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC))),
|
||||
_ => ("", Brushes.Transparent),
|
||||
};
|
||||
|
||||
// ─── 정적 호출 메서드 (기존 MessageBox.Show 시그니처 호환) ──────────────
|
||||
|
||||
public static MessageBoxResult Show(string message)
|
||||
=> Show(message, "AX Copilot", MessageBoxButton.OK, MessageBoxImage.None);
|
||||
|
||||
public static MessageBoxResult Show(string message, string title)
|
||||
=> Show(message, title, MessageBoxButton.OK, MessageBoxImage.None);
|
||||
|
||||
public static MessageBoxResult Show(string message, string title, MessageBoxButton buttons)
|
||||
=> Show(message, title, buttons, MessageBoxImage.None);
|
||||
|
||||
public static MessageBoxResult Show(string message, string title, MessageBoxButton buttons, MessageBoxImage icon)
|
||||
{
|
||||
var dlg = new CustomMessageBox(message, title, buttons, icon);
|
||||
|
||||
// 등장 애니메이션 (페이드인 + 스케일)
|
||||
dlg.Opacity = 0;
|
||||
dlg.Loaded += (_, _) =>
|
||||
{
|
||||
var fadeIn = new System.Windows.Media.Animation.DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(150));
|
||||
dlg.BeginAnimation(OpacityProperty, fadeIn);
|
||||
};
|
||||
|
||||
// ─── 부모 Window 하단에 토스트 알림 표시 ───
|
||||
var parentWindow = Application.Current.Windows.OfType<Window>()
|
||||
.FirstOrDefault(w => w.IsActive && w is not CustomMessageBox)
|
||||
?? Application.Current.MainWindow;
|
||||
|
||||
Border? toast = null;
|
||||
Grid? rootGrid = null;
|
||||
|
||||
if (parentWindow?.Content is Border outerBorder && outerBorder.Child is Grid grid)
|
||||
{
|
||||
rootGrid = grid;
|
||||
toast = CreateToastBanner(title, icon);
|
||||
rootGrid.Children.Add(toast);
|
||||
}
|
||||
else if (parentWindow?.Content is Grid directGrid)
|
||||
{
|
||||
rootGrid = directGrid;
|
||||
toast = CreateToastBanner(title, icon);
|
||||
rootGrid.Children.Add(toast);
|
||||
}
|
||||
|
||||
dlg.ShowDialog();
|
||||
|
||||
// 다이얼로그 닫힌 후 토스트 페이드아웃
|
||||
if (toast != null && rootGrid != null)
|
||||
{
|
||||
var fadeOut = new DoubleAnimation(1, 0, TimeSpan.FromMilliseconds(400))
|
||||
{
|
||||
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseIn },
|
||||
};
|
||||
fadeOut.Completed += (_, _) => rootGrid.Children.Remove(toast);
|
||||
toast.BeginAnimation(OpacityProperty, fadeOut);
|
||||
}
|
||||
|
||||
return dlg._result;
|
||||
}
|
||||
|
||||
/// <summary>하단 토스트 배너 생성.</summary>
|
||||
private static Border CreateToastBanner(string title, MessageBoxImage icon)
|
||||
{
|
||||
var (iconText, iconColor) = GetIconInfo(icon);
|
||||
if (string.IsNullOrEmpty(iconText))
|
||||
{
|
||||
iconText = "\uE946"; // default info icon
|
||||
iconColor = new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
||||
}
|
||||
|
||||
var panel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = iconText,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12,
|
||||
Foreground = iconColor,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
});
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = title,
|
||||
FontSize = 11.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = Brushes.White,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
|
||||
var toast = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(0xE8, 0x2A, 0x2B, 0x40)),
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(16, 8, 16, 8),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Bottom,
|
||||
Margin = new Thickness(0, 0, 0, 12),
|
||||
Effect = new DropShadowEffect
|
||||
{
|
||||
BlurRadius = 12, ShadowDepth = 2, Opacity = 0.3, Color = Colors.Black,
|
||||
},
|
||||
Opacity = 0,
|
||||
Child = panel,
|
||||
};
|
||||
|
||||
// 모든 Grid Row/Column 스팬하도록 설정
|
||||
Grid.SetColumnSpan(toast, 10);
|
||||
Grid.SetRowSpan(toast, 10);
|
||||
|
||||
// 슬라이드업 + 페이드인 애니메이션
|
||||
var translate = new TranslateTransform(0, 20);
|
||||
toast.RenderTransform = translate;
|
||||
|
||||
toast.Loaded += (_, _) =>
|
||||
{
|
||||
var fadeIn = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(250))
|
||||
{
|
||||
EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut },
|
||||
};
|
||||
var slideUp = new DoubleAnimation(20, 0, TimeSpan.FromMilliseconds(300))
|
||||
{
|
||||
EasingFunction = new BackEase { EasingMode = EasingMode.EaseOut, Amplitude = 0.3 },
|
||||
};
|
||||
toast.BeginAnimation(OpacityProperty, fadeIn);
|
||||
translate.BeginAnimation(TranslateTransform.YProperty, slideUp);
|
||||
};
|
||||
|
||||
return toast;
|
||||
}
|
||||
|
||||
/// <summary>WinForms 호환 — 인스톨러에서도 사용 가능하도록 Window 없이 표시.</summary>
|
||||
public static MessageBoxResult ShowStandalone(string message, string title,
|
||||
MessageBoxButton buttons = MessageBoxButton.OK, MessageBoxImage icon = MessageBoxImage.None)
|
||||
{
|
||||
var dlg = new CustomMessageBox(message, title, buttons, icon);
|
||||
dlg.ShowDialog();
|
||||
return dlg._result;
|
||||
}
|
||||
}
|
||||
283
src/AxCopilot/Views/CustomMoodDialog.cs
Normal file
283
src/AxCopilot/Views/CustomMoodDialog.cs
Normal file
@@ -0,0 +1,283 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>커스텀 디자인 무드 추가/편집 다이얼로그.</summary>
|
||||
internal sealed partial class CustomMoodDialog : Window
|
||||
{
|
||||
private readonly TextBox _keyBox;
|
||||
private readonly TextBox _labelBox;
|
||||
private readonly TextBox _iconBox;
|
||||
private readonly TextBox _descBox;
|
||||
private readonly TextBox _cssBox;
|
||||
private readonly bool _isEdit;
|
||||
|
||||
public string MoodKey => _keyBox.Text.Trim().ToLowerInvariant();
|
||||
public string MoodLabel => _labelBox.Text.Trim();
|
||||
public string MoodIcon => _iconBox.Text.Trim();
|
||||
public string MoodDescription => _descBox.Text.Trim();
|
||||
public string MoodCss => _cssBox.Text;
|
||||
|
||||
public CustomMoodDialog(
|
||||
string existingKey = "",
|
||||
string existingLabel = "",
|
||||
string existingIcon = "🎯",
|
||||
string existingDesc = "",
|
||||
string existingCss = "")
|
||||
{
|
||||
_isEdit = !string.IsNullOrEmpty(existingKey);
|
||||
Title = _isEdit ? "무드 편집" : "무드 추가";
|
||||
Width = 560;
|
||||
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 = "\uE771",
|
||||
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 row1 = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 8) };
|
||||
|
||||
// 키
|
||||
var keyStack = new StackPanel { Margin = new Thickness(0, 0, 12, 0) };
|
||||
AddLabel(keyStack, "키 (영문)", primaryText);
|
||||
_keyBox = CreateTextBox(existingKey, primaryText, itemBg, accentBrush, borderBrush);
|
||||
_keyBox.Width = 140;
|
||||
_keyBox.IsEnabled = !_isEdit; // 편집 시 키 변경 불가
|
||||
keyStack.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _keyBox });
|
||||
row1.Children.Add(keyStack);
|
||||
|
||||
// 라벨
|
||||
var labelStack = new StackPanel { Margin = new Thickness(0, 0, 12, 0) };
|
||||
AddLabel(labelStack, "이름", primaryText);
|
||||
_labelBox = CreateTextBox(existingLabel, primaryText, itemBg, accentBrush, borderBrush);
|
||||
_labelBox.Width = 180;
|
||||
labelStack.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _labelBox });
|
||||
row1.Children.Add(labelStack);
|
||||
|
||||
// 아이콘 (이모지)
|
||||
var iconStack = new StackPanel();
|
||||
AddLabel(iconStack, "아이콘", primaryText);
|
||||
_iconBox = CreateTextBox(existingIcon, primaryText, itemBg, accentBrush, borderBrush);
|
||||
_iconBox.Width = 60;
|
||||
_iconBox.TextAlignment = TextAlignment.Center;
|
||||
iconStack.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _iconBox });
|
||||
row1.Children.Add(iconStack);
|
||||
|
||||
stack.Children.Add(row1);
|
||||
|
||||
// ── 설명 ──
|
||||
AddLabel(stack, "설명", primaryText);
|
||||
_descBox = CreateTextBox(existingDesc, primaryText, itemBg, accentBrush, borderBrush);
|
||||
stack.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _descBox, Margin = new Thickness(0, 0, 0, 8) });
|
||||
|
||||
// ── CSS ──
|
||||
AddSeparator(stack, borderBrush);
|
||||
AddLabel(stack, "CSS 스타일", primaryText);
|
||||
AddHint(stack, "문서에 적용될 CSS입니다. body, h1~h6, table, .callout 등의 스타일을 정의하세요.", secondaryText);
|
||||
_cssBox = CreateTextBox(existingCss, primaryText, itemBg, accentBrush, borderBrush, multiline: true, height: 200);
|
||||
_cssBox.FontFamily = new FontFamily("Consolas, Courier New, monospace");
|
||||
_cssBox.FontSize = 12;
|
||||
stack.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _cssBox });
|
||||
|
||||
// CSS 힌트 버튼
|
||||
var hintBtn = new TextBlock
|
||||
{
|
||||
Text = "CSS 예시 보기",
|
||||
FontSize = 11,
|
||||
Foreground = accentBrush,
|
||||
Cursor = Cursors.Hand,
|
||||
Margin = new Thickness(0, 6, 0, 0),
|
||||
TextDecorations = TextDecorations.Underline,
|
||||
};
|
||||
hintBtn.MouseLeftButtonDown += (_, _) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_cssBox.Text))
|
||||
{
|
||||
_cssBox.Text = CssExample;
|
||||
}
|
||||
};
|
||||
stack.Children.Add(hintBtn);
|
||||
|
||||
// ── 버튼 바 ──
|
||||
var btnBar = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(0, 20, 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 += (_, _) => Validate();
|
||||
btnBar.Children.Add(okBtn);
|
||||
stack.Children.Add(btnBar);
|
||||
|
||||
root.Child = stack;
|
||||
Content = root;
|
||||
|
||||
KeyDown += (_, ke) => { if (ke.Key == Key.Escape) { DialogResult = false; Close(); } };
|
||||
Loaded += (_, _) => { (_isEdit ? _labelBox : _keyBox).Focus(); };
|
||||
root.MouseLeftButtonDown += (_, me) =>
|
||||
{
|
||||
if (me.LeftButton == MouseButtonState.Pressed)
|
||||
try { DragMove(); } catch { }
|
||||
};
|
||||
}
|
||||
|
||||
private void Validate()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_keyBox.Text))
|
||||
{
|
||||
CustomMessageBox.Show("키를 입력하세요 (영문 소문자).", "입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
_keyBox.Focus(); return;
|
||||
}
|
||||
if (!Regex.IsMatch(_keyBox.Text.Trim(), @"^[a-z][a-z0-9_]{1,20}$"))
|
||||
{
|
||||
CustomMessageBox.Show("키는 영문 소문자로 시작하며, 소문자/숫자/밑줄만 허용됩니다 (2~21자).", "입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
_keyBox.Focus(); return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(_labelBox.Text))
|
||||
{
|
||||
CustomMessageBox.Show("이름을 입력하세요.", "입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
_labelBox.Focus(); return;
|
||||
}
|
||||
// 내장 무드 키와 충돌 확인
|
||||
if (!_isEdit)
|
||||
{
|
||||
var builtinKeys = new[] { "modern", "professional", "creative", "minimal", "elegant", "dark", "colorful", "corporate", "magazine", "dashboard" };
|
||||
if (builtinKeys.Contains(_keyBox.Text.Trim().ToLowerInvariant()))
|
||||
{
|
||||
CustomMessageBox.Show("내장 무드와 동일한 키는 사용할 수 없습니다.", "입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
_keyBox.Focus(); return;
|
||||
}
|
||||
}
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private static void AddLabel(StackPanel parent, string text, Brush fg) =>
|
||||
parent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = text, FontSize = 12, FontWeight = FontWeights.SemiBold,
|
||||
Foreground = fg, Margin = new Thickness(0, 0, 0, 6),
|
||||
});
|
||||
|
||||
private static void AddHint(StackPanel parent, string text, Brush fg) =>
|
||||
parent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = text, FontSize = 11, Foreground = fg,
|
||||
Margin = new Thickness(0, 0, 0, 8),
|
||||
});
|
||||
|
||||
private static void AddSeparator(StackPanel parent, Brush color) =>
|
||||
parent.Children.Add(new Rectangle
|
||||
{
|
||||
Height = 1, Fill = color,
|
||||
Margin = new Thickness(0, 8, 0, 12), Opacity = 0.5,
|
||||
});
|
||||
|
||||
private static TextBox CreateTextBox(string text, Brush fg, Brush bg, Brush caret, Brush border, bool multiline = false, int height = 100)
|
||||
{
|
||||
var tb = new TextBox
|
||||
{
|
||||
Text = text, FontSize = 13,
|
||||
Padding = new Thickness(12, 8, 12, 8),
|
||||
Foreground = fg, Background = bg,
|
||||
CaretBrush = caret, BorderBrush = border,
|
||||
BorderThickness = new Thickness(1),
|
||||
};
|
||||
if (multiline)
|
||||
{
|
||||
tb.AcceptsReturn = true;
|
||||
tb.AcceptsTab = true;
|
||||
tb.TextWrapping = TextWrapping.Wrap;
|
||||
tb.MinHeight = height;
|
||||
tb.MaxHeight = height + 100;
|
||||
tb.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
|
||||
}
|
||||
return tb;
|
||||
}
|
||||
|
||||
private const string CssExample = @"body {
|
||||
font-family: 'Pretendard', 'Noto Sans KR', sans-serif;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 40px;
|
||||
color: #1a1a2e;
|
||||
background: #f8f9fc;
|
||||
line-height: 1.8;
|
||||
}
|
||||
h1 { color: #2d3748; border-bottom: 2px solid #4a90d9; padding-bottom: 8px; }
|
||||
h2 { color: #4a5568; margin-top: 2em; }
|
||||
table { width: 100%; border-collapse: collapse; margin: 1.5em 0; }
|
||||
th { background: #4a90d9; color: white; padding: 10px; text-align: left; }
|
||||
td { padding: 8px 10px; border-bottom: 1px solid #e2e8f0; }
|
||||
tr:nth-child(even) { background: #f0f4f8; }
|
||||
";
|
||||
}
|
||||
483
src/AxCopilot/Views/CustomPresetDialog.cs
Normal file
483
src/AxCopilot/Views/CustomPresetDialog.cs
Normal file
@@ -0,0 +1,483 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>커스텀 프리셋 추가/편집 다이얼로그.</summary>
|
||||
internal sealed class CustomPresetDialog : Window
|
||||
{
|
||||
private readonly TextBox _nameBox;
|
||||
private readonly TextBox _descBox;
|
||||
private readonly TextBox _promptBox;
|
||||
private readonly ComboBox _tabCombo;
|
||||
private string _selectedColor;
|
||||
private string _selectedSymbol;
|
||||
private Border? _iconPreview;
|
||||
private TextBlock? _iconPreviewText;
|
||||
|
||||
public string PresetName => _nameBox.Text.Trim();
|
||||
public string PresetDescription => _descBox.Text.Trim();
|
||||
public string PresetSystemPrompt => _promptBox.Text.Trim();
|
||||
public string PresetTab => (_tabCombo.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Chat";
|
||||
public string PresetColor => _selectedColor;
|
||||
public string PresetSymbol => _selectedSymbol;
|
||||
|
||||
// 사전 정의 색상 팔레트
|
||||
private static readonly (string Label, string Hex)[] Colors =
|
||||
{
|
||||
("인디고", "#6366F1"),
|
||||
("블루", "#3B82F6"),
|
||||
("티일", "#14B8A6"),
|
||||
("그린", "#22C55E"),
|
||||
("옐로우", "#EAB308"),
|
||||
("오렌지", "#F97316"),
|
||||
("레드", "#EF4444"),
|
||||
("핑크", "#EC4899"),
|
||||
("퍼플", "#A855F7"),
|
||||
("슬레이트", "#64748B"),
|
||||
};
|
||||
|
||||
// 아이콘 셋트 — (카테고리, 아이콘 목록)
|
||||
private static readonly (string Category, (string Label, string Symbol)[] Icons)[] IconSets =
|
||||
{
|
||||
("업무", new[]
|
||||
{
|
||||
("일반", "\uE771"), ("문서", "\uE8A5"), ("메일", "\uE715"), ("캘린더", "\uE787"),
|
||||
("차트", "\uE9D9"), ("발표", "\uE7F4"), ("계산기", "\uE8EF"), ("메모", "\uE70B"),
|
||||
}),
|
||||
("기술", new[]
|
||||
{
|
||||
("코드", "\uE943"), ("설정", "\uE713"), ("데이터", "\uEA86"), ("검색", "\uE721"),
|
||||
("보안", "\uE72E"), ("서버", "\uE839"), ("버그", "\uEBE8"), ("배포", "\uE7F8"),
|
||||
}),
|
||||
("커뮤니케이션", new[]
|
||||
{
|
||||
("대화", "\uE8BD"), ("사람", "\uE77B"), ("팀", "\uE716"), ("전화", "\uE717"),
|
||||
("알림", "\uEA8F"), ("공유", "\uE72D"), ("피드백", "\uE939"), ("질문", "\uE897"),
|
||||
}),
|
||||
("콘텐츠", new[]
|
||||
{
|
||||
("사진", "\uE722"), ("동영상", "\uE714"), ("음악", "\uE8D6"), ("글쓰기", "\uE70F"),
|
||||
("책", "\uE82D"), ("웹", "\uE774"), ("디자인", "\uE790"), ("다운로드", "\uE896"),
|
||||
}),
|
||||
("분석", new[]
|
||||
{
|
||||
("인사이트", "\uE9D9"), ("대시보드", "\uE809"), ("리포트", "\uE9F9"),("트렌드", "\uE8A1"),
|
||||
("필터", "\uE71C"), ("목표", "\uE7C1"), ("비교", "\uE8FD"), ("측정", "\uE7C8"),
|
||||
}),
|
||||
};
|
||||
|
||||
public CustomPresetDialog(
|
||||
string existingName = "",
|
||||
string existingDesc = "",
|
||||
string existingPrompt = "",
|
||||
string existingColor = "#6366F1",
|
||||
string existingSymbol = "\uE713",
|
||||
string existingTab = "Chat")
|
||||
{
|
||||
bool isEdit = !string.IsNullOrEmpty(existingName);
|
||||
Title = isEdit ? "프리셋 편집" : "프리셋 추가";
|
||||
Width = 520;
|
||||
SizeToContent = SizeToContent.Height;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
ResizeMode = ResizeMode.NoResize;
|
||||
WindowStyle = WindowStyle.None;
|
||||
AllowsTransparency = true;
|
||||
Background = Brushes.Transparent;
|
||||
|
||||
_selectedColor = existingColor;
|
||||
_selectedSymbol = existingSymbol;
|
||||
|
||||
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 = System.Windows.Media.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 = "\uE710",
|
||||
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);
|
||||
|
||||
// ── 프리셋 이름 ──
|
||||
AddLabel(stack, "프리셋 이름", primaryText);
|
||||
AddHint(stack, "대화 주제 버튼에 표시될 이름입니다", secondaryText);
|
||||
_nameBox = CreateTextBox(existingName, primaryText, itemBg, accentBrush, borderBrush);
|
||||
stack.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _nameBox });
|
||||
|
||||
// ── 설명 ──
|
||||
AddSeparator(stack, borderBrush);
|
||||
AddLabel(stack, "설명", primaryText);
|
||||
AddHint(stack, "프리셋 카드 하단에 표시될 짧은 설명입니다", secondaryText);
|
||||
_descBox = CreateTextBox(existingDesc, primaryText, itemBg, accentBrush, borderBrush);
|
||||
stack.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _descBox });
|
||||
|
||||
// ── 아이콘 + 색상 ──
|
||||
AddSeparator(stack, borderBrush);
|
||||
AddLabel(stack, "아이콘 & 배경색", primaryText);
|
||||
|
||||
var iconColorRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 8) };
|
||||
|
||||
// 아이콘 미리보기 (클릭하면 아이콘 선택 팝업)
|
||||
_iconPreview = new Border
|
||||
{
|
||||
Width = 48, Height = 48,
|
||||
CornerRadius = new CornerRadius(24),
|
||||
Background = new SolidColorBrush(ParseColor(_selectedColor)) { Opacity = 0.2 },
|
||||
Cursor = Cursors.Hand,
|
||||
Margin = new Thickness(0, 0, 16, 0),
|
||||
ToolTip = "아이콘 선택",
|
||||
};
|
||||
_iconPreviewText = new TextBlock
|
||||
{
|
||||
Text = _selectedSymbol,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 20,
|
||||
Foreground = BrushFromHex(_selectedColor),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
_iconPreview.Child = _iconPreviewText;
|
||||
_iconPreview.MouseLeftButtonDown += (_, _) => ShowIconPickerPopup();
|
||||
iconColorRow.Children.Add(_iconPreview);
|
||||
|
||||
// 탭 선택
|
||||
var tabStack = new StackPanel { Margin = new Thickness(0, 0, 16, 0) };
|
||||
tabStack.Children.Add(new TextBlock { Text = "탭", FontSize = 11, Foreground = secondaryText, Margin = new Thickness(0, 0, 0, 4) });
|
||||
_tabCombo = new ComboBox
|
||||
{
|
||||
Width = 100, FontSize = 12,
|
||||
Foreground = primaryText, Background = itemBg,
|
||||
BorderBrush = borderBrush, BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(6, 4, 6, 4),
|
||||
};
|
||||
_tabCombo.Items.Add(new ComboBoxItem { Content = "Chat", Tag = "Chat", IsSelected = existingTab == "Chat" });
|
||||
_tabCombo.Items.Add(new ComboBoxItem { Content = "Cowork", Tag = "Cowork", IsSelected = existingTab == "Cowork" });
|
||||
tabStack.Children.Add(_tabCombo);
|
||||
iconColorRow.Children.Add(tabStack);
|
||||
|
||||
// 색상 팔레트
|
||||
var colorStack = new StackPanel();
|
||||
colorStack.Children.Add(new TextBlock { Text = "배경색", FontSize = 11, Foreground = secondaryText, Margin = new Thickness(0, 0, 0, 4) });
|
||||
var colorPanel = new WrapPanel();
|
||||
foreach (var (label, hex) in Colors)
|
||||
{
|
||||
var colorHex = hex;
|
||||
var swatch = new Border
|
||||
{
|
||||
Width = 22, Height = 22,
|
||||
CornerRadius = new CornerRadius(11),
|
||||
Background = BrushFromHex(hex),
|
||||
Margin = new Thickness(0, 0, 5, 4),
|
||||
Cursor = Cursors.Hand,
|
||||
BorderThickness = new Thickness(2),
|
||||
BorderBrush = hex == _selectedColor ? primaryText : Brushes.Transparent,
|
||||
ToolTip = label,
|
||||
};
|
||||
swatch.MouseLeftButtonDown += (s, _) =>
|
||||
{
|
||||
_selectedColor = colorHex;
|
||||
foreach (var child in colorPanel.Children)
|
||||
if (child is Border b) b.BorderBrush = Brushes.Transparent;
|
||||
if (s is Border clicked) clicked.BorderBrush = primaryText;
|
||||
UpdateIconPreview();
|
||||
};
|
||||
colorPanel.Children.Add(swatch);
|
||||
}
|
||||
colorStack.Children.Add(colorPanel);
|
||||
iconColorRow.Children.Add(colorStack);
|
||||
stack.Children.Add(iconColorRow);
|
||||
|
||||
// ── 시스템 프롬프트 ──
|
||||
AddSeparator(stack, borderBrush);
|
||||
AddLabel(stack, "시스템 프롬프트", primaryText);
|
||||
AddHint(stack, "AI가 이 프리셋에서 따를 지침입니다. 비워두면 기본 프롬프트가 사용됩니다.", secondaryText);
|
||||
_promptBox = CreateTextBox(existingPrompt, primaryText, itemBg, accentBrush, borderBrush, multiline: true);
|
||||
stack.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _promptBox });
|
||||
|
||||
// 글자 수
|
||||
var charCount = new TextBlock
|
||||
{
|
||||
FontSize = 10.5, Foreground = secondaryText,
|
||||
Margin = new Thickness(0, 6, 0, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
};
|
||||
UpdateCharCount(charCount);
|
||||
_promptBox.TextChanged += (_, _) => UpdateCharCount(charCount);
|
||||
stack.Children.Add(charCount);
|
||||
|
||||
// ── 버튼 바 ──
|
||||
var btnBar = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(0, 20, 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(_nameBox.Text))
|
||||
{
|
||||
CustomMessageBox.Show("프리셋 이름을 입력하세요.", "입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
_nameBox.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 += (_, _) => { _nameBox.Focus(); _nameBox.SelectAll(); };
|
||||
root.MouseLeftButtonDown += (_, me) =>
|
||||
{
|
||||
if (me.LeftButton == MouseButtonState.Pressed)
|
||||
try { DragMove(); } catch { }
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateIconPreview()
|
||||
{
|
||||
if (_iconPreview == null || _iconPreviewText == null) return;
|
||||
_iconPreview.Background = new SolidColorBrush(ParseColor(_selectedColor)) { Opacity = 0.2 };
|
||||
_iconPreviewText.Text = _selectedSymbol;
|
||||
_iconPreviewText.Foreground = BrushFromHex(_selectedColor);
|
||||
}
|
||||
|
||||
// ── 아이콘 피커 팝업 ────────────────────────────────────────────
|
||||
private void ShowIconPickerPopup()
|
||||
{
|
||||
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 borderBrush = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var hoverBg = Application.Current.TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
|
||||
var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||||
|
||||
// 별도 Window로 표시 (모달 다이얼로그 위에서 Popup 충돌 방지)
|
||||
var pickerWin = new Window
|
||||
{
|
||||
Title = "아이콘 선택",
|
||||
Width = 340,
|
||||
SizeToContent = SizeToContent.Height,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||
Owner = this,
|
||||
ResizeMode = ResizeMode.NoResize,
|
||||
WindowStyle = WindowStyle.None,
|
||||
AllowsTransparency = true,
|
||||
Background = Brushes.Transparent,
|
||||
};
|
||||
|
||||
var popupBorder = new Border
|
||||
{
|
||||
Background = bgBrush,
|
||||
CornerRadius = new CornerRadius(12),
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(12),
|
||||
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
||||
{
|
||||
BlurRadius = 16, ShadowDepth = 3, Opacity = 0.3, Color = System.Windows.Media.Colors.Black,
|
||||
},
|
||||
};
|
||||
|
||||
var mainStack = new StackPanel();
|
||||
|
||||
// 타이틀 + 닫기
|
||||
var titleRow = new Grid();
|
||||
titleRow.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "아이콘 선택",
|
||||
FontSize = 14, FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
Margin = new Thickness(0, 0, 0, 10),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
});
|
||||
var closeBtn = new TextBlock
|
||||
{
|
||||
Text = "\uE711", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12, Foreground = secondaryText,
|
||||
Cursor = Cursors.Hand,
|
||||
HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Top,
|
||||
};
|
||||
closeBtn.MouseLeftButtonDown += (_, _) => pickerWin.Close();
|
||||
titleRow.Children.Add(closeBtn);
|
||||
mainStack.Children.Add(titleRow);
|
||||
|
||||
// 드래그로 창 이동
|
||||
popupBorder.MouseLeftButtonDown += (_, e) => { try { pickerWin.DragMove(); } catch { } };
|
||||
|
||||
// 카테고리별 아이콘 그리드
|
||||
foreach (var (category, icons) in IconSets)
|
||||
{
|
||||
mainStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = category,
|
||||
FontSize = 11, FontWeight = FontWeights.SemiBold,
|
||||
Foreground = secondaryText,
|
||||
Margin = new Thickness(0, 6, 0, 4),
|
||||
});
|
||||
|
||||
var wrapPanel = new WrapPanel();
|
||||
foreach (var (label, symbol) in icons)
|
||||
{
|
||||
var capturedSymbol = symbol;
|
||||
var isSelected = _selectedSymbol == symbol;
|
||||
var iconBtn = new Border
|
||||
{
|
||||
Width = 36, Height = 36,
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Background = isSelected ? new SolidColorBrush(ParseColor(_selectedColor)) { Opacity = 0.2 } : Brushes.Transparent,
|
||||
BorderBrush = isSelected ? accentBrush : Brushes.Transparent,
|
||||
BorderThickness = new Thickness(isSelected ? 1.5 : 0),
|
||||
Cursor = Cursors.Hand,
|
||||
Margin = new Thickness(0, 0, 4, 4),
|
||||
ToolTip = label,
|
||||
};
|
||||
iconBtn.Child = new TextBlock
|
||||
{
|
||||
Text = symbol,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 16,
|
||||
Foreground = isSelected ? BrushFromHex(_selectedColor) : primaryText,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
|
||||
iconBtn.MouseEnter += (s, _) => { if (s is Border b && !(_selectedSymbol == capturedSymbol)) b.Background = hoverBg; };
|
||||
iconBtn.MouseLeave += (s, _) => { if (s is Border b && !(_selectedSymbol == capturedSymbol)) b.Background = Brushes.Transparent; };
|
||||
iconBtn.MouseLeftButtonDown += (_, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
_selectedSymbol = capturedSymbol;
|
||||
UpdateIconPreview();
|
||||
pickerWin.Close();
|
||||
};
|
||||
|
||||
wrapPanel.Children.Add(iconBtn);
|
||||
}
|
||||
mainStack.Children.Add(wrapPanel);
|
||||
}
|
||||
|
||||
popupBorder.Child = mainStack;
|
||||
pickerWin.Content = popupBorder;
|
||||
pickerWin.ShowDialog();
|
||||
}
|
||||
|
||||
private static void AddLabel(StackPanel parent, string text, Brush fg) =>
|
||||
parent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = text, FontSize = 12, FontWeight = FontWeights.SemiBold,
|
||||
Foreground = fg, Margin = new Thickness(0, 0, 0, 6),
|
||||
});
|
||||
|
||||
private static void AddHint(StackPanel parent, string text, Brush fg) =>
|
||||
parent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = text, FontSize = 11, Foreground = fg,
|
||||
Margin = new Thickness(0, 0, 0, 8),
|
||||
});
|
||||
|
||||
private static void AddSeparator(StackPanel parent, Brush color) =>
|
||||
parent.Children.Add(new Rectangle
|
||||
{
|
||||
Height = 1, Fill = color,
|
||||
Margin = new Thickness(0, 12, 0, 12), Opacity = 0.5,
|
||||
});
|
||||
|
||||
private static TextBox CreateTextBox(string text, Brush fg, Brush bg, Brush caret, Brush border, bool multiline = false)
|
||||
{
|
||||
var tb = new TextBox
|
||||
{
|
||||
Text = text, FontSize = 13,
|
||||
Padding = new Thickness(12, 8, 12, 8),
|
||||
Foreground = fg, Background = bg,
|
||||
CaretBrush = caret, BorderBrush = border,
|
||||
BorderThickness = new Thickness(1),
|
||||
};
|
||||
if (multiline)
|
||||
{
|
||||
tb.AcceptsReturn = true;
|
||||
tb.TextWrapping = TextWrapping.Wrap;
|
||||
tb.MinHeight = 100;
|
||||
tb.MaxHeight = 200;
|
||||
tb.VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
|
||||
}
|
||||
return tb;
|
||||
}
|
||||
|
||||
private void UpdateCharCount(TextBlock tb) =>
|
||||
tb.Text = $"{_promptBox.Text.Length}자";
|
||||
|
||||
private static Brush BrushFromHex(string hex)
|
||||
{
|
||||
try { return new SolidColorBrush((Color)ColorConverter.ConvertFromString(hex)); }
|
||||
catch { return Brushes.Gray; }
|
||||
}
|
||||
|
||||
private static Color ParseColor(string hex)
|
||||
{
|
||||
try { return (Color)ColorConverter.ConvertFromString(hex); }
|
||||
catch { return System.Windows.Media.Colors.Gray; }
|
||||
}
|
||||
}
|
||||
210
src/AxCopilot/Views/DiffViewerPanel.cs
Normal file
210
src/AxCopilot/Views/DiffViewerPanel.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 에이전트 파일 수정 시 변경 전/후 diff를 보여주는 패널.
|
||||
/// DiffService의 결과를 색상 하이라이트로 렌더링하고
|
||||
/// Accept/Reject 버튼으로 변경 승인/취소를 지원합니다.
|
||||
/// </summary>
|
||||
public class DiffViewerPanel : Border
|
||||
{
|
||||
private readonly string _filePath;
|
||||
private readonly string _oldContent;
|
||||
private readonly string _newContent;
|
||||
private readonly List<DiffService.DiffLine> _diffLines;
|
||||
|
||||
/// <summary>사용자가 Accept를 눌렀을 때 발생합니다.</summary>
|
||||
public event EventHandler? Accepted;
|
||||
|
||||
/// <summary>사용자가 Reject를 눌렀을 때 발생합니다.</summary>
|
||||
public event EventHandler? Rejected;
|
||||
|
||||
public DiffViewerPanel(string filePath, string oldContent, string newContent)
|
||||
{
|
||||
_filePath = filePath;
|
||||
_oldContent = oldContent;
|
||||
_newContent = newContent;
|
||||
_diffLines = DiffService.ComputeDiff(oldContent, newContent);
|
||||
|
||||
CornerRadius = new CornerRadius(10);
|
||||
Background = new SolidColorBrush(Color.FromRgb(0xFA, 0xFA, 0xFF));
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(0xE0, 0xE0, 0xEC));
|
||||
BorderThickness = new Thickness(1);
|
||||
Padding = new Thickness(0);
|
||||
Margin = new Thickness(0, 8, 0, 8);
|
||||
|
||||
Child = BuildContent();
|
||||
}
|
||||
|
||||
private UIElement BuildContent()
|
||||
{
|
||||
var mainStack = new StackPanel();
|
||||
|
||||
// 헤더
|
||||
var header = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromRgb(0xF0, 0xF2, 0xFF)),
|
||||
CornerRadius = new CornerRadius(10, 10, 0, 0),
|
||||
Padding = new Thickness(14, 10, 14, 10),
|
||||
};
|
||||
var headerGrid = new Grid();
|
||||
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
|
||||
|
||||
var summary = DiffService.GetSummary(_diffLines);
|
||||
var titlePanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
titlePanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE89A",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 13, Foreground = new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
});
|
||||
titlePanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = System.IO.Path.GetFileName(_filePath),
|
||||
FontSize = 13, FontWeight = FontWeights.SemiBold,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E)),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
titlePanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $" · {summary}",
|
||||
FontSize = 11,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0x88, 0x88, 0xAA)),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
Grid.SetColumn(titlePanel, 0);
|
||||
headerGrid.Children.Add(titlePanel);
|
||||
|
||||
// Accept / Reject 버튼
|
||||
var btnPanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
var acceptBtn = CreateButton("적용", Color.FromRgb(0x10, 0x7C, 0x10), Colors.White);
|
||||
acceptBtn.Click += (_, _) => Accepted?.Invoke(this, EventArgs.Empty);
|
||||
btnPanel.Children.Add(acceptBtn);
|
||||
|
||||
var rejectBtn = CreateButton("취소", Colors.Transparent, Color.FromRgb(0xC5, 0x0F, 0x1F));
|
||||
rejectBtn.Click += (_, _) => Rejected?.Invoke(this, EventArgs.Empty);
|
||||
btnPanel.Children.Add(rejectBtn);
|
||||
|
||||
Grid.SetColumn(btnPanel, 1);
|
||||
headerGrid.Children.Add(btnPanel);
|
||||
|
||||
header.Child = headerGrid;
|
||||
mainStack.Children.Add(header);
|
||||
|
||||
// Diff 내용
|
||||
var scrollViewer = new ScrollViewer
|
||||
{
|
||||
MaxHeight = 300,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
};
|
||||
|
||||
var diffStack = new StackPanel { Margin = new Thickness(0) };
|
||||
|
||||
foreach (var line in _diffLines)
|
||||
{
|
||||
var (bg, fg, prefix) = line.Type switch
|
||||
{
|
||||
DiffService.DiffType.Added => (
|
||||
Color.FromRgb(0xE6, 0xFF, 0xED),
|
||||
Color.FromRgb(0x16, 0x6C, 0x34),
|
||||
"+"),
|
||||
DiffService.DiffType.Removed => (
|
||||
Color.FromRgb(0xFF, 0xEB, 0xE9),
|
||||
Color.FromRgb(0xCF, 0x22, 0x2E),
|
||||
"-"),
|
||||
_ => (
|
||||
Color.FromRgb(0xFF, 0xFF, 0xFF),
|
||||
Color.FromRgb(0x66, 0x66, 0x88),
|
||||
" ")
|
||||
};
|
||||
|
||||
var lineNo = line.OldLineNo?.ToString() ?? "";
|
||||
var newLineNo = line.NewLineNo?.ToString() ?? "";
|
||||
|
||||
var linePanel = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(bg),
|
||||
Padding = new Thickness(8, 1, 8, 1),
|
||||
};
|
||||
|
||||
var lineGrid = new Grid();
|
||||
lineGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(36) }); // old line
|
||||
lineGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(36) }); // new line
|
||||
lineGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(16) }); // prefix
|
||||
lineGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); // content
|
||||
|
||||
var oldLineText = new TextBlock
|
||||
{
|
||||
Text = lineNo, FontSize = 10, FontFamily = new FontFamily("Consolas"),
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0xAA, 0xAA, 0xCC)),
|
||||
HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 0, 4, 0),
|
||||
};
|
||||
Grid.SetColumn(oldLineText, 0);
|
||||
lineGrid.Children.Add(oldLineText);
|
||||
|
||||
var newLineText = new TextBlock
|
||||
{
|
||||
Text = newLineNo, FontSize = 10, FontFamily = new FontFamily("Consolas"),
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0xAA, 0xAA, 0xCC)),
|
||||
HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 0, 4, 0),
|
||||
};
|
||||
Grid.SetColumn(newLineText, 1);
|
||||
lineGrid.Children.Add(newLineText);
|
||||
|
||||
var prefixText = new TextBlock
|
||||
{
|
||||
Text = prefix, FontSize = 11, FontFamily = new FontFamily("Consolas"),
|
||||
Foreground = new SolidColorBrush(fg), FontWeight = FontWeights.Bold,
|
||||
};
|
||||
Grid.SetColumn(prefixText, 2);
|
||||
lineGrid.Children.Add(prefixText);
|
||||
|
||||
var contentText = new TextBlock
|
||||
{
|
||||
Text = line.Content, FontSize = 11, FontFamily = new FontFamily("Consolas"),
|
||||
Foreground = new SolidColorBrush(fg),
|
||||
TextWrapping = TextWrapping.NoWrap,
|
||||
};
|
||||
Grid.SetColumn(contentText, 3);
|
||||
lineGrid.Children.Add(contentText);
|
||||
|
||||
linePanel.Child = lineGrid;
|
||||
diffStack.Children.Add(linePanel);
|
||||
}
|
||||
|
||||
scrollViewer.Content = diffStack;
|
||||
mainStack.Children.Add(scrollViewer);
|
||||
|
||||
return mainStack;
|
||||
}
|
||||
|
||||
private static Button CreateButton(string text, Color bg, Color fg)
|
||||
{
|
||||
var btn = new Button { Cursor = System.Windows.Input.Cursors.Hand, Margin = new Thickness(4, 0, 0, 0) };
|
||||
var template = new ControlTemplate(typeof(Button));
|
||||
var bdFactory = new FrameworkElementFactory(typeof(Border));
|
||||
bdFactory.SetValue(Border.BackgroundProperty, new SolidColorBrush(bg));
|
||||
bdFactory.SetValue(Border.CornerRadiusProperty, new CornerRadius(6));
|
||||
bdFactory.SetValue(Border.PaddingProperty, new Thickness(12, 5, 12, 5));
|
||||
if (bg == Colors.Transparent)
|
||||
{
|
||||
bdFactory.SetValue(Border.BorderBrushProperty, new SolidColorBrush(fg));
|
||||
bdFactory.SetValue(Border.BorderThicknessProperty, new Thickness(1));
|
||||
}
|
||||
var cpFactory = new FrameworkElementFactory(typeof(ContentPresenter));
|
||||
cpFactory.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Center);
|
||||
bdFactory.AppendChild(cpFactory);
|
||||
template.VisualTree = bdFactory;
|
||||
btn.Template = template;
|
||||
btn.Content = new TextBlock { Text = text, FontSize = 11, Foreground = new SolidColorBrush(fg), FontWeight = FontWeights.SemiBold };
|
||||
return btn;
|
||||
}
|
||||
}
|
||||
43
src/AxCopilot/Views/DockBarWindow.xaml
Normal file
43
src/AxCopilot/Views/DockBarWindow.xaml
Normal file
@@ -0,0 +1,43 @@
|
||||
<Window x:Class="AxCopilot.Views.DockBarWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="AX Dock"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
ShowInTaskbar="False"
|
||||
Topmost="True"
|
||||
ResizeMode="NoResize"
|
||||
SizeToContent="WidthAndHeight"
|
||||
KeyDown="Window_KeyDown">
|
||||
|
||||
<Grid Margin="4">
|
||||
<!-- 무지개 글로우 보더 (설정 시 표시) -->
|
||||
<Border x:Name="RainbowGlowBorder" CornerRadius="15" Visibility="Collapsed"
|
||||
Margin="-2">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush x:Name="RainbowBrush" StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Color="#FF0000" Offset="0.0"/>
|
||||
<GradientStop Color="#FF8800" Offset="0.17"/>
|
||||
<GradientStop Color="#FFFF00" Offset="0.33"/>
|
||||
<GradientStop Color="#00FF00" Offset="0.50"/>
|
||||
<GradientStop Color="#0088FF" Offset="0.67"/>
|
||||
<GradientStop Color="#4400FF" Offset="0.83"/>
|
||||
<GradientStop Color="#FF0088" Offset="1.0"/>
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
<Border.Opacity>0.45</Border.Opacity>
|
||||
<Border.Effect>
|
||||
<BlurEffect Radius="4"/>
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
<!-- 메인 독 보더 -->
|
||||
<Border x:Name="DockBorder" CornerRadius="14"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource AccentColor}"
|
||||
BorderThickness="1"
|
||||
Padding="8,5">
|
||||
<StackPanel x:Name="DockContent" Orientation="Horizontal"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
327
src/AxCopilot/Views/DockBarWindow.xaml.cs
Normal file
327
src/AxCopilot/Views/DockBarWindow.xaml.cs
Normal file
@@ -0,0 +1,327 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 화면 하단에 고정되는 미니 독 바.
|
||||
/// 설정(DockBarItems)에 따라 표시 항목이 결정됩니다.
|
||||
/// 가능한 항목: launcher, clipboard, capture, agent, clock, cpu, ram, quickinput
|
||||
/// </summary>
|
||||
public partial class DockBarWindow : Window
|
||||
{
|
||||
private DispatcherTimer? _timer;
|
||||
private PerformanceCounter? _cpuCounter;
|
||||
private TextBlock? _cpuText;
|
||||
private TextBlock? _ramText;
|
||||
private TextBlock? _clockText;
|
||||
private TextBox? _quickInput;
|
||||
|
||||
/// <summary>런처에 검색어를 전달하는 콜백.</summary>
|
||||
public Action<string>? OnQuickSearch { get; set; }
|
||||
|
||||
/// <summary>캡처 직접 실행 콜백.</summary>
|
||||
public Action? OnCapture { get; set; }
|
||||
|
||||
/// <summary>AX Agent 대화창 열기 콜백.</summary>
|
||||
public Action? OnOpenAgent { get; set; }
|
||||
|
||||
// 표시 가능한 모든 아이템 정의
|
||||
private static readonly (string Key, string Icon, string Tooltip)[] AllItems =
|
||||
{
|
||||
("launcher", "\uE721", "AX Commander"),
|
||||
("clipboard", "\uE77F", "클립보드 히스토리"),
|
||||
("capture", "\uE722", "화면 캡처"),
|
||||
("agent", "\uE8BD", "AX Agent"),
|
||||
("clock", "\uE823", "시계"),
|
||||
("cpu", "\uE950", "CPU"),
|
||||
("ram", "\uE7F4", "RAM"),
|
||||
("quickinput", "\uE8D3", "빠른 입력"),
|
||||
};
|
||||
|
||||
private DispatcherTimer? _glowTimer;
|
||||
|
||||
/// <summary>설정 저장 콜백 (위치 저장용).</summary>
|
||||
public Action<double, double>? OnPositionChanged { get; set; }
|
||||
|
||||
public DockBarWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
MouseLeftButtonDown += (_, e) => { if (e.LeftButton == MouseButtonState.Pressed) try { DragMove(); } catch { } };
|
||||
LocationChanged += (_, _) => OnPositionChanged?.Invoke(Left, Top);
|
||||
Loaded += (_, _) => PositionDock();
|
||||
Closed += (_, _) => { _timer?.Stop(); _glowTimer?.Stop(); _cpuCounter?.Dispose(); };
|
||||
}
|
||||
|
||||
/// <summary>투명도, 위치, 글로우를 설정에서 적용합니다.</summary>
|
||||
public void ApplySettings(double opacity, double left, double top, bool rainbowGlow)
|
||||
{
|
||||
Opacity = Math.Clamp(opacity, 0.3, 1.0);
|
||||
|
||||
if (left >= 0 && top >= 0)
|
||||
{
|
||||
Left = left;
|
||||
Top = top;
|
||||
}
|
||||
else
|
||||
{
|
||||
// -1이면 중앙으로 이동
|
||||
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded, PositionDock);
|
||||
}
|
||||
|
||||
if (rainbowGlow)
|
||||
StartRainbowGlow();
|
||||
else
|
||||
StopRainbowGlow();
|
||||
}
|
||||
|
||||
private void StartRainbowGlow()
|
||||
{
|
||||
RainbowGlowBorder.Visibility = Visibility.Visible;
|
||||
_glowTimer ??= new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(50) };
|
||||
var startAngle = 0.0;
|
||||
_glowTimer.Tick += (_, _) =>
|
||||
{
|
||||
startAngle += 2;
|
||||
if (startAngle >= 360) startAngle -= 360;
|
||||
var rad = startAngle * Math.PI / 180.0;
|
||||
RainbowBrush.StartPoint = new Point(0.5 + 0.5 * Math.Cos(rad), 0.5 + 0.5 * Math.Sin(rad));
|
||||
RainbowBrush.EndPoint = new Point(0.5 - 0.5 * Math.Cos(rad), 0.5 - 0.5 * Math.Sin(rad));
|
||||
};
|
||||
_glowTimer.Start();
|
||||
}
|
||||
|
||||
private void StopRainbowGlow()
|
||||
{
|
||||
_glowTimer?.Stop();
|
||||
RainbowGlowBorder.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
// 기획된 고정 표시 순서
|
||||
private static readonly string[] FixedOrder =
|
||||
{ "launcher", "clipboard", "capture", "agent", "clock", "cpu", "ram", "quickinput" };
|
||||
|
||||
/// <summary>설정에서 표시 항목 목록을 받아 독 바를 빌드합니다. 표시 순서는 기획 순서 고정.</summary>
|
||||
public void BuildFromSettings(List<string> itemKeys)
|
||||
{
|
||||
DockContent.Children.Clear();
|
||||
_cpuText = null; _ramText = null; _clockText = null; _quickInput = null;
|
||||
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
bool needTimer = false;
|
||||
bool addedFirst = false;
|
||||
|
||||
// 기획 순서에서 활성화된 항목만 순서대로 표시
|
||||
var enabledSet = new HashSet<string>(itemKeys, StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var key in FixedOrder.Where(k => enabledSet.Contains(k)))
|
||||
{
|
||||
if (addedFirst) AddSeparator();
|
||||
addedFirst = true;
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case "launcher":
|
||||
AddButton("\uE721", "AX Commander", primaryText, () => OnQuickSearch?.Invoke(""));
|
||||
break;
|
||||
case "clipboard":
|
||||
AddButton("\uE77F", "클립보드", primaryText, () => OnQuickSearch?.Invoke("#"));
|
||||
break;
|
||||
case "capture":
|
||||
AddButton("\uE722", "캡처", primaryText, () =>
|
||||
{
|
||||
if (OnCapture != null) OnCapture();
|
||||
else OnQuickSearch?.Invoke("cap");
|
||||
});
|
||||
break;
|
||||
case "agent":
|
||||
AddButton("\uE8BD", "AI", primaryText, () =>
|
||||
{
|
||||
if (OnOpenAgent != null) OnOpenAgent();
|
||||
else OnQuickSearch?.Invoke("!");
|
||||
});
|
||||
break;
|
||||
case "clock":
|
||||
_clockText = new TextBlock
|
||||
{
|
||||
Text = DateTime.Now.ToString("HH:mm"),
|
||||
FontSize = 12, FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(6, 0, 6, 0),
|
||||
};
|
||||
DockContent.Children.Add(_clockText);
|
||||
needTimer = true;
|
||||
break;
|
||||
case "cpu":
|
||||
var cpuPanel = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
|
||||
cpuPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE950", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 11, Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 3, 0)
|
||||
});
|
||||
_cpuText = new TextBlock { Text = "0%", FontSize = 11, Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, Width = 28, TextAlignment = TextAlignment.Right };
|
||||
cpuPanel.Children.Add(_cpuText);
|
||||
DockContent.Children.Add(cpuPanel);
|
||||
needTimer = true;
|
||||
if (_cpuCounter == null)
|
||||
try { _cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total"); _cpuCounter.NextValue(); } catch { }
|
||||
break;
|
||||
case "ram":
|
||||
var ramPanel = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center };
|
||||
ramPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE7F4", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 11, Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 3, 0)
|
||||
});
|
||||
_ramText = new TextBlock { Text = "0%", FontSize = 11, Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, Width = 28, TextAlignment = TextAlignment.Right };
|
||||
ramPanel.Children.Add(_ramText);
|
||||
DockContent.Children.Add(ramPanel);
|
||||
needTimer = true;
|
||||
break;
|
||||
case "quickinput":
|
||||
var inputBorder = new Border
|
||||
{
|
||||
Background = TryFindResource("ItemBackground") as Brush ?? Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(8), Padding = new Thickness(8, 3, 8, 3),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
var inputPanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
inputPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE721", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 11, Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 5, 0)
|
||||
});
|
||||
_quickInput = new TextBox
|
||||
{
|
||||
Width = 100, FontSize = 11,
|
||||
Foreground = primaryText,
|
||||
CaretBrush = accentBrush,
|
||||
Background = Brushes.Transparent, BorderThickness = new Thickness(0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
_quickInput.KeyDown += QuickInput_KeyDown;
|
||||
inputPanel.Children.Add(_quickInput);
|
||||
inputBorder.Child = inputPanel;
|
||||
DockContent.Children.Add(inputBorder);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (needTimer)
|
||||
{
|
||||
_timer ??= new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||
_timer.Tick -= OnTick;
|
||||
_timer.Tick += OnTick;
|
||||
_timer.Start();
|
||||
OnTick(null, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>사용 가능한 모든 아이템 키와 라벨 목록을 반환합니다.</summary>
|
||||
public static IReadOnlyList<(string Key, string Icon, string Tooltip)> AvailableItems => AllItems;
|
||||
|
||||
private void AddButton(string icon, string tooltip, Brush foreground, Action click)
|
||||
{
|
||||
var border = new Border
|
||||
{
|
||||
Width = 30, Height = 30,
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x01, 0xFF, 0xFF, 0xFF)), // 거의 투명하지만 히트 테스트 가능
|
||||
Cursor = Cursors.Hand,
|
||||
ToolTip = tooltip,
|
||||
Margin = new Thickness(2, 0, 2, 0),
|
||||
};
|
||||
border.Child = new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 14, Foreground = foreground,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
IsHitTestVisible = false, // 텍스트가 이벤트를 가로채지 않도록
|
||||
};
|
||||
border.MouseEnter += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x22, 0xFF, 0xFF, 0xFF)); };
|
||||
border.MouseLeave += (s, _) => { if (s is Border b) b.Background = new SolidColorBrush(Color.FromArgb(0x01, 0xFF, 0xFF, 0xFF)); };
|
||||
border.MouseLeftButtonDown += (_, e) => { e.Handled = true; click(); };
|
||||
DockContent.Children.Add(border);
|
||||
}
|
||||
|
||||
private void AddSeparator()
|
||||
{
|
||||
DockContent.Children.Add(new Border
|
||||
{
|
||||
Width = 1, Height = 20, Margin = new Thickness(6, 0, 6, 0),
|
||||
Background = TryFindResource("SeparatorColor") as Brush ?? Brushes.Gray,
|
||||
});
|
||||
}
|
||||
|
||||
private void PositionDock()
|
||||
{
|
||||
var screen = SystemParameters.WorkArea;
|
||||
Left = (screen.Width - ActualWidth) / 2 + screen.Left;
|
||||
Top = screen.Bottom - ActualHeight - 8;
|
||||
}
|
||||
|
||||
private void OnTick(object? sender, EventArgs e)
|
||||
{
|
||||
if (_clockText != null)
|
||||
_clockText.Text = DateTime.Now.ToString("HH:mm");
|
||||
|
||||
if (_cpuText != null)
|
||||
{
|
||||
try { _cpuText.Text = $"{_cpuCounter?.NextValue() ?? 0:F0}%"; }
|
||||
catch { _cpuText.Text = "-"; }
|
||||
}
|
||||
|
||||
if (_ramText != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var m = new MEMORYSTATUSEX { dwLength = (uint)Marshal.SizeOf<MEMORYSTATUSEX>() };
|
||||
_ramText.Text = GlobalMemoryStatusEx(ref m) ? $"{m.dwMemoryLoad}%" : "-";
|
||||
}
|
||||
catch { _ramText.Text = "-"; }
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll")] private static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MEMORYSTATUSEX
|
||||
{
|
||||
public uint dwLength; public uint dwMemoryLoad;
|
||||
public ulong ullTotalPhys, ullAvailPhys, ullTotalPageFile, ullAvailPageFile, ullTotalVirtual, ullAvailVirtual, ullAvailExtendedVirtual;
|
||||
}
|
||||
|
||||
private void QuickInput_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter && _quickInput != null && !string.IsNullOrWhiteSpace(_quickInput.Text))
|
||||
{
|
||||
OnQuickSearch?.Invoke(_quickInput.Text);
|
||||
_quickInput.Text = "";
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Key.Escape && _quickInput != null)
|
||||
{
|
||||
_quickInput.Text = "";
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void Window_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape) Hide();
|
||||
}
|
||||
}
|
||||
52
src/AxCopilot/Views/EyeDropperWindow.xaml
Normal file
52
src/AxCopilot/Views/EyeDropperWindow.xaml
Normal file
@@ -0,0 +1,52 @@
|
||||
<Window x:Class="AxCopilot.Views.EyeDropperWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
UseLayoutRounding="True"
|
||||
SnapsToDevicePixels="True"
|
||||
Background="Transparent"
|
||||
Topmost="True"
|
||||
ShowInTaskbar="False"
|
||||
ResizeMode="NoResize"
|
||||
WindowStartupLocation="Manual"
|
||||
Cursor="Cross"
|
||||
MouseDown="Window_MouseDown"
|
||||
MouseMove="Window_MouseMove"
|
||||
KeyDown="Window_KeyDown">
|
||||
|
||||
<!-- 전체 화면 투명 오버레이 (클릭 이벤트 수신용) -->
|
||||
<Canvas x:Name="RootCanvas" Background="#01000000">
|
||||
<!-- 마우스 따라다니는 돋보기 -->
|
||||
<Border x:Name="Magnifier"
|
||||
Width="90" Height="90"
|
||||
CornerRadius="45"
|
||||
BorderBrush="#88FFFFFF" BorderThickness="2"
|
||||
IsHitTestVisible="False"
|
||||
Visibility="Collapsed">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="Black" BlurRadius="8" ShadowDepth="2" Opacity="0.28"/>
|
||||
</Border.Effect>
|
||||
<Grid>
|
||||
<Ellipse x:Name="MagPreview" Width="86" Height="86"/>
|
||||
<!-- 중앙 십자선 -->
|
||||
<Rectangle Width="1" Height="20" Fill="#88FFFFFF" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Rectangle Width="20" Height="1" Fill="#88FFFFFF" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- HEX 코드 레이블 -->
|
||||
<Border x:Name="HexLabel"
|
||||
Background="#CC1A1B2E"
|
||||
CornerRadius="6"
|
||||
Padding="8,4"
|
||||
IsHitTestVisible="False"
|
||||
Visibility="Collapsed">
|
||||
<TextBlock x:Name="HexLabelText"
|
||||
FontSize="12"
|
||||
FontFamily="Consolas"
|
||||
FontWeight="Bold"
|
||||
Foreground="White"/>
|
||||
</Border>
|
||||
</Canvas>
|
||||
</Window>
|
||||
135
src/AxCopilot/Views/EyeDropperWindow.xaml.cs
Normal file
135
src/AxCopilot/Views/EyeDropperWindow.xaml.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 전체 화면 스포이드 모드. 마우스를 따라다니는 돋보기와 HEX 코드를 표시하고,
|
||||
/// 클릭 시 해당 픽셀 색상을 캡처하여 ColorPickResultWindow를 표시합니다.
|
||||
/// </summary>
|
||||
public partial class EyeDropperWindow : Window
|
||||
{
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetCursorPos(out POINT lpPoint);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
private static extern uint GetPixel(IntPtr hdc, int x, int y);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetDC(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct POINT { public int X, Y; }
|
||||
|
||||
/// <summary>클릭으로 선택된 색상. null이면 취소.</summary>
|
||||
public System.Drawing.Color? PickedColor { get; private set; }
|
||||
public double PickX { get; private set; }
|
||||
public double PickY { get; private set; }
|
||||
|
||||
private readonly System.Drawing.Rectangle _screenBounds;
|
||||
|
||||
public EyeDropperWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// 전체 모니터 영역
|
||||
_screenBounds = GetAllScreenBounds();
|
||||
Left = _screenBounds.X;
|
||||
Top = _screenBounds.Y;
|
||||
Width = _screenBounds.Width;
|
||||
Height = _screenBounds.Height;
|
||||
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
Magnifier.Visibility = Visibility.Visible;
|
||||
HexLabel.Visibility = Visibility.Visible;
|
||||
};
|
||||
}
|
||||
|
||||
private void Window_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
var pos = e.GetPosition(RootCanvas);
|
||||
var color = GetScreenPixelColor();
|
||||
|
||||
// 돋보기 위치 (커서 오른쪽 위)
|
||||
Canvas.SetLeft(Magnifier, pos.X + 16);
|
||||
Canvas.SetTop(Magnifier, pos.Y - 100);
|
||||
|
||||
// HEX 레이블 위치
|
||||
Canvas.SetLeft(HexLabel, pos.X + 16);
|
||||
Canvas.SetTop(HexLabel, pos.Y - 10);
|
||||
|
||||
// 돋보기 색상 미리보기
|
||||
var wpfColor = System.Windows.Media.Color.FromRgb(color.R, color.G, color.B);
|
||||
MagPreview.Fill = new SolidColorBrush(wpfColor);
|
||||
HexLabelText.Text = $"#{color.R:X2}{color.G:X2}{color.B:X2}";
|
||||
}
|
||||
|
||||
private void Window_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ChangedButton == MouseButton.Left)
|
||||
{
|
||||
PickedColor = GetScreenPixelColor();
|
||||
GetCursorPos(out var pt);
|
||||
PickX = pt.X;
|
||||
PickY = pt.Y;
|
||||
Close();
|
||||
}
|
||||
else if (e.ChangedButton == MouseButton.Right)
|
||||
{
|
||||
PickedColor = null;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void Window_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
PickedColor = null;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
private static System.Drawing.Color GetScreenPixelColor()
|
||||
{
|
||||
GetCursorPos(out var pt);
|
||||
var hdc = GetDC(IntPtr.Zero);
|
||||
try
|
||||
{
|
||||
uint pixel = GetPixel(hdc, pt.X, pt.Y);
|
||||
int r = (int)(pixel & 0xFF);
|
||||
int g = (int)((pixel >> 8) & 0xFF);
|
||||
int b = (int)((pixel >> 16) & 0xFF);
|
||||
return System.Drawing.Color.FromArgb(r, g, b);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReleaseDC(IntPtr.Zero, hdc);
|
||||
}
|
||||
}
|
||||
|
||||
private static System.Drawing.Rectangle GetAllScreenBounds()
|
||||
{
|
||||
int left = 0, top = 0, right = 0, bottom = 0;
|
||||
foreach (var screen in System.Windows.Forms.Screen.AllScreens)
|
||||
{
|
||||
var b = screen.Bounds;
|
||||
left = Math.Min(left, b.Left);
|
||||
top = Math.Min(top, b.Top);
|
||||
right = Math.Max(right, b.Right);
|
||||
bottom = Math.Max(bottom, b.Bottom);
|
||||
}
|
||||
return new System.Drawing.Rectangle(left, top, right - left, bottom - top);
|
||||
}
|
||||
}
|
||||
78
src/AxCopilot/Views/GuideViewerWindow.xaml
Normal file
78
src/AxCopilot/Views/GuideViewerWindow.xaml
Normal file
@@ -0,0 +1,78 @@
|
||||
<Window x:Class="AxCopilot.Views.GuideViewerWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
|
||||
Title="AX Copilot — 가이드"
|
||||
Width="920" Height="740"
|
||||
WindowStyle="None" AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="CanResizeWithGrip">
|
||||
<Border Background="{DynamicResource LauncherBackground}" CornerRadius="12"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Margin="6">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="20" ShadowDepth="4" Opacity="0.3" Color="Black" Direction="270"/>
|
||||
</Border.Effect>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="44"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 타이틀바 -->
|
||||
<Border Grid.Row="0" CornerRadius="12,12,0,0"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
|
||||
<Grid Margin="16,0,8,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 좌측: 아이콘 + 제목 -->
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="15"
|
||||
Foreground="{DynamicResource AccentColor}"
|
||||
VerticalAlignment="Center" Margin="0,1,10,0"/>
|
||||
<TextBlock x:Name="TitleText" Text="AX Copilot 사용 가이드"
|
||||
FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 우측: 최소화, 최대화, 닫기 -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border Width="40" Height="40" Background="Transparent" Cursor="Hand"
|
||||
CornerRadius="8"
|
||||
MouseLeftButtonDown="MinBtn_Click"
|
||||
MouseEnter="TitleBtn_Enter" MouseLeave="TitleBtn_Leave">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border Width="40" Height="40" Background="Transparent" Cursor="Hand"
|
||||
CornerRadius="8"
|
||||
MouseLeftButtonDown="MaxBtn_Click"
|
||||
MouseEnter="TitleBtn_Enter" MouseLeave="TitleBtn_Leave">
|
||||
<TextBlock x:Name="MaxBtnIcon" Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border Width="40" Height="40" Background="Transparent" Cursor="Hand"
|
||||
CornerRadius="8"
|
||||
MouseLeftButtonDown="CloseBtn_Click"
|
||||
MouseEnter="CloseBtnEnter" MouseLeave="TitleBtn_Leave">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- WebView2 -->
|
||||
<wv2:WebView2 x:Name="GuideBrowser" Grid.Row="1"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
165
src/AxCopilot/Views/GuideViewerWindow.xaml.cs
Normal file
165
src/AxCopilot/Views/GuideViewerWindow.xaml.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class GuideViewerWindow : Window
|
||||
{
|
||||
private string? _pendingHtml;
|
||||
private Uri? _pendingUri;
|
||||
|
||||
public GuideViewerWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += OnLoaded;
|
||||
KeyDown += (_, e) => { if (e.Key == Key.Escape) Close(); };
|
||||
StateChanged += (_, _) =>
|
||||
{
|
||||
MaxBtnIcon.Text = WindowState == WindowState.Maximized ? "\uE923" : "\uE922";
|
||||
};
|
||||
}
|
||||
|
||||
private async void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var app = Application.Current as App;
|
||||
var devMode = app?.SettingsService?.Settings?.Llm?.DevMode ?? false;
|
||||
PrepareGuide(devMode);
|
||||
|
||||
try
|
||||
{
|
||||
// WebView2 초기화
|
||||
var env = await CoreWebView2Environment.CreateAsync(
|
||||
userDataFolder: Path.Combine(Path.GetTempPath(), "AxCopilot_GuideViewer"));
|
||||
await GuideBrowser.EnsureCoreWebView2Async(env);
|
||||
|
||||
// 초기화 완료 후 대기 중인 콘텐츠 로드
|
||||
if (_pendingHtml != null)
|
||||
GuideBrowser.NavigateToString(_pendingHtml);
|
||||
else if (_pendingUri != null)
|
||||
GuideBrowser.Source = _pendingUri;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// WebView2 런타임 미설치 시 오류 메시지 표시
|
||||
System.Diagnostics.Debug.WriteLine($"WebView2 init error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>개발자 모드 여부에 따라 적절한 가이드를 준비합니다.</summary>
|
||||
public void PrepareGuide(bool devMode)
|
||||
{
|
||||
TitleText.Text = devMode ? "AX Copilot 개발자 가이드" : "AX Copilot 사용 가이드";
|
||||
Title = devMode ? "AX Copilot — 개발자 가이드" : "AX Copilot — 사용 가이드";
|
||||
|
||||
try
|
||||
{
|
||||
var exeDir = Path.GetDirectoryName(Environment.ProcessPath)
|
||||
?? AppContext.BaseDirectory;
|
||||
var assetsDir = Path.Combine(exeDir, "Assets");
|
||||
|
||||
// 1차: 암호화된 가이드 파일 (.enc) 시도
|
||||
var encFile = devMode
|
||||
? Path.Combine(assetsDir, "guide_dev.enc")
|
||||
: Path.Combine(assetsDir, "guide_user.enc");
|
||||
|
||||
if (File.Exists(encFile))
|
||||
{
|
||||
_pendingHtml = Services.GuideEncryptor.DecryptToString(encFile);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2차: 평문 가이드 파일 (.htm) 폴백 (개발 환경)
|
||||
var htmFile = devMode
|
||||
? Path.Combine(assetsDir, "AX Copilot 개발자가이드.htm")
|
||||
: Path.Combine(assetsDir, "AX Copilot 사용가이드.htm");
|
||||
|
||||
if (File.Exists(htmFile))
|
||||
{
|
||||
_pendingUri = new Uri(htmFile);
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingHtml = "<html><body style='font-family:Segoe UI;padding:40px;color:#666;'>" +
|
||||
"<h2>가이드를 찾을 수 없습니다</h2>" +
|
||||
$"<p>경로: {encFile}</p></body></html>";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_pendingHtml = "<html><body style='font-family:Segoe UI;padding:40px;color:#c00;'>" +
|
||||
$"<h2>가이드 로드 오류</h2><p>{ex.Message}</p></body></html>";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>외부에서 직접 가이드를 로드합니다 (WebView2 초기화 후 호출).</summary>
|
||||
public void LoadGuide(bool devMode)
|
||||
{
|
||||
PrepareGuide(devMode);
|
||||
if (GuideBrowser.CoreWebView2 != null)
|
||||
{
|
||||
if (_pendingHtml != null)
|
||||
GuideBrowser.NavigateToString(_pendingHtml);
|
||||
else if (_pendingUri != null)
|
||||
GuideBrowser.Source = _pendingUri;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 타이틀 바 ───────────────────────────────────────────────
|
||||
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ClickCount == 2)
|
||||
{
|
||||
ToggleMaximize();
|
||||
return;
|
||||
}
|
||||
DragMove();
|
||||
}
|
||||
|
||||
private void MinBtn_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
WindowState = WindowState.Minimized;
|
||||
}
|
||||
|
||||
private void MaxBtn_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
ToggleMaximize();
|
||||
}
|
||||
|
||||
private void CloseBtn_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void ToggleMaximize()
|
||||
{
|
||||
WindowState = WindowState == WindowState.Maximized
|
||||
? WindowState.Normal
|
||||
: WindowState.Maximized;
|
||||
}
|
||||
|
||||
private void TitleBtn_Enter(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (sender is Border b)
|
||||
b.Background = TryFindResource("ItemHoverBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
}
|
||||
|
||||
private void CloseBtnEnter(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (sender is Border b)
|
||||
b.Background = new SolidColorBrush(Color.FromArgb(0x44, 0xFF, 0x40, 0x40));
|
||||
}
|
||||
|
||||
private void TitleBtn_Leave(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (sender is Border b)
|
||||
b.Background = Brushes.Transparent;
|
||||
}
|
||||
}
|
||||
212
src/AxCopilot/Views/HelpDetailWindow.xaml
Normal file
212
src/AxCopilot/Views/HelpDetailWindow.xaml
Normal file
@@ -0,0 +1,212 @@
|
||||
<Window x:Class="AxCopilot.Views.HelpDetailWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="AX Commander — 기능 목록"
|
||||
Width="1020" Height="720"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
UseLayoutRounding="True"
|
||||
SnapsToDevicePixels="True"
|
||||
Background="Transparent"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ShowInTaskbar="False"
|
||||
MouseDown="Window_MouseDown"
|
||||
KeyDown="Window_KeyDown">
|
||||
|
||||
<Window.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
|
||||
</Window.Resources>
|
||||
|
||||
<Border Margin="20"
|
||||
CornerRadius="16"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource AccentColor}"
|
||||
BorderThickness="1">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="{DynamicResource ShadowColor}" BlurRadius="28" ShadowDepth="5" Opacity="0.32"/>
|
||||
</Border.Effect>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ─── 헤더 ─────────────────────────────────────── -->
|
||||
<Border Grid.Row="0" CornerRadius="16,16,0,0" Padding="24,18,24,14">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Color="#1A1B2E" Offset="0"/>
|
||||
<GradientStop Color="#2B2D5B" Offset="0.5"/>
|
||||
<GradientStop Color="#3B4ECC" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
<Grid>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="22"
|
||||
Foreground="#6496FF"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0"/>
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock Text="AX Commander — 전체 기능 목록"
|
||||
FontSize="16" FontWeight="SemiBold"
|
||||
Foreground="White"/>
|
||||
<TextBlock x:Name="SubtitleText"
|
||||
FontSize="11"
|
||||
Foreground="#8899CC"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<Button HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
Width="32" Height="32"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
Cursor="Hand" Click="Close_Click">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="13"
|
||||
Foreground="#8899CC"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ─── 상단 탭 메뉴 + 검색 ── -->
|
||||
<Border Grid.Row="1" Background="{DynamicResource HintBackground}" Padding="16,8">
|
||||
<Grid>
|
||||
<StackPanel x:Name="TopMenuBar"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"/>
|
||||
<!-- 검색 박스 -->
|
||||
<Border HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
Background="{DynamicResource ItemBackground}" CornerRadius="8" Padding="8,4">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="12" Foreground="{DynamicResource AccentColor}"
|
||||
VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBox x:Name="SearchBox"
|
||||
Width="140" FontSize="11"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
CaretBrush="{DynamicResource AccentColor}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
VerticalAlignment="Center"
|
||||
TextChanged="SearchBox_TextChanged"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ─── 하위 카테고리 탭 바 ────────────────────────── -->
|
||||
<Border Grid.Row="2" Background="{DynamicResource SeparatorColor}" Padding="12,6">
|
||||
<WrapPanel x:Name="CategoryBar"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
|
||||
<!-- ─── 기능 목록 (현재 페이지) ──────────────────────── -->
|
||||
<ScrollViewer Grid.Row="3"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
Padding="16,8">
|
||||
<ItemsControl x:Name="ItemsHost" Margin="4,0">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Margin="0,3" CornerRadius="10" Padding="14,10"
|
||||
Background="{DynamicResource ItemBackground}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="38"/>
|
||||
<ColumnDefinition Width="120"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="200"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 심볼 아이콘 -->
|
||||
<Border Grid.Column="0"
|
||||
Width="32" Height="32"
|
||||
CornerRadius="8"
|
||||
Background="{Binding ColorBrush}"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Symbol}"
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="14"
|
||||
Foreground="White"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
|
||||
<!-- 카테고리 + 예약어 -->
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="10,0,8,0">
|
||||
<TextBlock Text="{Binding Category}"
|
||||
FontSize="10" Foreground="{DynamicResource SecondaryText}"/>
|
||||
<TextBlock Text="{Binding Command}"
|
||||
FontFamily="Consolas"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 제목 + 설명 -->
|
||||
<StackPanel Grid.Column="2" VerticalAlignment="Center" Margin="0,0,12,0">
|
||||
<TextBlock Text="{Binding Title}"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}" TextWrapping="Wrap"/>
|
||||
<TextBlock Text="{Binding Description}"
|
||||
FontSize="11" Foreground="{DynamicResource SecondaryText}"
|
||||
TextWrapping="Wrap" Margin="0,2,0,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 예시 -->
|
||||
<Border Grid.Column="3"
|
||||
Background="{DynamicResource HintBackground}"
|
||||
CornerRadius="6"
|
||||
Padding="8,4"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding HasExample, Converter={StaticResource BoolToVis}}">
|
||||
<TextBlock Text="{Binding Example}"
|
||||
FontFamily="Consolas"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource AccentColor}"
|
||||
TextWrapping="Wrap"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ─── 푸터 (네비게이션 + 안내) ───────────────────── -->
|
||||
<Border Grid.Row="4"
|
||||
Background="{DynamicResource SeparatorColor}"
|
||||
CornerRadius="0,0,16,16"
|
||||
Padding="20,10">
|
||||
<Grid>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||
<Button x:Name="PrevBtn" Content="◀ 이전"
|
||||
Click="Prev_Click"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
Foreground="{DynamicResource AccentColor}" FontSize="12"
|
||||
Cursor="Hand" Padding="8,4" Margin="0,0,8,0"/>
|
||||
<TextBlock x:Name="PageIndicator"
|
||||
FontSize="11" Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<Button x:Name="NextBtn" Content="다음 ▶"
|
||||
Click="Next_Click"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
Foreground="{DynamicResource AccentColor}" FontSize="12"
|
||||
Cursor="Hand" Padding="8,4" Margin="8,0,0,0"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="← → 페이지 이동 · ESC 닫기"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
673
src/AxCopilot/Views/HelpDetailWindow.xaml.cs
Normal file
673
src/AxCopilot/Views/HelpDetailWindow.xaml.cs
Normal file
@@ -0,0 +1,673 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class HelpDetailWindow : Window
|
||||
{
|
||||
private const string CatAll = "전체";
|
||||
private const string CatPopular = "\u2B50 인기";
|
||||
|
||||
// ─── 상단 3탭 메뉴 ──────────────────────────────────────────────────────
|
||||
private enum TopMenu { Overview, Shortcuts, Prefixes }
|
||||
|
||||
private static readonly (TopMenu Key, string Label, string Icon)[] TopMenus =
|
||||
{
|
||||
(TopMenu.Overview, "개요", "\uE946"),
|
||||
(TopMenu.Shortcuts, "단축키 현황", "\uE765"),
|
||||
(TopMenu.Prefixes, "예약어 현황", "\uE8F4"),
|
||||
};
|
||||
|
||||
private TopMenu _currentTopMenu = TopMenu.Overview;
|
||||
|
||||
// ─── 데이터 ──────────────────────────────────────────────────────────────
|
||||
private readonly List<HelpItemModel> _allItems;
|
||||
private readonly List<HelpItemModel> _shortcutItems;
|
||||
private readonly List<HelpItemModel> _prefixItems;
|
||||
private readonly List<HelpItemModel> _overviewItems;
|
||||
private readonly string _globalHotkey;
|
||||
|
||||
private List<string> _categories = new();
|
||||
private int _currentPage;
|
||||
|
||||
public HelpDetailWindow(IEnumerable<HelpItemModel> items, int totalCount,
|
||||
string globalHotkey = "Alt+Space")
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_allItems = items.ToList();
|
||||
_globalHotkey = globalHotkey;
|
||||
|
||||
// ── 개요 항목 (전체 / 개요 / AI / 업무 보조) ───────────────────────
|
||||
_overviewItems = new List<HelpItemModel>
|
||||
{
|
||||
// ── 개요 ────────────────────────────────────────────────────
|
||||
new()
|
||||
{
|
||||
Category = "개요", Command = "AX Commander",
|
||||
Title = "키보드 하나로 모든 업무를 제어하는 AX Commander",
|
||||
Description = "단축키 한 번으로 앱 실행, 파일/폴더 검색, 계산, 클립보드 관리, 화면 캡처, 창 전환, 시스템 제어, 업무 일지, 루틴 자동화, AI 대화까지. 40개+ 명령어를 예약어로 즉시 접근.",
|
||||
Symbol = "\uE946",
|
||||
ColorBrush = ParseColor("#4B5EFC")
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "개요", Command = "사용법",
|
||||
Title = "예약어 + 키워드 → Enter",
|
||||
Description = "특수 문자를 맨 앞에 입력하면 해당 기능 모드로 전환됩니다. 예) = 수식, # 클립보드, ? 웹검색, ! AI 대화, cap 캡처. 예약어 없이 입력하면 앱/파일/폴더 검색.",
|
||||
Symbol = "\uE765",
|
||||
ColorBrush = ParseColor("#0078D4")
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "개요", Command = "데이터 보호",
|
||||
Title = "모든 데이터는 내 PC에만 안전하게 저장",
|
||||
Description = "설정, 클립보드, 대화 내역, 통계, 로그 등 모든 데이터가 로컬에 암호화 저장됩니다. 다른 PC에서는 열 수 없으며, 보존 기간 만료 시 자동 삭제됩니다.",
|
||||
Symbol = "\uE8B7",
|
||||
ColorBrush = ParseColor("#107C10")
|
||||
},
|
||||
|
||||
// ── AI ──────────────────────────────────────────────────────
|
||||
new()
|
||||
{
|
||||
Category = "AI", Command = "! (AX Agent)",
|
||||
Title = "AX Agent — AI 어시스턴트와 대화",
|
||||
Description = "AX Commander에서 ! 를 입력하면 AI 대화가 열립니다. 질문을 바로 물어보거나, 이전 대화를 이어갈 수 있습니다. 대화 주제별 분류, 검색, 사이드바 토글을 지원합니다.",
|
||||
Example = "! 오늘 회의 요약해줘\n! 이메일 초안 작성해줘",
|
||||
Symbol = "\uE8BD",
|
||||
ColorBrush = ParseColor("#8B2FC9")
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "AI", Command = "3탭 구조",
|
||||
Title = "Chat · Cowork · Code — 용도별 3탭 구조",
|
||||
Description = "Chat: 자유로운 AI 대화 — 질문, 번역, 요약, 아이디어 브레인스토밍.\n" +
|
||||
"Cowork: 업무 보조 — 파일 읽기/쓰기, 문서 생성(Excel·Word·PPT·HTML), 데이터 분석, 보고서 작성.\n" +
|
||||
"Code: 코딩 도우미 — 개발, 리팩터링, 코드 리뷰, 보안 점검, 테스트 작성. 개발 환경 자동 감지.",
|
||||
Symbol = "\uE71B",
|
||||
ColorBrush = ParseColor("#3B82F6")
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "AI", Command = "AI 서비스",
|
||||
Title = "사내 AI · 외부 AI 서비스 선택 가능",
|
||||
Description = "관리자가 설정한 AI 서비스에 자동 연결됩니다. 사내 서버와 외부 서비스 중 선택할 수 있으며, 연결 정보는 암호화되어 보호됩니다.",
|
||||
Symbol = "\uE968",
|
||||
ColorBrush = ParseColor("#0078D4")
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "AI", Command = "실시간 응답",
|
||||
Title = "AI 응답이 실시간으로 표시",
|
||||
Description = "AI의 응답이 생성되는 즉시 화면에 표시됩니다. 긴 답변도 기다리지 않고 바로 읽기 시작할 수 있습니다.",
|
||||
Symbol = "\uE8AB",
|
||||
ColorBrush = ParseColor("#107C10")
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "AI", Command = "대화 보호",
|
||||
Title = "모든 대화 내역이 암호화 저장",
|
||||
Description = "대화 내역은 내 PC에서만 열람할 수 있도록 암호화됩니다. 다른 PC로 복사해도 열 수 없으며, 보존 기간 만료 시 자동 삭제됩니다.",
|
||||
Symbol = "\uE72E",
|
||||
ColorBrush = ParseColor("#C50F1F")
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "AI", Command = "AI 규칙 설정",
|
||||
Title = "관리자가 설정한 응답 규칙이 자동 적용",
|
||||
Description = "관리자가 지정한 역할, 응답 톤, 업무 규칙이 모든 대화에 자동 적용됩니다. AI의 응답 스타일을 일괄 관리할 수 있습니다.",
|
||||
Symbol = "\uE771",
|
||||
ColorBrush = ParseColor("#D97706")
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "AI", Command = "대화 관리",
|
||||
Title = "분류 · 검색 · 타임라인",
|
||||
Description = "대화를 주제별로 분류하고, 오늘/이전 타임라인으로 구분합니다. 검색과 카테고리 필터로 빠르게 찾을 수 있습니다.",
|
||||
Symbol = "\uE8F1",
|
||||
ColorBrush = ParseColor("#4B5EFC")
|
||||
},
|
||||
|
||||
// ── 업무 보조 ───────────────────────────────────────────────
|
||||
new()
|
||||
{
|
||||
Category = "업무 보조", Command = "note",
|
||||
Title = "빠른 메모 저장 · 검색",
|
||||
Description = "note 뒤에 내용을 입력하면 즉시 메모가 저장됩니다. 나중에 note 키워드로 검색하면 이전 메모를 다시 찾을 수 있습니다. 간단한 아이디어나 할 일을 빠르게 기록하세요.",
|
||||
Example = "note 내일 보고서 마감\nnote 김부장님 연락처 확인",
|
||||
Symbol = "\uE70B",
|
||||
ColorBrush = ParseColor("#D97706")
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "업무 보조", Command = "journal",
|
||||
Title = "업무 일지 작성",
|
||||
Description = "journal 뒤에 내용을 입력하면 날짜별 업무 일지로 자동 기록됩니다. 하루 동안의 업무 내용을 간결하게 쌓아가면 주간 보고나 회고에 활용할 수 있습니다.",
|
||||
Example = "journal 배포 완료\njournal 클라이언트 미팅 30분",
|
||||
Symbol = "\uE8F2",
|
||||
ColorBrush = ParseColor("#0078D4")
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "업무 보조", Command = "pipe",
|
||||
Title = "클립보드 텍스트 파이프라인 변환",
|
||||
Description = "클립보드에 복사된 텍스트를 여러 변환 함수로 한 번에 처리합니다. upper(대문자), lower(소문자), trim(공백 제거), wrap(줄바꿈), sort(정렬) 등을 연결할 수 있습니다.",
|
||||
Example = "pipe upper | trim | wrap 80",
|
||||
Symbol = "\uE8AB",
|
||||
ColorBrush = ParseColor("#107C10")
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "업무 보조", Command = "diff",
|
||||
Title = "두 텍스트 비교 (Diff)",
|
||||
Description = "클립보드의 두 텍스트를 비교하여 차이점을 한눈에 보여줍니다. 코드 변경 사항 확인, 문서 버전 비교 등에 유용합니다.",
|
||||
Example = "diff",
|
||||
Symbol = "\uE8FD",
|
||||
ColorBrush = ParseColor("#6B2C91")
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "업무 보조", Command = "encode",
|
||||
Title = "인코딩 · 디코딩 변환",
|
||||
Description = "Base64, URL, HTML, Unicode 등 다양한 형식으로 텍스트를 인코딩하거나 디코딩합니다. 개발자에게 특히 유용한 변환 도구입니다.",
|
||||
Example = "encode base64 hello\nencode url 한글 테스트",
|
||||
Symbol = "\uE943",
|
||||
ColorBrush = ParseColor("#323130")
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "업무 보조", Command = "routine",
|
||||
Title = "반복 작업 루틴 자동화",
|
||||
Description = "자주 하는 작업 묶음을 루틴으로 등록해 두면 한 번의 명령으로 여러 앱을 동시에 열거나 작업 환경을 구성할 수 있습니다. 출근·퇴근 루틴 등을 만들어 보세요.",
|
||||
Example = "routine start 출근\nroutine list",
|
||||
Symbol = "\uE8F5",
|
||||
ColorBrush = ParseColor("#BE185D")
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "업무 보조", Command = "stats",
|
||||
Title = "텍스트 통계 (글자수·단어수·줄수)",
|
||||
Description = "클립보드에 복사된 텍스트의 글자 수, 단어 수, 줄 수를 즉시 계산합니다. 보고서 분량 체크, SNS 글자 수 제한 확인 등에 활용하세요.",
|
||||
Example = "stats",
|
||||
Symbol = "\uE9D9",
|
||||
ColorBrush = ParseColor("#44546A")
|
||||
},
|
||||
new()
|
||||
{
|
||||
Category = "업무 보조", Command = "json",
|
||||
Title = "JSON 파싱 · 정리 · 미리보기",
|
||||
Description = "클립보드에 복사된 JSON을 자동으로 파싱하고 읽기 좋은 형태로 정리합니다. 들여쓰기, 키 하이라이트, 복사 기능을 제공합니다.",
|
||||
Example = "json {\"key\":\"value\"}",
|
||||
Symbol = "\uE8A5",
|
||||
ColorBrush = ParseColor("#4B5EFC")
|
||||
},
|
||||
};
|
||||
|
||||
// ── 단축키 항목 생성 ─────────────────────────────────────────────────
|
||||
_shortcutItems = BuildShortcutItems(_globalHotkey);
|
||||
|
||||
// ── 예약어 항목 분류 ────────────────────────────────────────────────
|
||||
_prefixItems = _allItems.ToList();
|
||||
|
||||
SubtitleText.Text = $"총 {totalCount}개 명령어 · 단축키 {_shortcutItems.Count}개 · 예약어 {_prefixItems.Count}개";
|
||||
|
||||
BuildTopMenu();
|
||||
SwitchTopMenu(TopMenu.Overview);
|
||||
|
||||
KeyDown += OnKeyDown;
|
||||
}
|
||||
|
||||
// ─── 단축키 항목 빌드 ─────────────────────────────────────────────────────
|
||||
|
||||
private static List<HelpItemModel> BuildShortcutItems(string globalHotkey = "Alt+Space")
|
||||
{
|
||||
var items = new List<HelpItemModel>();
|
||||
|
||||
// 설정에서 변경된 글로벌 단축키를 표시에 맞게 포맷 (예: "Alt+Space" → "Alt + Space")
|
||||
var hotkeyDisplay = globalHotkey.Replace("+", " + ");
|
||||
|
||||
// ── 전역 단축키 ──────────────────────────────────────────────────────
|
||||
items.Add(MakeShortcut("전역", hotkeyDisplay,
|
||||
"AX Commander 열기/닫기",
|
||||
"어느 창에서든 눌러 AX Commander를 즉시 호출하거나 닫습니다. 설정 › 일반에서 원하는 키 조합으로 변경할 수 있습니다.",
|
||||
"\uE765", "#4B5EFC"));
|
||||
items.Add(MakeShortcut("전역", "PrintScreen",
|
||||
"화면 캡처 즉시 실행",
|
||||
"AX Commander를 열지 않고 곧바로 캡처를 시작합니다. 설정 › 캡처 탭에서 '글로벌 단축키 활성화'를 켜야 동작합니다.",
|
||||
"\uE722", "#BE185D"));
|
||||
|
||||
// ── 런처 탐색 ────────────────────────────────────────────────────────
|
||||
items.Add(MakeShortcut("AX Commander 탐색", "Escape",
|
||||
"창 닫기 / 이전 단계로",
|
||||
"액션 모드(→ 로 진입)에 있을 때는 일반 검색 화면으로 돌아갑니다. 일반 화면이면 AX Commander를 숨깁니다.",
|
||||
"\uE711", "#999999"));
|
||||
items.Add(MakeShortcut("AX Commander 탐색", "Enter",
|
||||
"선택 항목 실행",
|
||||
"파일·앱이면 열기, URL이면 브라우저 열기, 시스템 명령이면 즉시 실행, 계산기 결과면 클립보드에 복사합니다.",
|
||||
"\uE768", "#107C10"));
|
||||
items.Add(MakeShortcut("AX Commander 탐색", "Shift + Enter",
|
||||
"대형 텍스트(Large Type) 표시 / 클립보드 병합 실행",
|
||||
"선택된 텍스트·검색어를 화면 전체에 크게 띄웁니다. 클립보드 병합 항목이 있을 때는 선택한 항목들을 줄바꿈으로 합쳐 클립보드에 복사합니다.",
|
||||
"\uE8C1", "#8764B8"));
|
||||
items.Add(MakeShortcut("AX Commander 탐색", "↑ / ↓",
|
||||
"결과 목록 위/아래 이동",
|
||||
"목록 끝에서 계속 누르면 처음/끝으로 순환합니다.",
|
||||
"\uE74A", "#0078D4"));
|
||||
items.Add(MakeShortcut("AX Commander 탐색", "PageUp / PageDown",
|
||||
"목록 5칸 빠른 이동",
|
||||
"한 번에 5항목씩 건너뜁니다. 빠른 목록 탐색에 유용합니다.",
|
||||
"\uE74A", "#0078D4"));
|
||||
items.Add(MakeShortcut("AX Commander 탐색", "Home / End",
|
||||
"목록 처음 / 마지막 항목으로 점프",
|
||||
"입력창 커서가 맨 앞(또는 입력이 없을 때)이면 첫 항목으로, 맨 끝이면 마지막 항목으로 선택이 이동합니다.",
|
||||
"\uE74A", "#0078D4"));
|
||||
items.Add(MakeShortcut("AX Commander 탐색", "→ (오른쪽 화살표)",
|
||||
"액션 모드 진입",
|
||||
"파일·앱 항목을 선택한 상태에서 → 를 누르면 경로 복사, 탐색기 열기, 관리자 실행, 터미널, 속성, 이름 변경, 삭제 메뉴가 나타납니다.",
|
||||
"\uE76C", "#44546A"));
|
||||
items.Add(MakeShortcut("AX Commander 탐색", "Tab",
|
||||
"선택 항목 제목으로 자동완성",
|
||||
"현재 선택된 항목의 이름을 입력창에 채웁니다. 이후 계속 타이핑하거나 Enter로 실행합니다.",
|
||||
"\uE748", "#006EAF"));
|
||||
items.Add(MakeShortcut("AX Commander 탐색", "Shift + ↑/↓",
|
||||
"클립보드 병합 선택",
|
||||
"클립보드 히스토리(# 모드) 에서 여러 항목을 이동하면서 선택/해제합니다. Shift+Enter로 선택한 항목들을 한 번에 붙여넣을 수 있습니다.",
|
||||
"\uE8C1", "#B7791F"));
|
||||
|
||||
// ── 런처 기능 단축키 ─────────────────────────────────────────────────
|
||||
items.Add(MakeShortcut("런처 기능", "F1",
|
||||
"도움말 창 열기",
|
||||
"이 화면을 직접 엽니다. 'help' 를 입력하는 것과 동일합니다.",
|
||||
"\uE897", "#6B7280"));
|
||||
items.Add(MakeShortcut("런처 기능", "F2",
|
||||
"선택 파일 이름 변경",
|
||||
"파일·폴더 항목을 선택한 상태에서 누르면 rename [경로] 형태로 입력창에 채워지고 이름 변경 핸들러가 실행됩니다.",
|
||||
"\uE70F", "#6B2C91"));
|
||||
items.Add(MakeShortcut("런처 기능", "F5",
|
||||
"파일 인덱스 즉시 재구축",
|
||||
"백그라운드에서 파일·앱 인덱싱을 다시 실행합니다. 새 파일을 추가했거나 목록이 오래됐을 때 사용합니다.",
|
||||
"\uE72C", "#059669"));
|
||||
items.Add(MakeShortcut("런처 기능", "Delete",
|
||||
"최근 실행 목록에서 항목 제거",
|
||||
"recent 목록에 있는 항목을 제거합니다. 확인 다이얼로그가 표시되며 OK를 눌러야 실제로 제거됩니다.",
|
||||
"\uE74D", "#DC2626"));
|
||||
items.Add(MakeShortcut("런처 기능", "Ctrl + ,",
|
||||
"설정 창 열기",
|
||||
"AX Copilot 설정 창을 엽니다. 런처가 자동으로 숨겨집니다.",
|
||||
"\uE713", "#44546A"));
|
||||
items.Add(MakeShortcut("런처 기능", "Ctrl + L",
|
||||
"입력창 전체 초기화",
|
||||
"현재 입력된 검색어·예약어를 모두 지우고 커서를 빈 입력창으로 돌립니다.",
|
||||
"\uE894", "#4B5EFC"));
|
||||
items.Add(MakeShortcut("런처 기능", "Ctrl + C",
|
||||
"선택 항목 파일 이름 복사",
|
||||
"파일·앱 항목이 선택된 경우 확장자를 제외한 파일 이름을 클립보드에 복사하고 토스트로 알립니다.",
|
||||
"\uE8C8", "#8764B8"));
|
||||
items.Add(MakeShortcut("런처 기능", "Ctrl + Shift + C",
|
||||
"선택 항목 전체 경로 복사",
|
||||
"선택된 파일·폴더의 절대 경로(예: C:\\Users\\...)를 클립보드에 복사합니다.",
|
||||
"\uE8C8", "#C55A11"));
|
||||
items.Add(MakeShortcut("런처 기능", "Ctrl + Shift + E",
|
||||
"파일 탐색기에서 선택 항목 열기",
|
||||
"Windows 탐색기가 열리고 해당 파일·폴더가 하이라이트 선택된 상태로 표시됩니다.",
|
||||
"\uE838", "#107C10"));
|
||||
items.Add(MakeShortcut("런처 기능", "Ctrl + Enter",
|
||||
"관리자(UAC) 권한으로 실행",
|
||||
"선택된 파일·앱을 UAC 권한 상승 후 실행합니다. 설치 프로그램이나 시스템 설정 앱에 유용합니다.",
|
||||
"\uE7EF", "#C50F1F"));
|
||||
items.Add(MakeShortcut("런처 기능", "Alt + Enter",
|
||||
"파일 속성 대화 상자 열기",
|
||||
"Windows의 '파일 속성' 창(크기·날짜·권한 등)을 엽니다.",
|
||||
"\uE946", "#6B2C91"));
|
||||
items.Add(MakeShortcut("런처 기능", "Ctrl + T",
|
||||
"선택 항목 위치에서 터미널 열기",
|
||||
"선택된 파일이면 해당 폴더에서, 폴더이면 그 경로에서 Windows Terminal(wt.exe)이 열립니다. wt가 없으면 cmd로 대체됩니다.",
|
||||
"\uE756", "#323130"));
|
||||
items.Add(MakeShortcut("런처 기능", "Ctrl + P",
|
||||
"즐겨찾기 즉시 추가 / 제거 (핀)",
|
||||
"파일·폴더 항목을 선택한 상태에서 누르면 favorites.json 에 추가하거나 이미 있으면 제거합니다. 토스트로 결과를 알립니다.",
|
||||
"\uE734", "#D97706"));
|
||||
items.Add(MakeShortcut("런처 기능", "Ctrl + B",
|
||||
"즐겨찾기 목록 보기 / 닫기 토글",
|
||||
"입력창이 'fav' 이면 초기화하고, 아니면 'fav' 를 입력해 즐겨찾기 목록을 표시합니다.",
|
||||
"\uE735", "#D97706"));
|
||||
items.Add(MakeShortcut("런처 기능", "Ctrl + R",
|
||||
"최근 실행 목록 보기 / 닫기 토글",
|
||||
"'recent' 를 입력해 최근 실행 항목을 표시합니다.",
|
||||
"\uE81C", "#0078D4"));
|
||||
items.Add(MakeShortcut("런처 기능", "Ctrl + H",
|
||||
"클립보드 히스토리 목록 열기",
|
||||
"'#' 를 입력해 클립보드에 저장된 최근 복사 항목 목록을 표시합니다.",
|
||||
"\uE77F", "#8B2FC9"));
|
||||
items.Add(MakeShortcut("런처 기능", "Ctrl + D",
|
||||
"다운로드 폴더 바로가기",
|
||||
"사용자 홈의 Downloads 폴더 경로를 입력창에 채워 탐색기로 열 수 있게 합니다.",
|
||||
"\uE8B7", "#107C10"));
|
||||
items.Add(MakeShortcut("런처 기능", "Ctrl + F",
|
||||
"파일 검색 모드로 전환",
|
||||
"입력창을 초기화하고 포커스를 이동합니다. 이후 파일명을 바로 타이핑해 검색할 수 있습니다.",
|
||||
"\uE71E", "#4B5EFC"));
|
||||
items.Add(MakeShortcut("런처 기능", "Ctrl + W",
|
||||
"런처 창 즉시 닫기",
|
||||
"현재 입력 내용에 관계없이 런처를 즉시 숨깁니다.",
|
||||
"\uE711", "#9999BB"));
|
||||
items.Add(MakeShortcut("런처 기능", "Ctrl + K",
|
||||
"단축키 참조 모달 창 열기",
|
||||
"모든 단축키와 설명을 보여주는 별도 모달 창이 열립니다. Esc 또는 닫기 버튼으로 닫습니다.",
|
||||
"\uE8FD", "#4B5EFC"));
|
||||
items.Add(MakeShortcut("런처 기능", "Ctrl + 1 ~ 9",
|
||||
"N번째 결과 항목 바로 실행",
|
||||
"목록에 번호 배지(1~9)가 표시된 항목을 해당 숫자 키로 즉시 실행합니다. 마우스 없이 빠른 실행에 유용합니다.",
|
||||
"\uE8C4", "#107C10"));
|
||||
|
||||
// ── 기타 창 단축키 ────────────────────────────────────────────────────
|
||||
items.Add(MakeShortcut("기타 창", "← / →",
|
||||
"헬프 창 카테고리 이동",
|
||||
"이 도움말 창에서 하위 카테고리 탭을 왼쪽/오른쪽으로 이동합니다.",
|
||||
"\uE76B", "#4455AA"));
|
||||
items.Add(MakeShortcut("기타 창", "1 / 2 / 3",
|
||||
"헬프 창 상단 메뉴 전환",
|
||||
"이 도움말 창에서 개요(1), 단축키 현황(2), 예약어 현황(3)을 키보드로 전환합니다.",
|
||||
"\uE8BD", "#4455AA"));
|
||||
items.Add(MakeShortcut("기타 창", "방향키 (캡처 중)",
|
||||
"영역 선택 경계 1px 미세 조정",
|
||||
"화면 캡처의 영역 선택 모드에서 선택 영역 경계를 1픽셀씩 정밀 조정합니다.",
|
||||
"\uE745", "#BE185D"));
|
||||
items.Add(MakeShortcut("기타 창", "Shift + 방향키 (캡처 중)",
|
||||
"영역 선택 경계 10px 이동",
|
||||
"화면 캡처의 영역 선택 모드에서 선택 영역 경계를 10픽셀씩 빠르게 이동합니다.",
|
||||
"\uE745", "#BE185D"));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private static SolidColorBrush ParseColor(string hex)
|
||||
{
|
||||
try { return new SolidColorBrush((Color)ColorConverter.ConvertFromString(hex)); }
|
||||
catch { return new SolidColorBrush(Colors.Gray); }
|
||||
}
|
||||
|
||||
private static HelpItemModel MakeShortcut(string cat, string key, string title, string desc, string symbol, string color)
|
||||
{
|
||||
return new HelpItemModel
|
||||
{
|
||||
Category = cat,
|
||||
Command = key,
|
||||
Title = title,
|
||||
Description = desc,
|
||||
Symbol = symbol,
|
||||
ColorBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString(color))
|
||||
};
|
||||
}
|
||||
|
||||
// ─── 상단 3탭 메뉴 빌드 ──────────────────────────────────────────────────
|
||||
|
||||
// ─── 테마 색상 헬퍼 ───────────────────────────────────────────────────────
|
||||
private Brush ThemeAccent => TryFindResource("AccentColor") as Brush ?? ParseColor("#4B5EFC");
|
||||
private Brush ThemePrimary => TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
private Brush ThemeSecondary => TryFindResource("SecondaryText") as Brush ?? ParseColor("#8899CC");
|
||||
|
||||
private void BuildTopMenu()
|
||||
{
|
||||
TopMenuBar.Children.Clear();
|
||||
foreach (var (key, label, icon) in TopMenus)
|
||||
{
|
||||
var k = key;
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 14,
|
||||
Foreground = ThemeAccent,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 6, 0)
|
||||
});
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 12,
|
||||
VerticalAlignment = VerticalAlignment.Center
|
||||
});
|
||||
|
||||
var btn = new Button
|
||||
{
|
||||
Content = sp,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Padding = new Thickness(16, 8, 16, 8),
|
||||
Margin = new Thickness(4, 0, 4, 0),
|
||||
Cursor = Cursors.Hand,
|
||||
Background = Brushes.Transparent,
|
||||
Foreground = ThemeSecondary,
|
||||
BorderThickness = new Thickness(0, 0, 0, 2),
|
||||
BorderBrush = Brushes.Transparent,
|
||||
Tag = k,
|
||||
};
|
||||
btn.Click += (_, _) => SwitchTopMenu(k);
|
||||
TopMenuBar.Children.Add(btn);
|
||||
}
|
||||
}
|
||||
|
||||
private void SwitchTopMenu(TopMenu menu)
|
||||
{
|
||||
_currentTopMenu = menu;
|
||||
|
||||
// 상단 메뉴 하이라이트
|
||||
foreach (var child in TopMenuBar.Children)
|
||||
{
|
||||
if (child is Button btn)
|
||||
{
|
||||
var isActive = btn.Tag is TopMenu t && t == menu;
|
||||
btn.BorderBrush = isActive ? ThemeAccent : Brushes.Transparent;
|
||||
btn.Foreground = isActive ? ThemePrimary : ThemeSecondary;
|
||||
|
||||
// 내부 TextBlock의 Foreground도 업데이트
|
||||
if (btn.Content is StackPanel sp)
|
||||
{
|
||||
foreach (var spChild in sp.Children)
|
||||
{
|
||||
if (spChild is TextBlock tb && tb.FontFamily.Source != "Segoe MDL2 Assets")
|
||||
tb.Foreground = btn.Foreground;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 하위 카테고리 탭 빌드
|
||||
switch (menu)
|
||||
{
|
||||
case TopMenu.Overview:
|
||||
BuildCategoryBarFor(_overviewItems);
|
||||
break;
|
||||
case TopMenu.Shortcuts:
|
||||
BuildCategoryBarFor(_shortcutItems);
|
||||
break;
|
||||
case TopMenu.Prefixes:
|
||||
BuildCategoryBarFor(_prefixItems);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 하위 카테고리 탭 빌드 ────────────────────────────────────────────────
|
||||
|
||||
private void BuildCategoryBarFor(List<HelpItemModel> sourceItems)
|
||||
{
|
||||
var seen = new HashSet<string>();
|
||||
_categories = new List<string> { CatAll };
|
||||
|
||||
// 예약어 탭에서만 인기 카테고리 표시
|
||||
if (_currentTopMenu == TopMenu.Prefixes)
|
||||
_categories.Add(CatPopular);
|
||||
|
||||
foreach (var item in sourceItems)
|
||||
{
|
||||
if (seen.Add(item.Category))
|
||||
_categories.Add(item.Category);
|
||||
}
|
||||
|
||||
BuildCategoryBar();
|
||||
NavigateToPage(0);
|
||||
}
|
||||
|
||||
private void BuildCategoryBar()
|
||||
{
|
||||
CategoryBar.Children.Clear();
|
||||
for (int i = 0; i < _categories.Count; i++)
|
||||
{
|
||||
var cat = _categories[i];
|
||||
var idx = i;
|
||||
|
||||
var btn = new Button
|
||||
{
|
||||
Content = cat,
|
||||
FontSize = 10.5,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Padding = new Thickness(9, 4, 9, 4),
|
||||
Margin = new Thickness(0, 0, 3, 0),
|
||||
Cursor = Cursors.Hand,
|
||||
Background = Brushes.Transparent,
|
||||
Foreground = ThemeSecondary,
|
||||
BorderThickness = new Thickness(0),
|
||||
Tag = idx,
|
||||
};
|
||||
|
||||
btn.Click += (_, _) => NavigateToPage(idx);
|
||||
CategoryBar.Children.Add(btn);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 페이지 네비게이션 ──────────────────────────────────────────────────
|
||||
|
||||
private List<HelpItemModel> GetCurrentSourceItems() => _currentTopMenu switch
|
||||
{
|
||||
TopMenu.Overview => _overviewItems,
|
||||
TopMenu.Shortcuts => _shortcutItems,
|
||||
TopMenu.Prefixes => _prefixItems,
|
||||
_ => _overviewItems,
|
||||
};
|
||||
|
||||
private void NavigateToPage(int pageIndex)
|
||||
{
|
||||
if (_categories.Count == 0) return;
|
||||
_currentPage = Math.Clamp(pageIndex, 0, _categories.Count - 1);
|
||||
|
||||
var source = GetCurrentSourceItems();
|
||||
var cat = _categories[_currentPage];
|
||||
List<HelpItemModel> filtered;
|
||||
|
||||
if (cat == CatAll)
|
||||
filtered = source;
|
||||
else if (cat == CatPopular)
|
||||
{
|
||||
var popularCmds = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{ "파일/폴더", "?", "#", "!", "clip", "pipe", "diff", "win" };
|
||||
filtered = source.Where(i =>
|
||||
popularCmds.Contains(i.Command) ||
|
||||
i.Command.StartsWith("?") ||
|
||||
i.Title.Contains("검색") ||
|
||||
i.Title.Contains("클립보드") ||
|
||||
(i.Title.Contains("파일") && i.Category != "키보드")
|
||||
).ToList();
|
||||
}
|
||||
else
|
||||
filtered = source.Where(i => i.Category == cat).ToList();
|
||||
|
||||
ItemsHost.ItemsSource = filtered;
|
||||
|
||||
// 탭 하이라이트
|
||||
for (int i = 0; i < CategoryBar.Children.Count; i++)
|
||||
{
|
||||
if (CategoryBar.Children[i] is Button btn)
|
||||
{
|
||||
if (i == _currentPage)
|
||||
{
|
||||
btn.Background = ThemeAccent;
|
||||
btn.Foreground = Brushes.White;
|
||||
}
|
||||
else
|
||||
{
|
||||
btn.Background = Brushes.Transparent;
|
||||
btn.Foreground = ThemeSecondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PageIndicator.Text = $"{cat} ({filtered.Count}개)";
|
||||
PrevBtn.Visibility = _currentPage > 0 ? Visibility.Visible : Visibility.Hidden;
|
||||
NextBtn.Visibility = _currentPage < _categories.Count - 1 ? Visibility.Visible : Visibility.Hidden;
|
||||
}
|
||||
|
||||
// ─── 이벤트 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void OnKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Escape:
|
||||
Close();
|
||||
break;
|
||||
case Key.Left:
|
||||
if (_currentPage > 0) NavigateToPage(_currentPage - 1);
|
||||
e.Handled = true;
|
||||
break;
|
||||
case Key.Right:
|
||||
if (_currentPage < _categories.Count - 1) NavigateToPage(_currentPage + 1);
|
||||
e.Handled = true;
|
||||
break;
|
||||
// 1, 2, 3 숫자키로 상단 메뉴 전환
|
||||
case Key.D1: SwitchTopMenu(TopMenu.Overview); e.Handled = true; break;
|
||||
case Key.D2: SwitchTopMenu(TopMenu.Shortcuts); e.Handled = true; break;
|
||||
case Key.D3: SwitchTopMenu(TopMenu.Prefixes); e.Handled = true; break;
|
||||
}
|
||||
}
|
||||
|
||||
private void Prev_Click(object sender, RoutedEventArgs e) => NavigateToPage(_currentPage - 1);
|
||||
private void Next_Click(object sender, RoutedEventArgs e) => NavigateToPage(_currentPage + 1);
|
||||
private void Close_Click(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private void SearchBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
||||
{
|
||||
var query = SearchBox.Text.Trim();
|
||||
if (string.IsNullOrEmpty(query))
|
||||
{
|
||||
NavigateToPage(_currentPage);
|
||||
return;
|
||||
}
|
||||
|
||||
var source = GetCurrentSourceItems();
|
||||
var filtered = source.Where(i =>
|
||||
i.Title.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||
i.Command.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||
i.Description.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||
i.Category.Contains(query, StringComparison.OrdinalIgnoreCase)
|
||||
).ToList();
|
||||
|
||||
ItemsHost.ItemsSource = filtered;
|
||||
PageIndicator.Text = $"검색: \"{query}\" ({filtered.Count}개)";
|
||||
}
|
||||
|
||||
private void Window_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ChangedButton == MouseButton.Left && e.LeftButton == MouseButtonState.Pressed)
|
||||
try { DragMove(); } catch { }
|
||||
}
|
||||
|
||||
private void Window_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape) Close();
|
||||
}
|
||||
}
|
||||
|
||||
public class HelpItemModel
|
||||
{
|
||||
public string Category { get; init; } = "";
|
||||
public string Command { get; init; } = "";
|
||||
public string Title { get; init; } = "";
|
||||
public string Description { get; init; } = "";
|
||||
public string Example { get; init; } = "";
|
||||
public string Symbol { get; init; } = "";
|
||||
public SolidColorBrush ColorBrush { get; init; } = new(Colors.Gray);
|
||||
public bool HasExample => !string.IsNullOrEmpty(Example);
|
||||
}
|
||||
191
src/AxCopilot/Views/InputDialog.cs
Normal file
191
src/AxCopilot/Views/InputDialog.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Effects;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>테마에 맞는 커스텀 텍스트 입력 다이얼로그.</summary>
|
||||
internal sealed class InputDialog : Window
|
||||
{
|
||||
private readonly TextBox _textBox;
|
||||
public string ResponseText => _textBox.Text;
|
||||
|
||||
public InputDialog(string title, string prompt, string defaultValue = "", string placeholder = "")
|
||||
{
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||
ResizeMode = ResizeMode.NoResize;
|
||||
WindowStyle = WindowStyle.None;
|
||||
AllowsTransparency = true;
|
||||
Background = Brushes.Transparent;
|
||||
Width = 400;
|
||||
SizeToContent = SizeToContent.Height;
|
||||
|
||||
var bg = Application.Current.TryFindResource("LauncherBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E));
|
||||
var borderBrush = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
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.CornflowerBlue;
|
||||
var itemBg = Application.Current.TryFindResource("ItemBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x3E));
|
||||
|
||||
// 외부 컨테이너 (그림자 + 둥근 모서리)
|
||||
var outerBorder = new Border
|
||||
{
|
||||
Background = bg,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(14),
|
||||
Margin = new Thickness(16),
|
||||
Effect = new DropShadowEffect
|
||||
{
|
||||
BlurRadius = 24, ShadowDepth = 6, Opacity = 0.35,
|
||||
Color = Colors.Black, Direction = 270,
|
||||
},
|
||||
};
|
||||
|
||||
var stack = new StackPanel { Margin = new Thickness(24, 20, 24, 20) };
|
||||
|
||||
// 타이틀 행 (아이콘 + 제목)
|
||||
var titlePanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 0, 0, 16) };
|
||||
titlePanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE8AC", // 편집 아이콘
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 16, Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
});
|
||||
titlePanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = title, FontSize = 15, FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
stack.Children.Add(titlePanel);
|
||||
|
||||
// 프롬프트
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = prompt, FontSize = 12.5, Foreground = secondaryText,
|
||||
Margin = new Thickness(0, 0, 0, 8),
|
||||
});
|
||||
|
||||
// 텍스트 입력 (둥근 테두리 + 플레이스홀더)
|
||||
_textBox = new TextBox
|
||||
{
|
||||
Text = defaultValue,
|
||||
FontSize = 13,
|
||||
Padding = new Thickness(14, 8, 14, 8),
|
||||
Foreground = primaryText,
|
||||
Background = Brushes.Transparent,
|
||||
CaretBrush = accentBrush,
|
||||
BorderThickness = new Thickness(0),
|
||||
BorderBrush = Brushes.Transparent,
|
||||
};
|
||||
_textBox.KeyDown += (_, e) =>
|
||||
{
|
||||
if (e.Key == Key.Enter) { DialogResult = true; Close(); }
|
||||
if (e.Key == Key.Escape) { DialogResult = false; Close(); }
|
||||
};
|
||||
|
||||
// 플레이스홀더 오버레이
|
||||
var placeholderBlock = new TextBlock
|
||||
{
|
||||
Text = placeholder,
|
||||
FontSize = 13,
|
||||
Foreground = secondaryText,
|
||||
Opacity = 0.5,
|
||||
IsHitTestVisible = false,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Padding = new Thickness(14, 8, 14, 8),
|
||||
Visibility = string.IsNullOrEmpty(defaultValue) && !string.IsNullOrEmpty(placeholder)
|
||||
? Visibility.Visible : Visibility.Collapsed,
|
||||
};
|
||||
_textBox.TextChanged += (_, _) =>
|
||||
placeholderBlock.Visibility = string.IsNullOrEmpty(_textBox.Text) ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
var inputGrid = new Grid();
|
||||
inputGrid.Children.Add(_textBox);
|
||||
inputGrid.Children.Add(placeholderBlock);
|
||||
|
||||
var inputBorder = new Border
|
||||
{
|
||||
CornerRadius = new CornerRadius(10),
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
Background = itemBg,
|
||||
Child = inputGrid,
|
||||
};
|
||||
// 포커스 시 액센트 테두리
|
||||
_textBox.GotFocus += (_, _) => inputBorder.BorderBrush = accentBrush;
|
||||
_textBox.LostFocus += (_, _) => inputBorder.BorderBrush = borderBrush;
|
||||
stack.Children.Add(inputBorder);
|
||||
|
||||
// 버튼 패널
|
||||
var btnPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(0, 18, 0, 0),
|
||||
};
|
||||
|
||||
var cancelBtn = CreateButton("취소", Brushes.Transparent, secondaryText, borderBrush);
|
||||
cancelBtn.Click += (_, _) => { DialogResult = false; Close(); };
|
||||
btnPanel.Children.Add(cancelBtn);
|
||||
|
||||
var okBtn = CreateButton("확인", accentBrush, Brushes.White, null);
|
||||
okBtn.Click += (_, _) => { DialogResult = true; Close(); };
|
||||
btnPanel.Children.Add(okBtn);
|
||||
|
||||
stack.Children.Add(btnPanel);
|
||||
outerBorder.Child = stack;
|
||||
Content = outerBorder;
|
||||
|
||||
// 드래그 이동 (제목 영역)
|
||||
titlePanel.MouseLeftButtonDown += (_, e) => { if (e.ClickCount == 1) DragMove(); };
|
||||
|
||||
Loaded += (_, _) => { _textBox.Focus(); _textBox.SelectAll(); };
|
||||
}
|
||||
|
||||
private static Button CreateButton(string text, Brush background, Brush foreground, Brush? borderBrush)
|
||||
{
|
||||
var btn = new Button
|
||||
{
|
||||
Cursor = Cursors.Hand,
|
||||
Margin = new Thickness(0, 0, 0, 0),
|
||||
};
|
||||
if (text == "취소") btn.Margin = new Thickness(0, 0, 8, 0);
|
||||
|
||||
// 커스텀 템플릿 (둥근 버튼)
|
||||
var template = new ControlTemplate(typeof(Button));
|
||||
var bdFactory = new FrameworkElementFactory(typeof(Border));
|
||||
bdFactory.SetValue(Border.BackgroundProperty, background);
|
||||
bdFactory.SetValue(Border.CornerRadiusProperty, new CornerRadius(8));
|
||||
bdFactory.SetValue(Border.PaddingProperty, new Thickness(20, 8, 20, 8));
|
||||
bdFactory.Name = "Bd";
|
||||
if (borderBrush != null)
|
||||
{
|
||||
bdFactory.SetValue(Border.BorderBrushProperty, borderBrush);
|
||||
bdFactory.SetValue(Border.BorderThicknessProperty, new Thickness(1));
|
||||
}
|
||||
var cpFactory = new FrameworkElementFactory(typeof(ContentPresenter));
|
||||
cpFactory.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Center);
|
||||
cpFactory.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
|
||||
bdFactory.AppendChild(cpFactory);
|
||||
template.VisualTree = bdFactory;
|
||||
|
||||
var hoverTrigger = new Trigger { Property = UIElement.IsMouseOverProperty, Value = true };
|
||||
hoverTrigger.Setters.Add(new Setter(UIElement.OpacityProperty, 0.85));
|
||||
template.Triggers.Add(hoverTrigger);
|
||||
|
||||
btn.Template = template;
|
||||
btn.Content = new TextBlock
|
||||
{
|
||||
Text = text, FontSize = 13, Foreground = foreground,
|
||||
};
|
||||
return btn;
|
||||
}
|
||||
}
|
||||
87
src/AxCopilot/Views/LargeTypeWindow.xaml
Normal file
87
src/AxCopilot/Views/LargeTypeWindow.xaml
Normal file
@@ -0,0 +1,87 @@
|
||||
<Window x:Class="AxCopilot.Views.LargeTypeWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
UseLayoutRounding="True"
|
||||
SnapsToDevicePixels="True"
|
||||
Background="Transparent"
|
||||
WindowState="Maximized"
|
||||
Topmost="True"
|
||||
ShowInTaskbar="False"
|
||||
Focusable="True"
|
||||
KeyDown="Window_KeyDown"
|
||||
MouseDown="Window_MouseDown"
|
||||
Loaded="Window_Loaded">
|
||||
|
||||
<!-- 반투명 오버레이 -->
|
||||
<Border x:Name="Overlay" Background="#D9000000" Opacity="0">
|
||||
<Border.Triggers>
|
||||
<!-- 표시될 때 페이드인 -->
|
||||
<EventTrigger RoutedEvent="Loaded">
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetName="Overlay"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
From="0" To="1" Duration="0:0:0.18"/>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</EventTrigger>
|
||||
</Border.Triggers>
|
||||
|
||||
<Grid>
|
||||
<!-- ── 닫기 버튼 (우상단) ─── -->
|
||||
<Button HorizontalAlignment="Right" VerticalAlignment="Top"
|
||||
Margin="0,32,40,0" Width="40" Height="40"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
Cursor="Hand" Click="Close_Click"
|
||||
ToolTip="닫기 (Esc / 클릭)">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="18" Foreground="#99FFFFFF"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" CornerRadius="20"
|
||||
Background="{TemplateBinding Background}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#33FFFFFF"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
|
||||
<!-- ── 메인 텍스트 (중앙) ─── -->
|
||||
<Viewbox Stretch="Uniform" MaxWidth="1200" MaxHeight="700"
|
||||
Margin="80,80,80,120"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="LargeText"
|
||||
FontFamily="Segoe UI, Malgun Gothic"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="White"
|
||||
TextWrapping="Wrap"
|
||||
TextAlignment="Center">
|
||||
<TextBlock.Effect>
|
||||
<DropShadowEffect Color="Black" BlurRadius="40"
|
||||
ShadowDepth="0" Opacity="0.7"/>
|
||||
</TextBlock.Effect>
|
||||
</TextBlock>
|
||||
</Viewbox>
|
||||
|
||||
<!-- ── 하단 힌트 ─── -->
|
||||
<StackPanel VerticalAlignment="Bottom" HorizontalAlignment="Center"
|
||||
Margin="0,0,0,40" Orientation="Horizontal">
|
||||
<Border Background="#22FFFFFF" CornerRadius="6" Padding="10,5" Margin="0,0,8,0">
|
||||
<TextBlock Text="Esc" FontSize="12" FontFamily="Segoe UI Mono, Consolas"
|
||||
Foreground="#99FFFFFF" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock Text="또는 클릭하여 닫기" FontSize="13"
|
||||
Foreground="#66FFFFFF" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
48
src/AxCopilot/Views/LargeTypeWindow.xaml.cs
Normal file
48
src/AxCopilot/Views/LargeTypeWindow.xaml.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class LargeTypeWindow : Window
|
||||
{
|
||||
private readonly string _text;
|
||||
|
||||
public LargeTypeWindow(string text)
|
||||
{
|
||||
_text = text;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void Window_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
LargeText.Text = _text;
|
||||
|
||||
// 텍스트 길이에 따라 기본 폰트 크기 결정
|
||||
// (Viewbox가 최종 크기를 조정하지만, 줄바꿈 결정을 위해 명시적 크기 필요)
|
||||
LargeText.FontSize = _text.Length switch
|
||||
{
|
||||
<= 10 => 120,
|
||||
<= 30 => 96,
|
||||
<= 60 => 72,
|
||||
<= 120 => 52,
|
||||
_ => 38,
|
||||
};
|
||||
|
||||
Focus();
|
||||
}
|
||||
|
||||
private void Window_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape) Close();
|
||||
}
|
||||
|
||||
private void Window_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
|
||||
private void Close_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
740
src/AxCopilot/Views/LauncherWindow.xaml
Normal file
740
src/AxCopilot/Views/LauncherWindow.xaml
Normal file
@@ -0,0 +1,740 @@
|
||||
<Window x:Class="AxCopilot.Views.LauncherWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Title="AX Commander"
|
||||
Width="680"
|
||||
SizeToContent="Height"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
UseLayoutRounding="True"
|
||||
SnapsToDevicePixels="True"
|
||||
Background="Transparent"
|
||||
Topmost="True"
|
||||
ShowInTaskbar="False"
|
||||
ResizeMode="NoResize"
|
||||
WindowStartupLocation="Manual"
|
||||
Loaded="Window_Loaded"
|
||||
Deactivated="Window_Deactivated"
|
||||
PreviewKeyDown="Window_PreviewKeyDown"
|
||||
KeyDown="Window_KeyDown">
|
||||
|
||||
<Window.Resources>
|
||||
|
||||
<!-- ─── 토스트 애니메이션 ─────────────────────────────────────────── -->
|
||||
<Storyboard x:Key="ToastFadeIn">
|
||||
<DoubleAnimation Storyboard.TargetName="ToastOverlay"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
From="0" To="1" Duration="0:0:0.18">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<QuadraticEase EasingMode="EaseOut"/>
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
<Storyboard x:Key="ToastFadeOut">
|
||||
<DoubleAnimation Storyboard.TargetName="ToastOverlay"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
From="1" To="0" Duration="0:0:0.28">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<QuadraticEase EasingMode="EaseIn"/>
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
|
||||
<!-- ─── 결과 항목 스타일 ─────────────────────────────────────────── -->
|
||||
<Style x:Key="ResultItemStyle" TargetType="ListViewItem">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Padding" Value="13,8,13,8"/>
|
||||
<Setter Property="Margin" Value="4,1,4,1"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListViewItem">
|
||||
<Grid>
|
||||
<!-- 선택 아이템 무지개 글로우 보더 (RainbowGlowBorder와 동일한 방식) -->
|
||||
<Border x:Name="SelGlow"
|
||||
CornerRadius="{DynamicResource ItemCornerRadius}"
|
||||
Margin="-2,0"
|
||||
IsHitTestVisible="False"
|
||||
Visibility="Collapsed"
|
||||
BorderThickness="2"
|
||||
BorderBrush="{DynamicResource SelectionGlowBrush}">
|
||||
<Border.Effect>
|
||||
<BlurEffect Radius="8"/>
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
<!-- 선택 시 좌측 액센트 바 -->
|
||||
<Border x:Name="AccentBar"
|
||||
HorizontalAlignment="Left"
|
||||
Width="3"
|
||||
CornerRadius="1.5"
|
||||
Margin="1,4,0,4"
|
||||
Background="{DynamicResource AccentColor}"
|
||||
Visibility="Collapsed"/>
|
||||
<Border x:Name="Bd"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="0"
|
||||
CornerRadius="{DynamicResource ItemCornerRadius}"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ItemSelectedBackground}"/>
|
||||
<Setter TargetName="AccentBar" Property="Visibility" Value="Visible"/>
|
||||
<!-- 무지개 글로우 보더: EnableSelectionGlow 설정에 따라 Visible/Collapsed -->
|
||||
<Setter TargetName="SelGlow" Property="Visibility" Value="{DynamicResource SelectionGlowVisibility}"/>
|
||||
</Trigger>
|
||||
<!-- 미선택 아이템 호버 -->
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsMouseOver" Value="True"/>
|
||||
<Condition Property="IsSelected" Value="False"/>
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ItemHoverBackground}"/>
|
||||
</MultiTrigger>
|
||||
<!-- 선택 + 호버: 더 밝은 배경 + 강조 글로우 -->
|
||||
<MultiTrigger>
|
||||
<MultiTrigger.Conditions>
|
||||
<Condition Property="IsSelected" Value="True"/>
|
||||
<Condition Property="IsMouseOver" Value="True"/>
|
||||
</MultiTrigger.Conditions>
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ItemSelectedHoverBackground}"/>
|
||||
<Setter TargetName="Bd" Property="Effect">
|
||||
<Setter.Value>
|
||||
<DropShadowEffect ShadowDepth="0" BlurRadius="14" Opacity="0.4"
|
||||
Color="#4B5EFC"/>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</MultiTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ─── 스크롤바 스타일 ─────────────────────────────────────────── -->
|
||||
<Style x:Key="SlimScrollThumb" TargetType="Thumb">
|
||||
<Setter Property="Background" Value="{DynamicResource ScrollbarThumb}"/>
|
||||
<Setter Property="Width" Value="3"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Thumb">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="2"/>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SlimScrollBar" TargetType="ScrollBar">
|
||||
<Setter Property="Width" Value="5"/>
|
||||
<Setter Property="Margin" Value="0,4,2,4"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ScrollBar">
|
||||
<Grid>
|
||||
<Track x:Name="PART_Track" IsDirectionReversed="True">
|
||||
<Track.Thumb>
|
||||
<Thumb Style="{StaticResource SlimScrollThumb}"/>
|
||||
</Track.Thumb>
|
||||
</Track>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
</Window.Resources>
|
||||
|
||||
<!-- ─── 메인 컨테이너 ──────────────────────────────────────────────── -->
|
||||
<Grid Margin="24">
|
||||
<!-- 그림자 전용 레이어 -->
|
||||
<Border CornerRadius="{DynamicResource WindowCornerRadius}"
|
||||
Background="{DynamicResource LauncherBackground}">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="{DynamicResource ShadowColor}"
|
||||
BlurRadius="32" ShadowDepth="8" Opacity="0.32"/>
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
|
||||
<!-- 무지개 글로우 레이어 (상시 애니메이션) -->
|
||||
<Border x:Name="RainbowGlowBorder"
|
||||
CornerRadius="{DynamicResource WindowCornerRadius}"
|
||||
Margin="-3" Opacity="1.0"
|
||||
IsHitTestVisible="False">
|
||||
<Border.BorderBrush>
|
||||
<LinearGradientBrush x:Name="LauncherRainbowBrush" StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Color="#FF6B6B" Offset="0.0"/>
|
||||
<GradientStop Color="#FECA57" Offset="0.17"/>
|
||||
<GradientStop Color="#48DBFB" Offset="0.33"/>
|
||||
<GradientStop Color="#FF9FF3" Offset="0.5"/>
|
||||
<GradientStop Color="#54A0FF" Offset="0.67"/>
|
||||
<GradientStop Color="#5F27CD" Offset="0.83"/>
|
||||
<GradientStop Color="#FF6B6B" Offset="1.0"/>
|
||||
</LinearGradientBrush>
|
||||
</Border.BorderBrush>
|
||||
<Border.BorderThickness>
|
||||
<Thickness>3.5</Thickness>
|
||||
</Border.BorderThickness>
|
||||
<Border.Effect>
|
||||
<BlurEffect Radius="10"/>
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
|
||||
<!-- 콘텐츠 레이어 -->
|
||||
<Border x:Name="MainBorder"
|
||||
CornerRadius="{DynamicResource WindowCornerRadius}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderThickness="0"
|
||||
BorderBrush="Transparent">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ─── 입력 영역 ─── -->
|
||||
<Grid Grid.Row="0" Margin="20,16,20,16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 검색 아이콘 / Prefix 배지 -->
|
||||
<Grid Grid.Column="0" Margin="0,0,10,0" VerticalAlignment="Center">
|
||||
|
||||
<!-- 기본 아이콘: 다이아몬드 픽셀 (prefix 없을 때) -->
|
||||
<Canvas x:Name="DiamondIcon" Width="22" Height="22"
|
||||
VerticalAlignment="Center" Cursor="Hand"
|
||||
ClipToBounds="False"
|
||||
MouseLeftButtonDown="DiamondIcon_Click"
|
||||
Visibility="{Binding HasActivePrefix, Converter={StaticResource InverseBoolToVisibilityConverter}}">
|
||||
<Canvas.RenderTransformOrigin>0.5,0.5</Canvas.RenderTransformOrigin>
|
||||
<Canvas.RenderTransform>
|
||||
<TransformGroup>
|
||||
<RotateTransform x:Name="IconRotate" Angle="45"/>
|
||||
<ScaleTransform x:Name="IconScale" ScaleX="1" ScaleY="1"/>
|
||||
</TransformGroup>
|
||||
</Canvas.RenderTransform>
|
||||
<!-- 마우스 오버 시 각 픽셀 색상이 퍼져나가는 글로우 (픽셀보다 먼저 렌더링 = 아래에 배치) -->
|
||||
<Rectangle x:Name="GlowBlue"
|
||||
Canvas.Left="-2.5" Canvas.Top="-2.5"
|
||||
Width="14.5" Height="14.5"
|
||||
RadiusX="7" RadiusY="7"
|
||||
Fill="#4488FF" Opacity="0">
|
||||
<Rectangle.Effect><BlurEffect Radius="9"/></Rectangle.Effect>
|
||||
</Rectangle>
|
||||
<Rectangle x:Name="GlowGreen1"
|
||||
Canvas.Left="9" Canvas.Top="-2.5"
|
||||
Width="14.5" Height="14.5"
|
||||
RadiusX="7" RadiusY="7"
|
||||
Fill="#44DD66" Opacity="0">
|
||||
<Rectangle.Effect><BlurEffect Radius="9"/></Rectangle.Effect>
|
||||
</Rectangle>
|
||||
<Rectangle x:Name="GlowGreen2"
|
||||
Canvas.Left="-2.5" Canvas.Top="9"
|
||||
Width="14.5" Height="14.5"
|
||||
RadiusX="7" RadiusY="7"
|
||||
Fill="#44DD66" Opacity="0">
|
||||
<Rectangle.Effect><BlurEffect Radius="9"/></Rectangle.Effect>
|
||||
</Rectangle>
|
||||
<Rectangle x:Name="GlowRed"
|
||||
Canvas.Left="9" Canvas.Top="9"
|
||||
Width="14.5" Height="14.5"
|
||||
RadiusX="7" RadiusY="7"
|
||||
Fill="#FF4466" Opacity="0">
|
||||
<Rectangle.Effect><BlurEffect Radius="9"/></Rectangle.Effect>
|
||||
</Rectangle>
|
||||
|
||||
<!-- 파란 픽셀 (좌상→상) -->
|
||||
<Rectangle x:Name="PixelBlue"
|
||||
Canvas.Left="0.5" Canvas.Top="0.5"
|
||||
Width="9.5" Height="9.5"
|
||||
RadiusX="1.5" RadiusY="1.5"
|
||||
Fill="#4488FF" Opacity="1"/>
|
||||
<!-- 초록 픽셀 (우상→우) -->
|
||||
<Rectangle x:Name="PixelGreen1"
|
||||
Canvas.Left="12" Canvas.Top="0.5"
|
||||
Width="9.5" Height="9.5"
|
||||
RadiusX="1.5" RadiusY="1.5"
|
||||
Fill="#44DD66" Opacity="1"/>
|
||||
<!-- 초록 픽셀 (좌하→좌) -->
|
||||
<Rectangle x:Name="PixelGreen2"
|
||||
Canvas.Left="0.5" Canvas.Top="12"
|
||||
Width="9.5" Height="9.5"
|
||||
RadiusX="1.5" RadiusY="1.5"
|
||||
Fill="#44DD66" Opacity="1"/>
|
||||
<!-- 빨간 픽셀 (우하→하) -->
|
||||
<Rectangle x:Name="PixelRed"
|
||||
Canvas.Left="12" Canvas.Top="12"
|
||||
Width="9.5" Height="9.5"
|
||||
RadiusX="1.5" RadiusY="1.5"
|
||||
Fill="#FF4466" Opacity="1"/>
|
||||
|
||||
<!-- 마우스 오버 → 각 픽셀 색 글로우 페이드인/아웃 -->
|
||||
<Canvas.Triggers>
|
||||
<EventTrigger RoutedEvent="UIElement.MouseEnter">
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetName="GlowBlue" Storyboard.TargetProperty="Opacity" To="0.9" Duration="0:0:0.15">
|
||||
<DoubleAnimation.EasingFunction><QuadraticEase EasingMode="EaseOut"/></DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<DoubleAnimation Storyboard.TargetName="GlowGreen1" Storyboard.TargetProperty="Opacity" To="0.9" Duration="0:0:0.15">
|
||||
<DoubleAnimation.EasingFunction><QuadraticEase EasingMode="EaseOut"/></DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<DoubleAnimation Storyboard.TargetName="GlowGreen2" Storyboard.TargetProperty="Opacity" To="0.9" Duration="0:0:0.15">
|
||||
<DoubleAnimation.EasingFunction><QuadraticEase EasingMode="EaseOut"/></DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<DoubleAnimation Storyboard.TargetName="GlowRed" Storyboard.TargetProperty="Opacity" To="0.9" Duration="0:0:0.15">
|
||||
<DoubleAnimation.EasingFunction><QuadraticEase EasingMode="EaseOut"/></DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</EventTrigger>
|
||||
<EventTrigger RoutedEvent="UIElement.MouseLeave">
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetName="GlowBlue" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.22">
|
||||
<DoubleAnimation.EasingFunction><QuadraticEase EasingMode="EaseIn"/></DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<DoubleAnimation Storyboard.TargetName="GlowGreen1" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.22">
|
||||
<DoubleAnimation.EasingFunction><QuadraticEase EasingMode="EaseIn"/></DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<DoubleAnimation Storyboard.TargetName="GlowGreen2" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.22">
|
||||
<DoubleAnimation.EasingFunction><QuadraticEase EasingMode="EaseIn"/></DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<DoubleAnimation Storyboard.TargetName="GlowRed" Storyboard.TargetProperty="Opacity" To="0" Duration="0:0:0.22">
|
||||
<DoubleAnimation.EasingFunction><QuadraticEase EasingMode="EaseIn"/></DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</EventTrigger>
|
||||
</Canvas.Triggers>
|
||||
<!-- 애니메이션은 코드비하인드에서 5가지 효과 중 랜덤 적용 -->
|
||||
</Canvas>
|
||||
|
||||
<!-- Prefix 배지 (prefix 있을 때) -->
|
||||
<Border CornerRadius="6"
|
||||
Padding="7,4,7,4"
|
||||
Background="{Binding ActivePrefixBrush}"
|
||||
Visibility="{Binding HasActivePrefix, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding ActivePrefixSymbol}"
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="11"
|
||||
Foreground="White"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding ActivePrefixLabel}"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="White"
|
||||
Margin="4,0,0,0"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- 텍스트 입력창 -->
|
||||
<TextBox x:Name="InputBox"
|
||||
Grid.Column="1"
|
||||
FontSize="20"
|
||||
FontFamily="Segoe UI, Malgun Gothic"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
CaretBrush="{DynamicResource AccentColor}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding InputText, UpdateSourceTrigger=PropertyChanged}"
|
||||
TextChanged="InputBox_TextChanged"
|
||||
>
|
||||
<TextBox.Style>
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TextBox">
|
||||
<Grid>
|
||||
<TextBlock x:Name="Placeholder"
|
||||
Text="{Binding PlaceholderText}"
|
||||
FontSize="17"
|
||||
Foreground="{DynamicResource PlaceholderText}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="4,0,0,0"
|
||||
IsHitTestVisible="False"
|
||||
Visibility="Collapsed"/>
|
||||
<ScrollViewer x:Name="PART_ContentHost" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding Text, RelativeSource={RelativeSource Self}}" Value="">
|
||||
<Setter TargetName="Placeholder" Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</TextBox.Style>
|
||||
</TextBox>
|
||||
|
||||
<!-- 로딩 인디케이터 -->
|
||||
<ProgressBar Grid.Column="2"
|
||||
Width="16" Height="16"
|
||||
IsIndeterminate="True"
|
||||
Foreground="{DynamicResource AccentColor}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
|
||||
<!-- 도움말 힌트 -->
|
||||
<Border Grid.Column="2"
|
||||
Background="{DynamicResource HintBackground}"
|
||||
CornerRadius="6"
|
||||
Padding="9,4"
|
||||
Visibility="{Binding IsLoading, Converter={StaticResource InverseBoolToVisibilityConverter}}">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="help"
|
||||
FontSize="11"
|
||||
FontFamily="Segoe UI Mono, Consolas"
|
||||
Foreground="{DynamicResource HintText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="↵"
|
||||
FontSize="12"
|
||||
FontFamily="Segoe UI"
|
||||
Foreground="{DynamicResource HintText}"
|
||||
Margin="3,0,0,0"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- ─── 파일 액션 모드 breadcrumb 바 ─── -->
|
||||
<Border Grid.Row="1"
|
||||
Padding="16,7"
|
||||
Visibility="{Binding ShowActionModeBar, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="{Binding Color, Source={StaticResource AccentColor}}" Opacity="0.10"/>
|
||||
</Border.Background>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource AccentColor}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,7,0"/>
|
||||
<TextBlock Text="{Binding ActionModeBreadcrumb}"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AccentColor}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text=" 에 대한 액션"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource AccentColor}"
|
||||
Opacity="0.65"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text=" · Esc로 돌아가기"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource AccentColor}"
|
||||
Opacity="0.45"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ─── 구분선 (가장자리 gradient 페이드) ─── -->
|
||||
<Rectangle Grid.Row="2"
|
||||
Height="1"
|
||||
Fill="{DynamicResource SeparatorColor}"
|
||||
Visibility="{Binding Results.Count, Converter={StaticResource CountToVisibilityConverter}}">
|
||||
<Rectangle.OpacityMask>
|
||||
<LinearGradientBrush StartPoint="0,0.5" EndPoint="1,0.5">
|
||||
<GradientStop Color="Transparent" Offset="0"/>
|
||||
<GradientStop Color="White" Offset="0.08"/>
|
||||
<GradientStop Color="White" Offset="0.92"/>
|
||||
<GradientStop Color="Transparent" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
</Rectangle.OpacityMask>
|
||||
</Rectangle>
|
||||
|
||||
<!-- ─── 클립보드 병합 힌트 바 ─── -->
|
||||
<Border Grid.Row="3"
|
||||
Background="#158764B8"
|
||||
Padding="16,6"
|
||||
Visibility="{Binding ShowMergeHint, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="10"
|
||||
Foreground="#8764B8"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,7,0"/>
|
||||
<TextBlock Text="{Binding MergeHintText}"
|
||||
FontSize="11"
|
||||
Foreground="#8764B8"
|
||||
FontFamily="Segoe UI, Malgun Gothic"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ─── 결과 리스트 ─── -->
|
||||
<ListView x:Name="ResultList"
|
||||
Grid.Row="4"
|
||||
ItemsSource="{Binding Results}"
|
||||
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Margin="6,6,6,14"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
MaxHeight="380"
|
||||
VirtualizingStackPanel.VirtualizationMode="Recycling"
|
||||
VirtualizingPanel.ScrollUnit="Item"
|
||||
ItemContainerStyle="{StaticResource ResultItemStyle}"
|
||||
PreviewMouseLeftButtonUp="ResultList_PreviewMouseLeftButtonUp"
|
||||
MouseDoubleClick="ResultList_MouseDoubleClick"
|
||||
Visibility="{Binding Results.Count, Converter={StaticResource CountToVisibilityConverter}}">
|
||||
|
||||
<ListView.Resources>
|
||||
<Style TargetType="ScrollBar" BasedOn="{StaticResource SlimScrollBar}"/>
|
||||
</ListView.Resources>
|
||||
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 번호 뱃지 (1~9, Ctrl+N으로 실행 가능) — ShowNumberBadges 설정으로 제어 -->
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding RelativeSource={RelativeSource AncestorType=ListViewItem},
|
||||
Converter={StaticResource IndexToNumberConverter}}"
|
||||
FontFamily="Consolas"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
Opacity="0.55"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Width="16"
|
||||
Margin="0,0,4,0"
|
||||
Visibility="{Binding DataContext.ShowNumberBadges,
|
||||
RelativeSource={RelativeSource AncestorType=Window},
|
||||
Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
|
||||
<!-- 아이콘: IconPath가 있으면 이미지, 없으면 심볼 글리프 -->
|
||||
<Border Grid.Column="1"
|
||||
Width="34" Height="34"
|
||||
CornerRadius="8"
|
||||
Margin="0,0,12,0"
|
||||
VerticalAlignment="Center"
|
||||
Background="{Binding Symbol, Converter={StaticResource SymbolToBackgroundConverter}}">
|
||||
<!-- 외부 글로우: 마우스 오버 시 아이콘 테두리에서 빛이 퍼지는 효과 -->
|
||||
<Border.Effect>
|
||||
<DropShadowEffect ShadowDepth="0" BlurRadius="14" Opacity="0" Color="White"/>
|
||||
</Border.Effect>
|
||||
<Border.Triggers>
|
||||
<EventTrigger RoutedEvent="UIElement.MouseEnter">
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<!-- 외부 글로우 페이드인 -->
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetProperty="(UIElement.Effect).(DropShadowEffect.Opacity)"
|
||||
To="0.65" Duration="0:0:0.14">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<QuadraticEase EasingMode="EaseOut"/>
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<!-- 내부 빛 발산 오버레이 페이드인 -->
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="IconInnerGlow"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="1" Duration="0:0:0.14">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<QuadraticEase EasingMode="EaseOut"/>
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</EventTrigger>
|
||||
<EventTrigger RoutedEvent="UIElement.MouseLeave">
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetProperty="(UIElement.Effect).(DropShadowEffect.Opacity)"
|
||||
To="0" Duration="0:0:0.2">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<QuadraticEase EasingMode="EaseIn"/>
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<DoubleAnimation
|
||||
Storyboard.TargetName="IconInnerGlow"
|
||||
Storyboard.TargetProperty="Opacity"
|
||||
To="0" Duration="0:0:0.2">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<QuadraticEase EasingMode="EaseIn"/>
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</EventTrigger>
|
||||
</Border.Triggers>
|
||||
<Grid>
|
||||
<TextBlock x:Name="SymbolText"
|
||||
Text="{Binding Symbol}"
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="15"
|
||||
Foreground="White"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
<Image Source="{Binding IconPath}"
|
||||
Width="22" Height="22"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding IconPath, Converter={StaticResource NullToCollapsedConverter}}"/>
|
||||
<!-- 클립보드 이미지 썸네일 -->
|
||||
<Image Source="{Binding Data, Converter={StaticResource ClipboardThumbnailConverter}}"
|
||||
Stretch="UniformToFill"
|
||||
RenderOptions.BitmapScalingMode="HighQuality"
|
||||
Visibility="{Binding Data, Converter={StaticResource ClipboardHasImageConverter}}"/>
|
||||
<!-- 내부 빛 발산 오버레이 (RadialGradient: 중심 → 투명 가장자리) -->
|
||||
<Border x:Name="IconInnerGlow"
|
||||
CornerRadius="8"
|
||||
Opacity="0"
|
||||
IsHitTestVisible="False">
|
||||
<Border.Background>
|
||||
<RadialGradientBrush GradientOrigin="0.5,0.35"
|
||||
Center="0.5,0.35"
|
||||
RadiusX="0.75" RadiusY="0.75">
|
||||
<GradientStop Color="#70FFFFFF" Offset="0.0"/>
|
||||
<GradientStop Color="#20FFFFFF" Offset="0.55"/>
|
||||
<GradientStop Color="#00FFFFFF" Offset="1.0"/>
|
||||
</RadialGradientBrush>
|
||||
</Border.Background>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 제목 + 부제목 -->
|
||||
<StackPanel Grid.Column="2" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Title}"
|
||||
FontFamily="Segoe UI, Malgun Gothic"
|
||||
FontSize="14"
|
||||
FontWeight="Medium"
|
||||
TextTrimming="CharacterEllipsis">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryText}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListViewItem}}" Value="True">
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding Subtitle}"
|
||||
FontFamily="Segoe UI, Malgun Gothic"
|
||||
FontSize="11"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Margin="0,2,0,0">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryText}"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListViewItem}}" Value="True">
|
||||
<Setter Property="Foreground" Value="#E0E8FF"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Subtitle, Converter={StaticResource WarningSubtitleColorConverter}}" Value="{x:Static Visibility.Visible}">
|
||||
<Setter Property="Foreground" Value="#E53E3E"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 우측 화살표 (선택 항목에만 표시) -->
|
||||
<TextBlock Grid.Column="3"
|
||||
Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="10"
|
||||
Foreground="#AABBFF"
|
||||
VerticalAlignment="Center"
|
||||
Margin="8,0,2,0">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListViewItem}}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
|
||||
<!-- ─── 인덱싱 상태 바 ─── -->
|
||||
<TextBlock x:Name="IndexStatusText"
|
||||
Grid.Row="5"
|
||||
Visibility="Collapsed"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,0,0,8"
|
||||
Opacity="0.7"/>
|
||||
|
||||
<!-- ─── 토스트 오버레이 ─── -->
|
||||
<Border x:Name="ToastOverlay"
|
||||
Grid.Row="4" Grid.RowSpan="2"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="0,0,0,12"
|
||||
CornerRadius="8"
|
||||
Padding="14,7"
|
||||
Opacity="0"
|
||||
Visibility="Collapsed"
|
||||
IsHitTestVisible="False">
|
||||
<Border.Background>
|
||||
<SolidColorBrush Color="#107C10" Opacity="0.92"/>
|
||||
</Border.Background>
|
||||
<Border.Triggers>
|
||||
<EventTrigger RoutedEvent="Border.Loaded">
|
||||
<!-- 최초 로드 시 아무것도 안 함 -->
|
||||
</EventTrigger>
|
||||
</Border.Triggers>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock x:Name="ToastIcon"
|
||||
Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="12" Foreground="White"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,8,0"/>
|
||||
<TextBlock x:Name="ToastText"
|
||||
FontSize="11" Foreground="White"
|
||||
FontWeight="SemiBold"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
1562
src/AxCopilot/Views/LauncherWindow.xaml.cs
Normal file
1562
src/AxCopilot/Views/LauncherWindow.xaml.cs
Normal file
File diff suppressed because it is too large
Load Diff
414
src/AxCopilot/Views/ModelRegistrationDialog.cs
Normal file
414
src/AxCopilot/Views/ModelRegistrationDialog.cs
Normal file
@@ -0,0 +1,414 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>모델 등록/편집 다이얼로그. 별칭 + 실제 모델명 입력.</summary>
|
||||
internal sealed class ModelRegistrationDialog : Window
|
||||
{
|
||||
private readonly TextBox _aliasBox;
|
||||
private readonly TextBox _modelBox;
|
||||
private readonly TextBox _endpointBox;
|
||||
private readonly TextBox _apiKeyBox;
|
||||
|
||||
// CP4D 인증 필드
|
||||
private readonly ComboBox _authTypeBox;
|
||||
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 string AuthType => (_authTypeBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "bearer";
|
||||
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 = "",
|
||||
string existingAuthType = "bearer", string existingCp4dUrl = "",
|
||||
string existingCp4dUsername = "", string existingCp4dPassword = "")
|
||||
{
|
||||
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 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 });
|
||||
|
||||
// ── 인증 방식 선택 ──────────────────────────────────────────────────
|
||||
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 cp4dItem = new ComboBoxItem { Content = "CP4D (IBM Cloud Pak for Data)", Tag = "cp4d" };
|
||||
_authTypeBox.Items.Add(bearerItem);
|
||||
_authTypeBox.Items.Add(cp4dItem);
|
||||
_authTypeBox.SelectedItem = existingAuthType == "cp4d" ? cp4dItem : 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 + 사용자명 + 비밀번호 ────────────────────────────
|
||||
_cp4dPanel = new StackPanel { Visibility = existingAuthType == "cp4d" ? 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 });
|
||||
|
||||
_cp4dPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "비밀번호 / API 키",
|
||||
FontSize = 12, FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText, Margin = new Thickness(0, 10, 0, 6),
|
||||
});
|
||||
_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);
|
||||
|
||||
// 인증 방식 전환 시 패널 표시/숨김
|
||||
_authTypeBox.SelectionChanged += (_, _) =>
|
||||
{
|
||||
var isCp4d = AuthType == "cp4d";
|
||||
_cp4dPanel.Visibility = isCp4d ? Visibility.Visible : Visibility.Collapsed;
|
||||
apiKeyPanel.Visibility = isCp4d ? Visibility.Collapsed : Visibility.Visible;
|
||||
};
|
||||
// 초기 상태 설정
|
||||
apiKeyPanel.Visibility = existingAuthType == "cp4d" ? Visibility.Collapsed : Visibility.Visible;
|
||||
|
||||
// 보안 안내
|
||||
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 { }
|
||||
};
|
||||
}
|
||||
}
|
||||
951
src/AxCopilot/Views/PlanViewerWindow.cs
Normal file
951
src/AxCopilot/Views/PlanViewerWindow.cs
Normal file
@@ -0,0 +1,951 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Effects;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 에이전트 실행 계획을 상세히 보여주는 별도 창.
|
||||
/// - 항목 기본 접힘 / 클릭으로 펼침
|
||||
/// - 모두 열기 / 모두 닫기 툴바
|
||||
/// - 사방 가장자리 드래그 리사이즈
|
||||
/// - 항목 드래그로 순서 변경
|
||||
/// </summary>
|
||||
internal sealed class PlanViewerWindow : Window
|
||||
{
|
||||
// ── Win32 Resize ──
|
||||
[DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
|
||||
private const int WM_NCHITTEST = 0x0084;
|
||||
private const int HTLEFT = 10, HTRIGHT = 11, HTTOP = 12, HTTOPLEFT = 13,
|
||||
HTTOPRIGHT = 14, HTBOTTOM = 15, HTBOTTOMLEFT = 16, HTBOTTOMRIGHT = 17;
|
||||
private const int ResizeGrip = 12; // 사방 12px 영역에서 리사이즈 가능
|
||||
|
||||
private const string DragDataFormat = "PlanStepIndex";
|
||||
|
||||
private readonly StackPanel _stepsPanel;
|
||||
private readonly ScrollViewer _scrollViewer;
|
||||
private readonly StackPanel _btnPanel;
|
||||
private readonly Border _statusBar;
|
||||
private readonly TextBlock _statusText;
|
||||
private readonly TextBlock _progressText;
|
||||
|
||||
// 펼침 상태 관리
|
||||
private readonly HashSet<int> _expandedSteps = new();
|
||||
|
||||
// 드래그 상태
|
||||
private int _dragSourceIndex = -1;
|
||||
private Point _dragStartPoint;
|
||||
|
||||
private TaskCompletionSource<string?>? _tcs;
|
||||
private string _planText = "";
|
||||
private List<string> _steps = new();
|
||||
private int _currentStep = -1;
|
||||
private bool _isExecuting;
|
||||
|
||||
public PlanViewerWindow()
|
||||
{
|
||||
Width = 640;
|
||||
Height = 520;
|
||||
MinWidth = 480;
|
||||
MinHeight = 360;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||
WindowStyle = WindowStyle.None;
|
||||
AllowsTransparency = true;
|
||||
Background = Brushes.Transparent;
|
||||
ResizeMode = ResizeMode.CanResize; // WndProc로 직접 처리
|
||||
ShowInTaskbar = false;
|
||||
|
||||
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
|
||||
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
||||
var borderBrush = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
|
||||
var root = new Border
|
||||
{
|
||||
Background = bgBrush,
|
||||
CornerRadius = new CornerRadius(14),
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
Effect = new DropShadowEffect { BlurRadius = 20, ShadowDepth = 4, Opacity = 0.35, Color = Colors.Black },
|
||||
};
|
||||
|
||||
var mainGrid = new Grid();
|
||||
mainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // 0: 타이틀 바
|
||||
mainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // 1: 상태 바
|
||||
mainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // 2: 툴바 (모두 열기/닫기)
|
||||
mainGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); // 3: 단계 목록
|
||||
mainGrid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); // 4: 하단 버튼
|
||||
|
||||
// ── 타이틀 바 ──
|
||||
var titleBar = new Grid { Background = Brushes.Transparent, Margin = new Thickness(20, 14, 12, 0) };
|
||||
titleBar.MouseLeftButtonDown += TitleBar_MouseLeftButtonDown;
|
||||
|
||||
var titleSp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
titleSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE9D2", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 18, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
});
|
||||
titleSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "실행 계획", FontSize = 16, FontWeight = FontWeights.Bold,
|
||||
Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
titleBar.Children.Add(titleSp);
|
||||
|
||||
var closeBtn = new Border
|
||||
{
|
||||
Width = 32, Height = 32, CornerRadius = new CornerRadius(8),
|
||||
Background = Brushes.Transparent, Cursor = Cursors.Hand,
|
||||
HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = "\uE8BB", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12, Foreground = secondaryText,
|
||||
HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
};
|
||||
closeBtn.MouseEnter += (s, _) => ((Border)s).Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
closeBtn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent;
|
||||
closeBtn.MouseLeftButtonUp += (_, _) => Hide();
|
||||
titleBar.Children.Add(closeBtn);
|
||||
Grid.SetRow(titleBar, 0);
|
||||
mainGrid.Children.Add(titleBar);
|
||||
|
||||
// ── 상태 바 (진행률) ──
|
||||
_statusBar = new Border
|
||||
{
|
||||
Visibility = Visibility.Collapsed,
|
||||
Margin = new Thickness(20, 8, 20, 0),
|
||||
Padding = new Thickness(12, 6, 12, 6),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x15,
|
||||
((SolidColorBrush)accentBrush).Color.R,
|
||||
((SolidColorBrush)accentBrush).Color.G,
|
||||
((SolidColorBrush)accentBrush).Color.B)),
|
||||
};
|
||||
var statusGrid = new Grid();
|
||||
_statusText = new TextBlock { Text = "실행 중...", FontSize = 12, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center };
|
||||
statusGrid.Children.Add(_statusText);
|
||||
_progressText = new TextBlock
|
||||
{
|
||||
Text = "", FontSize = 12, Foreground = secondaryText,
|
||||
HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
statusGrid.Children.Add(_progressText);
|
||||
_statusBar.Child = statusGrid;
|
||||
Grid.SetRow(_statusBar, 1);
|
||||
mainGrid.Children.Add(_statusBar);
|
||||
|
||||
// ── 툴바: 모두 열기 / 모두 닫기 ──
|
||||
var hoverBgTb = Application.Current.TryFindResource("ItemHoverBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
var toolBar = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(20, 6, 20, 0),
|
||||
};
|
||||
|
||||
var expandAllBtn = MakeToolbarButton("\uE70D", "모두 열기", secondaryText, hoverBgTb);
|
||||
expandAllBtn.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
for (int i = 0; i < _steps.Count; i++) _expandedSteps.Add(i);
|
||||
RenderSteps();
|
||||
};
|
||||
toolBar.Children.Add(expandAllBtn);
|
||||
|
||||
var collapseAllBtn = MakeToolbarButton("\uE70E", "모두 닫기", secondaryText, hoverBgTb);
|
||||
collapseAllBtn.MouseLeftButtonUp += (_, _) => { _expandedSteps.Clear(); RenderSteps(); };
|
||||
toolBar.Children.Add(collapseAllBtn);
|
||||
|
||||
Grid.SetRow(toolBar, 2);
|
||||
mainGrid.Children.Add(toolBar);
|
||||
|
||||
// ── 컨텐츠: 단계 목록 ──
|
||||
_scrollViewer = new ScrollViewer
|
||||
{
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
Margin = new Thickness(16, 6, 16, 0),
|
||||
Padding = new Thickness(4),
|
||||
};
|
||||
_stepsPanel = new StackPanel();
|
||||
_scrollViewer.Content = _stepsPanel;
|
||||
Grid.SetRow(_scrollViewer, 3);
|
||||
mainGrid.Children.Add(_scrollViewer);
|
||||
|
||||
// ── 하단 버튼 패널 ──
|
||||
_btnPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Margin = new Thickness(20, 12, 20, 16),
|
||||
};
|
||||
Grid.SetRow(_btnPanel, 4);
|
||||
mainGrid.Children.Add(_btnPanel);
|
||||
|
||||
root.Child = mainGrid;
|
||||
Content = root;
|
||||
|
||||
// Win32 Resize 훅
|
||||
SourceInitialized += (_, _) =>
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
var src = HwndSource.FromHwnd(hwnd);
|
||||
src?.AddHook(WndProc);
|
||||
};
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// 툴바 버튼 팩토리
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
||||
private static Border MakeToolbarButton(string icon, string label, Brush fg, Brush hoverBg)
|
||||
{
|
||||
var btn = new Border
|
||||
{
|
||||
CornerRadius = new CornerRadius(6),
|
||||
Padding = new Thickness(8, 3, 8, 3),
|
||||
Margin = new Thickness(0, 0, 4, 0),
|
||||
Background = Brushes.Transparent,
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 9, Foreground = fg,
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0),
|
||||
});
|
||||
sp.Children.Add(new TextBlock { Text = label, FontSize = 11.5, Foreground = fg });
|
||||
btn.Child = sp;
|
||||
btn.MouseEnter += (s, _) => ((Border)s).Background = hoverBg;
|
||||
btn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent;
|
||||
return btn;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// 창 이동 / 리사이즈
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
||||
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ClickCount == 2) return;
|
||||
try { DragMove(); } catch { }
|
||||
}
|
||||
|
||||
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||
{
|
||||
if (msg == WM_NCHITTEST)
|
||||
{
|
||||
var pt = PointFromScreen(new Point(
|
||||
(short)(lParam.ToInt32() & 0xFFFF),
|
||||
(short)(lParam.ToInt32() >> 16)));
|
||||
var w = ActualWidth;
|
||||
var h = ActualHeight;
|
||||
|
||||
int hit = 0;
|
||||
if (pt.X < ResizeGrip && pt.Y < ResizeGrip) hit = HTTOPLEFT;
|
||||
else if (pt.X > w - ResizeGrip && pt.Y < ResizeGrip) hit = HTTOPRIGHT;
|
||||
else if (pt.X < ResizeGrip && pt.Y > h - ResizeGrip) hit = HTBOTTOMLEFT;
|
||||
else if (pt.X > w - ResizeGrip && pt.Y > h - ResizeGrip) hit = HTBOTTOMRIGHT;
|
||||
else if (pt.X < ResizeGrip) hit = HTLEFT;
|
||||
else if (pt.X > w - ResizeGrip) hit = HTRIGHT;
|
||||
else if (pt.Y < ResizeGrip) hit = HTTOP;
|
||||
else if (pt.Y > h - ResizeGrip) hit = HTBOTTOM;
|
||||
|
||||
if (hit != 0) { handled = true; return (IntPtr)hit; }
|
||||
}
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// 공개 API
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
||||
public Task<string?> ShowPlanAsync(string planText, List<string> steps, TaskCompletionSource<string?> tcs)
|
||||
{
|
||||
_planText = planText;
|
||||
_steps = steps;
|
||||
_tcs = tcs;
|
||||
_currentStep = -1;
|
||||
_isExecuting = false;
|
||||
_expandedSteps.Clear(); // 새 계획 표시 시 모두 접힌 상태로 시작
|
||||
|
||||
RenderSteps();
|
||||
BuildApprovalButtons();
|
||||
_statusBar.Visibility = Visibility.Collapsed;
|
||||
|
||||
Show();
|
||||
Activate();
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public void SwitchToExecutionMode()
|
||||
{
|
||||
_isExecuting = true;
|
||||
_statusBar.Visibility = Visibility.Visible;
|
||||
_statusText.Text = "▶ 계획 실행 중...";
|
||||
_progressText.Text = $"0 / {_steps.Count}";
|
||||
BuildExecutionButtons();
|
||||
}
|
||||
|
||||
public void UpdateCurrentStep(int stepIndex)
|
||||
{
|
||||
if (stepIndex < 0 || stepIndex >= _steps.Count) return;
|
||||
_currentStep = stepIndex;
|
||||
_progressText.Text = $"{stepIndex + 1} / {_steps.Count}";
|
||||
_expandedSteps.Add(stepIndex); // 현재 실행 중인 단계는 자동 펼침
|
||||
RenderSteps();
|
||||
}
|
||||
|
||||
public void MarkComplete()
|
||||
{
|
||||
_isExecuting = false;
|
||||
_statusText.Text = "✓ 계획 실행 완료";
|
||||
_statusText.Foreground = new SolidColorBrush(Color.FromRgb(0x10, 0xB9, 0x81));
|
||||
_progressText.Text = $"{_steps.Count} / {_steps.Count}";
|
||||
_currentStep = _steps.Count;
|
||||
RenderSteps();
|
||||
BuildCloseButton();
|
||||
}
|
||||
|
||||
public string PlanText => _planText;
|
||||
public List<string> Steps => _steps;
|
||||
|
||||
public string? BuildApprovedDecisionPayload(string prefix)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(prefix))
|
||||
return null;
|
||||
|
||||
var normalized = _steps
|
||||
.Select(step => step?.Trim() ?? "")
|
||||
.Where(step => !string.IsNullOrWhiteSpace(step))
|
||||
.ToList();
|
||||
if (normalized.Count == 0)
|
||||
return null;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(prefix.Trim());
|
||||
foreach (var step in normalized)
|
||||
sb.AppendLine(step);
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// 단계 목록 렌더링
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
||||
private void RenderSteps()
|
||||
{
|
||||
_stepsPanel.Children.Clear();
|
||||
|
||||
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
|
||||
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
||||
var itemBg = Application.Current.TryFindResource("ItemBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40));
|
||||
var hoverBg = Application.Current.TryFindResource("ItemHoverBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
|
||||
var canEdit = !_isExecuting && _currentStep < 0; // 승인 대기 중에만 편집/순서변경 가능
|
||||
|
||||
for (int i = 0; i < _steps.Count; i++)
|
||||
{
|
||||
var step = _steps[i];
|
||||
var capturedIdx = i;
|
||||
var isComplete = i < _currentStep;
|
||||
var isCurrent = i == _currentStep;
|
||||
var isPending = i > _currentStep;
|
||||
var isExpanded = _expandedSteps.Contains(i);
|
||||
|
||||
// ─ 카드 Border ─
|
||||
var card = new Border
|
||||
{
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(10, 7, 10, 7),
|
||||
Margin = new Thickness(0, 0, 0, 5),
|
||||
Background = isCurrent
|
||||
? new SolidColorBrush(Color.FromArgb(0x18,
|
||||
((SolidColorBrush)accentBrush).Color.R,
|
||||
((SolidColorBrush)accentBrush).Color.G,
|
||||
((SolidColorBrush)accentBrush).Color.B))
|
||||
: itemBg,
|
||||
BorderBrush = isCurrent ? accentBrush : Brushes.Transparent,
|
||||
BorderThickness = new Thickness(isCurrent ? 1.5 : 0),
|
||||
AllowDrop = canEdit,
|
||||
};
|
||||
|
||||
// 열기/닫기 토글 — 텍스트 또는 배경 클릭
|
||||
card.Cursor = Cursors.Hand;
|
||||
card.MouseLeftButtonUp += (s, e) =>
|
||||
{
|
||||
// 드래그 직후 클릭이 발생하는 경우 무시
|
||||
if (e.OriginalSource is Border src && src.Tag?.ToString() == "DragHandle") return;
|
||||
if (_expandedSteps.Contains(capturedIdx)) _expandedSteps.Remove(capturedIdx);
|
||||
else _expandedSteps.Add(capturedIdx);
|
||||
RenderSteps();
|
||||
};
|
||||
|
||||
// ─ 카드 Grid: [drag?][badge][*text][chevron][edit?] ─
|
||||
var cardGrid = new Grid();
|
||||
int badgeCol = canEdit ? 1 : 0;
|
||||
int textCol = canEdit ? 2 : 1;
|
||||
int chevCol = canEdit ? 3 : 2;
|
||||
int editCol = canEdit ? 4 : -1;
|
||||
|
||||
if (canEdit)
|
||||
cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // drag
|
||||
cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // badge
|
||||
cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); // text
|
||||
cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // chevron
|
||||
if (canEdit)
|
||||
cardGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); // edit btns
|
||||
|
||||
// ── 드래그 핸들 (편집 모드 전용) ──
|
||||
if (canEdit)
|
||||
{
|
||||
var dimColor = Color.FromArgb(0x55, 0x80, 0x80, 0x80);
|
||||
var dimBrush = new SolidColorBrush(dimColor);
|
||||
var dragHandle = new Border
|
||||
{
|
||||
Tag = "DragHandle",
|
||||
Width = 20, Cursor = Cursors.SizeAll,
|
||||
Background = Brushes.Transparent,
|
||||
Margin = new Thickness(0, 0, 6, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = "\uE8FD", // Sort/Lines 아이콘 (드래그 핸들)
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 11,
|
||||
Foreground = dimBrush,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
};
|
||||
dragHandle.MouseEnter += (s, _) =>
|
||||
((TextBlock)((Border)s).Child).Foreground = secondaryText;
|
||||
dragHandle.MouseLeave += (s, _) =>
|
||||
((TextBlock)((Border)s).Child).Foreground = dimBrush;
|
||||
|
||||
// 드래그 시작 — 마우스 눌림 위치 기록
|
||||
dragHandle.PreviewMouseLeftButtonDown += (s, e) =>
|
||||
{
|
||||
_dragSourceIndex = capturedIdx;
|
||||
_dragStartPoint = e.GetPosition(_stepsPanel);
|
||||
e.Handled = true; // 카드 클릭(expand) 이벤트 방지
|
||||
};
|
||||
// 충분히 움직이면 DragDrop 시작
|
||||
dragHandle.PreviewMouseMove += (s, e) =>
|
||||
{
|
||||
if (_dragSourceIndex < 0 || e.LeftButton != MouseButtonState.Pressed) return;
|
||||
var cur = e.GetPosition(_stepsPanel);
|
||||
if (Math.Abs(cur.X - _dragStartPoint.X) > SystemParameters.MinimumHorizontalDragDistance ||
|
||||
Math.Abs(cur.Y - _dragStartPoint.Y) > SystemParameters.MinimumVerticalDragDistance)
|
||||
{
|
||||
int idx = _dragSourceIndex;
|
||||
_dragSourceIndex = -1;
|
||||
DragDrop.DoDragDrop((DependencyObject)s,
|
||||
new DataObject(DragDataFormat, idx), DragDropEffects.Move);
|
||||
// DoDragDrop 완료 후 비주얼 정리
|
||||
Dispatcher.InvokeAsync(RenderSteps);
|
||||
}
|
||||
};
|
||||
dragHandle.PreviewMouseLeftButtonUp += (_, _) => _dragSourceIndex = -1;
|
||||
|
||||
Grid.SetColumn(dragHandle, 0);
|
||||
cardGrid.Children.Add(dragHandle);
|
||||
|
||||
// ── 카드 Drop 이벤트 ──
|
||||
card.DragOver += (s, e) =>
|
||||
{
|
||||
if (!e.Data.GetDataPresent(DragDataFormat)) return;
|
||||
int src = (int)e.Data.GetData(DragDataFormat);
|
||||
if (src != capturedIdx)
|
||||
{
|
||||
((Border)s).BorderBrush = accentBrush;
|
||||
((Border)s).BorderThickness = new Thickness(1.5);
|
||||
e.Effects = DragDropEffects.Move;
|
||||
}
|
||||
else e.Effects = DragDropEffects.None;
|
||||
e.Handled = true;
|
||||
};
|
||||
card.DragLeave += (s, _) =>
|
||||
{
|
||||
bool isCurr = _currentStep == capturedIdx;
|
||||
((Border)s).BorderBrush = isCurr ? accentBrush : Brushes.Transparent;
|
||||
((Border)s).BorderThickness = new Thickness(isCurr ? 1.5 : 0);
|
||||
};
|
||||
card.Drop += (s, e) =>
|
||||
{
|
||||
if (!e.Data.GetDataPresent(DragDataFormat)) { e.Handled = true; return; }
|
||||
int srcIdx = (int)e.Data.GetData(DragDataFormat);
|
||||
int dstIdx = capturedIdx;
|
||||
if (srcIdx != dstIdx && srcIdx >= 0 && srcIdx < _steps.Count)
|
||||
{
|
||||
var item = _steps[srcIdx];
|
||||
_steps.RemoveAt(srcIdx);
|
||||
// srcIdx < dstIdx 이면 제거 후 인덱스가 1 감소
|
||||
int insertAt = srcIdx < dstIdx ? dstIdx - 1 : dstIdx;
|
||||
_steps.Insert(insertAt, item);
|
||||
_expandedSteps.Clear();
|
||||
RenderSteps();
|
||||
}
|
||||
e.Handled = true;
|
||||
};
|
||||
}
|
||||
|
||||
// ── 상태 배지 ──
|
||||
UIElement badge;
|
||||
if (isComplete)
|
||||
{
|
||||
badge = new TextBlock
|
||||
{
|
||||
Text = "\uE73E", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 13, Foreground = new SolidColorBrush(Color.FromRgb(0x10, 0xB9, 0x81)),
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0),
|
||||
Width = 20, TextAlignment = TextAlignment.Center,
|
||||
};
|
||||
}
|
||||
else if (isCurrent)
|
||||
{
|
||||
badge = new TextBlock
|
||||
{
|
||||
Text = "\uE768", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 13, Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0),
|
||||
Width = 20, TextAlignment = TextAlignment.Center,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
badge = new Border
|
||||
{
|
||||
Width = 22, Height = 22, CornerRadius = new CornerRadius(11),
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x25, 0x80, 0x80, 0x80)),
|
||||
Margin = new Thickness(0, 0, 10, 0), VerticalAlignment = VerticalAlignment.Center,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = $"{i + 1}", FontSize = 11, Foreground = secondaryText,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
};
|
||||
}
|
||||
Grid.SetColumn(badge, badgeCol);
|
||||
cardGrid.Children.Add(badge);
|
||||
|
||||
// ── 단계 텍스트 ──
|
||||
var textBlock = new TextBlock
|
||||
{
|
||||
Text = step,
|
||||
FontSize = 13,
|
||||
Foreground = isComplete ? secondaryText : primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Opacity = isPending && _isExecuting ? 0.6 : 1.0,
|
||||
TextDecorations = isComplete ? TextDecorations.Strikethrough : null,
|
||||
Margin = new Thickness(0, 0, 4, 0),
|
||||
};
|
||||
if (isExpanded)
|
||||
{
|
||||
textBlock.TextWrapping = TextWrapping.Wrap;
|
||||
textBlock.TextTrimming = TextTrimming.None;
|
||||
}
|
||||
else
|
||||
{
|
||||
textBlock.TextWrapping = TextWrapping.NoWrap;
|
||||
textBlock.TextTrimming = TextTrimming.CharacterEllipsis;
|
||||
textBlock.ToolTip = step; // 접힌 상태: 호버 시 전체 텍스트 툴팁
|
||||
}
|
||||
Grid.SetColumn(textBlock, textCol);
|
||||
cardGrid.Children.Add(textBlock);
|
||||
|
||||
// ── 펼침/접힘 Chevron ──
|
||||
var chevron = new Border
|
||||
{
|
||||
Width = 22, Height = 22, CornerRadius = new CornerRadius(4),
|
||||
Background = Brushes.Transparent, Cursor = Cursors.Hand,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, canEdit ? 4 : 0, 0),
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = isExpanded ? "\uE70E" : "\uE70D", // ChevronUp / ChevronDown
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 9,
|
||||
Foreground = new SolidColorBrush(Color.FromArgb(0x70, 0x80, 0x80, 0x80)),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
};
|
||||
chevron.MouseEnter += (s, _) => ((Border)s).Background = hoverBg;
|
||||
chevron.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent;
|
||||
chevron.MouseLeftButtonUp += (_, e) =>
|
||||
{
|
||||
if (_expandedSteps.Contains(capturedIdx)) _expandedSteps.Remove(capturedIdx);
|
||||
else _expandedSteps.Add(capturedIdx);
|
||||
RenderSteps();
|
||||
e.Handled = true;
|
||||
};
|
||||
Grid.SetColumn(chevron, chevCol);
|
||||
cardGrid.Children.Add(chevron);
|
||||
|
||||
// ── 편집 버튼 (위/아래/편집/삭제) ──
|
||||
if (canEdit)
|
||||
{
|
||||
var editBtnPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(2, 0, 0, 0),
|
||||
};
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
var upBtn = CreateMiniButton("\uE70E", secondaryText, hoverBg);
|
||||
upBtn.ToolTip = "위로 이동";
|
||||
upBtn.MouseLeftButtonUp += (_, e) => { SwapSteps(capturedIdx, capturedIdx - 1); e.Handled = true; };
|
||||
editBtnPanel.Children.Add(upBtn);
|
||||
}
|
||||
if (i < _steps.Count - 1)
|
||||
{
|
||||
var downBtn = CreateMiniButton("\uE70D", secondaryText, hoverBg);
|
||||
downBtn.ToolTip = "아래로 이동";
|
||||
downBtn.MouseLeftButtonUp += (_, e) => { SwapSteps(capturedIdx, capturedIdx + 1); e.Handled = true; };
|
||||
editBtnPanel.Children.Add(downBtn);
|
||||
}
|
||||
|
||||
var editBtn = CreateMiniButton("\uE70F", accentBrush, hoverBg);
|
||||
editBtn.ToolTip = "편집";
|
||||
editBtn.MouseLeftButtonUp += (_, e) => { EditStep(capturedIdx); e.Handled = true; };
|
||||
editBtnPanel.Children.Add(editBtn);
|
||||
|
||||
var delBtn = CreateMiniButton("\uE74D", new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)), hoverBg);
|
||||
delBtn.ToolTip = "삭제";
|
||||
delBtn.MouseLeftButtonUp += (_, e) =>
|
||||
{
|
||||
if (_steps.Count > 1)
|
||||
{
|
||||
_steps.RemoveAt(capturedIdx);
|
||||
_expandedSteps.Remove(capturedIdx);
|
||||
RenderSteps();
|
||||
}
|
||||
e.Handled = true;
|
||||
};
|
||||
editBtnPanel.Children.Add(delBtn);
|
||||
|
||||
Grid.SetColumn(editBtnPanel, editCol);
|
||||
cardGrid.Children.Add(editBtnPanel);
|
||||
}
|
||||
|
||||
card.Child = cardGrid;
|
||||
_stepsPanel.Children.Add(card);
|
||||
}
|
||||
|
||||
// ── 단계 추가 버튼 (편집 모드) ──
|
||||
if (canEdit)
|
||||
{
|
||||
var st2 = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var hb2 = Application.Current.TryFindResource("ItemHoverBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
var addBtn = new Border
|
||||
{
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(14, 8, 14, 8),
|
||||
Margin = new Thickness(0, 4, 0, 0),
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = new SolidColorBrush(Color.FromArgb(0x30, 0x80, 0x80, 0x80)),
|
||||
BorderThickness = new Thickness(1),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var addSp = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center };
|
||||
addSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE710", FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12, Foreground = st2,
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0),
|
||||
});
|
||||
addSp.Children.Add(new TextBlock { Text = "단계 추가", FontSize = 12, Foreground = st2 });
|
||||
addBtn.Child = addSp;
|
||||
addBtn.MouseEnter += (s, _) => ((Border)s).Background = hb2;
|
||||
addBtn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent;
|
||||
addBtn.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
_steps.Add("새 단계");
|
||||
RenderSteps();
|
||||
EditStep(_steps.Count - 1);
|
||||
};
|
||||
_stepsPanel.Children.Add(addBtn);
|
||||
}
|
||||
|
||||
// 현재 단계로 자동 스크롤
|
||||
if (_currentStep >= 0 && _stepsPanel.Children.Count > _currentStep)
|
||||
{
|
||||
_stepsPanel.UpdateLayout();
|
||||
var target = (FrameworkElement)_stepsPanel.Children[Math.Min(_currentStep, _stepsPanel.Children.Count - 1)];
|
||||
target.BringIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// 단계 편집 / 교환
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
||||
private void SwapSteps(int a, int b)
|
||||
{
|
||||
if (a < 0 || b < 0 || a >= _steps.Count || b >= _steps.Count) return;
|
||||
(_steps[a], _steps[b]) = (_steps[b], _steps[a]);
|
||||
RenderSteps();
|
||||
}
|
||||
|
||||
private void EditStep(int index)
|
||||
{
|
||||
if (index < 0 || index >= _steps.Count) return;
|
||||
var primaryText = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var itemBg = Application.Current.TryFindResource("ItemBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40));
|
||||
var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
||||
|
||||
if (index >= _stepsPanel.Children.Count) return;
|
||||
|
||||
var editCard = new Border
|
||||
{
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(10, 8, 10, 8),
|
||||
Margin = new Thickness(0, 0, 0, 5),
|
||||
Background = itemBg,
|
||||
BorderBrush = accentBrush,
|
||||
BorderThickness = new Thickness(1.5),
|
||||
};
|
||||
|
||||
var textBox = new TextBox
|
||||
{
|
||||
Text = _steps[index],
|
||||
FontSize = 13,
|
||||
Background = Brushes.Transparent,
|
||||
Foreground = primaryText,
|
||||
CaretBrush = primaryText,
|
||||
BorderThickness = new Thickness(0),
|
||||
AcceptsReturn = false,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Padding = new Thickness(4),
|
||||
};
|
||||
|
||||
var capturedIdx = index;
|
||||
textBox.KeyDown += (_, e) =>
|
||||
{
|
||||
if (e.Key == Key.Enter) { _steps[capturedIdx] = textBox.Text.Trim(); RenderSteps(); e.Handled = true; }
|
||||
if (e.Key == Key.Escape) { RenderSteps(); e.Handled = true; }
|
||||
};
|
||||
textBox.LostFocus += (_, _) => { _steps[capturedIdx] = textBox.Text.Trim(); RenderSteps(); };
|
||||
|
||||
editCard.Child = textBox;
|
||||
_stepsPanel.Children[index] = editCard;
|
||||
textBox.Focus();
|
||||
textBox.SelectAll();
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// 하단 버튼 빌드
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
||||
private void BuildApprovalButtons()
|
||||
{
|
||||
_btnPanel.Children.Clear();
|
||||
var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
||||
|
||||
var approveBtn = CreateActionButton("\uE73E", "승인", accentBrush, Brushes.White, true);
|
||||
approveBtn.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
_tcs?.TrySetResult(null);
|
||||
SwitchToExecutionMode();
|
||||
};
|
||||
_btnPanel.Children.Add(approveBtn);
|
||||
|
||||
var editBtn = CreateActionButton("\uE70F", "수정 요청", accentBrush, accentBrush, false);
|
||||
editBtn.MouseLeftButtonUp += (_, _) => ShowEditInput();
|
||||
_btnPanel.Children.Add(editBtn);
|
||||
|
||||
var reconfirmBtn = CreateActionButton("\uE72C", "재확인",
|
||||
Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White, false);
|
||||
reconfirmBtn.MouseLeftButtonUp += (_, _) =>
|
||||
_tcs?.TrySetResult("계획을 다시 검토하고 더 구체적으로 수정해주세요.");
|
||||
_btnPanel.Children.Add(reconfirmBtn);
|
||||
|
||||
var cancelBrush = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26));
|
||||
var cancelBtn = CreateActionButton("\uE711", "취소", cancelBrush, cancelBrush, false);
|
||||
cancelBtn.MouseLeftButtonUp += (_, _) => { _tcs?.TrySetResult("취소"); Hide(); };
|
||||
_btnPanel.Children.Add(cancelBtn);
|
||||
}
|
||||
|
||||
private void BuildExecutionButtons()
|
||||
{
|
||||
_btnPanel.Children.Clear();
|
||||
var secondaryText = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var hideBtn = CreateActionButton("\uE921", "숨기기", secondaryText,
|
||||
Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White, false);
|
||||
hideBtn.MouseLeftButtonUp += (_, _) => Hide();
|
||||
_btnPanel.Children.Add(hideBtn);
|
||||
}
|
||||
|
||||
private void BuildCloseButton()
|
||||
{
|
||||
_btnPanel.Children.Clear();
|
||||
var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
||||
var closeBtn = CreateActionButton("\uE73E", "닫기", accentBrush, Brushes.White, true);
|
||||
closeBtn.MouseLeftButtonUp += (_, _) => Hide();
|
||||
_btnPanel.Children.Add(closeBtn);
|
||||
}
|
||||
|
||||
private void ShowEditInput()
|
||||
{
|
||||
var editPanel = new Border
|
||||
{
|
||||
Margin = new Thickness(20, 0, 20, 12),
|
||||
Padding = new Thickness(12, 8, 12, 8),
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Background = Application.Current.TryFindResource("ItemBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40)),
|
||||
};
|
||||
var editStack = new StackPanel();
|
||||
editStack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "수정 사항을 입력하세요:",
|
||||
FontSize = 11.5,
|
||||
Foreground = Application.Current.TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
});
|
||||
var textBox = new TextBox
|
||||
{
|
||||
MinHeight = 44,
|
||||
MaxHeight = 120,
|
||||
AcceptsReturn = true,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
FontSize = 13,
|
||||
Background = Application.Current.TryFindResource("LauncherBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x1A, 0x1B, 0x2E)),
|
||||
Foreground = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||||
CaretBrush = Application.Current.TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||||
BorderBrush = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(10, 8, 10, 8),
|
||||
};
|
||||
editStack.Children.Add(textBox);
|
||||
|
||||
var accentBrush = Application.Current.TryFindResource("AccentColor") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
||||
var sendBtn = new Border
|
||||
{
|
||||
Background = accentBrush,
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(14, 6, 14, 6),
|
||||
Margin = new Thickness(0, 8, 0, 0),
|
||||
Cursor = Cursors.Hand,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = "전송", FontSize = 12.5, FontWeight = FontWeights.SemiBold, Foreground = Brushes.White,
|
||||
},
|
||||
};
|
||||
sendBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85;
|
||||
sendBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
||||
sendBtn.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
var feedback = textBox.Text.Trim();
|
||||
if (string.IsNullOrEmpty(feedback)) return;
|
||||
_tcs?.TrySetResult(feedback);
|
||||
};
|
||||
editStack.Children.Add(sendBtn);
|
||||
editPanel.Child = editStack;
|
||||
|
||||
if (_btnPanel.Parent is Grid parentGrid)
|
||||
{
|
||||
for (int i = parentGrid.Children.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (parentGrid.Children[i] is Border b && b.Tag?.ToString() == "EditPanel")
|
||||
parentGrid.Children.RemoveAt(i);
|
||||
}
|
||||
editPanel.Tag = "EditPanel";
|
||||
Grid.SetRow(editPanel, 4); // row 4 = 하단 버튼 행 (toolBar 추가로 1 증가)
|
||||
parentGrid.Children.Add(editPanel);
|
||||
_btnPanel.Margin = new Thickness(20, 0, 20, 16);
|
||||
textBox.Focus();
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// 공통 버튼 팩토리
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
||||
private static Border CreateMiniButton(string icon, Brush fg, Brush hoverBg)
|
||||
{
|
||||
var btn = new Border
|
||||
{
|
||||
Width = 24, Height = 24,
|
||||
CornerRadius = new CornerRadius(6),
|
||||
Background = Brushes.Transparent,
|
||||
Cursor = Cursors.Hand,
|
||||
Margin = new Thickness(1, 0, 1, 0),
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 10, Foreground = fg,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
};
|
||||
btn.MouseEnter += (s, _) => ((Border)s).Background = hoverBg;
|
||||
btn.MouseLeave += (s, _) => ((Border)s).Background = Brushes.Transparent;
|
||||
return btn;
|
||||
}
|
||||
|
||||
private static Border CreateActionButton(string icon, string text, Brush borderColor,
|
||||
Brush textColor, bool filled)
|
||||
{
|
||||
var color = ((SolidColorBrush)borderColor).Color;
|
||||
var btn = new Border
|
||||
{
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = new Thickness(16, 8, 16, 8),
|
||||
Margin = new Thickness(4, 0, 4, 0),
|
||||
Cursor = Cursors.Hand,
|
||||
Background = filled ? borderColor
|
||||
: new SolidColorBrush(Color.FromArgb(0x18, color.R, color.G, color.B)),
|
||||
BorderBrush = filled ? Brushes.Transparent
|
||||
: new SolidColorBrush(Color.FromArgb(0x80, color.R, color.G, color.B)),
|
||||
BorderThickness = new Thickness(filled ? 0 : 1.2),
|
||||
};
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12, Foreground = filled ? Brushes.White : textColor,
|
||||
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0),
|
||||
});
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = text, FontSize = 12.5, FontWeight = FontWeights.SemiBold,
|
||||
Foreground = filled ? Brushes.White : textColor,
|
||||
});
|
||||
btn.Child = sp;
|
||||
btn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85;
|
||||
btn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
||||
return btn;
|
||||
}
|
||||
}
|
||||
119
src/AxCopilot/Views/PreviewWindow.xaml
Normal file
119
src/AxCopilot/Views/PreviewWindow.xaml
Normal file
@@ -0,0 +1,119 @@
|
||||
<Window x:Class="AxCopilot.Views.PreviewWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
|
||||
Title="AX Copilot — 미리보기"
|
||||
Width="860" Height="680"
|
||||
WindowStyle="None" AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="CanResizeWithGrip"
|
||||
MinWidth="400" MinHeight="300">
|
||||
<Border Background="{DynamicResource LauncherBackground}" CornerRadius="12"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Margin="6">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="20" ShadowDepth="4" Opacity="0.3" Color="Black" Direction="270"/>
|
||||
</Border.Effect>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="44"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 타이틀바 -->
|
||||
<Border Grid.Row="0" CornerRadius="12,12,0,0"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
|
||||
<Grid Margin="16,0,8,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 좌측: 아이콘 + 제목 -->
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="15"
|
||||
Foreground="{DynamicResource AccentColor}"
|
||||
VerticalAlignment="Center" Margin="0,1,10,0"/>
|
||||
<TextBlock x:Name="TitleText" Text="미리보기"
|
||||
FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 우측: 외부열기, 최소화, 최대화, 닫기 -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<!-- 외부 프로그램으로 열기 -->
|
||||
<Border Width="40" Height="40" Background="Transparent" Cursor="Hand"
|
||||
CornerRadius="8" ToolTip="외부 프로그램으로 열기"
|
||||
MouseLeftButtonDown="OpenExternalBtn_Click"
|
||||
MouseEnter="TitleBtn_Enter" MouseLeave="TitleBtn_Leave">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border Width="40" Height="40" Background="Transparent" Cursor="Hand"
|
||||
CornerRadius="8"
|
||||
MouseLeftButtonDown="MinBtn_Click"
|
||||
MouseEnter="TitleBtn_Enter" MouseLeave="TitleBtn_Leave">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border Width="40" Height="40" Background="Transparent" Cursor="Hand"
|
||||
CornerRadius="8"
|
||||
MouseLeftButtonDown="MaxBtn_Click"
|
||||
MouseEnter="TitleBtn_Enter" MouseLeave="TitleBtn_Leave">
|
||||
<TextBlock x:Name="MaxBtnIcon" Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border Width="40" Height="40" Background="Transparent" Cursor="Hand"
|
||||
CornerRadius="8"
|
||||
MouseLeftButtonDown="CloseBtn_Click"
|
||||
MouseEnter="CloseBtnEnter" MouseLeave="TitleBtn_Leave">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 탭 바 -->
|
||||
<Border Grid.Row="1" Background="#0AFFFFFF"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0,0,0,1"
|
||||
Padding="4,0">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Hidden"
|
||||
VerticalScrollBarVisibility="Disabled" CanContentScroll="True">
|
||||
<StackPanel x:Name="TabPanel" Orientation="Horizontal"/>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- 콘텐츠 영역 -->
|
||||
<Grid Grid.Row="2" ClipToBounds="True">
|
||||
<wv2:WebView2 x:Name="PreviewBrowser" Visibility="Collapsed"/>
|
||||
<ScrollViewer x:Name="TextScroll" Visibility="Collapsed"
|
||||
VerticalScrollBarVisibility="Auto" Padding="16,12">
|
||||
<TextBlock x:Name="TextContent" TextWrapping="Wrap"
|
||||
FontFamily="Consolas" FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
</ScrollViewer>
|
||||
<DataGrid x:Name="DataGridContent" Visibility="Collapsed"
|
||||
AutoGenerateColumns="True" IsReadOnly="True"
|
||||
HeadersVisibility="Column" GridLinesVisibility="All"
|
||||
Background="Transparent"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
BorderThickness="0" CanUserAddRows="False"
|
||||
RowBackground="#0AFFFFFF" AlternatingRowBackground="#15FFFFFF"
|
||||
ColumnHeaderHeight="30" RowHeight="28" FontSize="11.5"/>
|
||||
<TextBlock x:Name="EmptyMessage" Text="미리보기할 파일이 없습니다"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
FontSize="13" Foreground="{DynamicResource SecondaryText}"
|
||||
Visibility="Collapsed"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
505
src/AxCopilot/Views/PreviewWindow.xaml.cs
Normal file
505
src/AxCopilot/Views/PreviewWindow.xaml.cs
Normal file
@@ -0,0 +1,505 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>파일 미리보기 별도 창. WebView2 HWND airspace 문제를 근본적으로 회피합니다.</summary>
|
||||
public partial class PreviewWindow : Window
|
||||
{
|
||||
private static PreviewWindow? _instance;
|
||||
|
||||
private readonly List<string> _tabs = new();
|
||||
private string? _activeTab;
|
||||
private bool _webViewInitialized;
|
||||
private string? _selectedMood;
|
||||
|
||||
private static readonly string WebView2DataFolder =
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"AxCopilot", "WebView2_Preview");
|
||||
|
||||
private static readonly HashSet<string> PreviewableExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".html", ".htm", ".md", ".csv", ".txt", ".json", ".xml", ".log",
|
||||
};
|
||||
|
||||
public PreviewWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += OnLoaded;
|
||||
SourceInitialized += OnSourceInitialized;
|
||||
KeyDown += (_, e) => { if (e.Key == Key.Escape) Close(); };
|
||||
StateChanged += (_, _) =>
|
||||
{
|
||||
MaxBtnIcon.Text = WindowState == WindowState.Maximized ? "\uE923" : "\uE922";
|
||||
};
|
||||
Closed += (_, _) => _instance = null;
|
||||
}
|
||||
|
||||
// ─── 상하좌우 리사이즈 (WindowStyle=None 대응) ─────────────
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
private const int WM_NCHITTEST = 0x0084;
|
||||
private const int HTLEFT = 10, HTRIGHT = 11, HTTOP = 12, HTTOPLEFT = 13, HTTOPRIGHT = 14;
|
||||
private const int HTBOTTOM = 15, HTBOTTOMLEFT = 16, HTBOTTOMRIGHT = 17;
|
||||
|
||||
private void OnSourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
var hwndSource = (HwndSource)PresentationSource.FromVisual(this);
|
||||
hwndSource?.AddHook(WndProc);
|
||||
}
|
||||
|
||||
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||
{
|
||||
if (msg == WM_NCHITTEST)
|
||||
{
|
||||
var pt = PointFromScreen(new Point(
|
||||
(short)(lParam.ToInt32() & 0xFFFF),
|
||||
(short)((lParam.ToInt32() >> 16) & 0xFFFF)));
|
||||
const double grip = 8; // 리사이즈 가능 영역 (px)
|
||||
var w = ActualWidth;
|
||||
var h = ActualHeight;
|
||||
|
||||
if (pt.X < grip && pt.Y < grip) { handled = true; return (IntPtr)HTTOPLEFT; }
|
||||
if (pt.X > w - grip && pt.Y < grip) { handled = true; return (IntPtr)HTTOPRIGHT; }
|
||||
if (pt.X < grip && pt.Y > h - grip) { handled = true; return (IntPtr)HTBOTTOMLEFT; }
|
||||
if (pt.X > w - grip && pt.Y > h - grip) { handled = true; return (IntPtr)HTBOTTOMRIGHT; }
|
||||
if (pt.X < grip) { handled = true; return (IntPtr)HTLEFT; }
|
||||
if (pt.X > w - grip) { handled = true; return (IntPtr)HTRIGHT; }
|
||||
if (pt.Y < grip) { handled = true; return (IntPtr)HTTOP; }
|
||||
if (pt.Y > h - grip) { handled = true; return (IntPtr)HTBOTTOM; }
|
||||
}
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
// ─── 싱글턴 팩토리 ──────────────────────────────────────────
|
||||
|
||||
/// <summary>파일을 미리보기 창에 표시합니다. 이미 열려 있으면 탭을 추가합니다.</summary>
|
||||
public static void ShowPreview(string filePath, string? mood = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
|
||||
return;
|
||||
|
||||
var ext = Path.GetExtension(filePath).ToLowerInvariant();
|
||||
if (!PreviewableExtensions.Contains(ext))
|
||||
return;
|
||||
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (_instance == null || !_instance.IsLoaded)
|
||||
{
|
||||
_instance = new PreviewWindow();
|
||||
// 부모 창의 테마 리소스 전달
|
||||
var mainWindow = Application.Current.MainWindow;
|
||||
if (mainWindow != null)
|
||||
{
|
||||
foreach (var dict in mainWindow.Resources.MergedDictionaries)
|
||||
_instance.Resources.MergedDictionaries.Add(dict);
|
||||
}
|
||||
_instance._selectedMood = mood;
|
||||
_instance.Show();
|
||||
}
|
||||
|
||||
_instance.AddTab(filePath);
|
||||
_instance.Activate();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>이미 열린 파일의 콘텐츠만 새로고침합니다. 활성 탭이 아닌 파일도 탭 목록에 있으면 활성 탭으로 전환 후 새로고침.</summary>
|
||||
public static void RefreshIfOpen(string filePath)
|
||||
{
|
||||
if (_instance == null || !_instance.IsLoaded) return;
|
||||
Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
// 현재 활성 탭이면 즉시 새로고침
|
||||
if (_instance._activeTab != null &&
|
||||
string.Equals(_instance._activeTab, filePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_instance.LoadContent(filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
// 탭 목록에 있으면 해당 탭으로 전환 후 새로고침
|
||||
var existing = _instance._tabs.FirstOrDefault(
|
||||
t => string.Equals(t, filePath, StringComparison.OrdinalIgnoreCase));
|
||||
if (existing != null)
|
||||
{
|
||||
_instance._activeTab = existing;
|
||||
_instance.LoadContent(existing);
|
||||
_instance.RebuildTabs();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>현재 미리보기 창이 열려 있는지 반환합니다.</summary>
|
||||
public static bool IsOpen => _instance != null && _instance.IsLoaded;
|
||||
|
||||
// ─── 초기화 ─────────────────────────────────────────────────
|
||||
|
||||
private async void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var env = await CoreWebView2Environment.CreateAsync(
|
||||
userDataFolder: WebView2DataFolder);
|
||||
await PreviewBrowser.EnsureCoreWebView2Async(env);
|
||||
_webViewInitialized = true;
|
||||
|
||||
// 대기 중인 콘텐츠 로드
|
||||
if (_activeTab != null)
|
||||
LoadContent(_activeTab);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Services.LogService.Warn($"PreviewWindow WebView2 초기화 실패: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 탭 관리 ────────────────────────────────────────────────
|
||||
|
||||
private void AddTab(string filePath)
|
||||
{
|
||||
if (!_tabs.Contains(filePath, StringComparer.OrdinalIgnoreCase))
|
||||
_tabs.Add(filePath);
|
||||
|
||||
_activeTab = filePath;
|
||||
RebuildTabs();
|
||||
LoadContent(filePath);
|
||||
}
|
||||
|
||||
private void CloseTab(string filePath)
|
||||
{
|
||||
_tabs.RemoveAll(t => string.Equals(t, filePath, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (_tabs.Count == 0)
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.Equals(_activeTab, filePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_activeTab = _tabs[^1];
|
||||
LoadContent(_activeTab);
|
||||
}
|
||||
|
||||
RebuildTabs();
|
||||
}
|
||||
|
||||
private void RebuildTabs()
|
||||
{
|
||||
TabPanel.Children.Clear();
|
||||
|
||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
|
||||
foreach (var tabPath in _tabs)
|
||||
{
|
||||
var fileName = Path.GetFileName(tabPath);
|
||||
var isActive = string.Equals(tabPath, _activeTab, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var tabBorder = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = isActive ? accentBrush : Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0, 0, 0, isActive ? 2 : 0),
|
||||
Padding = new Thickness(10, 6, 6, 6),
|
||||
Cursor = Cursors.Hand,
|
||||
MaxWidth = _tabs.Count <= 3 ? 220 : (_tabs.Count <= 5 ? 160 : 110),
|
||||
};
|
||||
|
||||
var tabContent = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
|
||||
tabContent.Children.Add(new TextBlock
|
||||
{
|
||||
Text = fileName,
|
||||
FontSize = 11.5,
|
||||
Foreground = isActive ? primaryText : secondaryText,
|
||||
FontWeight = isActive ? FontWeights.SemiBold : FontWeights.Normal,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
MaxWidth = tabBorder.MaxWidth - 36,
|
||||
ToolTip = tabPath,
|
||||
});
|
||||
|
||||
// 닫기 버튼
|
||||
var closePath = tabPath;
|
||||
var closeBtn = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(3),
|
||||
Padding = new Thickness(4, 2, 4, 2),
|
||||
Margin = new Thickness(6, 0, 0, 0),
|
||||
Cursor = Cursors.Hand,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = "\uE711",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 8,
|
||||
Foreground = secondaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
};
|
||||
closeBtn.MouseEnter += (s, _) =>
|
||||
{
|
||||
if (s is Border b)
|
||||
b.Background = new SolidColorBrush(Color.FromArgb(0x30, 0xFF, 0x50, 0x50));
|
||||
};
|
||||
closeBtn.MouseLeave += (s, _) =>
|
||||
{
|
||||
if (s is Border b) b.Background = Brushes.Transparent;
|
||||
};
|
||||
closeBtn.MouseLeftButtonUp += (_, me) =>
|
||||
{
|
||||
me.Handled = true;
|
||||
CloseTab(closePath);
|
||||
};
|
||||
|
||||
tabContent.Children.Add(closeBtn);
|
||||
tabBorder.Child = tabContent;
|
||||
|
||||
// 탭 클릭 → 활성화
|
||||
var clickPath = tabPath;
|
||||
tabBorder.MouseLeftButtonUp += (_, me) =>
|
||||
{
|
||||
if (me.Handled) return;
|
||||
me.Handled = true;
|
||||
_activeTab = clickPath;
|
||||
RebuildTabs();
|
||||
LoadContent(clickPath);
|
||||
};
|
||||
|
||||
// 호버 효과
|
||||
tabBorder.MouseEnter += (s, _) =>
|
||||
{
|
||||
if (s is Border b && !string.Equals(clickPath, _activeTab, StringComparison.OrdinalIgnoreCase))
|
||||
b.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
};
|
||||
tabBorder.MouseLeave += (s, _) =>
|
||||
{
|
||||
if (s is Border b) b.Background = Brushes.Transparent;
|
||||
};
|
||||
|
||||
TabPanel.Children.Add(tabBorder);
|
||||
|
||||
// 구분선
|
||||
if (tabPath != _tabs[^1])
|
||||
{
|
||||
TabPanel.Children.Add(new Border
|
||||
{
|
||||
Width = 1, Height = 14,
|
||||
Background = borderBrush,
|
||||
Margin = new Thickness(2, 0, 2, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 타이틀 업데이트
|
||||
if (_activeTab != null)
|
||||
TitleText.Text = $"미리보기 — {Path.GetFileName(_activeTab)}";
|
||||
}
|
||||
|
||||
// ─── 콘텐츠 로드 ────────────────────────────────────────────
|
||||
|
||||
private async void LoadContent(string filePath)
|
||||
{
|
||||
var ext = Path.GetExtension(filePath).ToLowerInvariant();
|
||||
|
||||
PreviewBrowser.Visibility = Visibility.Collapsed;
|
||||
TextScroll.Visibility = Visibility.Collapsed;
|
||||
DataGridContent.Visibility = Visibility.Collapsed;
|
||||
EmptyMessage.Visibility = Visibility.Collapsed;
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
EmptyMessage.Text = "파일을 찾을 수 없습니다";
|
||||
EmptyMessage.Visibility = Visibility.Visible;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
switch (ext)
|
||||
{
|
||||
case ".html":
|
||||
case ".htm":
|
||||
if (!_webViewInitialized) return; // OnLoaded에서 재시도
|
||||
PreviewBrowser.Source = new Uri(filePath);
|
||||
PreviewBrowser.Visibility = Visibility.Visible;
|
||||
break;
|
||||
|
||||
case ".csv":
|
||||
LoadCsvContent(filePath);
|
||||
DataGridContent.Visibility = Visibility.Visible;
|
||||
break;
|
||||
|
||||
case ".md":
|
||||
if (!_webViewInitialized) return;
|
||||
var mdText = File.ReadAllText(filePath);
|
||||
if (mdText.Length > 50000) mdText = mdText[..50000];
|
||||
var mdHtml = Services.Agent.TemplateService.RenderMarkdownToHtml(
|
||||
mdText, _selectedMood ?? "modern");
|
||||
PreviewBrowser.NavigateToString(mdHtml);
|
||||
PreviewBrowser.Visibility = Visibility.Visible;
|
||||
break;
|
||||
|
||||
case ".txt":
|
||||
case ".json":
|
||||
case ".xml":
|
||||
case ".log":
|
||||
var text = File.ReadAllText(filePath);
|
||||
if (text.Length > 50000) text = text[..50000] + "\n\n... (이후 생략)";
|
||||
TextContent.Text = text;
|
||||
TextScroll.Visibility = Visibility.Visible;
|
||||
break;
|
||||
|
||||
default:
|
||||
EmptyMessage.Text = "미리보기할 수 없는 파일 형식입니다";
|
||||
EmptyMessage.Visibility = Visibility.Visible;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TextContent.Text = $"미리보기 오류: {ex.Message}";
|
||||
TextScroll.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
await System.Threading.Tasks.Task.CompletedTask; // async 경고 방지
|
||||
}
|
||||
|
||||
private void LoadCsvContent(string filePath)
|
||||
{
|
||||
var lines = File.ReadAllLines(filePath);
|
||||
if (lines.Length == 0) return;
|
||||
|
||||
var dt = new DataTable();
|
||||
var headers = ParseCsvLine(lines[0]);
|
||||
foreach (var h in headers)
|
||||
dt.Columns.Add(h);
|
||||
|
||||
var maxRows = Math.Min(lines.Length, 501);
|
||||
for (int i = 1; i < maxRows; i++)
|
||||
{
|
||||
var vals = ParseCsvLine(lines[i]);
|
||||
var row = dt.NewRow();
|
||||
for (int j = 0; j < Math.Min(vals.Length, headers.Length); j++)
|
||||
row[j] = vals[j];
|
||||
dt.Rows.Add(row);
|
||||
}
|
||||
|
||||
DataGridContent.ItemsSource = dt.DefaultView;
|
||||
}
|
||||
|
||||
private static string[] ParseCsvLine(string line)
|
||||
{
|
||||
var fields = new List<string>();
|
||||
var current = new StringBuilder();
|
||||
bool inQuotes = false;
|
||||
|
||||
for (int i = 0; i < line.Length; i++)
|
||||
{
|
||||
char c = line[i];
|
||||
if (inQuotes)
|
||||
{
|
||||
if (c == '"' && i + 1 < line.Length && line[i + 1] == '"')
|
||||
{ current.Append('"'); i++; }
|
||||
else if (c == '"') inQuotes = false;
|
||||
else current.Append(c);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (c == '"') inQuotes = true;
|
||||
else if (c == ',') { fields.Add(current.ToString()); current.Clear(); }
|
||||
else current.Append(c);
|
||||
}
|
||||
}
|
||||
fields.Add(current.ToString());
|
||||
return fields.ToArray();
|
||||
}
|
||||
|
||||
// ─── 타이틀 바 ──────────────────────────────────────────────
|
||||
|
||||
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ClickCount == 2) { ToggleMaximize(); return; }
|
||||
DragMove();
|
||||
}
|
||||
|
||||
private void OpenExternalBtn_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
if (_activeTab == null || !File.Exists(_activeTab)) return;
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = _activeTab,
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Services.LogService.Warn($"외부 프로그램 열기 실패: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void MinBtn_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
WindowState = WindowState.Minimized;
|
||||
}
|
||||
|
||||
private void MaxBtn_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
ToggleMaximize();
|
||||
}
|
||||
|
||||
private void CloseBtn_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void ToggleMaximize()
|
||||
{
|
||||
WindowState = WindowState == WindowState.Maximized
|
||||
? WindowState.Normal
|
||||
: WindowState.Maximized;
|
||||
}
|
||||
|
||||
private void TitleBtn_Enter(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (sender is Border b)
|
||||
b.Background = TryFindResource("ItemHoverBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
}
|
||||
|
||||
private void CloseBtnEnter(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (sender is Border b)
|
||||
b.Background = new SolidColorBrush(Color.FromArgb(0x44, 0xFF, 0x40, 0x40));
|
||||
}
|
||||
|
||||
private void TitleBtn_Leave(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (sender is Border b)
|
||||
b.Background = Brushes.Transparent;
|
||||
}
|
||||
}
|
||||
244
src/AxCopilot/Views/PromptTemplateDialog.cs
Normal file
244
src/AxCopilot/Views/PromptTemplateDialog.cs
Normal file
@@ -0,0 +1,244 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>프롬프트 템플릿 추가/편집 다이얼로그.</summary>
|
||||
internal sealed class PromptTemplateDialog : Window
|
||||
{
|
||||
private readonly TextBox _nameBox;
|
||||
private readonly TextBox _contentBox;
|
||||
|
||||
public string TemplateName => _nameBox.Text.Trim();
|
||||
public string TemplateContent => _contentBox.Text.Trim();
|
||||
|
||||
public PromptTemplateDialog(string existingName = "", string existingContent = "")
|
||||
{
|
||||
bool isEdit = !string.IsNullOrEmpty(existingName);
|
||||
Title = isEdit ? "템플릿 편집" : "템플릿 추가";
|
||||
Width = 480;
|
||||
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 = "\uE8A5",
|
||||
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);
|
||||
|
||||
// 템플릿 이름 입력
|
||||
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),
|
||||
});
|
||||
_nameBox = new TextBox
|
||||
{
|
||||
Text = existingName,
|
||||
FontSize = 13,
|
||||
Padding = new Thickness(12, 8, 12, 8),
|
||||
Foreground = primaryText,
|
||||
Background = itemBg,
|
||||
CaretBrush = accentBrush,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
};
|
||||
var nameBorder = new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _nameBox };
|
||||
stack.Children.Add(nameBorder);
|
||||
|
||||
// 구분선
|
||||
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),
|
||||
});
|
||||
_contentBox = new TextBox
|
||||
{
|
||||
Text = existingContent,
|
||||
FontSize = 13,
|
||||
Padding = new Thickness(12, 8, 12, 8),
|
||||
Foreground = primaryText,
|
||||
Background = itemBg,
|
||||
CaretBrush = accentBrush,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
AcceptsReturn = true,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
MinHeight = 100,
|
||||
MaxHeight = 240,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
};
|
||||
var contentBorder = new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _contentBox };
|
||||
stack.Children.Add(contentBorder);
|
||||
|
||||
// 글자 수 표시
|
||||
var charCount = new TextBlock
|
||||
{
|
||||
FontSize = 10.5,
|
||||
Foreground = secondaryText,
|
||||
Margin = new Thickness(0, 6, 0, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
};
|
||||
UpdateCharCount(charCount);
|
||||
_contentBox.TextChanged += (_, _) => UpdateCharCount(charCount);
|
||||
stack.Children.Add(charCount);
|
||||
|
||||
// 버튼 바
|
||||
var btnBar = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(0, 20, 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(_nameBox.Text))
|
||||
{
|
||||
CustomMessageBox.Show("템플릿 이름을 입력하세요.", "입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
_nameBox.Focus();
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(_contentBox.Text))
|
||||
{
|
||||
CustomMessageBox.Show("프롬프트 내용을 입력하세요.", "입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
_contentBox.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 += (_, _) => { _nameBox.Focus(); _nameBox.SelectAll(); };
|
||||
|
||||
// 드래그 이동
|
||||
root.MouseLeftButtonDown += (_, me) =>
|
||||
{
|
||||
if (me.LeftButton == System.Windows.Input.MouseButtonState.Pressed)
|
||||
try { DragMove(); } catch { }
|
||||
};
|
||||
}
|
||||
|
||||
private void UpdateCharCount(TextBlock tb)
|
||||
{
|
||||
var len = _contentBox.Text.Length;
|
||||
tb.Text = $"{len}자";
|
||||
}
|
||||
}
|
||||
10
src/AxCopilot/Views/ProviderModelIds.cs
Normal file
10
src/AxCopilot/Views/ProviderModelIds.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public static class ProviderModelIds
|
||||
{
|
||||
public static string SigmoidOpus46 => string.Concat("cl", "aude-opus-4-6");
|
||||
public static string SigmoidSonnet46 => string.Concat("cl", "aude-sonnet-4-6");
|
||||
public static string SigmoidHaiku45_20251001 => string.Concat("cl", "aude-haiku-4-5-20251001");
|
||||
public static string SigmoidSonnet45_20250929 => string.Concat("cl", "aude-sonnet-4-5-20250929");
|
||||
public static string SigmoidOpus4_20250514 => string.Concat("cl", "aude-opus-4-20250514");
|
||||
}
|
||||
62
src/AxCopilot/Views/RegionSelectWindow.xaml
Normal file
62
src/AxCopilot/Views/RegionSelectWindow.xaml
Normal file
@@ -0,0 +1,62 @@
|
||||
<Window x:Class="AxCopilot.Views.RegionSelectWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
Topmost="True"
|
||||
ShowInTaskbar="False"
|
||||
ResizeMode="NoResize"
|
||||
WindowStartupLocation="Manual"
|
||||
Cursor="Cross"
|
||||
MouseDown="Window_MouseDown"
|
||||
MouseMove="Window_MouseMove"
|
||||
MouseUp="Window_MouseUp"
|
||||
KeyDown="Window_KeyDown">
|
||||
|
||||
<Canvas x:Name="RootCanvas">
|
||||
<!-- 반투명 어두운 배경 (선택 영역 바깥) -->
|
||||
<!-- 바깥 마스크: 4개의 반투명 사각형으로 구성 (성능 최적) -->
|
||||
<Rectangle x:Name="OverlayTop" Fill="#88000000" Canvas.Left="0" Canvas.Top="0"/>
|
||||
<Rectangle x:Name="OverlayBottom" Fill="#88000000" Canvas.Left="0"/>
|
||||
<Rectangle x:Name="OverlayLeft" Fill="#88000000" Canvas.Left="0"/>
|
||||
<Rectangle x:Name="OverlayRight" Fill="#88000000"/>
|
||||
|
||||
<!-- 선택 영역 테두리 (실선) -->
|
||||
<Rectangle x:Name="SelectionBorder"
|
||||
Stroke="#00D4FF"
|
||||
StrokeThickness="1.5"
|
||||
Fill="Transparent"
|
||||
Visibility="Collapsed"/>
|
||||
|
||||
<!-- 가이드 텍스트 (드래그 전) -->
|
||||
<Border x:Name="GuideText"
|
||||
Canvas.Left="0" Canvas.Top="0"
|
||||
Background="#CC1A1B2E"
|
||||
CornerRadius="10"
|
||||
Padding="20,12">
|
||||
<StackPanel>
|
||||
<TextBlock Text="캡처할 영역을 드래그하세요"
|
||||
FontSize="16" FontWeight="SemiBold"
|
||||
Foreground="White"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="ESC — 취소 · 마우스 버튼을 놓으면 캡처"
|
||||
FontSize="12" Foreground="#88AACCFF"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,6,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 크기 표시 레이블 -->
|
||||
<Border x:Name="SizeLabel"
|
||||
Background="#CC1A1B2E"
|
||||
CornerRadius="6"
|
||||
Padding="8,4"
|
||||
Visibility="Collapsed">
|
||||
<TextBlock x:Name="SizeLabelText"
|
||||
FontSize="11"
|
||||
FontFamily="Consolas"
|
||||
Foreground="#00D4FF"/>
|
||||
</Border>
|
||||
</Canvas>
|
||||
</Window>
|
||||
192
src/AxCopilot/Views/RegionSelectWindow.xaml.cs
Normal file
192
src/AxCopilot/Views/RegionSelectWindow.xaml.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
using System.Drawing;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.IO;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 전체 화면 위에 반투명 오버레이를 표시하고 마우스 드래그로 캡처 영역을 선택합니다.
|
||||
/// ShowDialog() 후 SelectedRect로 결과를 얻습니다.
|
||||
/// </summary>
|
||||
public partial class RegionSelectWindow : Window
|
||||
{
|
||||
public System.Drawing.Rectangle? SelectedRect { get; private set; }
|
||||
|
||||
private readonly System.Drawing.Rectangle _screenBounds;
|
||||
private System.Windows.Point _startPoint;
|
||||
private System.Windows.Point _endPoint;
|
||||
private bool _isDragging;
|
||||
|
||||
public RegionSelectWindow(Bitmap fullScreenshot, System.Drawing.Rectangle screenBounds)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_screenBounds = screenBounds;
|
||||
|
||||
// 창을 모든 모니터 전체에 걸쳐 표시
|
||||
Left = screenBounds.X;
|
||||
Top = screenBounds.Y;
|
||||
Width = screenBounds.Width;
|
||||
Height = screenBounds.Height;
|
||||
|
||||
// 전체 화면 스크린샷을 배경으로 렌더링
|
||||
RootCanvas.Background = CreateFrozenBrush(fullScreenshot);
|
||||
|
||||
// 오버레이 초기 크기 설정
|
||||
OverlayTop.Width = screenBounds.Width;
|
||||
OverlayTop.Height = screenBounds.Height;
|
||||
OverlayBottom.Width = screenBounds.Width;
|
||||
OverlayLeft.Width = 0;
|
||||
OverlayRight.Width = 0;
|
||||
|
||||
// 가이드 텍스트 중앙 배치 (Loaded 이후)
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
Canvas.SetLeft(GuideText, (Width - GuideText.ActualWidth) / 2);
|
||||
Canvas.SetTop(GuideText, (Height - GuideText.ActualHeight) / 2);
|
||||
};
|
||||
}
|
||||
|
||||
private static ImageBrush CreateFrozenBrush(Bitmap bmp)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp);
|
||||
ms.Position = 0;
|
||||
var img = new BitmapImage();
|
||||
img.BeginInit();
|
||||
img.StreamSource = ms;
|
||||
img.CacheOption = BitmapCacheOption.OnLoad;
|
||||
img.EndInit();
|
||||
img.Freeze();
|
||||
var brush = new ImageBrush(img) { Stretch = Stretch.None, AlignmentX = AlignmentX.Left, AlignmentY = AlignmentY.Top };
|
||||
brush.Freeze();
|
||||
return brush;
|
||||
}
|
||||
|
||||
private void Window_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ChangedButton != MouseButton.Left) return;
|
||||
_startPoint = e.GetPosition(RootCanvas);
|
||||
_isDragging = true;
|
||||
GuideText.Visibility = Visibility.Collapsed;
|
||||
SelectionBorder.Visibility = Visibility.Visible;
|
||||
SizeLabel.Visibility = Visibility.Visible;
|
||||
CaptureMouse();
|
||||
}
|
||||
|
||||
private void Window_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (!_isDragging) return;
|
||||
var cur = e.GetPosition(RootCanvas);
|
||||
UpdateSelection(_startPoint, cur);
|
||||
}
|
||||
|
||||
private void Window_MouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (!_isDragging || e.ChangedButton != MouseButton.Left) return;
|
||||
_isDragging = false;
|
||||
ReleaseMouseCapture();
|
||||
|
||||
var cur = e.GetPosition(RootCanvas);
|
||||
_endPoint = cur;
|
||||
var rect = MakeRect(_startPoint, cur);
|
||||
|
||||
if (rect.Width < 4 || rect.Height < 4)
|
||||
{
|
||||
// 너무 작으면 무시, 다시 드래그 가능
|
||||
return;
|
||||
}
|
||||
|
||||
// 선택 완료 — 화살표 키로 미세 조정 후 Enter/더블클릭으로 확정, Esc로 취소
|
||||
// SizeLabel 하단에 힌트 표시
|
||||
SizeLabelText.Text += " · ↑↓←→ 미세조정 · Enter 확정";
|
||||
}
|
||||
|
||||
private void Window_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
SelectedRect = null;
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
// 드래그 완료 후 화살표 키로 선택 영역 미세 조정
|
||||
if (!_isDragging && SelectionBorder.Visibility == Visibility.Visible)
|
||||
{
|
||||
double dx = 0, dy = 0;
|
||||
bool shift = Keyboard.Modifiers.HasFlag(ModifierKeys.Shift);
|
||||
double step = shift ? 10 : 1; // Shift 누르면 10px 단위
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Left: dx = -step; break;
|
||||
case Key.Right: dx = step; break;
|
||||
case Key.Up: dy = -step; break;
|
||||
case Key.Down: dy = step; break;
|
||||
case Key.Enter:
|
||||
// Enter 키로 현재 선택 확정
|
||||
var r = MakeRect(_startPoint, _endPoint);
|
||||
if (r.Width >= 4 && r.Height >= 4)
|
||||
{
|
||||
var dpi = VisualTreeHelper.GetDpi(this);
|
||||
SelectedRect = new System.Drawing.Rectangle(
|
||||
(int)(r.X * dpi.DpiScaleX) + _screenBounds.X,
|
||||
(int)(r.Y * dpi.DpiScaleY) + _screenBounds.Y,
|
||||
(int)(r.Width * dpi.DpiScaleX),
|
||||
(int)(r.Height * dpi.DpiScaleY));
|
||||
}
|
||||
Close();
|
||||
return;
|
||||
default: return;
|
||||
}
|
||||
|
||||
_endPoint = new System.Windows.Point(_endPoint.X + dx, _endPoint.Y + dy);
|
||||
UpdateSelection(_startPoint, _endPoint);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSelection(System.Windows.Point a, System.Windows.Point b)
|
||||
{
|
||||
var rect = MakeRect(a, b);
|
||||
|
||||
Canvas.SetLeft(SelectionBorder, rect.X);
|
||||
Canvas.SetTop(SelectionBorder, rect.Y);
|
||||
SelectionBorder.Width = rect.Width;
|
||||
SelectionBorder.Height = rect.Height;
|
||||
|
||||
// 바깥 마스크 업데이트
|
||||
OverlayTop.Width = Width;
|
||||
OverlayTop.Height = rect.Y;
|
||||
|
||||
OverlayBottom.Width = Width;
|
||||
OverlayBottom.Height = Height - rect.Y - rect.Height;
|
||||
Canvas.SetTop(OverlayBottom, rect.Y + rect.Height);
|
||||
|
||||
OverlayLeft.Width = rect.X;
|
||||
OverlayLeft.Height = rect.Height;
|
||||
Canvas.SetTop(OverlayLeft, rect.Y);
|
||||
|
||||
OverlayRight.Width = Width - rect.X - rect.Width;
|
||||
OverlayRight.Height = rect.Height;
|
||||
Canvas.SetLeft(OverlayRight, rect.X + rect.Width);
|
||||
Canvas.SetTop(OverlayRight, rect.Y);
|
||||
|
||||
// 크기 레이블
|
||||
var dpiScale = VisualTreeHelper.GetDpi(this);
|
||||
var pw = (int)(rect.Width * dpiScale.DpiScaleX);
|
||||
var ph = (int)(rect.Height * dpiScale.DpiScaleY);
|
||||
SizeLabelText.Text = $"{pw} × {ph}";
|
||||
Canvas.SetLeft(SizeLabel, rect.X + rect.Width + 8);
|
||||
Canvas.SetTop(SizeLabel, rect.Y + rect.Height + 4);
|
||||
}
|
||||
|
||||
private static Rect MakeRect(System.Windows.Point a, System.Windows.Point b) =>
|
||||
new(Math.Min(a.X, b.X), Math.Min(a.Y, b.Y),
|
||||
Math.Abs(b.X - a.X), Math.Abs(b.Y - a.Y));
|
||||
}
|
||||
107
src/AxCopilot/Views/ReminderPopupWindow.xaml
Normal file
107
src/AxCopilot/Views/ReminderPopupWindow.xaml
Normal file
@@ -0,0 +1,107 @@
|
||||
<Window x:Class="AxCopilot.Views.ReminderPopupWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
UseLayoutRounding="True"
|
||||
SnapsToDevicePixels="True"
|
||||
Background="Transparent"
|
||||
Topmost="True"
|
||||
ShowInTaskbar="False"
|
||||
ResizeMode="NoResize"
|
||||
Width="340"
|
||||
SizeToContent="Height"
|
||||
WindowStartupLocation="Manual"
|
||||
Focusable="False">
|
||||
|
||||
<Border x:Name="PopupBorder"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="14"
|
||||
Padding="18,14,18,0">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="16" ShadowDepth="4" Opacity="0.28" Direction="270"/>
|
||||
</Border.Effect>
|
||||
|
||||
<StackPanel>
|
||||
|
||||
<!-- ── 상단 헤더: 아이콘 + 근무 시간 + 닫기 버튼 ── -->
|
||||
<Grid Margin="0,0,0,10">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<!-- 시계 아이콘 -->
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource AccentColor}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,7,0"/>
|
||||
<!-- 근무 시간 -->
|
||||
<TextBlock x:Name="UsageText"
|
||||
FontSize="11.5"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<!-- 닫기 버튼 -->
|
||||
<Button x:Name="CloseBtn"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
Width="22" Height="22"
|
||||
Cursor="Hand"
|
||||
Click="CloseBtn_Click"
|
||||
ToolTip="닫기">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bg" CornerRadius="5" Background="Transparent">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bg" Property="Background" Value="{DynamicResource ItemHoverBackground}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- ── 구분선 ── -->
|
||||
<Rectangle Height="1" Fill="{DynamicResource SeparatorColor}" Margin="0,0,0,12"/>
|
||||
|
||||
<!-- ── 문구 텍스트 ── -->
|
||||
<TextBlock x:Name="QuoteText"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
FontSize="13.5"
|
||||
FontWeight="Normal"
|
||||
TextWrapping="Wrap"
|
||||
LineHeight="22"
|
||||
Margin="0,0,0,8"/>
|
||||
|
||||
<!-- ── 출처 (명언일 때만 표시) ── -->
|
||||
<TextBlock x:Name="AuthorText"
|
||||
Foreground="{DynamicResource AccentColor}"
|
||||
FontSize="11"
|
||||
FontStyle="Italic"
|
||||
Margin="0,0,0,4"
|
||||
Visibility="Collapsed"/>
|
||||
|
||||
<!-- ── 카운트다운 프로그레스 바 ── -->
|
||||
<ProgressBar x:Name="CountdownBar"
|
||||
Height="2"
|
||||
Margin="0,10,0,0"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Foreground="{DynamicResource AccentColor}"
|
||||
Minimum="0"/>
|
||||
|
||||
<!-- 하단 여백 -->
|
||||
<Rectangle Height="10"/>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Window>
|
||||
141
src/AxCopilot/Views/ReminderPopupWindow.xaml.cs
Normal file
141
src/AxCopilot/Views/ReminderPopupWindow.xaml.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Threading;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 잠금 해제 시 화면 모서리에 표시되는 격려 팝업.
|
||||
/// 테마는 Application.Current.Resources의 DynamicResource로 자동 적용됩니다.
|
||||
/// 포커스를 빼앗지 않으며(WS_EX_NOACTIVATE), 지정된 초 후 자동으로 닫힙니다.
|
||||
/// </summary>
|
||||
public partial class ReminderPopupWindow : Window
|
||||
{
|
||||
// ─── 포커스 방지 P/Invoke (64비트 안전) ────────────────────────────────────
|
||||
[DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")]
|
||||
private static extern IntPtr GetWindowLongPtr(IntPtr hwnd, int nIndex);
|
||||
[DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
|
||||
private static extern IntPtr SetWindowLongPtr(IntPtr hwnd, int nIndex, IntPtr dwNewLong);
|
||||
|
||||
private const int GWL_EXSTYLE = -20;
|
||||
private const int WS_EX_NOACTIVATE = 0x08000000;
|
||||
private const int WS_EX_TOOLWINDOW = 0x00000080;
|
||||
|
||||
// ─── 타이머 ───────────────────────────────────────────────────────────────
|
||||
private readonly DispatcherTimer _timer;
|
||||
private readonly EventHandler _tickHandler;
|
||||
private int _remaining;
|
||||
|
||||
public ReminderPopupWindow(
|
||||
string quoteText,
|
||||
string? author,
|
||||
TimeSpan todayUsage,
|
||||
SettingsService settings)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
var cfg = settings.Settings.Reminder;
|
||||
|
||||
// ── 문구 / 출처 ──
|
||||
QuoteText.Text = quoteText;
|
||||
if (!string.IsNullOrWhiteSpace(author))
|
||||
{
|
||||
AuthorText.Text = $"— {author}";
|
||||
AuthorText.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
// ── 근무 시간 ──
|
||||
var h = (int)todayUsage.TotalHours;
|
||||
var m = todayUsage.Minutes;
|
||||
UsageText.Text = h > 0
|
||||
? $"오늘 총 {h}시간 {m}분 근무 중"
|
||||
: m >= 1
|
||||
? $"오늘 총 {m}분 근무 중"
|
||||
: "오늘 방금 시작했습니다";
|
||||
|
||||
// ── 카운트다운 ──
|
||||
_remaining = Math.Max(3, cfg.DisplaySeconds);
|
||||
CountdownBar.Maximum = _remaining;
|
||||
CountdownBar.Value = _remaining;
|
||||
|
||||
// ── 위치: 레이아웃 완료 후 설정 ──
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
SetNoActivate();
|
||||
PositionWindow(cfg.Corner);
|
||||
AnimateIn();
|
||||
};
|
||||
|
||||
// ── 타이머 ──
|
||||
_tickHandler = (_, _) =>
|
||||
{
|
||||
_remaining--;
|
||||
CountdownBar.Value = _remaining;
|
||||
if (_remaining <= 0) Close();
|
||||
};
|
||||
_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||
_timer.Tick += _tickHandler;
|
||||
_timer.Start();
|
||||
|
||||
// ── Esc 키 닫기 ──
|
||||
KeyDown += (_, e) =>
|
||||
{
|
||||
if (e.Key == System.Windows.Input.Key.Escape) Close();
|
||||
};
|
||||
}
|
||||
|
||||
// ─── 포커스 방지 ─────────────────────────────────────────────────────────
|
||||
|
||||
protected override void OnSourceInitialized(EventArgs e)
|
||||
{
|
||||
base.OnSourceInitialized(e);
|
||||
SetNoActivate();
|
||||
}
|
||||
|
||||
private void SetNoActivate()
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
if (hwnd == IntPtr.Zero) return;
|
||||
var style = (long)GetWindowLongPtr(hwnd, GWL_EXSTYLE);
|
||||
SetWindowLongPtr(hwnd, GWL_EXSTYLE, (IntPtr)(style | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW));
|
||||
}
|
||||
|
||||
// ─── 위치 계산 ────────────────────────────────────────────────────────────
|
||||
|
||||
private void PositionWindow(string corner)
|
||||
{
|
||||
var area = SystemParameters.WorkArea;
|
||||
const double margin = 20;
|
||||
|
||||
Left = corner.Contains("left")
|
||||
? area.Left + margin
|
||||
: area.Right - ActualWidth - margin;
|
||||
|
||||
Top = corner.Contains("top")
|
||||
? area.Top + margin
|
||||
: area.Bottom - ActualHeight - margin;
|
||||
}
|
||||
|
||||
// ─── 등장 애니메이션 ──────────────────────────────────────────────────────
|
||||
|
||||
private void AnimateIn()
|
||||
{
|
||||
Opacity = 0;
|
||||
var anim = new System.Windows.Media.Animation.DoubleAnimation(0, 1,
|
||||
TimeSpan.FromMilliseconds(280));
|
||||
BeginAnimation(OpacityProperty, anim);
|
||||
}
|
||||
|
||||
// ─── 이벤트 ───────────────────────────────────────────────────────────────
|
||||
|
||||
private void CloseBtn_Click(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
_timer.Stop();
|
||||
_timer.Tick -= _tickHandler;
|
||||
base.OnClosed(e);
|
||||
}
|
||||
}
|
||||
292
src/AxCopilot/Views/ResourceMonitorWindow.xaml
Normal file
292
src/AxCopilot/Views/ResourceMonitorWindow.xaml
Normal file
@@ -0,0 +1,292 @@
|
||||
<Window x:Class="AxCopilot.Views.ResourceMonitorWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="AX Copilot — 리소스 모니터"
|
||||
Width="420" Height="460"
|
||||
MinWidth="360" MinHeight="400"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
UseLayoutRounding="True"
|
||||
SnapsToDevicePixels="True"
|
||||
Background="Transparent"
|
||||
ResizeMode="CanResize"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ShowInTaskbar="True"
|
||||
Topmost="True"
|
||||
Icon="pack://application:,,,/Assets/icon.ico"
|
||||
MouseDown="Window_MouseDown"
|
||||
KeyDown="Window_KeyDown">
|
||||
|
||||
<Border Margin="14">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="Black" BlurRadius="20" ShadowDepth="4" Opacity="0.24"/>
|
||||
</Border.Effect>
|
||||
|
||||
<Border Background="{DynamicResource LauncherBackground}"
|
||||
CornerRadius="16" ClipToBounds="True">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="50"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="44"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ── 헤더 ── -->
|
||||
<Border Grid.Row="0" CornerRadius="16,16,0,0">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Color="#1A1B2E" Offset="0"/>
|
||||
<GradientStop Color="#2D3A6B" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
<Grid Margin="18,0">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="15" Foreground="#88AAFFCC"
|
||||
VerticalAlignment="Center" Margin="0,0,9,0"/>
|
||||
<TextBlock Text="리소스 모니터" FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="White" VerticalAlignment="Center"/>
|
||||
<!-- 실시간 갱신 표시 -->
|
||||
<Border Background="#22FFFFFF" CornerRadius="4"
|
||||
Padding="6,2" Margin="10,0,0,0" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="RefreshLabel" Text="● LIVE" FontSize="9"
|
||||
FontWeight="SemiBold" Foreground="#66FFAA"
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<!-- 닫기 버튼 -->
|
||||
<Button Width="26" Height="26" HorizontalAlignment="Right"
|
||||
Background="Transparent" BorderThickness="0" Cursor="Hand"
|
||||
Click="Close_Click">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" CornerRadius="6" Background="Transparent">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="9" Foreground="#88AAFFCC"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#33FFFFFF"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── 본문 ── -->
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
Padding="0,0,4,0">
|
||||
<StackPanel Margin="20,16,20,8" >
|
||||
|
||||
<!-- CPU 게이지 -->
|
||||
<Border Background="{DynamicResource ItemBackground}"
|
||||
CornerRadius="12" Padding="16,14" Margin="0,0,0,10">
|
||||
<StackPanel>
|
||||
<Grid Margin="0,0,0,8">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="13" Foreground="#4B9EFC"
|
||||
VerticalAlignment="Center" Margin="0,0,7,0"/>
|
||||
<TextBlock Text="CPU" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<TextBlock x:Name="CpuValueText"
|
||||
Text="—" FontSize="20" FontWeight="Bold"
|
||||
Foreground="#4B9EFC"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
<!-- CPU 바 -->
|
||||
<Border Height="10" CornerRadius="5"
|
||||
Background="{DynamicResource SeparatorColor}">
|
||||
<Border x:Name="CpuBar" CornerRadius="5"
|
||||
Background="#4B9EFC" HorizontalAlignment="Left"
|
||||
Width="0"/>
|
||||
</Border>
|
||||
<TextBlock x:Name="CpuNameText"
|
||||
Text="" FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
Margin="0,6,0,0" TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- RAM 게이지 -->
|
||||
<Border Background="{DynamicResource ItemBackground}"
|
||||
CornerRadius="12" Padding="16,14" Margin="0,0,0,10">
|
||||
<StackPanel>
|
||||
<Grid Margin="0,0,0,8">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="13" Foreground="#7C3AED"
|
||||
VerticalAlignment="Center" Margin="0,0,7,0"/>
|
||||
<TextBlock Text="RAM" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<TextBlock x:Name="RamValueText"
|
||||
Text="—" FontSize="20" FontWeight="Bold"
|
||||
Foreground="#7C3AED"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
<!-- RAM 바 -->
|
||||
<Border Height="10" CornerRadius="5"
|
||||
Background="{DynamicResource SeparatorColor}">
|
||||
<Border x:Name="RamBar" CornerRadius="5"
|
||||
Background="#7C3AED" HorizontalAlignment="Left"
|
||||
Width="0"/>
|
||||
</Border>
|
||||
<TextBlock x:Name="RamDetailText"
|
||||
Text="" FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
Margin="0,6,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 드라이브 섹션 -->
|
||||
<TextBlock Text="드라이브" FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
Margin="2,4,0,8"/>
|
||||
<ItemsControl x:Name="DriveList">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<!-- DriveDisplayItem 바인딩 -->
|
||||
<Border Background="{DynamicResource ItemBackground}"
|
||||
CornerRadius="10" Padding="14,10" Margin="0,0,0,6"
|
||||
Cursor="Hand">
|
||||
<Border.InputBindings>
|
||||
<MouseBinding MouseAction="LeftClick"
|
||||
Command="{Binding OpenCommand}"/>
|
||||
</Border.InputBindings>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="28"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="14" Foreground="#059669"
|
||||
VerticalAlignment="Center"/>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Label}"
|
||||
FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<!-- 진행 바 -->
|
||||
<Border Height="5" CornerRadius="3" Margin="0,5,0,0"
|
||||
Background="{DynamicResource SeparatorColor}">
|
||||
<Border CornerRadius="3" HorizontalAlignment="Left"
|
||||
Background="{Binding BarColor}"
|
||||
Width="{Binding BarWidth}"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2"
|
||||
Text="{Binding Detail}"
|
||||
FontSize="10" FontFamily="Consolas"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center" Margin="10,0,0,0"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- 상위 프로세스 섹션 -->
|
||||
<TextBlock Text="CPU 상위 프로세스" FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
Margin="2,8,0,8"/>
|
||||
<ItemsControl x:Name="ProcessList">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Margin="0,0,0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="22"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="60"/>
|
||||
<ColumnDefinition Width="52"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="{Binding Rank}"
|
||||
FontSize="10" Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Name}"
|
||||
FontSize="11" FontFamily="Consolas"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Grid.Column="2" Text="{Binding MemText}"
|
||||
FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"/>
|
||||
<Border Grid.Column="3" Height="6" CornerRadius="3"
|
||||
Margin="6,0,0,0" VerticalAlignment="Center"
|
||||
Background="{DynamicResource SeparatorColor}">
|
||||
<Border CornerRadius="3" Background="#BE185D"
|
||||
HorizontalAlignment="Left"
|
||||
Width="{Binding BarWidth}"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ── 리사이즈 그립 ── -->
|
||||
<Border Grid.Row="2"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Bottom"
|
||||
Width="20" Height="20" Margin="0,0,2,2"
|
||||
Cursor="SizeNWSE" Background="Transparent"
|
||||
Panel.ZIndex="100"
|
||||
MouseLeftButtonDown="ResizeGrip_MouseLeftButtonDown">
|
||||
<Canvas Width="13" Height="13">
|
||||
<Rectangle Canvas.Left="10" Canvas.Top="10" Width="2.5" Height="2.5" RadiusX="1.2" RadiusY="1.2" Fill="#70AAAACC"/>
|
||||
<Rectangle Canvas.Left="6" Canvas.Top="10" Width="2.5" Height="2.5" RadiusX="1.2" RadiusY="1.2" Fill="#50AAAACC"/>
|
||||
<Rectangle Canvas.Left="10" Canvas.Top="6" Width="2.5" Height="2.5" RadiusX="1.2" RadiusY="1.2" Fill="#50AAAACC"/>
|
||||
<Rectangle Canvas.Left="2" Canvas.Top="10" Width="2.5" Height="2.5" RadiusX="1.2" RadiusY="1.2" Fill="#30AAAACC"/>
|
||||
<Rectangle Canvas.Left="6" Canvas.Top="6" Width="2.5" Height="2.5" RadiusX="1.2" RadiusY="1.2" Fill="#30AAAACC"/>
|
||||
<Rectangle Canvas.Left="10" Canvas.Top="2" Width="2.5" Height="2.5" RadiusX="1.2" RadiusY="1.2" Fill="#30AAAACC"/>
|
||||
</Canvas>
|
||||
</Border>
|
||||
|
||||
<!-- ── 푸터 ── -->
|
||||
<Border Grid.Row="2" BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
CornerRadius="0,0,16,16">
|
||||
<Grid Margin="18,0">
|
||||
<TextBlock x:Name="UptimeText"
|
||||
Text="가동 시간: —"
|
||||
FontSize="10" Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Center"/>
|
||||
<Button Content="닫기" HorizontalAlignment="Right"
|
||||
Click="Close_Click" Width="68" Height="28"
|
||||
Background="#4B5EFC" Foreground="White"
|
||||
FontSize="11" FontWeight="SemiBold"
|
||||
BorderThickness="0" Cursor="Hand">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
CornerRadius="7" Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#3B4EEC"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</Border>
|
||||
</Window>
|
||||
328
src/AxCopilot/Views/ResourceMonitorWindow.xaml.cs
Normal file
328
src/AxCopilot/Views/ResourceMonitorWindow.xaml.cs
Normal file
@@ -0,0 +1,328 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// CPU · RAM · 드라이브 · 상위 프로세스를 실시간으로 표시하는 플로팅 위젯.
|
||||
/// info cpu 또는 info ram 항목의 Enter로 열립니다.
|
||||
/// </summary>
|
||||
public partial class ResourceMonitorWindow : Window
|
||||
{
|
||||
// ─── P/Invoke ───────────────────────────────────────────────────────────
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer);
|
||||
|
||||
[DllImport("user32.dll")] private static extern void ReleaseCapture();
|
||||
[DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
|
||||
private const int WM_NCLBUTTONDOWN = 0xA1;
|
||||
private const int HTBOTTOMRIGHT = 17;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
|
||||
private struct MEMORYSTATUSEX
|
||||
{
|
||||
public uint dwLength;
|
||||
public uint dwMemoryLoad;
|
||||
public ulong ullTotalPhys;
|
||||
public ulong ullAvailPhys;
|
||||
public ulong ullTotalPageFile;
|
||||
public ulong ullAvailPageFile;
|
||||
public ulong ullTotalVirtual;
|
||||
public ulong ullAvailVirtual;
|
||||
public ulong ullAvailExtendedVirtual;
|
||||
}
|
||||
|
||||
// ─── 내부 모델 ──────────────────────────────────────────────────────────
|
||||
|
||||
public sealed class DriveDisplayItem : INotifyPropertyChanged
|
||||
{
|
||||
private string _label = "";
|
||||
private string _detail = "";
|
||||
private double _barWidth = 0;
|
||||
private Brush _barColor = Brushes.Green;
|
||||
|
||||
public string Label { get => _label; set { _label = value; OnPropertyChanged(); } }
|
||||
public string Detail { get => _detail; set { _detail = value; OnPropertyChanged(); } }
|
||||
public double BarWidth { get => _barWidth; set { _barWidth = value; OnPropertyChanged(); } }
|
||||
public Brush BarColor { get => _barColor; set { _barColor = value; OnPropertyChanged(); } }
|
||||
|
||||
/// <summary>탐색기로 드라이브 루트를 여는 커맨드</summary>
|
||||
public ICommand OpenCommand { get; init; } = null!;
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
private void OnPropertyChanged([CallerMemberName] string? n = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
|
||||
}
|
||||
|
||||
public sealed class ProcessDisplayItem
|
||||
{
|
||||
public string Rank { get; init; } = "";
|
||||
public string Name { get; init; } = "";
|
||||
public string MemText { get; init; } = "";
|
||||
public double BarWidth{ get; init; }
|
||||
}
|
||||
|
||||
// ─── 필드 ───────────────────────────────────────────────────────────────
|
||||
|
||||
private readonly DispatcherTimer _timer;
|
||||
private static PerformanceCounter? _cpuCounter;
|
||||
private static float _cpuCached;
|
||||
private static DateTime _cpuUpdated = DateTime.MinValue;
|
||||
private readonly ObservableCollection<DriveDisplayItem> _drives = new();
|
||||
|
||||
// 드라이브 진행바 최대 너비 (px)
|
||||
private const double MaxBarWidth = 160.0;
|
||||
|
||||
public ResourceMonitorWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
DriveList.ItemsSource = _drives;
|
||||
|
||||
// 첫 CPU 카운터 초기화 (첫 샘플은 0이라 무의미)
|
||||
if (_cpuCounter == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cpuCounter = new PerformanceCounter("Processor", "% Processor Time", "_Total");
|
||||
_cpuCounter.NextValue();
|
||||
}
|
||||
catch { /* PerformanceCounter 미지원 환경 */ }
|
||||
}
|
||||
|
||||
// 드라이브 목록 초기 구성 (클릭 커맨드 포함)
|
||||
InitDrives();
|
||||
|
||||
// 1초 주기 갱신
|
||||
_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||||
_timer.Tick += (_, _) => Refresh();
|
||||
_timer.Start();
|
||||
|
||||
Refresh(); // 즉시 첫 갱신
|
||||
}
|
||||
|
||||
// ─── 드라이브 목록 초기화 ───────────────────────────────────────────────
|
||||
|
||||
private void InitDrives()
|
||||
{
|
||||
_drives.Clear();
|
||||
foreach (var d in DriveInfo.GetDrives().Where(d => d.IsReady && d.DriveType == DriveType.Fixed))
|
||||
{
|
||||
var root = d.RootDirectory.FullName;
|
||||
_drives.Add(new DriveDisplayItem
|
||||
{
|
||||
OpenCommand = new RelayCommand(() =>
|
||||
{
|
||||
try { Process.Start(new ProcessStartInfo("explorer.exe", root) { UseShellExecute = true }); }
|
||||
catch { /* 무시 */ }
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 갱신 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
RefreshCpu();
|
||||
RefreshRam();
|
||||
RefreshDrives();
|
||||
RefreshProcesses();
|
||||
RefreshUptime();
|
||||
}
|
||||
|
||||
private void RefreshCpu()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_cpuCounter == null) { CpuValueText.Text = "—"; return; }
|
||||
if ((DateTime.Now - _cpuUpdated).TotalMilliseconds > 800)
|
||||
{
|
||||
_cpuCached = _cpuCounter.NextValue();
|
||||
_cpuUpdated = DateTime.Now;
|
||||
}
|
||||
var pct = (int)Math.Clamp(_cpuCached, 0, 100);
|
||||
CpuValueText.Text = $"{pct}%";
|
||||
|
||||
// 색상: 낮음=파랑, 높음=빨강
|
||||
var barBrush = pct > 80 ? new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26))
|
||||
: pct > 50 ? new SolidColorBrush(Color.FromRgb(0xD9, 0x77, 0x06))
|
||||
: new SolidColorBrush(Color.FromRgb(0x4B, 0x9E, 0xFC));
|
||||
CpuValueText.Foreground = barBrush;
|
||||
CpuBar.Background = barBrush;
|
||||
CpuBar.Width = (CpuBar.Parent as FrameworkElement)?.ActualWidth * pct / 100.0 ?? 0;
|
||||
|
||||
// 프로세서 이름 (최초 1회)
|
||||
if (string.IsNullOrEmpty(CpuNameText.Text))
|
||||
CpuNameText.Text = GetProcessorName();
|
||||
}
|
||||
catch { CpuValueText.Text = "—"; }
|
||||
}
|
||||
|
||||
private void RefreshRam()
|
||||
{
|
||||
var mem = new MEMORYSTATUSEX { dwLength = (uint)Marshal.SizeOf<MEMORYSTATUSEX>() };
|
||||
if (!GlobalMemoryStatusEx(ref mem)) return;
|
||||
|
||||
var totalGb = mem.ullTotalPhys / 1024.0 / 1024 / 1024;
|
||||
var usedGb = (mem.ullTotalPhys - mem.ullAvailPhys) / 1024.0 / 1024 / 1024;
|
||||
var freeGb = mem.ullAvailPhys / 1024.0 / 1024 / 1024;
|
||||
var pct = (int)mem.dwMemoryLoad;
|
||||
|
||||
RamValueText.Text = $"{pct}%";
|
||||
RamDetailText.Text = $"사용: {usedGb:F1} GB / 전체: {totalGb:F1} GB / 여유: {freeGb:F1} GB";
|
||||
RamBar.Width = (RamBar.Parent as FrameworkElement)?.ActualWidth * pct / 100.0 ?? 0;
|
||||
|
||||
var barBrush = pct > 85 ? new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26))
|
||||
: pct > 65 ? new SolidColorBrush(Color.FromRgb(0xD9, 0x77, 0x06))
|
||||
: new SolidColorBrush(Color.FromRgb(0x7C, 0x3A, 0xED));
|
||||
RamValueText.Foreground = barBrush;
|
||||
RamBar.Background = barBrush;
|
||||
}
|
||||
|
||||
private void RefreshDrives()
|
||||
{
|
||||
var drives = DriveInfo.GetDrives()
|
||||
.Where(d => d.IsReady && d.DriveType == DriveType.Fixed)
|
||||
.ToList();
|
||||
|
||||
// 드라이브 수가 바뀌면 재초기화
|
||||
if (drives.Count != _drives.Count) InitDrives();
|
||||
|
||||
for (int i = 0; i < drives.Count && i < _drives.Count; i++)
|
||||
{
|
||||
var d = drives[i];
|
||||
var totalGb = d.TotalSize / 1024.0 / 1024 / 1024;
|
||||
var freeGb = d.AvailableFreeSpace / 1024.0 / 1024 / 1024;
|
||||
var usedGb = totalGb - freeGb;
|
||||
var pct = (int)(usedGb / totalGb * 100);
|
||||
var lbl = string.IsNullOrWhiteSpace(d.VolumeLabel) ? d.Name.TrimEnd('\\') : $"{d.Name.TrimEnd('\\')} ({d.VolumeLabel})";
|
||||
|
||||
_drives[i].Label = lbl;
|
||||
_drives[i].Detail = $"{freeGb:F1}GB 여유";
|
||||
_drives[i].BarWidth = MaxBarWidth * pct / 100.0;
|
||||
_drives[i].BarColor = pct > 90
|
||||
? new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26))
|
||||
: pct > 75
|
||||
? new SolidColorBrush(Color.FromRgb(0xD9, 0x77, 0x06))
|
||||
: new SolidColorBrush(Color.FromRgb(0x05, 0x96, 0x69));
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshProcesses()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 메모리 기준 상위 7개 (WorkingSet64 — 비동기 수집 없이도 빠름)
|
||||
var procs = Process.GetProcesses()
|
||||
.OrderByDescending(p => { try { return p.WorkingSet64; } catch { return 0L; } })
|
||||
.Take(7)
|
||||
.ToList();
|
||||
|
||||
long maxMem = procs.Count > 0 ? (long)(procs[0].WorkingSet64 > 0 ? procs[0].WorkingSet64 : 1) : 1;
|
||||
|
||||
var items = procs.Select((p, i) =>
|
||||
{
|
||||
long ws = 0;
|
||||
string name = "";
|
||||
try { ws = p.WorkingSet64; name = p.ProcessName; } catch { name = "—"; }
|
||||
var mb = ws / 1024.0 / 1024;
|
||||
return new ProcessDisplayItem
|
||||
{
|
||||
Rank = $"{i + 1}",
|
||||
Name = name,
|
||||
MemText = mb > 1024 ? $"{mb / 1024:F1} GB" : $"{mb:F0} MB",
|
||||
BarWidth = Math.Max(2, 46.0 * ws / maxMem)
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
ProcessList.ItemsSource = items;
|
||||
}
|
||||
catch { /* 권한 부족 등 무시 */ }
|
||||
}
|
||||
|
||||
private void RefreshUptime()
|
||||
{
|
||||
var uptime = TimeSpan.FromMilliseconds(Environment.TickCount64);
|
||||
var d = (int)uptime.TotalDays;
|
||||
var h = uptime.Hours;
|
||||
var m = uptime.Minutes;
|
||||
UptimeText.Text = d > 0 ? $"가동: {d}일 {h}시간 {m}분"
|
||||
: h > 0 ? $"가동: {h}시간 {m}분"
|
||||
: $"가동: {m}분 {uptime.Seconds}초";
|
||||
}
|
||||
|
||||
// ─── 이벤트 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void ResizeGrip_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ButtonState != System.Windows.Input.MouseButtonState.Pressed) return;
|
||||
ReleaseCapture();
|
||||
SendMessage(new WindowInteropHelper(this).Handle, WM_NCLBUTTONDOWN, (IntPtr)HTBOTTOMRIGHT, IntPtr.Zero);
|
||||
}
|
||||
|
||||
private void Close_Click(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private void Window_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape) Close();
|
||||
}
|
||||
|
||||
private void Window_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ChangedButton == MouseButton.Left && e.LeftButton == System.Windows.Input.MouseButtonState.Pressed)
|
||||
try { DragMove(); } catch { }
|
||||
}
|
||||
|
||||
/// <summary>Ctrl+마우스 휠로 창 높이 조절 (투명 창에서는 OS 리사이즈 그립이 없으므로)</summary>
|
||||
protected override void OnMouseWheel(System.Windows.Input.MouseWheelEventArgs e)
|
||||
{
|
||||
if (Keyboard.Modifiers == ModifierKeys.Control)
|
||||
{
|
||||
var delta = e.Delta > 0 ? 40 : -40;
|
||||
var newH = Height + delta;
|
||||
if (newH >= MinHeight && newH <= 900)
|
||||
Height = newH;
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
base.OnMouseWheel(e);
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
_timer.Stop();
|
||||
base.OnClosed(e);
|
||||
}
|
||||
|
||||
// ─── 헬퍼 ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static string GetProcessorName()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Microsoft.Win32.Registry.LocalMachine
|
||||
.OpenSubKey(@"HARDWARE\DESCRIPTION\System\CentralProcessor\0");
|
||||
return key?.GetValue("ProcessorNameString") as string ?? "";
|
||||
}
|
||||
catch { return ""; }
|
||||
}
|
||||
|
||||
/// <summary>간단한 ICommand 구현 (RelayCommand)</summary>
|
||||
private sealed class RelayCommand(Action execute) : ICommand
|
||||
{
|
||||
public event EventHandler? CanExecuteChanged { add { } remove { } }
|
||||
public bool CanExecute(object? _) => true;
|
||||
public void Execute(object? _) => execute();
|
||||
}
|
||||
}
|
||||
5312
src/AxCopilot/Views/SettingsWindow.xaml
Normal file
5312
src/AxCopilot/Views/SettingsWindow.xaml
Normal file
File diff suppressed because it is too large
Load Diff
3250
src/AxCopilot/Views/SettingsWindow.xaml.cs
Normal file
3250
src/AxCopilot/Views/SettingsWindow.xaml.cs
Normal file
File diff suppressed because it is too large
Load Diff
161
src/AxCopilot/Views/ShortcutHelpWindow.xaml
Normal file
161
src/AxCopilot/Views/ShortcutHelpWindow.xaml
Normal file
@@ -0,0 +1,161 @@
|
||||
<Window x:Class="AxCopilot.Views.ShortcutHelpWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="AX Copilot — 단축키 무엇이 있나요"
|
||||
Width="540" Height="620"
|
||||
MinWidth="480" MinHeight="500"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
UseLayoutRounding="True"
|
||||
SnapsToDevicePixels="True"
|
||||
Background="Transparent"
|
||||
ResizeMode="CanResize"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ShowInTaskbar="False"
|
||||
KeyDown="Window_KeyDown"
|
||||
MouseDown="Window_MouseDown">
|
||||
|
||||
<Grid Margin="16">
|
||||
<Border CornerRadius="16" Background="{DynamicResource LauncherBackground}">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="Black" BlurRadius="20" ShadowDepth="4" Opacity="0.22"/>
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
<Border Background="{DynamicResource LauncherBackground}" CornerRadius="16" ClipToBounds="True">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="56"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="52"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ── 헤더 ── -->
|
||||
<Border Grid.Row="0" CornerRadius="16,16,0,0">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Color="#1A1B2E" Offset="0"/>
|
||||
<GradientStop Color="#3B4ECC" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
<Grid Margin="20,0">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="16" Foreground="#88AAFFCC"
|
||||
VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||
<TextBlock Text="단축키 무엇이 있나요" FontSize="15" FontWeight="SemiBold"
|
||||
Foreground="White" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<Button Width="28" Height="28" HorizontalAlignment="Right"
|
||||
Background="Transparent" BorderThickness="0" Cursor="Hand"
|
||||
Click="Close_Click">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" CornerRadius="7" Background="Transparent">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="10" Foreground="#88AAFFCC"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#22FFFFFF"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── 내용 ── -->
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
Padding="0,0,4,0">
|
||||
<StackPanel Margin="20,16,20,8">
|
||||
|
||||
<!-- 탐색 -->
|
||||
<TextBlock Text="탐색" FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="2,0,0,6"/>
|
||||
<ItemsControl x:Name="NavigationItems"/>
|
||||
|
||||
<!-- 파일 동작 -->
|
||||
<TextBlock Text="파일 동작" FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="2,14,0,6"/>
|
||||
<ItemsControl x:Name="FileActionItems"/>
|
||||
|
||||
<!-- 뷰 전환 -->
|
||||
<TextBlock Text="뷰 전환" FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="2,14,0,6"/>
|
||||
<ItemsControl x:Name="ViewItems"/>
|
||||
|
||||
<!-- 실행 및 검색 -->
|
||||
<TextBlock Text="실행 및 기타" FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="2,14,0,6"/>
|
||||
<ItemsControl x:Name="RunItems"/>
|
||||
|
||||
<!-- 예약어 -->
|
||||
<TextBlock Text="입력 예약어" FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="2,14,0,6"/>
|
||||
<ItemsControl x:Name="PrefixItems"/>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ── 리사이즈 그립 ── -->
|
||||
<Border Grid.Row="2"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Bottom"
|
||||
Width="20" Height="20" Margin="0,0,2,2"
|
||||
Cursor="SizeNWSE" Background="Transparent"
|
||||
Panel.ZIndex="100"
|
||||
MouseLeftButtonDown="ResizeGrip_MouseLeftButtonDown">
|
||||
<Canvas Width="13" Height="13">
|
||||
<Rectangle Canvas.Left="10" Canvas.Top="10" Width="2.5" Height="2.5" RadiusX="1.2" RadiusY="1.2" Fill="#70AAAACC"/>
|
||||
<Rectangle Canvas.Left="6" Canvas.Top="10" Width="2.5" Height="2.5" RadiusX="1.2" RadiusY="1.2" Fill="#50AAAACC"/>
|
||||
<Rectangle Canvas.Left="10" Canvas.Top="6" Width="2.5" Height="2.5" RadiusX="1.2" RadiusY="1.2" Fill="#50AAAACC"/>
|
||||
<Rectangle Canvas.Left="2" Canvas.Top="10" Width="2.5" Height="2.5" RadiusX="1.2" RadiusY="1.2" Fill="#30AAAACC"/>
|
||||
<Rectangle Canvas.Left="6" Canvas.Top="6" Width="2.5" Height="2.5" RadiusX="1.2" RadiusY="1.2" Fill="#30AAAACC"/>
|
||||
<Rectangle Canvas.Left="10" Canvas.Top="2" Width="2.5" Height="2.5" RadiusX="1.2" RadiusY="1.2" Fill="#30AAAACC"/>
|
||||
</Canvas>
|
||||
</Border>
|
||||
|
||||
<!-- ── 푸터 ── -->
|
||||
<Border Grid.Row="2" BorderBrush="{DynamicResource BorderColor}" BorderThickness="0,1,0,0"
|
||||
Background="{DynamicResource ItemBackground}">
|
||||
<Grid Margin="20,0">
|
||||
<!-- 왼쪽: 테마 색상 토글 + Esc 안내 -->
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||
<CheckBox x:Name="ThemeColorChk"
|
||||
Content="테마 색상으로 표시"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
Checked="ThemeColorChk_Changed"
|
||||
Unchecked="ThemeColorChk_Changed"
|
||||
Margin="0,0,0,2"/>
|
||||
<TextBlock Text="Esc 또는 클릭하면 닫힙니다"
|
||||
FontSize="10" Foreground="{DynamicResource SecondaryText}"
|
||||
Opacity="0.6"/>
|
||||
</StackPanel>
|
||||
<Button Content="닫기" HorizontalAlignment="Right"
|
||||
Click="Close_Click" Width="72" Height="32"
|
||||
Background="#4B5EFC" Foreground="White"
|
||||
FontSize="12" FontWeight="SemiBold"
|
||||
BorderThickness="0" Cursor="Hand">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="8"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#3B4EEC"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
238
src/AxCopilot/Views/ShortcutHelpWindow.xaml.cs
Normal file
238
src/AxCopilot/Views/ShortcutHelpWindow.xaml.cs
Normal file
@@ -0,0 +1,238 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>Ctrl+K로 열리는 단축키 참조 모달 창</summary>
|
||||
public partial class ShortcutHelpWindow : Window
|
||||
{
|
||||
// ─── P/Invoke ────────────────────────────────────────────────────────────
|
||||
[DllImport("user32.dll")] private static extern void ReleaseCapture();
|
||||
[DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
|
||||
private const int WM_NCLBUTTONDOWN = 0xA1;
|
||||
private const int HTBOTTOMRIGHT = 17;
|
||||
|
||||
// (키 문자열, 설명, 아이콘 코드, 배경색)
|
||||
private record ShortcutRow(string Key, string Description, string Icon = "\uE72D", string Color = "#4B5EFC");
|
||||
|
||||
public ShortcutHelpWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
// 저장된 설정 로드
|
||||
var svc = (System.Windows.Application.Current as App)?.SettingsService;
|
||||
if (svc != null)
|
||||
ThemeColorChk.IsChecked = svc.Settings.Launcher.ShortcutHelpUseThemeColor;
|
||||
|
||||
BuildItems();
|
||||
};
|
||||
}
|
||||
|
||||
private void BuildItems()
|
||||
{
|
||||
bool useTheme = ThemeColorChk.IsChecked == true;
|
||||
|
||||
// ── 탐색 ──────────────────────────────────────────────────────────
|
||||
var nav = new[]
|
||||
{
|
||||
new ShortcutRow("↑ / ↓", "결과 목록 위/아래로 이동", "\uE74A", "#4B5EFC"),
|
||||
new ShortcutRow("PageUp / PageDown","목록 5칸 빠른 이동", "\uE74A", "#4B5EFC"),
|
||||
new ShortcutRow("Home / End", "목록 처음 항목 / 마지막 항목으로 점프", "\uE74A", "#4B5EFC"),
|
||||
new ShortcutRow("Tab", "선택 항목 제목을 입력창에 자동 완성", "\uE748", "#7B68EE"),
|
||||
new ShortcutRow("→", "파일/앱 선택 시 액션 모드(복사·실행 등) 진입", "\uE76C", "#107C10"),
|
||||
new ShortcutRow("Escape", "액션 모드면 일반 모드로 복귀, 아니면 AX Commander 닫기", "\uE711", "#C50F1F"),
|
||||
};
|
||||
|
||||
// ── 파일 동작 ──────────────────────────────────────────────────────
|
||||
var file = new[]
|
||||
{
|
||||
new ShortcutRow("Enter", "선택 항목 실행 (파일 열기·명령 실행·URL 열기 등)", "\uE768", "#107C10"),
|
||||
new ShortcutRow("Ctrl+Enter", "선택 파일·앱을 관리자(UAC 상승) 권한으로 실행", "\uE7EF", "#C50F1F"),
|
||||
new ShortcutRow("Alt+Enter", "선택 항목의 Windows 속성 대화 상자 열기", "\uE7EE", "#8B2FC9"),
|
||||
new ShortcutRow("Ctrl+C", "선택 항목의 파일 이름(확장자 제외)을 클립보드에 복사","\uE8C8", "#0078D4"),
|
||||
new ShortcutRow("Ctrl+Shift+C", "선택 항목의 전체 경로를 클립보드에 복사", "\uE8C8", "#0078D4"),
|
||||
new ShortcutRow("Ctrl+Shift+E", "파일 탐색기를 열고 선택 항목 위치를 하이라이트", "\uE8DA", "#107C10"),
|
||||
new ShortcutRow("Ctrl+T", "선택 항목 폴더 경로에서 터미널(wt/cmd) 열기", "\uE756", "#323130"),
|
||||
new ShortcutRow("Ctrl+P", "선택 항목을 즐겨찾기에 추가 (이미 있으면 제거)", "\uE734", "#D97706"),
|
||||
new ShortcutRow("F2", "선택 파일·폴더의 이름 변경 모드로 전환", "\uE70F", "#6B2C91"),
|
||||
new ShortcutRow("Delete", "선택 항목을 최근 목록에서 제거 (확인 후 실행)", "\uE74D", "#C50F1F"),
|
||||
};
|
||||
|
||||
// ── 뷰 전환 ────────────────────────────────────────────────────────
|
||||
var view = new[]
|
||||
{
|
||||
new ShortcutRow("Ctrl+B", "즐겨찾기 목록 보기 (다시 누르면 닫기, 토글)", "\uE735", "#D97706"),
|
||||
new ShortcutRow("Ctrl+R", "최근 실행 목록 보기 (다시 누르면 닫기, 토글)", "\uE81C", "#0078D4"),
|
||||
new ShortcutRow("Ctrl+H", "클립보드 히스토리 목록 보기", "\uE77F", "#8B2FC9"),
|
||||
new ShortcutRow("Ctrl+D", "다운로드 폴더 경로 바로 열기", "\uE8B7", "#107C10"),
|
||||
new ShortcutRow("Ctrl+F", "입력 초기화 후 파일 검색 모드로 포커스 이동", "\uE71E", "#4B5EFC"),
|
||||
new ShortcutRow("F1", "헬프 화면 열기 (help 입력과 동일)", "\uE897", "#4B5EFC"),
|
||||
};
|
||||
|
||||
// ── 실행 및 기타 ───────────────────────────────────────────────────
|
||||
var run = new[]
|
||||
{
|
||||
new ShortcutRow("Ctrl+1 ~ Ctrl+9", "표시 중인 N번째 결과를 즉시 실행 (번호 배지와 연동)", "\uE8C4", "#107C10"),
|
||||
new ShortcutRow("Ctrl+L", "입력창 전체 초기화", "\uE711", "#9999BB"),
|
||||
new ShortcutRow("Ctrl+W", "AX Commander 창 즉시 닫기", "\uE711", "#9999BB"),
|
||||
new ShortcutRow("Ctrl+K", "이 단축키 참조 창 열기", "\uE8FD", "#4B5EFC"),
|
||||
new ShortcutRow("Ctrl+,", "설정 창 열기", "\uE713", "#4B5EFC"),
|
||||
new ShortcutRow("F5", "파일 인덱스 즉시 재구축", "\uE72C", "#107C10"),
|
||||
new ShortcutRow("Shift+Enter", "Large Type / 병합 실행 / 캡처 모드: 지연 캡처(3/5/10초) 타이머", "\uE8A7", "#7B68EE"),
|
||||
};
|
||||
|
||||
// ── 입력 예약어 ───────────────────────────────────────────────────
|
||||
var prefix = new[]
|
||||
{
|
||||
new ShortcutRow("cd [경로/별칭]", "지정한 경로 또는 등록된 폴더 별칭을 탐색기로 열기", "\uE8DA", "#107C10"),
|
||||
new ShortcutRow("~ [이름]", "워크스페이스 저장(save)/복원(restore)/목록(list)", "\uE8B7", "#C50F1F"),
|
||||
new ShortcutRow("# [검색어]", "클립보드 히스토리 검색·붙여넣기", "\uE77F", "#8B2FC9"),
|
||||
new ShortcutRow("fav [검색어]", "즐겨찾기 목록 검색 및 열기. add/del 로 관리", "\uE735", "#D97706"),
|
||||
new ShortcutRow("recent [검색어]", "최근 실행 항목 검색", "\uE81C", "#0078D4"),
|
||||
new ShortcutRow("cap [모드]", "화면 캡처 (region/window/scroll/screen) · Shift+Enter: 지연 캡처", "\uE722", "#C09010"),
|
||||
new ShortcutRow("help [검색어]", "도움말 및 기능 목록 검색", "\uE897", "#4B5EFC"),
|
||||
new ShortcutRow("/ [명령]", "시스템 명령 (lock/sleep/restart/shutdown 등)", "\uE7E8", "#C50F1F"),
|
||||
new ShortcutRow("! [질문]", "AX Agent — Ollama/vLLM/Gemini/Claude", "\uE8BD", "#8B2FC9"),
|
||||
};
|
||||
|
||||
NavigationItems.Items.Clear();
|
||||
FileActionItems.Items.Clear();
|
||||
ViewItems.Items.Clear();
|
||||
RunItems.Items.Clear();
|
||||
PrefixItems.Items.Clear();
|
||||
|
||||
Populate(NavigationItems, nav, useTheme);
|
||||
Populate(FileActionItems, file, useTheme);
|
||||
Populate(ViewItems, view, useTheme);
|
||||
Populate(RunItems, run, useTheme);
|
||||
Populate(PrefixItems, prefix, useTheme);
|
||||
}
|
||||
|
||||
private static void Populate(ItemsControl control, ShortcutRow[] rows, bool useThemeColor)
|
||||
{
|
||||
// 현재 테마에서 텍스트/배경 색상 읽기
|
||||
var primaryText = System.Windows.Application.Current.TryFindResource("PrimaryText") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x33, 0x33, 0x55));
|
||||
var itemBg = System.Windows.Application.Current.TryFindResource("ItemBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0xEE, 0xEE, 0xFF));
|
||||
var accentBrush = System.Windows.Application.Current.TryFindResource("AccentColor") as SolidColorBrush;
|
||||
var accentHex = accentBrush != null ? $"#{accentBrush.Color.R:X2}{accentBrush.Color.G:X2}{accentBrush.Color.B:X2}" : "#4B5EFC";
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var colorHex = useThemeColor ? accentHex : row.Color;
|
||||
|
||||
var grid = new Grid { Margin = new Thickness(0, 0, 0, 3) };
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(28) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(148) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
|
||||
// 아이콘 배지
|
||||
var iconBorder = new Border
|
||||
{
|
||||
Width = 22, Height = 22,
|
||||
CornerRadius = new CornerRadius(5),
|
||||
Background = ParseBrush(colorHex + "22"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
};
|
||||
iconBorder.Child = new TextBlock
|
||||
{
|
||||
Text = row.Icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 10,
|
||||
Foreground = ParseBrush(colorHex),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
Grid.SetColumn(iconBorder, 0);
|
||||
grid.Children.Add(iconBorder);
|
||||
|
||||
// 키 배지 — 테마 ItemBackground 사용
|
||||
var keyBorder = new Border
|
||||
{
|
||||
Background = itemBg,
|
||||
CornerRadius = new CornerRadius(5),
|
||||
Padding = new Thickness(7, 3, 7, 3),
|
||||
Margin = new Thickness(4, 1, 8, 1),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
};
|
||||
keyBorder.Child = new TextBlock
|
||||
{
|
||||
Text = row.Key,
|
||||
FontFamily = new FontFamily("Consolas, Courier New"),
|
||||
FontSize = 11,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
Grid.SetColumn(keyBorder, 1);
|
||||
grid.Children.Add(keyBorder);
|
||||
|
||||
// 설명 — 테마 PrimaryText 사용
|
||||
var desc = new TextBlock
|
||||
{
|
||||
Text = row.Description,
|
||||
FontSize = 11.5,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
LineHeight = 16,
|
||||
};
|
||||
Grid.SetColumn(desc, 2);
|
||||
grid.Children.Add(desc);
|
||||
|
||||
control.Items.Add(grid);
|
||||
}
|
||||
}
|
||||
|
||||
private static SolidColorBrush ParseBrush(string hex)
|
||||
{
|
||||
try { return new SolidColorBrush((Color)ColorConverter.ConvertFromString(hex)); }
|
||||
catch { return new SolidColorBrush(Colors.Transparent); }
|
||||
}
|
||||
|
||||
// ─── 테마 색상 체크박스 ─────────────────────────────────────────────────
|
||||
|
||||
private void ThemeColorChk_Changed(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 설정 저장
|
||||
var svc = (System.Windows.Application.Current as App)?.SettingsService;
|
||||
if (svc != null)
|
||||
{
|
||||
svc.Settings.Launcher.ShortcutHelpUseThemeColor = ThemeColorChk.IsChecked == true;
|
||||
svc.Save();
|
||||
}
|
||||
// 목록 다시 그리기
|
||||
BuildItems();
|
||||
}
|
||||
|
||||
// ─── 리사이즈 그립 ──────────────────────────────────────────────────────
|
||||
|
||||
private void ResizeGrip_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ButtonState != MouseButtonState.Pressed) return;
|
||||
ReleaseCapture();
|
||||
SendMessage(new WindowInteropHelper(this).Handle, WM_NCLBUTTONDOWN, (IntPtr)HTBOTTOMRIGHT, IntPtr.Zero);
|
||||
}
|
||||
|
||||
// ─── 이벤트 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void Close_Click(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private void Window_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape) Close();
|
||||
}
|
||||
|
||||
private void Window_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ChangedButton == MouseButton.Left && e.LeftButton == MouseButtonState.Pressed)
|
||||
try { DragMove(); } catch { }
|
||||
}
|
||||
}
|
||||
258
src/AxCopilot/Views/SkillEditorWindow.xaml
Normal file
258
src/AxCopilot/Views/SkillEditorWindow.xaml
Normal file
@@ -0,0 +1,258 @@
|
||||
<Window x:Class="AxCopilot.Views.SkillEditorWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="스킬 편집기"
|
||||
Width="780" Height="660"
|
||||
WindowStyle="None" AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="CanResizeWithGrip">
|
||||
<Border Background="{DynamicResource LauncherBackground}" CornerRadius="12"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="20" ShadowDepth="4" Opacity="0.3" Color="Black"/>
|
||||
</Border.Effect>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="44"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ── 타이틀바 ── -->
|
||||
<Border Grid.Row="0" CornerRadius="12,12,0,0" Background="{DynamicResource ItemBackground}"
|
||||
MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
|
||||
<Grid>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="16,0,0,0">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="14"
|
||||
Foreground="#4B5EFC" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock x:Name="TitleText" Text="새 스킬 만들기" FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,12,0">
|
||||
<Border Width="28" Height="28" CornerRadius="6" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnClose_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#33FF4444"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── 메인 콘텐츠 ── -->
|
||||
<Grid Grid.Row="1" Margin="16,10,16,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="260"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- ═══ 좌측: 편집 폼 ═══ -->
|
||||
<ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto" Margin="0,0,12,0">
|
||||
<StackPanel>
|
||||
<!-- 이름 + 레이블 -->
|
||||
<Grid Margin="0,0,0,10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Margin="0,0,6,0">
|
||||
<TextBlock Text="이름 (영문, 슬래시 명령)" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,4"/>
|
||||
<TextBox x:Name="TxtName" FontSize="13" Padding="10,8,10,8"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
MaxLength="40">
|
||||
<TextBox.Resources>
|
||||
<Style TargetType="Border"><Setter Property="CornerRadius" Value="8"/></Style>
|
||||
</TextBox.Resources>
|
||||
</TextBox>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Margin="6,0,0,0">
|
||||
<TextBlock Text="표시 이름 (한글)" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,4"/>
|
||||
<TextBox x:Name="TxtLabel" FontSize="13" Padding="10,8,10,8"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
MaxLength="60">
|
||||
<TextBox.Resources>
|
||||
<Style TargetType="Border"><Setter Property="CornerRadius" Value="8"/></Style>
|
||||
</TextBox.Resources>
|
||||
</TextBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 설명 -->
|
||||
<StackPanel Margin="0,0,0,10">
|
||||
<TextBlock Text="설명" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,4"/>
|
||||
<TextBox x:Name="TxtDescription" FontSize="13" Padding="10,8,10,8"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
MaxLength="200">
|
||||
<TextBox.Resources>
|
||||
<Style TargetType="Border"><Setter Property="CornerRadius" Value="8"/></Style>
|
||||
</TextBox.Resources>
|
||||
</TextBox>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 아이콘 + 런타임 요구사항 -->
|
||||
<Grid Margin="0,0,0,10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Margin="0,0,12,0">
|
||||
<TextBlock Text="아이콘" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,4"/>
|
||||
<StackPanel x:Name="IconSelectorPanel" Orientation="Horizontal"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1">
|
||||
<TextBlock Text="런타임 요구사항" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,4"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ComboBox x:Name="CmbRequires" Width="160" VerticalAlignment="Center"
|
||||
SelectedIndex="0">
|
||||
<ComboBoxItem Content="없음 (기본)" Tag=""/>
|
||||
<ComboBoxItem Content="Python" Tag="python"/>
|
||||
<ComboBoxItem Content="Node.js" Tag="node"/>
|
||||
<ComboBoxItem Content="Python + Node.js" Tag="python,node"/>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- 지시사항 (마크다운 편집) -->
|
||||
<StackPanel Margin="0,0,0,6">
|
||||
<Grid Margin="0,0,0,4">
|
||||
<TextBlock Text="지시사항 (마크다운)" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Border x:Name="BtnInsertToolList" CornerRadius="4" Padding="8,2,8,2"
|
||||
Background="{DynamicResource ItemBackground}" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnInsertTemplate_Click" Tag="tools"
|
||||
Margin="0,0,4,0">
|
||||
<TextBlock Text="도구 목록 삽입" FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnInsertFormat" CornerRadius="4" Padding="8,2,8,2"
|
||||
Background="{DynamicResource ItemBackground}" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnInsertTemplate_Click" Tag="format"
|
||||
Margin="0,0,4,0">
|
||||
<TextBlock Text="출력 형식 삽입" FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
<Border CornerRadius="4" Padding="8,2,8,2"
|
||||
Background="{DynamicResource ItemBackground}" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnInsertTemplate_Click" Tag="steps">
|
||||
<TextBlock Text="단계별 삽입" FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<TextBox x:Name="TxtInstructions" FontSize="12.5" Padding="10,8,10,8"
|
||||
AcceptsReturn="True" AcceptsTab="True"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Height="220"
|
||||
FontFamily="Consolas, Cascadia Code, Segoe UI"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
TextWrapping="Wrap"
|
||||
TextChanged="TxtInstructions_TextChanged">
|
||||
<TextBox.Resources>
|
||||
<Style TargetType="Border"><Setter Property="CornerRadius" Value="8"/></Style>
|
||||
</TextBox.Resources>
|
||||
</TextBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ═══ 우측: 도구 선택 + 미리보기 ═══ -->
|
||||
<Grid Grid.Column="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 도구 체크리스트 -->
|
||||
<Border Grid.Row="0" Background="{DynamicResource ItemBackground}"
|
||||
CornerRadius="10" Padding="12,10,12,10">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" Text="사용할 도구 (allowed-tools)"
|
||||
FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,8"/>
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel x:Name="ToolCheckListPanel"/>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 미리보기 정보 -->
|
||||
<Border Grid.Row="1" Background="{DynamicResource ItemBackground}"
|
||||
CornerRadius="10" Padding="12,10,12,10" Margin="0,8,0,0">
|
||||
<StackPanel>
|
||||
<TextBlock Text="미리보기" FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,6"/>
|
||||
<TextBlock x:Name="PreviewTokens" FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
<TextBlock x:Name="PreviewFileName" FontSize="10.5" FontFamily="Consolas"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<!-- ── 하단 버튼 ── -->
|
||||
<Border Grid.Row="2" BorderBrush="{DynamicResource BorderColor}" BorderThickness="0,1,0,0"
|
||||
Padding="16,10,16,10" CornerRadius="0,0,12,12">
|
||||
<Grid>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="StatusText" FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Border CornerRadius="8" Padding="18,8,18,8" Margin="0,0,8,0"
|
||||
Background="{DynamicResource ItemBackground}" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnCancel_Click">
|
||||
<TextBlock Text="취소" FontSize="12.5"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
<Border CornerRadius="8" Padding="18,8,18,8" Margin="0,0,8,0"
|
||||
Background="{DynamicResource ItemBackground}" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnPreview_Click">
|
||||
<TextBlock Text="미리보기" FontSize="12.5"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
<Border CornerRadius="8" Padding="22,8,22,8"
|
||||
Background="#4B5EFC" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnSave_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
Foreground="White" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="저장" FontSize="12.5" FontWeight="SemiBold" Foreground="White"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
528
src/AxCopilot/Views/SkillEditorWindow.xaml.cs
Normal file
528
src/AxCopilot/Views/SkillEditorWindow.xaml.cs
Normal file
@@ -0,0 +1,528 @@
|
||||
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;
|
||||
|
||||
/// <summary>스킬 시각적 편집기. 새 스킬 생성 또는 기존 스킬 편집을 GUI로 지원합니다.</summary>
|
||||
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;
|
||||
|
||||
/// <summary>새 스킬 모드로 열기.</summary>
|
||||
public SkillEditorWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
_toolRegistry = ToolRegistry.CreateDefault();
|
||||
Loaded += (_, _) => { BuildIconSelector(); BuildToolChecklist(); UpdatePreview(); };
|
||||
}
|
||||
|
||||
/// <summary>편집 모드로 열기.</summary>
|
||||
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<string>(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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>체크된 도구 이름 목록을 가져옵니다.</summary>
|
||||
private List<string> GetCheckedTools()
|
||||
{
|
||||
var result = new List<string>();
|
||||
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<string> { "## 사용 가능한 도구", "" };
|
||||
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<string> { "---" };
|
||||
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();
|
||||
}
|
||||
}
|
||||
100
src/AxCopilot/Views/SkillGalleryWindow.xaml
Normal file
100
src/AxCopilot/Views/SkillGalleryWindow.xaml
Normal file
@@ -0,0 +1,100 @@
|
||||
<Window x:Class="AxCopilot.Views.SkillGalleryWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="스킬 갤러리"
|
||||
Width="740" Height="560"
|
||||
WindowStyle="None" AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="CanResizeWithGrip">
|
||||
<Border Background="{DynamicResource LauncherBackground}" CornerRadius="12"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="20" ShadowDepth="4" Opacity="0.3" Color="Black"/>
|
||||
</Border.Effect>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="44"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ── 타이틀바 ── -->
|
||||
<Border Grid.Row="0" CornerRadius="12,12,0,0" Background="{DynamicResource ItemBackground}"
|
||||
MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
|
||||
<Grid Margin="16,0,12,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- 왼쪽: 제목 -->
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="14"
|
||||
Foreground="#4B5EFC" VerticalAlignment="Center" Margin="0,1,8,0"/>
|
||||
<TextBlock Text="스킬 갤러리" FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<!-- 오른쪽: 버튼들 -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border CornerRadius="6" Padding="10,5" Margin="0,0,6,0"
|
||||
Background="{DynamicResource ItemBackground}" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnAddSkill_Click">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="11"
|
||||
Foreground="#34D399" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock Text="스킬 추가" FontSize="12" Foreground="#34D399"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border CornerRadius="6" Padding="10,5" Margin="0,0,6,0"
|
||||
Background="{DynamicResource ItemBackground}" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnImport_Click">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock Text="가져오기" FontSize="12" Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border x:Name="BtnCloseGallery" Width="28" Height="28" CornerRadius="6" Cursor="Hand"
|
||||
VerticalAlignment="Center"
|
||||
MouseLeftButtonUp="BtnClose_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#33FF4444"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── 카테고리 필터 ── -->
|
||||
<Border Grid.Row="1" Padding="16,10,16,0">
|
||||
<StackPanel x:Name="CategoryFilterBar" Orientation="Horizontal"/>
|
||||
</Border>
|
||||
|
||||
<!-- ── 스킬 목록 ── -->
|
||||
<ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto" Padding="16,10,16,0">
|
||||
<StackPanel x:Name="SkillListPanel"/>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ── 상태바 ── -->
|
||||
<Border Grid.Row="3" BorderBrush="{DynamicResource BorderColor}" BorderThickness="0,1,0,0"
|
||||
Padding="16,8" CornerRadius="0,0,12,12">
|
||||
<TextBlock x:Name="GalleryStatus" FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
Text="스킬을 불러오는 중..."/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
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();
|
||||
}
|
||||
}
|
||||
821
src/AxCopilot/Views/StatisticsWindow.xaml
Normal file
821
src/AxCopilot/Views/StatisticsWindow.xaml
Normal file
@@ -0,0 +1,821 @@
|
||||
<Window x:Class="AxCopilot.Views.StatisticsWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:AxCopilot.ViewModels"
|
||||
Title="AX Copilot — 사용 통계"
|
||||
Width="800" Height="900"
|
||||
MinWidth="680" MinHeight="700"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
UseLayoutRounding="True"
|
||||
SnapsToDevicePixels="True"
|
||||
Background="Transparent"
|
||||
ResizeMode="CanResize"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ShowInTaskbar="True"
|
||||
Topmost="False"
|
||||
Icon="pack://application:,,,/Assets/icon.ico"
|
||||
MouseDown="Window_MouseDown">
|
||||
|
||||
<Grid Margin="20">
|
||||
<Border CornerRadius="20" Background="White">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="Black" BlurRadius="22" ShadowDepth="4" Opacity="0.24"/>
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
<Border Background="White" CornerRadius="20" ClipToBounds="True">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="64"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ══ 헤더 ══ -->
|
||||
<Border Grid.Row="0" CornerRadius="20,20,0,0">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Color="#1A1B2E" Offset="0"/>
|
||||
<GradientStop Color="#2B2D5B" Offset="0.5"/>
|
||||
<GradientStop Color="#3B4ECC" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
<Grid Margin="24,0">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="18" Foreground="#88AAFFCC"
|
||||
VerticalAlignment="Center" Margin="0,0,12,0"/>
|
||||
<TextBlock Text="사용 통계" FontSize="17" FontWeight="SemiBold"
|
||||
Foreground="White" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="최근 30일" FontSize="11" Foreground="#6688BBCC"
|
||||
VerticalAlignment="Center" Margin="12,0,0,0"/>
|
||||
</StackPanel>
|
||||
<!-- 닫기 + 새로고침 -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<Button Width="32" Height="32" Background="Transparent" BorderThickness="0"
|
||||
Cursor="Hand" Click="Refresh_Click" ToolTip="새로고침">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" CornerRadius="8" Background="Transparent">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="13" Foreground="#88AAFFCC"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#22FFFFFF"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
<Button Width="32" Height="32" Background="Transparent" BorderThickness="0"
|
||||
Cursor="Hand" Click="Close_Click">
|
||||
<Button.Template>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" CornerRadius="8" Background="Transparent">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="11" Foreground="#88AAFFCC"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#22FFFFFF"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Button.Template>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ══ 본문 ══ -->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 탭 헤더 -->
|
||||
<Border Grid.Row="0" Background="#F0F2F8" Padding="4" Margin="20,12,20,0" CornerRadius="10">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<RadioButton x:Name="StatsTabMain" Content="메인" IsChecked="True"
|
||||
GroupName="StatsTab" Click="StatsTab_Click">
|
||||
<RadioButton.Style>
|
||||
<Style TargetType="RadioButton">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="RadioButton">
|
||||
<Border x:Name="Bd" CornerRadius="8" Padding="16,7" Cursor="Hand" Background="Transparent">
|
||||
<TextBlock x:Name="Tb" Text="{TemplateBinding Content}" FontSize="12" FontWeight="SemiBold" Foreground="#8888AA" HorizontalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="White"/>
|
||||
<Setter TargetName="Tb" Property="Foreground" Value="#1A1B2E"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</RadioButton.Style>
|
||||
</RadioButton>
|
||||
<RadioButton x:Name="StatsTabCommander" Content="AX Commander"
|
||||
GroupName="StatsTab" Click="StatsTab_Click"
|
||||
Style="{Binding Style, ElementName=StatsTabMain}"/>
|
||||
<RadioButton x:Name="StatsTabAgent" Content="AX Agent"
|
||||
GroupName="StatsTab" Click="StatsTab_Click"
|
||||
Style="{Binding Style, ElementName=StatsTabMain}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ═══ 메인 탭 ═══ -->
|
||||
<ScrollViewer x:Name="PanelMain" Grid.Row="1" VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="24,14,24,24">
|
||||
|
||||
<!-- ── 요약 카드 4개 ── -->
|
||||
<Grid Margin="0,0,0,20">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 오늘 -->
|
||||
<Border Grid.Column="0" Background="#F5F7FF" CornerRadius="12" Padding="12,11">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="11" Foreground="#4B5EFC" Margin="0,0,5,0" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="오늘" FontSize="11" FontWeight="SemiBold" Foreground="#4B5EFC" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding TodayDate}" FontSize="10" Foreground="#9999BB"
|
||||
Margin="0,0,0,2"/>
|
||||
<TextBlock Text="{Binding TodaySummary}" FontSize="12" Foreground="#333355"
|
||||
TextWrapping="Wrap" LineHeight="18"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 이번 주 -->
|
||||
<Border Grid.Column="2" Background="#F0FFF5" CornerRadius="12" Padding="12,11">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="11" Foreground="#107C10" Margin="0,0,5,0" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="주간" FontSize="11" FontWeight="SemiBold" Foreground="#107C10" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding WeekSummary}" FontSize="12" Foreground="#333355"
|
||||
TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 역대 최고 -->
|
||||
<Border Grid.Column="4" Background="#FFFBF0" CornerRadius="12" Padding="12,11">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="11" Foreground="#C09010" Margin="0,0,5,0" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="최고 기록" FontSize="11" FontWeight="SemiBold" Foreground="#C09010" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding PeakDate}" FontSize="10" Foreground="#9999BB"
|
||||
Margin="0,0,0,2"/>
|
||||
<TextBlock Text="{Binding PeakDaySummary}" FontSize="12" Foreground="#333355"
|
||||
TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 30일 합계 -->
|
||||
<Border Grid.Column="6" Background="#FFF0FF" CornerRadius="12" Padding="12,11">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,5">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="11" Foreground="#8B2FC9" Margin="0,0,5,0" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="30일" FontSize="11" FontWeight="SemiBold" Foreground="#8B2FC9" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding TotalOpensSummary}" FontSize="12" Foreground="#333355"
|
||||
TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- ── 메인: AX Commander 14일 활동 ── -->
|
||||
<Border Background="#F9FAFF" CornerRadius="14" Padding="18,12" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="13" Foreground="#4B5EFC" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="AX Commander 호출 (14일)" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="#1A1B2E" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<ItemsControl ItemsSource="{Binding LauncherOpenBars}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><StackPanel Orientation="Horizontal"/></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type vm:DayBarItem}">
|
||||
<StackPanel Width="38" Margin="1,0" ToolTip="{Binding ToolTipText}">
|
||||
<TextBlock Text="{Binding ValueLabel}" FontSize="8" Foreground="#9999BB"
|
||||
HorizontalAlignment="Center" Margin="0,0,0,1"/>
|
||||
<Border Height="52" VerticalAlignment="Bottom">
|
||||
<Border CornerRadius="3,3,0,0" VerticalAlignment="Bottom"
|
||||
Width="20"
|
||||
Height="{Binding BarHeight}">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#4B5EFC"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsToday}" Value="True">
|
||||
<Setter Property="Background" Value="#5C6EFF"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding HasData}" Value="False">
|
||||
<Setter Property="Background" Value="#EEEEF8"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding DayLabel}" FontSize="9" Foreground="#6B7280"
|
||||
HorizontalAlignment="Center" Margin="0,2,0,0"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── 메인: AX Agent 14일 대화 ── -->
|
||||
<Border Background="#F0F4FF" CornerRadius="14" Padding="18,12" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="13" Foreground="#818CF8" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="AX Agent 대화 (14일)" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="#1A1B2E" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<ItemsControl ItemsSource="{Binding ChatCountBars}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><StackPanel Orientation="Horizontal"/></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type vm:DayBarItem}">
|
||||
<StackPanel Width="38" Margin="1,0" ToolTip="{Binding ToolTipText}">
|
||||
<TextBlock Text="{Binding ValueLabel}" FontSize="8" Foreground="#818CF8"
|
||||
HorizontalAlignment="Center" Margin="0,0,0,1"/>
|
||||
<Border Height="52" VerticalAlignment="Bottom">
|
||||
<Border CornerRadius="3,3,0,0" VerticalAlignment="Bottom"
|
||||
Width="20"
|
||||
Height="{Binding BarHeight}">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#818CF8"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsToday}" Value="True">
|
||||
<Setter Property="Background" Value="#4B5EFC"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding HasData}" Value="False">
|
||||
<Setter Property="Background" Value="#E5E7EB"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding DayLabel}" FontSize="9" Foreground="#6B7280"
|
||||
HorizontalAlignment="Center" Margin="0,2,0,0"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ═══ AX Commander 탭 ═══ -->
|
||||
<ScrollViewer x:Name="PanelCommander" Grid.Row="1" VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled" Visibility="Collapsed">
|
||||
<StackPanel Margin="24,14,24,24">
|
||||
|
||||
<!-- ── 런처 호출 횟수 차트 ── -->
|
||||
<Border Background="#F9FAFF" CornerRadius="14" Padding="18,12" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,14">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="14" Foreground="#4B5EFC" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="AX Commander 호출 횟수 (최근 14일)" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="#1A1B2E" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<!-- 차트 영역 -->
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<ItemsControl ItemsSource="{Binding LauncherOpenBars}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type vm:DayBarItem}">
|
||||
<Grid Width="42" Height="88" VerticalAlignment="Bottom" Margin="1,0"
|
||||
ToolTip="{Binding ToolTipText, TargetNullValue={x:Null}}"
|
||||
ToolTipService.IsEnabled="{Binding HasData}">
|
||||
<!-- 값 레이블 -->
|
||||
<TextBlock Text="{Binding ValueLabel}"
|
||||
FontSize="9" Foreground="#9999BB"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Top"/>
|
||||
<!-- 막대 -->
|
||||
<Border CornerRadius="4,4,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Width="24"
|
||||
Height="{Binding BarHeight}"
|
||||
Margin="0,0,0,18">
|
||||
<Border.Background>
|
||||
<SolidColorBrush>
|
||||
<SolidColorBrush.Color>
|
||||
<Color A="255" R="75" G="94" B="252"/>
|
||||
</SolidColorBrush.Color>
|
||||
</SolidColorBrush>
|
||||
</Border.Background>
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsToday}" Value="True">
|
||||
<Setter Property="Background" Value="#FF5C6EFF"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding HasData}" Value="False">
|
||||
<Setter Property="Background" Value="#EEEEF8"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
<!-- 날짜 레이블 -->
|
||||
<StackPanel VerticalAlignment="Bottom" HorizontalAlignment="Center">
|
||||
<TextBlock Text="{Binding DayLabel}"
|
||||
FontSize="10" Foreground="#6B7280"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding DateLabel}"
|
||||
FontSize="8" Foreground="#9CA3AF"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── PC 활성 시간 차트 ── -->
|
||||
<Border Background="#F9FAFF" CornerRadius="14" Padding="18,12" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,14">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="14" Foreground="#107C10" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="PC 활성 시간 (최근 14일)" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="#1A1B2E" VerticalAlignment="Center"/>
|
||||
<TextBlock Text=" · 잠금 해제 후 활동 시간" FontSize="11"
|
||||
Foreground="#AAAACC" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<ItemsControl ItemsSource="{Binding ActiveTimeBars}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type vm:DayBarItem}">
|
||||
<Grid Width="42" Height="88" VerticalAlignment="Bottom" Margin="1,0"
|
||||
ToolTip="{Binding ToolTipText, TargetNullValue={x:Null}}"
|
||||
ToolTipService.IsEnabled="{Binding HasData}">
|
||||
<TextBlock Text="{Binding ValueLabel}"
|
||||
FontSize="8" Foreground="#9999BB"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Top"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
<Border CornerRadius="4,4,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Width="24"
|
||||
Height="{Binding BarHeight}"
|
||||
Margin="0,0,0,18">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#107C10"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsToday}" Value="True">
|
||||
<Setter Property="Background" Value="#15A815"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding HasData}" Value="False">
|
||||
<Setter Property="Background" Value="#EEEEF8"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
<StackPanel VerticalAlignment="Bottom" HorizontalAlignment="Center">
|
||||
<TextBlock Text="{Binding DayLabel}"
|
||||
FontSize="10" Foreground="#6B7280"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding DateLabel}"
|
||||
FontSize="8" Foreground="#9CA3AF"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── 인기 명령어 ── -->
|
||||
<Border Background="#F9FAFF" CornerRadius="14" Padding="18,14" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,14">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="14" Foreground="#8B2FC9" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="자주 쓴 명령어 (최근 30일)" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="#1A1B2E" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<ItemsControl ItemsSource="{Binding TopCommands}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type vm:CommandStatItem}">
|
||||
<Grid Margin="0,0,0,8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="22"/>
|
||||
<ColumnDefinition Width="140"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="44"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- 순위 -->
|
||||
<TextBlock Grid.Column="0" Text="{Binding Rank}"
|
||||
FontSize="11" FontWeight="Bold" Foreground="#BBBBCC"
|
||||
VerticalAlignment="Center"/>
|
||||
<!-- 명령어 -->
|
||||
<TextBlock Grid.Column="1" Text="{Binding Command}"
|
||||
FontSize="12" FontFamily="Consolas"
|
||||
Foreground="#2B3280" FontWeight="SemiBold"
|
||||
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
|
||||
<!-- 진행 바 -->
|
||||
<Border Grid.Column="2" Height="8" CornerRadius="4"
|
||||
Background="#EEEEF8" VerticalAlignment="Center">
|
||||
<Border CornerRadius="4" Background="#8B2FC9"
|
||||
HorizontalAlignment="Left"
|
||||
Width="{Binding BarWidth}"/>
|
||||
</Border>
|
||||
<!-- 횟수 -->
|
||||
<TextBlock Grid.Column="3" Text="{Binding Count}"
|
||||
FontSize="11" Foreground="#9999BB"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<!-- 데이터 없을 때 -->
|
||||
<TextBlock FontSize="12" Foreground="#AAAACC"
|
||||
HorizontalAlignment="Center" Margin="0,8">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Visibility" Value="Collapsed"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding TopCommands.Count}" Value="0">
|
||||
<Setter Property="Visibility" Value="Visible"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
아직 명령어 사용 데이터가 없습니다.
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── 요일별 평균 호출 패턴 ── -->
|
||||
<Border Background="#F5F7FF" CornerRadius="14" Padding="18,14" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,14">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="14" Foreground="#6366F1" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="요일별 평균 호출 (30일)" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="#1A1B2E" VerticalAlignment="Center"/>
|
||||
<TextBlock Text=" · 어느 요일에 가장 활발한지" FontSize="11"
|
||||
Foreground="#AAAACC" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<ItemsControl ItemsSource="{Binding WeekdayAvgBars}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type vm:DayBarItem}">
|
||||
<Grid Width="60" Height="100" VerticalAlignment="Bottom" Margin="4,0"
|
||||
ToolTip="{Binding ToolTipText}">
|
||||
<!-- 값 레이블 -->
|
||||
<TextBlock Text="{Binding ValueLabel}"
|
||||
FontSize="10" Foreground="#6366F1" FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Top"/>
|
||||
<!-- 막대 -->
|
||||
<Border CornerRadius="6,6,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Width="36"
|
||||
Height="{Binding BarHeight}"
|
||||
Margin="0,0,0,22">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#C7D2FE"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsToday}" Value="True">
|
||||
<Setter Property="Background" Value="#6366F1"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding HasData}" Value="False">
|
||||
<Setter Property="Background" Value="#EEEEF8"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
<!-- 요일 레이블 -->
|
||||
<TextBlock Text="{Binding DayLabel}"
|
||||
FontSize="12" FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Bottom">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="#9CA3AF"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsToday}" Value="True">
|
||||
<Setter Property="Foreground" Value="#6366F1"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ═══ AX Agent 탭 ═══ -->
|
||||
<ScrollViewer x:Name="PanelAgent" Grid.Row="1" VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled" Visibility="Collapsed">
|
||||
<StackPanel Margin="24,14,24,24">
|
||||
|
||||
<!-- ── AX Agent 대화 빈도 (탭별) ── -->
|
||||
<Border Background="#F0F4FF" CornerRadius="14" Padding="18,16" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="14" Foreground="#4B5EFC" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="AX Agent 대화 빈도 (최근 14일)" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="#1A1B2E" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding AgentSummary}" FontSize="11" Foreground="#6B7280" Margin="0,0,0,10"/>
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<ItemsControl ItemsSource="{Binding ChatCountBars}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><StackPanel Orientation="Horizontal"/></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type vm:DayBarItem}">
|
||||
<StackPanel Width="42" Margin="2,0" ToolTip="{Binding ToolTipText}">
|
||||
<TextBlock Text="{Binding ValueLabel}" FontSize="9" Foreground="#4B5EFC"
|
||||
HorizontalAlignment="Center" Margin="0,0,0,2"/>
|
||||
<Border Height="88" VerticalAlignment="Bottom" CornerRadius="4,4,0,0">
|
||||
<Border CornerRadius="4,4,0,0" VerticalAlignment="Bottom"
|
||||
Height="{Binding BarHeight}">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#818CF8"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsToday}" Value="True">
|
||||
<Setter Property="Background" Value="#4B5EFC"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding HasData}" Value="False">
|
||||
<Setter Property="Background" Value="#E5E7EB"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding DayLabel}" FontSize="10" Foreground="#6B7280"
|
||||
HorizontalAlignment="Center" Margin="0,4,0,0"/>
|
||||
<TextBlock Text="{Binding DateLabel}" FontSize="8" Foreground="#9CA3AF"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── AX Agent 토큰 사용량 ── -->
|
||||
<Border Background="#FFF7ED" CornerRadius="14" Padding="18,16" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,14">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="14" Foreground="#D97706" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="토큰 사용량 (최근 14일)" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="#1A1B2E" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<ItemsControl ItemsSource="{Binding TokenUsageBars}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><StackPanel Orientation="Horizontal"/></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type vm:DayBarItem}">
|
||||
<StackPanel Width="42" Margin="2,0" ToolTip="{Binding ToolTipText}">
|
||||
<TextBlock Text="{Binding ValueLabel}" FontSize="9" Foreground="#D97706"
|
||||
HorizontalAlignment="Center" Margin="0,0,0,2"/>
|
||||
<Border Height="88" VerticalAlignment="Bottom" CornerRadius="4,4,0,0">
|
||||
<Border CornerRadius="4,4,0,0" VerticalAlignment="Bottom"
|
||||
Height="{Binding BarHeight}">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#FBBF24"/>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsToday}" Value="True">
|
||||
<Setter Property="Background" Value="#D97706"/>
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding HasData}" Value="False">
|
||||
<Setter Property="Background" Value="#E5E7EB"/>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding DayLabel}" FontSize="10" Foreground="#6B7280"
|
||||
HorizontalAlignment="Center" Margin="0,4,0,0"/>
|
||||
<TextBlock Text="{Binding DateLabel}" FontSize="8" Foreground="#9CA3AF"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── 탭별 대화 비율 (Chat/Cowork/Code) ── -->
|
||||
<Border Background="#F5F0FF" CornerRadius="14" Padding="18,16" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,14">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="14" Foreground="#8B5CF6" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="탭별 대화 비율 (30일)" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="#1A1B2E" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<ItemsControl ItemsSource="{Binding TabChatRatios}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type vm:TabRatioItem}">
|
||||
<Grid Margin="0,0,0,10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="65"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- 탭 이름 -->
|
||||
<TextBlock Grid.Column="0" Text="{Binding TabName}"
|
||||
FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="#333355" VerticalAlignment="Center"/>
|
||||
<!-- 진행 바 -->
|
||||
<Border Grid.Column="1" Height="14" CornerRadius="7"
|
||||
Background="#EEEEF8" VerticalAlignment="Center">
|
||||
<Border CornerRadius="7"
|
||||
HorizontalAlignment="Left"
|
||||
Width="{Binding BarWidth}"
|
||||
Background="{Binding Color}"/>
|
||||
</Border>
|
||||
<!-- 비율 -->
|
||||
<TextBlock Grid.Column="2" Text="{Binding PercentLabel}"
|
||||
FontSize="11" Foreground="#9999BB"
|
||||
VerticalAlignment="Center" Margin="10,0,0,0"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── 프롬프트/완료 토큰 비율 ── -->
|
||||
<Border Background="#F0FFF5" CornerRadius="14" Padding="18,16" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="14" Foreground="#10B981" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="입력/출력 토큰 비율 (30일)" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="#1A1B2E" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding TokenRatioSummary}" FontSize="11" Foreground="#6B7280" Margin="0,0,0,12"/>
|
||||
<!-- 스택 바 -->
|
||||
<Border CornerRadius="8" Height="24" ClipToBounds="True" Background="#E5E7EB" Margin="0,0,0,8">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Border Background="#06B6D4" CornerRadius="8,0,0,8"
|
||||
Width="{Binding PromptBarWidth}"/>
|
||||
<Border Background="#F59E0B" CornerRadius="0,8,8,0"
|
||||
Width="{Binding CompletionBarWidth}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<!-- 범례 -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
|
||||
<Border Width="12" Height="12" CornerRadius="3" Background="#06B6D4" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="{Binding PromptTokenLabel}" FontSize="11" Foreground="#555577"
|
||||
Margin="0,0,20,0" VerticalAlignment="Center"/>
|
||||
<Border Width="12" Height="12" CornerRadius="3" Background="#F59E0B" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="{Binding CompletionTokenLabel}" FontSize="11" Foreground="#555577"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ── 즐겨찾기 상위 항목 ── -->
|
||||
<Border Background="#FFF9F0" CornerRadius="14" Padding="18,16"
|
||||
Visibility="{Binding HasFavorites, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,14">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="14" Foreground="#D97706" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="즐겨찾기 목록 (최근 추가순)" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="#1A1B2E" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<ItemsControl ItemsSource="{Binding TopFavorites}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type vm:FavoriteStatItem}">
|
||||
<Grid Margin="0,0,0,7">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="22"/>
|
||||
<ColumnDefinition Width="22"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<!-- 순위 -->
|
||||
<TextBlock Grid.Column="0" Text="{Binding Rank}"
|
||||
FontSize="11" FontWeight="Bold" Foreground="#DDBB88"
|
||||
VerticalAlignment="Center"/>
|
||||
<!-- 아이콘 -->
|
||||
<TextBlock Grid.Column="1" Text="{Binding Icon}"
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="13"
|
||||
Foreground="#D97706" VerticalAlignment="Center"/>
|
||||
<!-- 이름 + 경로 -->
|
||||
<StackPanel Grid.Column="2" VerticalAlignment="Center" Margin="4,0,0,0">
|
||||
<TextBlock Text="{Binding Name}" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="#2B3280" TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="{Binding Path}" FontSize="10" FontFamily="Consolas"
|
||||
Foreground="#AAAACC" TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- ── 리사이즈 그립 ── -->
|
||||
<Border Grid.Row="1"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Bottom"
|
||||
Width="22" Height="22" Margin="0,0,4,4"
|
||||
Cursor="SizeNWSE" Background="Transparent"
|
||||
Panel.ZIndex="100"
|
||||
MouseLeftButtonDown="ResizeGrip_MouseLeftButtonDown">
|
||||
<Canvas Width="13" Height="13">
|
||||
<Rectangle Canvas.Left="10" Canvas.Top="10" Width="2.5" Height="2.5" RadiusX="1.2" RadiusY="1.2" Fill="#60444488"/>
|
||||
<Rectangle Canvas.Left="6" Canvas.Top="10" Width="2.5" Height="2.5" RadiusX="1.2" RadiusY="1.2" Fill="#40444488"/>
|
||||
<Rectangle Canvas.Left="10" Canvas.Top="6" Width="2.5" Height="2.5" RadiusX="1.2" RadiusY="1.2" Fill="#40444488"/>
|
||||
<Rectangle Canvas.Left="2" Canvas.Top="10" Width="2.5" Height="2.5" RadiusX="1.2" RadiusY="1.2" Fill="#25444488"/>
|
||||
<Rectangle Canvas.Left="6" Canvas.Top="6" Width="2.5" Height="2.5" RadiusX="1.2" RadiusY="1.2" Fill="#25444488"/>
|
||||
<Rectangle Canvas.Left="10" Canvas.Top="2" Width="2.5" Height="2.5" RadiusX="1.2" RadiusY="1.2" Fill="#25444488"/>
|
||||
</Canvas>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
49
src/AxCopilot/Views/StatisticsWindow.xaml.cs
Normal file
49
src/AxCopilot/Views/StatisticsWindow.xaml.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using AxCopilot.ViewModels;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class StatisticsWindow : Window
|
||||
{
|
||||
[DllImport("user32.dll")] private static extern void ReleaseCapture();
|
||||
[DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
|
||||
private const int WM_NCLBUTTONDOWN = 0xA1;
|
||||
private const int HTBOTTOMRIGHT = 17;
|
||||
|
||||
private readonly StatisticsViewModel _vm;
|
||||
|
||||
public StatisticsWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
_vm = new StatisticsViewModel();
|
||||
DataContext = _vm;
|
||||
}
|
||||
|
||||
private void Window_MouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ChangedButton == MouseButton.Left && e.LeftButton == MouseButtonState.Pressed)
|
||||
try { DragMove(); } catch { }
|
||||
}
|
||||
|
||||
private void ResizeGrip_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ButtonState != MouseButtonState.Pressed) return;
|
||||
ReleaseCapture();
|
||||
SendMessage(new WindowInteropHelper(this).Handle, WM_NCLBUTTONDOWN, (IntPtr)HTBOTTOMRIGHT, IntPtr.Zero);
|
||||
}
|
||||
|
||||
private void Close_Click(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private void Refresh_Click(object sender, RoutedEventArgs e) => _vm.Refresh();
|
||||
|
||||
private void StatsTab_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (PanelMain == null || PanelCommander == null || PanelAgent == null) return;
|
||||
PanelMain.Visibility = StatsTabMain.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
PanelCommander.Visibility = StatsTabCommander.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
PanelAgent.Visibility = StatsTabAgent.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
37
src/AxCopilot/Views/TextActionPopup.xaml
Normal file
37
src/AxCopilot/Views/TextActionPopup.xaml
Normal file
@@ -0,0 +1,37 @@
|
||||
<Window x:Class="AxCopilot.Views.TextActionPopup"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
ShowInTaskbar="False"
|
||||
Topmost="True"
|
||||
ResizeMode="NoResize"
|
||||
SizeToContent="WidthAndHeight"
|
||||
Deactivated="Window_Deactivated"
|
||||
KeyDown="Window_KeyDown">
|
||||
|
||||
<Border CornerRadius="12"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource AccentColor}"
|
||||
BorderThickness="1"
|
||||
Padding="6"
|
||||
MinWidth="260">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect Color="Black" BlurRadius="20" ShadowDepth="4" Opacity="0.3"/>
|
||||
</Border.Effect>
|
||||
<StackPanel>
|
||||
<!-- 선택된 텍스트 미리보기 -->
|
||||
<Border Background="{DynamicResource HintBackground}" CornerRadius="8" Padding="10,6" Margin="0,0,0,4">
|
||||
<TextBlock x:Name="PreviewText"
|
||||
FontSize="11" FontStyle="Italic"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxWidth="300"/>
|
||||
</Border>
|
||||
|
||||
<!-- 메뉴 항목들 -->
|
||||
<StackPanel x:Name="MenuItems"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Window>
|
||||
204
src/AxCopilot/Views/TextActionPopup.xaml.cs
Normal file
204
src/AxCopilot/Views/TextActionPopup.xaml.cs
Normal file
@@ -0,0 +1,204 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 텍스트가 선택된 상태에서 핫키를 누르면 커서 위치에 표시되는 액션 선택 팝업.
|
||||
/// ↑↓ 화살표로 이동, Enter로 실행, Esc로 닫기.
|
||||
/// </summary>
|
||||
public partial class TextActionPopup : Window
|
||||
{
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool GetCursorPos(out POINT lpPoint);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct POINT { public int X; public int Y; }
|
||||
|
||||
public enum ActionResult { None, OpenLauncher, Translate, Summarize, GrammarFix, Explain, Rewrite }
|
||||
|
||||
public ActionResult SelectedAction { get; private set; } = ActionResult.None;
|
||||
public string SelectedText { get; private set; } = "";
|
||||
|
||||
private int _selectedIndex;
|
||||
private readonly List<(ActionResult Action, string Icon, string Label)> _items;
|
||||
|
||||
// 전체 명령 정의 (key → ActionResult, icon, label)
|
||||
private static readonly (string Key, ActionResult Action, string Icon, string Label)[] AllCommands =
|
||||
{
|
||||
("translate", ActionResult.Translate, "\uE774", "번역"),
|
||||
("summarize", ActionResult.Summarize, "\uE8D2", "요약"),
|
||||
("grammar", ActionResult.GrammarFix, "\uE8AC", "문법 교정"),
|
||||
("explain", ActionResult.Explain, "\uE946", "설명"),
|
||||
("rewrite", ActionResult.Rewrite, "\uE70F", "다시 쓰기"),
|
||||
};
|
||||
|
||||
/// <summary>사용 가능한 전체 명령 키와 라벨 목록.</summary>
|
||||
public static IReadOnlyList<(string Key, string Label)> AvailableCommands
|
||||
=> AllCommands.Select(c => (c.Key, c.Label)).ToList();
|
||||
|
||||
public TextActionPopup(string selectedText, List<string>? enabledCommands = null)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
SelectedText = selectedText;
|
||||
var preview = selectedText.Replace("\r\n", " ").Replace("\n", " ");
|
||||
PreviewText.Text = preview.Length > 60 ? preview[..57] + "…" : preview;
|
||||
|
||||
// 설정에서 활성화된 명령만 표시
|
||||
var enabled = enabledCommands ?? AllCommands.Select(c => c.Key).ToList();
|
||||
|
||||
_items = new() { (ActionResult.OpenLauncher, "\uE8A7", "AX Commander 열기") };
|
||||
foreach (var cmd in AllCommands)
|
||||
{
|
||||
if (enabled.Contains(cmd.Key, StringComparer.OrdinalIgnoreCase))
|
||||
_items.Add((cmd.Action, cmd.Icon, cmd.Label));
|
||||
}
|
||||
|
||||
BuildMenuItems();
|
||||
PositionAtCursor();
|
||||
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
Focus();
|
||||
UpdateSelection();
|
||||
};
|
||||
}
|
||||
|
||||
private void BuildMenuItems()
|
||||
{
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
|
||||
for (int i = 0; i < _items.Count; i++)
|
||||
{
|
||||
var (action, icon, label) = _items[i];
|
||||
var idx = i;
|
||||
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 14,
|
||||
Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
Width = 20,
|
||||
TextAlignment = TextAlignment.Center,
|
||||
});
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 13,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
|
||||
var border = new Border
|
||||
{
|
||||
Child = sp,
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Padding = new Thickness(10, 8, 14, 8),
|
||||
Margin = new Thickness(0, 1, 0, 1),
|
||||
Background = Brushes.Transparent,
|
||||
Cursor = Cursors.Hand,
|
||||
Tag = idx,
|
||||
};
|
||||
|
||||
border.MouseEnter += (s, _) =>
|
||||
{
|
||||
if (s is Border b && b.Tag is int n)
|
||||
{
|
||||
_selectedIndex = n;
|
||||
UpdateSelection();
|
||||
}
|
||||
};
|
||||
|
||||
border.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
SelectedAction = action;
|
||||
Close();
|
||||
};
|
||||
|
||||
MenuItems.Children.Add(border);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSelection()
|
||||
{
|
||||
var hoverBrush = TryFindResource("ItemHoverBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
var selectedBrush = TryFindResource("ItemSelectedBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x44, 0x4B, 0x5E, 0xFC));
|
||||
|
||||
for (int i = 0; i < MenuItems.Children.Count; i++)
|
||||
{
|
||||
if (MenuItems.Children[i] is Border b)
|
||||
b.Background = i == _selectedIndex ? selectedBrush : Brushes.Transparent;
|
||||
}
|
||||
}
|
||||
|
||||
private void PositionAtCursor()
|
||||
{
|
||||
GetCursorPos(out var pt);
|
||||
|
||||
// DPI 보정
|
||||
var source = PresentationSource.FromVisual(this);
|
||||
double dpiX = 1.0, dpiY = 1.0;
|
||||
if (source?.CompositionTarget != null)
|
||||
{
|
||||
dpiX = source.CompositionTarget.TransformFromDevice.M11;
|
||||
dpiY = source.CompositionTarget.TransformFromDevice.M22;
|
||||
}
|
||||
|
||||
Left = pt.X * dpiX;
|
||||
Top = pt.Y * dpiY + 10; // 커서 아래쪽에 표시
|
||||
|
||||
// 화면 밖으로 나가지 않도록 보정
|
||||
var screen = SystemParameters.WorkArea;
|
||||
if (Left + 280 > screen.Right) Left = screen.Right - 280;
|
||||
if (Top + 300 > screen.Bottom) Top = pt.Y * dpiY - 300; // 위쪽으로
|
||||
if (Left < screen.Left) Left = screen.Left;
|
||||
if (Top < screen.Top) Top = screen.Top;
|
||||
}
|
||||
|
||||
private void Window_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Up:
|
||||
_selectedIndex = (_selectedIndex - 1 + _items.Count) % _items.Count;
|
||||
UpdateSelection();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Down:
|
||||
_selectedIndex = (_selectedIndex + 1) % _items.Count;
|
||||
UpdateSelection();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Enter:
|
||||
SelectedAction = _items[_selectedIndex].Action;
|
||||
Close();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Escape:
|
||||
SelectedAction = ActionResult.None;
|
||||
Close();
|
||||
e.Handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void Window_Deactivated(object? sender, EventArgs e)
|
||||
{
|
||||
// 포커스를 잃으면 자동 닫기
|
||||
if (IsVisible) Close();
|
||||
}
|
||||
}
|
||||
373
src/AxCopilot/Views/TrayContextMenu.cs
Normal file
373
src/AxCopilot/Views/TrayContextMenu.cs
Normal file
@@ -0,0 +1,373 @@
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Text;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 모던 카드 스타일 트레이 컨텍스트 메뉴 팩토리.
|
||||
/// 배경색은 현재 WPF 테마 리소스를 실시간으로 읽어 적용합니다.
|
||||
/// </summary>
|
||||
internal static class TrayContextMenu
|
||||
{
|
||||
// Segoe MDL2 Assets 글리프 상수
|
||||
internal const string GlyphOpen = "\uE7C5"; // Launch
|
||||
internal const string GlyphSettings = "\uE713"; // Settings
|
||||
internal const string GlyphReload = "\uE72C"; // Refresh
|
||||
internal const string GlyphFolder = "\uE838"; // Folder
|
||||
internal const string GlyphInfo = "\uE946"; // Info
|
||||
internal const string GlyphStats = "\uE9D9"; // Chart
|
||||
internal const string GlyphChat = "\uE8BD"; // Chat
|
||||
internal const string GlyphGuide = "\uE736"; // ReadingList (사용 가이드)
|
||||
internal const string GlyphAutoRun = "\uE82F"; // Lightbulb (전구)
|
||||
internal const string GlyphExit = "\uE711"; // Cancel
|
||||
|
||||
// 1×1 투명 더미 이미지 — OnRenderItemImage 콜백을 강제로 트리거하기 위함
|
||||
private static readonly System.Drawing.Bitmap DummyImage = new(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// 현재 화면의 DPI 배율을 반환합니다 (100% = 1.0, 150% = 1.5).
|
||||
/// WinForms는 논리 픽셀 값을 DPI 배율만큼 자동 확대하므로,
|
||||
/// 이 값으로 역산하여 모든 PC에서 동일한 물리적 크기를 유지합니다.
|
||||
/// </summary>
|
||||
private static float GetDpiScale()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var g = System.Drawing.Graphics.FromHwnd(IntPtr.Zero);
|
||||
return Math.Max(1f, g.DpiX / 96f);
|
||||
}
|
||||
catch { return 1f; }
|
||||
}
|
||||
|
||||
/// <summary>논리 픽셀 값을 DPI 역산하여 물리적 크기가 동일하게 유지되도록 변환합니다.</summary>
|
||||
private static int Dp(int logicalPx, float dpiScale) =>
|
||||
Math.Max(1, (int)Math.Round(logicalPx / dpiScale));
|
||||
|
||||
/// <summary>외부에서 DPI 보정된 Padding을 생성할 때 사용합니다.</summary>
|
||||
public static System.Windows.Forms.Padding DpiPadding(int left, int top, int right, int bottom)
|
||||
{
|
||||
var s = GetDpiScale();
|
||||
return new System.Windows.Forms.Padding(Dp(left, s), Dp(top, s), Dp(right, s), Dp(bottom, s));
|
||||
}
|
||||
|
||||
public static ToolStripMenuItem MakeItem(string text, string glyph, EventHandler onClick)
|
||||
{
|
||||
var s = GetDpiScale();
|
||||
var item = new ToolStripMenuItem(text)
|
||||
{
|
||||
Tag = glyph,
|
||||
Image = DummyImage, // Image가 있어야 OnRenderItemImage가 호출됨
|
||||
Padding = new Padding(Dp(4, s), Dp(10, s), Dp(16, s), Dp(10, s)),
|
||||
};
|
||||
item.Click += onClick;
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>패딩이 적용된 ContextMenuStrip을 생성합니다.</summary>
|
||||
public static ContextMenuStrip CreateMenu()
|
||||
{
|
||||
var s = GetDpiScale();
|
||||
var menu = new ContextMenuStrip();
|
||||
menu.Renderer = new ModernTrayRenderer();
|
||||
// Point 단위 폰트는 DPI 자동 확대되므로 역산 적용
|
||||
// Point 단위는 DPI와 무관하게 동일한 물리 크기 → 역산하지 않음
|
||||
menu.Font = new Font("Segoe UI", 10f, GraphicsUnit.Point);
|
||||
menu.ShowImageMargin = true;
|
||||
// 너비를 넉넉히 잡아 좌측 아이콘~테두리 간 여백 확보
|
||||
menu.ImageScalingSize = new Size(Dp(52, s), Dp(32, s));
|
||||
menu.MinimumSize = new Size(Dp(280, s), 0);
|
||||
|
||||
// 팝업이 열릴 때 모서리를 둥글게 클리핑
|
||||
menu.Opened += (_, _) => ApplyRoundedCorners(menu);
|
||||
return menu;
|
||||
}
|
||||
|
||||
private static void ApplyRoundedCorners(ContextMenuStrip menu)
|
||||
{
|
||||
try
|
||||
{
|
||||
var rgn = CreateRoundRectRgn(0, 0, menu.Width, menu.Height, 16, 16);
|
||||
if (rgn == IntPtr.Zero) return;
|
||||
// SetWindowRgn 성공 시 시스템이 핸들 소유권을 가져감 → DeleteObject 불필요
|
||||
// 실패 시 직접 해제해야 GDI 누수 방지
|
||||
if (SetWindowRgn(menu.Handle, rgn, true) == 0)
|
||||
DeleteObject(rgn);
|
||||
}
|
||||
catch { /* 클리핑 실패 시 사각형으로 폴백 */ }
|
||||
}
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
private static extern IntPtr CreateRoundRectRgn(int x1, int y1, int x2, int y2, int cx, int cy);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
private static extern bool DeleteObject(IntPtr hObject);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern int SetWindowRgn(IntPtr hWnd, IntPtr hRgn, bool bRedraw);
|
||||
|
||||
/// <summary>
|
||||
/// 모든 아이템 추가 후 호출하여 상하 테두리 여백을 적용합니다.
|
||||
/// WinForms ContextMenuStrip은 Padding/DefaultPadding/DisplayRectangle 모두
|
||||
/// 내부 레이아웃이 무시하므로, 첫/마지막 아이템의 Margin으로 처리합니다.
|
||||
/// </summary>
|
||||
public static void ApplySpacing(ContextMenuStrip menu, int top = 10, int bottom = 10, int left = 0, int right = 0)
|
||||
{
|
||||
if (menu.Items.Count == 0) return;
|
||||
var s = GetDpiScale();
|
||||
int t = Dp(top, s), b = Dp(bottom, s), l = Dp(left, s), r = Dp(right, s);
|
||||
|
||||
foreach (ToolStripItem item in menu.Items)
|
||||
{
|
||||
var m = item.Margin;
|
||||
item.Margin = new Padding(m.Left + l, m.Top, m.Right + r, m.Bottom);
|
||||
}
|
||||
var first = menu.Items[0];
|
||||
first.Margin = new Padding(first.Margin.Left, t, first.Margin.Right, first.Margin.Bottom);
|
||||
var last = menu.Items[menu.Items.Count - 1];
|
||||
last.Margin = new Padding(last.Margin.Left, last.Margin.Top, last.Margin.Right, b);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
internal sealed class ModernTrayRenderer : ToolStripProfessionalRenderer
|
||||
{
|
||||
// 전구(자동 실행) 활성 색상 — 앰버 글로우
|
||||
private static readonly Color BulbOnColor = Color.FromArgb(255, 185, 0);
|
||||
private static readonly Color BulbGlowHalo = Color.FromArgb(50, 255, 185, 0);
|
||||
|
||||
public ModernTrayRenderer() : base(new ModernColorTable()) { }
|
||||
|
||||
// ─── 테마 색상 리더 ─────────────────────────────────────────────────
|
||||
|
||||
private static Color ThemeColor(string key, Color fallback)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (System.Windows.Application.Current?.Resources[key]
|
||||
is System.Windows.Media.SolidColorBrush b)
|
||||
return Color.FromArgb(b.Color.A, b.Color.R, b.Color.G, b.Color.B);
|
||||
}
|
||||
catch { }
|
||||
return fallback;
|
||||
}
|
||||
|
||||
private static Color BgColor => ThemeColor("LauncherBackground", Color.FromArgb(250, 250, 252));
|
||||
private static Color HoverColor => ThemeColor("ItemHoverBackground", Color.FromArgb(234, 234, 247));
|
||||
private static Color AccentColor => ThemeColor("AccentColor", Color.FromArgb(75, 94, 252));
|
||||
private static Color TextColor => ThemeColor("PrimaryText", Color.FromArgb(22, 23, 42));
|
||||
private static Color TextDimColor => ThemeColor("SecondaryText", Color.FromArgb(90, 92, 128));
|
||||
private static Color SepColor => ThemeColor("SeparatorColor", Color.FromArgb(228, 228, 242));
|
||||
private static Color BorderColor => ThemeColor("BorderColor", Color.FromArgb(210, 210, 232));
|
||||
private static Color IconColor => ThemeColor("SecondaryText", Color.FromArgb(110, 112, 148));
|
||||
|
||||
// ─── 배경 ────────────────────────────────────────────────────────────
|
||||
|
||||
protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e)
|
||||
{
|
||||
using var br = new SolidBrush(BgColor);
|
||||
e.Graphics.FillRectangle(br, e.AffectedBounds);
|
||||
}
|
||||
|
||||
protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e)
|
||||
{
|
||||
var g = e.Graphics;
|
||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
using var pen = new Pen(BorderColor, 1f);
|
||||
// ApplyRoundedCorners 의 clip radius(14) 보다 0.5px 안쪽에 그려야
|
||||
// 창 경계 밖으로 삐져나오지 않음
|
||||
var rect = new RectangleF(0.5f, 0.5f,
|
||||
e.ToolStrip.Width - 1f,
|
||||
e.ToolStrip.Height - 1f);
|
||||
using var path = BuildRoundedPath(rect, 15f);
|
||||
g.DrawPath(pen, path);
|
||||
g.SmoothingMode = SmoothingMode.Default;
|
||||
}
|
||||
|
||||
protected override void OnRenderImageMargin(ToolStripRenderEventArgs e)
|
||||
{
|
||||
// 거터 없음 — 전체 배경과 동일한 색
|
||||
using var br = new SolidBrush(BgColor);
|
||||
e.Graphics.FillRectangle(br, e.AffectedBounds);
|
||||
}
|
||||
|
||||
// ─── 아이템 배경 ─────────────────────────────────────────────────────
|
||||
|
||||
protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e)
|
||||
{
|
||||
var g = e.Graphics;
|
||||
var item = e.Item;
|
||||
|
||||
// 기본 배경 (항상 메뉴 배경색으로 초기화)
|
||||
using var bg = new SolidBrush(BgColor);
|
||||
g.FillRectangle(bg, 0, 0, item.Width, item.Height);
|
||||
|
||||
if (!item.Selected || !item.Enabled) return;
|
||||
|
||||
// 호버: 안쪽에 둥근 모서리 하이라이트
|
||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
using var hoverBr = new SolidBrush(HoverColor);
|
||||
DrawRoundedFill(g, hoverBr, new RectangleF(6, 2, item.Width - 12, item.Height - 4), 8f);
|
||||
g.SmoothingMode = SmoothingMode.Default;
|
||||
}
|
||||
|
||||
// ─── 구분선 ──────────────────────────────────────────────────────────
|
||||
|
||||
protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e)
|
||||
{
|
||||
int y = e.Item.Height / 2;
|
||||
using var pen = new Pen(SepColor);
|
||||
e.Graphics.DrawLine(pen, 16, y, e.Item.Width - 16, y);
|
||||
}
|
||||
|
||||
// ─── 텍스트 ──────────────────────────────────────────────────────────
|
||||
|
||||
protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e)
|
||||
{
|
||||
e.TextColor = e.Item.Enabled ? TextColor : TextDimColor;
|
||||
e.TextFormat = TextFormatFlags.Left | TextFormatFlags.VerticalCenter;
|
||||
base.OnRenderItemText(e);
|
||||
}
|
||||
|
||||
// ─── 아이콘 (일반 아이템 + CheckOnClick 모두 처리) ───────────────────
|
||||
|
||||
protected override void OnRenderItemImage(ToolStripItemImageRenderEventArgs e)
|
||||
{
|
||||
// 모든 아이템의 글리프를 여기서 그린다
|
||||
DrawGlyph(e.Graphics, e.Item, e.ImageRectangle);
|
||||
}
|
||||
|
||||
protected override void OnRenderItemCheck(ToolStripItemImageRenderEventArgs e)
|
||||
{
|
||||
// CheckOnClick 항목: 기본 체크마크 대신 글리프 아이콘을 그린다
|
||||
DrawGlyph(e.Graphics, e.Item, e.ImageRectangle);
|
||||
}
|
||||
|
||||
// ─── 글리프 렌더러 ───────────────────────────────────────────────────
|
||||
|
||||
private static void DrawGlyph(Graphics g, ToolStripItem item, Rectangle bounds)
|
||||
{
|
||||
if (item.Tag is not string glyph || string.IsNullOrEmpty(glyph)) return;
|
||||
if (bounds.Width <= 0 || bounds.Height <= 0) return;
|
||||
|
||||
g.TextRenderingHint = TextRenderingHint.AntiAlias;
|
||||
|
||||
bool isLightbulb = glyph == TrayContextMenu.GlyphAutoRun;
|
||||
bool isChecked = item is ToolStripMenuItem { Checked: true };
|
||||
|
||||
Color glyphColor;
|
||||
if (isLightbulb && isChecked)
|
||||
{
|
||||
// 전구 켜짐 — 앰버 글로우 효과
|
||||
DrawLightbulbGlow(g, glyph, bounds);
|
||||
glyphColor = BulbOnColor;
|
||||
}
|
||||
else if (isLightbulb && !isChecked)
|
||||
{
|
||||
// 전구 꺼짐 — 확실히 구분되는 진한 회색
|
||||
glyphColor = Color.FromArgb(120, 120, 140);
|
||||
}
|
||||
else if (item.Selected && item.Enabled)
|
||||
{
|
||||
glyphColor = AccentColor;
|
||||
}
|
||||
else
|
||||
{
|
||||
glyphColor = IconColor;
|
||||
}
|
||||
|
||||
// Point 단위는 DPI와 무관하게 동일한 물리 크기 → 역산하지 않음
|
||||
using var font = new Font("Segoe MDL2 Assets", 12f, GraphicsUnit.Point);
|
||||
using var br = new SolidBrush(glyphColor);
|
||||
var fmt = StringFormat.GenericTypographic;
|
||||
var size = g.MeasureString(glyph, font, 0, fmt);
|
||||
// 넓은 아이콘 영역 내 중앙 정렬
|
||||
float x = bounds.X + (bounds.Width - size.Width) / 2f + 2f;
|
||||
float y = bounds.Y + (bounds.Height - size.Height) / 2f;
|
||||
g.DrawString(glyph, font, br, x, y, fmt);
|
||||
}
|
||||
|
||||
private static void DrawLightbulbGlow(Graphics g, string glyph, Rectangle bounds)
|
||||
{
|
||||
// 중심에서 균일하게 퍼지는 원형 앰버 글로우
|
||||
g.SmoothingMode = SmoothingMode.AntiAlias;
|
||||
float cx = bounds.X + bounds.Width / 2f;
|
||||
float cy = bounds.Y + bounds.Height / 2f;
|
||||
float radius = Math.Max(bounds.Width, bounds.Height) * 0.85f;
|
||||
|
||||
var glowRect = new RectangleF(cx - radius, cy - radius, radius * 2, radius * 2);
|
||||
using var path = new GraphicsPath();
|
||||
path.AddEllipse(glowRect);
|
||||
using var brush = new PathGradientBrush(path)
|
||||
{
|
||||
CenterColor = Color.FromArgb(60, 255, 185, 0),
|
||||
SurroundColors = new[] { Color.FromArgb(0, 255, 185, 0) },
|
||||
CenterPoint = new PointF(cx, cy)
|
||||
};
|
||||
g.FillEllipse(brush, glowRect);
|
||||
g.SmoothingMode = SmoothingMode.Default;
|
||||
}
|
||||
|
||||
// ─── 유틸 ────────────────────────────────────────────────────────────
|
||||
|
||||
private static GraphicsPath BuildRoundedPath(RectangleF rect, float radius)
|
||||
{
|
||||
float d = radius * 2f;
|
||||
var path = new GraphicsPath();
|
||||
path.AddArc(rect.X, rect.Y, d, d, 180, 90);
|
||||
path.AddArc(rect.Right - d, rect.Y, d, d, 270, 90);
|
||||
path.AddArc(rect.Right - d, rect.Bottom - d, d, d, 0, 90);
|
||||
path.AddArc(rect.X, rect.Bottom - d, d, d, 90, 90);
|
||||
path.CloseFigure();
|
||||
return path;
|
||||
}
|
||||
|
||||
private static void DrawRoundedFill(Graphics g, Brush brush, RectangleF rect, float radius)
|
||||
{
|
||||
using var path = BuildRoundedPath(rect, radius);
|
||||
g.FillPath(brush, path);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
internal sealed class ModernColorTable : ProfessionalColorTable
|
||||
{
|
||||
private static Color TC(string key, Color fb)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (System.Windows.Application.Current?.Resources[key]
|
||||
is System.Windows.Media.SolidColorBrush b)
|
||||
return Color.FromArgb(b.Color.A, b.Color.R, b.Color.G, b.Color.B);
|
||||
}
|
||||
catch { }
|
||||
return fb;
|
||||
}
|
||||
|
||||
private static Color Bg => TC("LauncherBackground", Color.FromArgb(250, 250, 252));
|
||||
private static Color Hover => TC("ItemHoverBackground", Color.FromArgb(234, 234, 247));
|
||||
private static Color Border => TC("BorderColor", Color.FromArgb(210, 210, 232));
|
||||
private static Color Sep => TC("SeparatorColor", Color.FromArgb(228, 228, 242));
|
||||
|
||||
public override Color MenuBorder => Border;
|
||||
public override Color ToolStripDropDownBackground => Bg;
|
||||
public override Color ImageMarginGradientBegin => Bg;
|
||||
public override Color ImageMarginGradientMiddle => Bg;
|
||||
public override Color ImageMarginGradientEnd => Bg;
|
||||
public override Color MenuItemSelected => Hover;
|
||||
public override Color MenuItemSelectedGradientBegin => Hover;
|
||||
public override Color MenuItemSelectedGradientEnd => Hover;
|
||||
public override Color MenuItemBorder => Color.Transparent;
|
||||
public override Color MenuItemPressedGradientBegin => Hover;
|
||||
public override Color MenuItemPressedGradientEnd => Hover;
|
||||
public override Color SeparatorLight => Sep;
|
||||
public override Color SeparatorDark => Sep;
|
||||
public override Color CheckBackground => Color.Transparent;
|
||||
public override Color CheckSelectedBackground => Color.Transparent;
|
||||
public override Color CheckPressedBackground => Color.Transparent;
|
||||
}
|
||||
|
||||
26
src/AxCopilot/Views/TrayMenuWindow.xaml
Normal file
26
src/AxCopilot/Views/TrayMenuWindow.xaml
Normal file
@@ -0,0 +1,26 @@
|
||||
<Window x:Class="AxCopilot.Views.TrayMenuWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
WindowStyle="None"
|
||||
AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
Topmost="True"
|
||||
ShowInTaskbar="False"
|
||||
ResizeMode="NoResize"
|
||||
SizeToContent="WidthAndHeight"
|
||||
Deactivated="Window_Deactivated">
|
||||
|
||||
<Border x:Name="RootBorder"
|
||||
CornerRadius="12"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
Padding="8 10"
|
||||
MinWidth="240">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="20" ShadowDepth="4" Opacity="0.18" Color="Black"/>
|
||||
</Border.Effect>
|
||||
|
||||
<StackPanel x:Name="MenuPanel" />
|
||||
</Border>
|
||||
</Window>
|
||||
271
src/AxCopilot/Views/TrayMenuWindow.xaml.cs
Normal file
271
src/AxCopilot/Views/TrayMenuWindow.xaml.cs
Normal file
@@ -0,0 +1,271 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// WPF 기반 커스텀 트레이 컨텍스트 메뉴.
|
||||
/// WinForms ContextMenuStrip의 DPI 불일치 문제를 근본 해결합니다.
|
||||
/// </summary>
|
||||
public partial class TrayMenuWindow : Window
|
||||
{
|
||||
private System.Windows.Threading.DispatcherTimer? _autoCloseTimer;
|
||||
|
||||
public TrayMenuWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// 마우스가 메뉴 밖으로 나가면 일정 시간 후 자동 닫힘
|
||||
MouseLeave += (_, _) =>
|
||||
{
|
||||
_autoCloseTimer?.Stop();
|
||||
_autoCloseTimer = new System.Windows.Threading.DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(400)
|
||||
};
|
||||
_autoCloseTimer.Tick += (_, _) => { _autoCloseTimer.Stop(); Hide(); };
|
||||
_autoCloseTimer.Start();
|
||||
};
|
||||
|
||||
// 마우스가 다시 메뉴 위로 오면 타이머 취소
|
||||
MouseEnter += (_, _) =>
|
||||
{
|
||||
_autoCloseTimer?.Stop();
|
||||
};
|
||||
}
|
||||
|
||||
// ─── 아이템 빌더 API ─────────────────────────────────────────────────
|
||||
|
||||
/// <summary>일반 메뉴 항목을 추가합니다.</summary>
|
||||
public TrayMenuWindow AddItem(string glyph, string text, Action onClick)
|
||||
{
|
||||
var item = CreateItemBorder(glyph, text);
|
||||
item.MouseLeftButtonUp += (_, _) => { Hide(); onClick(); };
|
||||
MenuPanel.Children.Add(item);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>일반 메뉴 항목을 추가하고 Border 참조를 반환합니다 (동적 가시성 제어용).</summary>
|
||||
public TrayMenuWindow AddItem(string glyph, string text, Action onClick, out Border itemRef)
|
||||
{
|
||||
var item = CreateItemBorder(glyph, text);
|
||||
item.MouseLeftButtonUp += (_, _) => { Hide(); onClick(); };
|
||||
MenuPanel.Children.Add(item);
|
||||
itemRef = item;
|
||||
return this;
|
||||
}
|
||||
|
||||
// 전구 아이콘 활성 색상 — 앰버(노란) 글로우
|
||||
private static readonly Brush BulbOnBrush = new SolidColorBrush(Color.FromRgb(255, 185, 0));
|
||||
private static readonly Brush BulbOffBrush = new SolidColorBrush(Color.FromRgb(120, 120, 140));
|
||||
|
||||
/// <summary>토글(체크) 메뉴 항목을 추가합니다.</summary>
|
||||
public TrayMenuWindow AddToggleItem(string glyph, string text, bool initialChecked,
|
||||
Action<bool> onToggle, out Func<bool> getChecked, out Action<string> setText)
|
||||
{
|
||||
bool isChecked = initialChecked;
|
||||
var glyphBlock = new TextBlock
|
||||
{
|
||||
Text = glyph,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 14,
|
||||
Foreground = isChecked ? BulbOnBrush : BulbOffBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Width = 20,
|
||||
TextAlignment = TextAlignment.Center,
|
||||
};
|
||||
|
||||
// 켜진 상태에서 글로우 이펙트 적용
|
||||
if (isChecked)
|
||||
glyphBlock.Effect = new System.Windows.Media.Effects.DropShadowEffect
|
||||
{
|
||||
Color = Color.FromRgb(255, 185, 0), BlurRadius = 12, ShadowDepth = 0, Opacity = 0.7
|
||||
};
|
||||
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
FontSize = 13,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
label.SetResourceReference(TextBlock.ForegroundProperty, "PrimaryText");
|
||||
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Children = { glyphBlock, label },
|
||||
};
|
||||
|
||||
var border = new Border
|
||||
{
|
||||
Child = panel,
|
||||
CornerRadius = new CornerRadius(6),
|
||||
Padding = new Thickness(12, 8, 16, 8),
|
||||
Cursor = Cursors.Hand,
|
||||
Background = Brushes.Transparent,
|
||||
};
|
||||
|
||||
border.MouseEnter += (_, _) =>
|
||||
border.Background = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
|
||||
border.MouseLeave += (_, _) =>
|
||||
border.Background = Brushes.Transparent;
|
||||
|
||||
border.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
isChecked = !isChecked;
|
||||
glyphBlock.Foreground = isChecked ? BulbOnBrush : BulbOffBrush;
|
||||
glyphBlock.Effect = isChecked
|
||||
? new System.Windows.Media.Effects.DropShadowEffect
|
||||
{ Color = Color.FromRgb(255, 185, 0), BlurRadius = 12, ShadowDepth = 0, Opacity = 0.7 }
|
||||
: null;
|
||||
onToggle(isChecked);
|
||||
};
|
||||
|
||||
getChecked = () => isChecked;
|
||||
setText = t => label.Text = t;
|
||||
MenuPanel.Children.Add(border);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>구분선을 추가합니다.</summary>
|
||||
public TrayMenuWindow AddSeparator()
|
||||
{
|
||||
var sep = new Border
|
||||
{
|
||||
Height = 1,
|
||||
Margin = new Thickness(16, 5, 16, 5),
|
||||
};
|
||||
sep.SetResourceReference(Border.BackgroundProperty, "SeparatorColor");
|
||||
MenuPanel.Children.Add(sep);
|
||||
return this;
|
||||
}
|
||||
|
||||
// ─── 팝업 표시 ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>트레이 아이콘 근처에 메뉴를 표시합니다.</summary>
|
||||
public void ShowAtTray()
|
||||
{
|
||||
var workArea = SystemParameters.WorkArea;
|
||||
|
||||
// 먼저 표시하여 ActualWidth/ActualHeight 확정
|
||||
Opacity = 0;
|
||||
Show();
|
||||
UpdateLayout();
|
||||
|
||||
double menuW = ActualWidth;
|
||||
double menuH = ActualHeight;
|
||||
|
||||
// 마우스 커서 위치 (물리 픽셀 → WPF 논리 좌표)
|
||||
var cursorPos = GetCursorPosition();
|
||||
double dpiScale = VisualTreeHelper.GetDpi(this).PixelsPerDip;
|
||||
double cx = cursorPos.X / dpiScale;
|
||||
double cy = cursorPos.Y / dpiScale;
|
||||
|
||||
// 메뉴 우하단이 커서에서 살짝 떨어지도록 배치
|
||||
double left = cx - menuW - 8;
|
||||
double top = cy - menuH - 8;
|
||||
|
||||
// 화면 밖 보정
|
||||
if (left < workArea.Left) left = workArea.Left + 4;
|
||||
if (top < workArea.Top) top = workArea.Top + 4;
|
||||
if (left + menuW > workArea.Right) left = workArea.Right - menuW - 4;
|
||||
if (top + menuH > workArea.Bottom) top = workArea.Bottom - menuH - 4;
|
||||
|
||||
Left = left;
|
||||
Top = top;
|
||||
|
||||
// 활성화하여 Deactivated 이벤트가 정상 작동하도록 보장
|
||||
Activate();
|
||||
|
||||
// 페이드 인 애니메이션
|
||||
var fadeIn = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(120))
|
||||
{
|
||||
EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut }
|
||||
};
|
||||
BeginAnimation(OpacityProperty, fadeIn);
|
||||
}
|
||||
|
||||
/// <summary>메뉴를 표시하기 직전에 동적 항목을 갱신할 수 있는 이벤트.</summary>
|
||||
public event Action? Opening;
|
||||
|
||||
/// <summary>Opening 이벤트를 트리거하고 메뉴를 표시합니다.</summary>
|
||||
public void ShowWithUpdate()
|
||||
{
|
||||
Opening?.Invoke();
|
||||
ShowAtTray();
|
||||
}
|
||||
|
||||
// ─── 내부 헬퍼 ────────────────────────────────────────────────────────
|
||||
|
||||
private Border CreateItemBorder(string glyph, string text)
|
||||
{
|
||||
var glyphBlock = new TextBlock
|
||||
{
|
||||
Text = glyph,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 14,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Width = 20,
|
||||
TextAlignment = TextAlignment.Center,
|
||||
};
|
||||
glyphBlock.SetResourceReference(TextBlock.ForegroundProperty, "SecondaryText");
|
||||
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
FontSize = 13,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
label.SetResourceReference(TextBlock.ForegroundProperty, "PrimaryText");
|
||||
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Children = { glyphBlock, label },
|
||||
};
|
||||
|
||||
var border = new Border
|
||||
{
|
||||
Child = panel,
|
||||
CornerRadius = new CornerRadius(6),
|
||||
Padding = new Thickness(12, 8, 16, 8),
|
||||
Cursor = Cursors.Hand,
|
||||
Background = Brushes.Transparent,
|
||||
};
|
||||
|
||||
border.MouseEnter += (_, _) =>
|
||||
{
|
||||
border.Background = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent;
|
||||
glyphBlock.Foreground = TryFindResource("AccentColor") as Brush ?? Brushes.Blue;
|
||||
};
|
||||
border.MouseLeave += (_, _) =>
|
||||
{
|
||||
border.Background = Brushes.Transparent;
|
||||
glyphBlock.SetResourceReference(TextBlock.ForegroundProperty, "SecondaryText");
|
||||
};
|
||||
|
||||
return border;
|
||||
}
|
||||
|
||||
private void Window_Deactivated(object sender, EventArgs e)
|
||||
{
|
||||
Hide();
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GetCursorPos(out POINT lpPoint);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct POINT { public int X; public int Y; }
|
||||
|
||||
private static POINT GetCursorPosition()
|
||||
{
|
||||
GetCursorPos(out var pt);
|
||||
return pt;
|
||||
}
|
||||
}
|
||||
345
src/AxCopilot/Views/UserAskDialog.cs
Normal file
345
src/AxCopilot/Views/UserAskDialog.cs
Normal file
@@ -0,0 +1,345 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Effects;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 에이전트가 사용자에게 질문할 때 표시하는 커스텀 대화 상자.
|
||||
/// 선택지 버튼 + 직접 입력 텍스트 박스를 제공합니다.
|
||||
/// </summary>
|
||||
internal sealed class UserAskDialog : Window
|
||||
{
|
||||
private string _selectedResponse = "";
|
||||
private readonly TextBox _customInput;
|
||||
private readonly StackPanel _optionPanel;
|
||||
|
||||
public string SelectedResponse => _selectedResponse;
|
||||
|
||||
private UserAskDialog(string question, List<string> options, string defaultValue)
|
||||
{
|
||||
Width = 460;
|
||||
MinWidth = 380;
|
||||
MaxWidth = 560;
|
||||
SizeToContent = SizeToContent.Height;
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||
ResizeMode = ResizeMode.NoResize;
|
||||
WindowStyle = WindowStyle.None;
|
||||
AllowsTransparency = true;
|
||||
Background = Brushes.Transparent;
|
||||
Topmost = true;
|
||||
|
||||
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
|
||||
?? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC));
|
||||
var borderBrush = Application.Current.TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var itemBg = Application.Current.TryFindResource("ItemBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromRgb(0x2A, 0x2B, 0x40));
|
||||
var hoverBg = Application.Current.TryFindResource("ItemHoverBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
|
||||
// 루트
|
||||
var root = new Border
|
||||
{
|
||||
Background = bgBrush,
|
||||
CornerRadius = new CornerRadius(16),
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(24, 20, 24, 18),
|
||||
Effect = new DropShadowEffect
|
||||
{
|
||||
BlurRadius = 24, ShadowDepth = 4, Opacity = 0.3, Color = Colors.Black,
|
||||
},
|
||||
};
|
||||
|
||||
var stack = new StackPanel();
|
||||
|
||||
// 타이틀 바 (드래그 가능)
|
||||
var titleBar = new Grid { Margin = new Thickness(0, 0, 0, 12) };
|
||||
titleBar.MouseLeftButtonDown += (_, _) => { try { DragMove(); } catch { } };
|
||||
var titleSp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
titleSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\uE9CE",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 16,
|
||||
Foreground = accentBrush,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
});
|
||||
titleSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "AX Agent — 질문",
|
||||
FontSize = 14,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
titleBar.Children.Add(titleSp);
|
||||
stack.Children.Add(titleBar);
|
||||
|
||||
// 구분선
|
||||
stack.Children.Add(new Border
|
||||
{
|
||||
Height = 1,
|
||||
Background = borderBrush,
|
||||
Opacity = 0.3,
|
||||
Margin = new Thickness(0, 0, 0, 14),
|
||||
});
|
||||
|
||||
// 질문 텍스트
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = question,
|
||||
FontSize = 13.5,
|
||||
Foreground = primaryText,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(0, 0, 0, 16),
|
||||
LineHeight = 20,
|
||||
});
|
||||
|
||||
// 선택지 버튼들
|
||||
_optionPanel = new StackPanel { Margin = new Thickness(0, 0, 0, 12) };
|
||||
Border? selectedBorder = null;
|
||||
|
||||
foreach (var option in options)
|
||||
{
|
||||
var capturedOption = option;
|
||||
var optBorder = new Border
|
||||
{
|
||||
Background = itemBg,
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(14, 10, 14, 10),
|
||||
Margin = new Thickness(0, 0, 0, 6),
|
||||
Cursor = Cursors.Hand,
|
||||
BorderBrush = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(1.5),
|
||||
};
|
||||
|
||||
var optSp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
var radioCircle = new Border
|
||||
{
|
||||
Width = 18, Height = 18,
|
||||
CornerRadius = new CornerRadius(9),
|
||||
BorderBrush = secondaryText,
|
||||
BorderThickness = new Thickness(1.5),
|
||||
Margin = new Thickness(0, 0, 10, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Child = new Border
|
||||
{
|
||||
Width = 10, Height = 10,
|
||||
CornerRadius = new CornerRadius(5),
|
||||
Background = Brushes.Transparent,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
};
|
||||
optSp.Children.Add(radioCircle);
|
||||
optSp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = capturedOption,
|
||||
FontSize = 13,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
});
|
||||
optBorder.Child = optSp;
|
||||
|
||||
optBorder.MouseEnter += (s, _) =>
|
||||
{
|
||||
var b = (Border)s;
|
||||
if (b.BorderBrush != accentBrush)
|
||||
b.Background = hoverBg;
|
||||
};
|
||||
optBorder.MouseLeave += (s, _) =>
|
||||
{
|
||||
var b = (Border)s;
|
||||
if (b.BorderBrush != accentBrush)
|
||||
b.Background = itemBg;
|
||||
};
|
||||
optBorder.MouseLeftButtonUp += (s, _) =>
|
||||
{
|
||||
// 이전 선택 해제
|
||||
if (selectedBorder != null)
|
||||
{
|
||||
selectedBorder.BorderBrush = Brushes.Transparent;
|
||||
selectedBorder.Background = itemBg;
|
||||
var prevCircle = FindRadioCircle(selectedBorder);
|
||||
if (prevCircle != null) prevCircle.Background = Brushes.Transparent;
|
||||
}
|
||||
|
||||
// 현재 선택
|
||||
var cur = (Border)s;
|
||||
cur.BorderBrush = accentBrush;
|
||||
cur.Background = new SolidColorBrush(Color.FromArgb(0x15,
|
||||
((SolidColorBrush)accentBrush).Color.R,
|
||||
((SolidColorBrush)accentBrush).Color.G,
|
||||
((SolidColorBrush)accentBrush).Color.B));
|
||||
var circle = FindRadioCircle(cur);
|
||||
if (circle != null) circle.Background = accentBrush;
|
||||
selectedBorder = cur;
|
||||
|
||||
_selectedResponse = capturedOption;
|
||||
if (_customInput != null) _customInput.Text = ""; // 선택지 고르면 직접 입력 초기화
|
||||
};
|
||||
|
||||
_optionPanel.Children.Add(optBorder);
|
||||
}
|
||||
stack.Children.Add(_optionPanel);
|
||||
|
||||
// 직접 입력 영역
|
||||
stack.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "또는 직접 입력:",
|
||||
FontSize = 11.5,
|
||||
Foreground = secondaryText,
|
||||
Margin = new Thickness(2, 0, 0, 6),
|
||||
});
|
||||
|
||||
_customInput = new TextBox
|
||||
{
|
||||
MinHeight = 40,
|
||||
MaxHeight = 100,
|
||||
AcceptsReturn = true,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
FontSize = 13,
|
||||
Background = itemBg,
|
||||
Foreground = primaryText,
|
||||
CaretBrush = primaryText,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(10, 8, 10, 8),
|
||||
Text = defaultValue,
|
||||
};
|
||||
_customInput.TextChanged += (_, _) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_customInput.Text))
|
||||
{
|
||||
// 직접 입력 시 선택 해제
|
||||
if (selectedBorder != null)
|
||||
{
|
||||
selectedBorder.BorderBrush = Brushes.Transparent;
|
||||
selectedBorder.Background = itemBg;
|
||||
var prevCircle = FindRadioCircle(selectedBorder);
|
||||
if (prevCircle != null) prevCircle.Background = Brushes.Transparent;
|
||||
selectedBorder = null;
|
||||
}
|
||||
_selectedResponse = _customInput.Text;
|
||||
}
|
||||
};
|
||||
stack.Children.Add(_customInput);
|
||||
|
||||
// 하단 버튼
|
||||
var btnPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(0, 16, 0, 0),
|
||||
};
|
||||
|
||||
// 확인 버튼
|
||||
var confirmBtn = new Border
|
||||
{
|
||||
Background = accentBrush,
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(20, 9, 20, 9),
|
||||
Cursor = Cursors.Hand,
|
||||
Margin = new Thickness(8, 0, 0, 0),
|
||||
};
|
||||
confirmBtn.Child = new TextBlock
|
||||
{
|
||||
Text = "확인",
|
||||
FontSize = 13,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = Brushes.White,
|
||||
};
|
||||
confirmBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85;
|
||||
confirmBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
||||
confirmBtn.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_selectedResponse) && !string.IsNullOrWhiteSpace(defaultValue))
|
||||
_selectedResponse = defaultValue;
|
||||
DialogResult = true;
|
||||
Close();
|
||||
};
|
||||
btnPanel.Children.Add(confirmBtn);
|
||||
|
||||
// 취소 버튼
|
||||
var cancelBtn = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(10),
|
||||
Padding = new Thickness(16, 9, 16, 9),
|
||||
Cursor = Cursors.Hand,
|
||||
BorderBrush = new SolidColorBrush(Color.FromArgb(0x40, 0xDC, 0x26, 0x26)),
|
||||
BorderThickness = new Thickness(1),
|
||||
};
|
||||
cancelBtn.Child = new TextBlock
|
||||
{
|
||||
Text = "취소",
|
||||
FontSize = 13,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)),
|
||||
};
|
||||
cancelBtn.MouseEnter += (s, _) => ((Border)s).Opacity = 0.85;
|
||||
cancelBtn.MouseLeave += (s, _) => ((Border)s).Opacity = 1.0;
|
||||
cancelBtn.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
_selectedResponse = "";
|
||||
DialogResult = false;
|
||||
Close();
|
||||
};
|
||||
// 취소를 왼쪽에 배치
|
||||
btnPanel.Children.Insert(0, cancelBtn);
|
||||
|
||||
stack.Children.Add(btnPanel);
|
||||
|
||||
root.Child = stack;
|
||||
Content = root;
|
||||
|
||||
// 등장 애니메이션
|
||||
root.RenderTransformOrigin = new Point(0.5, 0.5);
|
||||
root.RenderTransform = new ScaleTransform(0.95, 0.95);
|
||||
root.Opacity = 0;
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
var fade = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(150));
|
||||
root.BeginAnimation(OpacityProperty, fade);
|
||||
var scaleX = new DoubleAnimation(0.95, 1, TimeSpan.FromMilliseconds(200))
|
||||
{ EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } };
|
||||
var scaleY = new DoubleAnimation(0.95, 1, TimeSpan.FromMilliseconds(200))
|
||||
{ EasingFunction = new QuadraticEase { EasingMode = EasingMode.EaseOut } };
|
||||
((ScaleTransform)root.RenderTransform).BeginAnimation(ScaleTransform.ScaleXProperty, scaleX);
|
||||
((ScaleTransform)root.RenderTransform).BeginAnimation(ScaleTransform.ScaleYProperty, scaleY);
|
||||
};
|
||||
|
||||
// ESC → 취소
|
||||
KeyDown += (_, e) =>
|
||||
{
|
||||
if (e.Key == Key.Escape) { _selectedResponse = ""; DialogResult = false; Close(); }
|
||||
if (e.Key == Key.Enter && !_customInput.IsFocused) { DialogResult = true; Close(); }
|
||||
};
|
||||
}
|
||||
|
||||
private static Border? FindRadioCircle(Border optionBorder)
|
||||
{
|
||||
if (optionBorder.Child is StackPanel sp && sp.Children.Count > 0 && sp.Children[0] is Border outer
|
||||
&& outer.Child is Border inner)
|
||||
return inner;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>에이전트 질문 다이얼로그를 표시합니다.</summary>
|
||||
/// <returns>사용자 응답 문자열. 취소 시 null.</returns>
|
||||
public static string? Show(string question, List<string> options, string defaultValue = "")
|
||||
{
|
||||
var dlg = new UserAskDialog(question, options, defaultValue);
|
||||
var result = dlg.ShowDialog();
|
||||
return result == true ? dlg.SelectedResponse : null;
|
||||
}
|
||||
}
|
||||
191
src/AxCopilot/Views/WorkflowAnalyzerWindow.xaml
Normal file
191
src/AxCopilot/Views/WorkflowAnalyzerWindow.xaml
Normal file
@@ -0,0 +1,191 @@
|
||||
<Window x:Class="AxCopilot.Views.WorkflowAnalyzerWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="AX Agent — 워크플로우 분석기" Width="560" Height="720"
|
||||
MinWidth="420" MinHeight="400"
|
||||
WindowStartupLocation="Manual"
|
||||
WindowStyle="None" AllowsTransparency="True" Background="Transparent"
|
||||
ResizeMode="CanResizeWithGrip">
|
||||
<Border Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
CornerRadius="12" Margin="6"
|
||||
>
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="18" ShadowDepth="4" Opacity="0.3" Color="Black" Direction="270"/>
|
||||
</Border.Effect>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="44"/> <!-- 타이틀 바 -->
|
||||
<RowDefinition Height="Auto"/> <!-- 요약 카드 -->
|
||||
<RowDefinition Height="Auto"/> <!-- 탭 바 -->
|
||||
<RowDefinition Height="*"/> <!-- 콘텐츠 (타임라인 또는 병목 분석) -->
|
||||
<RowDefinition Height="Auto"/> <!-- 상세 패널 -->
|
||||
<RowDefinition Height="32"/> <!-- 상태 바 -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ═══ 타이틀 바 ═══ -->
|
||||
<Grid Grid.Row="0" Background="Transparent" MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="16,0,0,0">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="14"
|
||||
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||
<TextBlock Text="워크플로우 분석기" FontSize="14" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,0,8,0">
|
||||
<Border x:Name="BtnClear" Width="32" Height="32" CornerRadius="6" Cursor="Hand"
|
||||
Background="Transparent" ToolTip="초기화"
|
||||
MouseLeftButtonUp="BtnClear_Click"
|
||||
MouseEnter="TitleBtn_MouseEnter" MouseLeave="TitleBtn_MouseLeave">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
Foreground="{DynamicResource SecondaryText}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnMinimize" Width="32" Height="32" CornerRadius="6" Cursor="Hand"
|
||||
Background="Transparent" ToolTip="최소화"
|
||||
MouseLeftButtonUp="BtnMinimize_Click"
|
||||
MouseEnter="TitleBtn_MouseEnter" MouseLeave="TitleBtn_MouseLeave">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnClose" Width="32" Height="32" CornerRadius="6" Cursor="Hand"
|
||||
Background="Transparent" ToolTip="닫기"
|
||||
MouseLeftButtonUp="BtnClose_Click"
|
||||
MouseEnter="TitleBtn_MouseEnter" MouseLeave="TitleBtn_MouseLeave">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="10"
|
||||
Foreground="{DynamicResource SecondaryText}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- ═══ 요약 카드 ═══ -->
|
||||
<Border Grid.Row="1" Margin="12,0,12,6">
|
||||
<UniformGrid Columns="4" Margin="0">
|
||||
<Border Background="{DynamicResource ItemBackground}" CornerRadius="8" Padding="10,8" Margin="2">
|
||||
<StackPanel>
|
||||
<TextBlock Text="소요 시간" FontSize="10" Foreground="{DynamicResource SecondaryText}"/>
|
||||
<TextBlock x:Name="CardElapsed" Text="0.0s" FontSize="16" FontWeight="Bold" Foreground="#60A5FA"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border Background="{DynamicResource ItemBackground}" CornerRadius="8" Padding="10,8" Margin="2">
|
||||
<StackPanel>
|
||||
<TextBlock Text="반복" FontSize="10" Foreground="{DynamicResource SecondaryText}"/>
|
||||
<TextBlock x:Name="CardIterations" Text="0" FontSize="16" FontWeight="Bold" Foreground="#A78BFA"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border Background="{DynamicResource ItemBackground}" CornerRadius="8" Padding="10,8" Margin="2"
|
||||
ToolTip="입력 토큰 / 출력 토큰">
|
||||
<StackPanel>
|
||||
<TextBlock Text="토큰" FontSize="10" Foreground="{DynamicResource SecondaryText}"/>
|
||||
<TextBlock x:Name="CardTokens" Text="0" FontSize="16" FontWeight="Bold" Foreground="#34D399"/>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,2,0,0">
|
||||
<TextBlock Text="↑" FontSize="9" Foreground="#3B82F6" VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="CardInputTokens" Text="0" FontSize="9" Foreground="#3B82F6" Margin="1,0,6,0" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="↓" FontSize="9" Foreground="#10B981" VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="CardOutputTokens" Text="0" FontSize="9" Foreground="#10B981" Margin="1,0,0,0" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border Background="{DynamicResource ItemBackground}" CornerRadius="8" Padding="10,8" Margin="2">
|
||||
<StackPanel>
|
||||
<TextBlock Text="도구 호출" FontSize="10" Foreground="{DynamicResource SecondaryText}"/>
|
||||
<TextBlock x:Name="CardToolCalls" Text="0" FontSize="16" FontWeight="Bold" Foreground="#FBBF24"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</UniformGrid>
|
||||
</Border>
|
||||
|
||||
<!-- ═══ 탭 바 ═══ -->
|
||||
<Border Grid.Row="2" Margin="12,0,12,6">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Border x:Name="TabTimeline" CornerRadius="6" Padding="12,6" Margin="0,0,4,0"
|
||||
Cursor="Hand" MouseLeftButtonUp="TabTimeline_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock x:Name="TabTimelineIcon" Text="" FontFamily="Segoe MDL2 Assets" FontSize="11"
|
||||
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock x:Name="TabTimelineText" Text="타임라인" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border x:Name="TabBottleneck" CornerRadius="6" Padding="12,6" Margin="0,0,4,0"
|
||||
Cursor="Hand" MouseLeftButtonUp="TabBottleneck_Click">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock x:Name="TabBottleneckIcon" Text="" FontFamily="Segoe MDL2 Assets" FontSize="11"
|
||||
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock x:Name="TabBottleneckText" Text="병목 분석" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ═══ 콘텐츠: 타임라인 ═══ -->
|
||||
<ScrollViewer Grid.Row="3" x:Name="TimelineScroller" VerticalScrollBarVisibility="Auto"
|
||||
Margin="12,0,12,0" Padding="0,0,4,0">
|
||||
<StackPanel x:Name="TimelinePanel" Margin="0,0,0,8"/>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ═══ 콘텐츠: 병목 분석 ═══ -->
|
||||
<ScrollViewer Grid.Row="3" x:Name="BottleneckScroller" VerticalScrollBarVisibility="Auto"
|
||||
Margin="12,0,12,0" Padding="0,0,4,0" Visibility="Collapsed">
|
||||
<StackPanel x:Name="BottleneckPanel" Margin="0,0,0,8">
|
||||
<!-- 워터폴 차트 -->
|
||||
<TextBlock Text="⏱ 실행 흐름 (워터폴)" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}" Margin="4,4,0,8"/>
|
||||
<Border Background="{DynamicResource ItemBackground}" CornerRadius="8"
|
||||
Padding="12,8" Margin="0,0,0,12" MinHeight="60">
|
||||
<Canvas x:Name="WaterfallCanvas" Height="120" ClipToBounds="True"/>
|
||||
</Border>
|
||||
|
||||
<!-- 도구별 소요시간 -->
|
||||
<TextBlock Text="🔧 도구별 소요시간" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}" Margin="4,4,0,8"/>
|
||||
<Border Background="{DynamicResource ItemBackground}" CornerRadius="8"
|
||||
Padding="12,8" Margin="0,0,0,12">
|
||||
<StackPanel x:Name="ToolTimePanel"/>
|
||||
</Border>
|
||||
|
||||
<!-- 토큰 추세 -->
|
||||
<TextBlock Text="📈 반복별 토큰 추세" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}" Margin="4,4,0,8"/>
|
||||
<Border Background="{DynamicResource ItemBackground}" CornerRadius="8"
|
||||
Padding="12,8" Margin="0,0,0,8" MinHeight="60">
|
||||
<Canvas x:Name="TokenTrendCanvas" Height="100" ClipToBounds="True"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ═══ 상세 패널 ═══ -->
|
||||
<Border Grid.Row="4" x:Name="DetailPanel" Visibility="Collapsed"
|
||||
Background="{DynamicResource ItemBackground}" CornerRadius="8"
|
||||
Margin="12,4,12,4" Padding="14,10" MaxHeight="180">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
|
||||
<TextBlock x:Name="DetailTitle" Text="" FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
<TextBlock x:Name="DetailBadge" Text="" FontSize="10" Foreground="White"
|
||||
Margin="8,0,0,0" VerticalAlignment="Center" Padding="6,1"
|
||||
Background="#34D399"/>
|
||||
</StackPanel>
|
||||
<TextBlock x:Name="DetailMeta" Text="" FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}" Margin="0,0,0,4"/>
|
||||
<TextBlock x:Name="DetailContent" Text="" FontSize="11.5" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource PrimaryText}" MaxHeight="100"
|
||||
LineHeight="17"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- ═══ 상태 바 ═══ -->
|
||||
<Border Grid.Row="5" Margin="12,0,12,6" ClipToBounds="True">
|
||||
<Grid>
|
||||
<TextBlock x:Name="StatusText" Text="대기 중..." FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}" VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" MaxWidth="400"/>
|
||||
<TextBlock x:Name="LogLevelBadge" Text="" FontSize="10" HorizontalAlignment="Right"
|
||||
Foreground="{DynamicResource SecondaryText}" VerticalAlignment="Center"
|
||||
Padding="6,1" Opacity="0.7"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
928
src/AxCopilot/Views/WorkflowAnalyzerWindow.xaml.cs
Normal file
928
src/AxCopilot/Views/WorkflowAnalyzerWindow.xaml.cs
Normal file
@@ -0,0 +1,928 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using AxCopilot.Services.Agent;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
/// <summary>
|
||||
/// 에이전트 워크플로우 분석기.
|
||||
/// 에이전트 실행 과정을 세로 타임라인 형태로 실시간 시각화합니다.
|
||||
/// 개발자 모드 → 워크플로우 시각화 설정이 켜져있을 때 자동으로 열립니다.
|
||||
/// </summary>
|
||||
public partial class WorkflowAnalyzerWindow : Window
|
||||
{
|
||||
private readonly DateTime _startTime = DateTime.Now;
|
||||
private int _totalToolCalls;
|
||||
private int _totalInputTokens;
|
||||
private int _totalOutputTokens;
|
||||
private int _maxIteration;
|
||||
private int _successCount;
|
||||
private int _failCount;
|
||||
private string _logLevel = "simple";
|
||||
private bool _autoScroll = true;
|
||||
private string _activeTab = "timeline"; // timeline | bottleneck
|
||||
|
||||
// 병목 분석 데이터 수집
|
||||
private readonly List<WaterfallEntry> _waterfallEntries = new();
|
||||
private readonly Dictionary<string, long> _toolTimeAccum = new();
|
||||
private readonly List<(int Iteration, int Input, int Output)> _tokenTrend = new();
|
||||
private long _lastEventMs; // 워터폴 시작 시간 기준
|
||||
|
||||
// ─── 상하좌우 리사이즈 (WindowStyle=None 대응) ─────────────
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
private const int WM_NCHITTEST = 0x0084;
|
||||
private const int HTLEFT = 10, HTRIGHT = 11, HTTOP = 12, HTTOPLEFT = 13, HTTOPRIGHT = 14;
|
||||
private const int HTBOTTOM = 15, HTBOTTOMLEFT = 16, HTBOTTOMRIGHT = 17;
|
||||
|
||||
public WorkflowAnalyzerWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
SourceInitialized += (_, _) =>
|
||||
{
|
||||
var hwndSource = (HwndSource)PresentationSource.FromVisual(this);
|
||||
hwndSource?.AddHook(WndProc);
|
||||
};
|
||||
|
||||
// 로그 레벨 표시
|
||||
var app = System.Windows.Application.Current as App;
|
||||
_logLevel = app?.SettingsService?.Settings.Llm.AgentLogLevel ?? "simple";
|
||||
LogLevelBadge.Text = _logLevel switch
|
||||
{
|
||||
"debug" => "DEBUG",
|
||||
"detailed" => "DETAILED",
|
||||
_ => "SIMPLE",
|
||||
};
|
||||
|
||||
// 시작 위치: 화면 우측 하단
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
var screen = SystemParameters.WorkArea;
|
||||
Left = screen.Right - ActualWidth - 20;
|
||||
Top = screen.Bottom - ActualHeight - 20;
|
||||
UpdateTabVisuals();
|
||||
};
|
||||
|
||||
// 자동 스크롤 감지
|
||||
TimelineScroller.ScrollChanged += (_, e) =>
|
||||
{
|
||||
_autoScroll = Math.Abs(e.ExtentHeight - e.ViewportHeight - e.VerticalOffset) < 30;
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>에이전트 이벤트를 수신하여 타임라인에 추가합니다.</summary>
|
||||
public void OnAgentEvent(AgentEvent evt)
|
||||
{
|
||||
if (!Dispatcher.CheckAccess())
|
||||
{
|
||||
Dispatcher.Invoke(() => OnAgentEvent(evt));
|
||||
return;
|
||||
}
|
||||
|
||||
// 요약 카드 업데이트
|
||||
if (evt.Iteration > _maxIteration) _maxIteration = evt.Iteration;
|
||||
if (evt.InputTokens > 0) _totalInputTokens += evt.InputTokens;
|
||||
if (evt.OutputTokens > 0) _totalOutputTokens += evt.OutputTokens;
|
||||
|
||||
if (evt.Type is AgentEventType.ToolCall)
|
||||
_totalToolCalls++;
|
||||
if (evt.Type is AgentEventType.ToolResult)
|
||||
_successCount++;
|
||||
if (evt.Type is AgentEventType.Error)
|
||||
_failCount++;
|
||||
|
||||
UpdateSummaryCards();
|
||||
|
||||
// ── 병목 분석 데이터 수집 ──
|
||||
CollectBottleneckData(evt);
|
||||
|
||||
// 타임라인 노드 추가
|
||||
var node = CreateTimelineNode(evt);
|
||||
TimelinePanel.Children.Add(node);
|
||||
|
||||
// 자동 스크롤
|
||||
if (_autoScroll)
|
||||
TimelineScroller.ScrollToEnd();
|
||||
|
||||
// 상태 바
|
||||
StatusText.Text = evt.Type switch
|
||||
{
|
||||
AgentEventType.Complete => "✓ 완료",
|
||||
AgentEventType.Error => $"⚠ 오류: {Truncate(evt.Summary, 60)}",
|
||||
AgentEventType.ToolCall => $"실행 중: {evt.ToolName}",
|
||||
AgentEventType.Thinking => "사고 중...",
|
||||
AgentEventType.Planning => "계획 수립 중...",
|
||||
_ => StatusText.Text,
|
||||
};
|
||||
|
||||
// 완료 시 병목 분석 차트 자동 갱신
|
||||
if (evt.Type == AgentEventType.Complete)
|
||||
RenderBottleneckCharts();
|
||||
}
|
||||
|
||||
/// <summary>타임라인을 초기화합니다.</summary>
|
||||
public void Reset()
|
||||
{
|
||||
TimelinePanel.Children.Clear();
|
||||
DetailPanel.Visibility = Visibility.Collapsed;
|
||||
_totalToolCalls = 0;
|
||||
_totalInputTokens = 0;
|
||||
_totalOutputTokens = 0;
|
||||
_maxIteration = 0;
|
||||
_successCount = 0;
|
||||
_failCount = 0;
|
||||
_waterfallEntries.Clear();
|
||||
_toolTimeAccum.Clear();
|
||||
_tokenTrend.Clear();
|
||||
_lastEventMs = 0;
|
||||
UpdateSummaryCards();
|
||||
ClearBottleneckCharts();
|
||||
StatusText.Text = "대기 중...";
|
||||
}
|
||||
|
||||
/// <summary>타임라인 탭을 활성화합니다.</summary>
|
||||
public void SwitchToTimelineTab()
|
||||
{
|
||||
_activeTab = "timeline";
|
||||
UpdateTabVisuals();
|
||||
}
|
||||
|
||||
/// <summary>병목 분석 탭을 활성화합니다. 외부(ChatWindow)에서 호출합니다.</summary>
|
||||
public void SwitchToBottleneckTab()
|
||||
{
|
||||
_activeTab = "bottleneck";
|
||||
UpdateTabVisuals();
|
||||
RenderBottleneckCharts();
|
||||
}
|
||||
|
||||
// ─── 탭 전환 ──────────────────────────────────────────────────
|
||||
|
||||
private void TabTimeline_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_activeTab = "timeline";
|
||||
UpdateTabVisuals();
|
||||
}
|
||||
|
||||
private void TabBottleneck_Click(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_activeTab = "bottleneck";
|
||||
UpdateTabVisuals();
|
||||
RenderBottleneckCharts();
|
||||
}
|
||||
|
||||
private void UpdateTabVisuals()
|
||||
{
|
||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
var itemBg = TryFindResource("ItemBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x15, 0xFF, 0xFF, 0xFF));
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
|
||||
var isTimeline = _activeTab == "timeline";
|
||||
TabTimeline.Background = isTimeline ? accentBrush : itemBg;
|
||||
TabBottleneck.Background = !isTimeline ? accentBrush : itemBg;
|
||||
|
||||
// 활성 탭: 흰색 텍스트 (AccentColor 배경 위 가시성), 비활성: PrimaryText
|
||||
TabTimelineIcon.Foreground = isTimeline ? Brushes.White : primaryText;
|
||||
TabTimelineText.Foreground = isTimeline ? Brushes.White : primaryText;
|
||||
TabBottleneckIcon.Foreground = !isTimeline ? Brushes.White : primaryText;
|
||||
TabBottleneckText.Foreground = !isTimeline ? Brushes.White : primaryText;
|
||||
|
||||
TimelineScroller.Visibility = isTimeline ? Visibility.Visible : Visibility.Collapsed;
|
||||
BottleneckScroller.Visibility = !isTimeline ? Visibility.Visible : Visibility.Collapsed;
|
||||
DetailPanel.Visibility = isTimeline && DetailPanel.Tag != null
|
||||
? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
// ─── 병목 분석 데이터 수집 ──────────────────────────────────────
|
||||
|
||||
private record WaterfallEntry(string Label, long StartMs, long DurationMs, string Type, bool IsBottleneck = false);
|
||||
|
||||
private void CollectBottleneckData(AgentEvent evt)
|
||||
{
|
||||
var currentMs = (long)(evt.Timestamp - _startTime).TotalMilliseconds;
|
||||
|
||||
// 워터폴: LLM Thinking과 ToolResult (소요시간이 있는 이벤트)
|
||||
if (evt.Type == AgentEventType.Thinking && evt.Iteration > 0)
|
||||
{
|
||||
// LLM 호출 시작 기록 (종료 시점은 다음 ToolCall/Complete 이벤트에서 계산)
|
||||
_lastEventMs = currentMs;
|
||||
}
|
||||
else if (evt.Type == AgentEventType.ToolCall)
|
||||
{
|
||||
// 이전 LLM 사고 시간 = 현재 - 마지막 이벤트
|
||||
if (_lastEventMs > 0)
|
||||
{
|
||||
var llmDuration = currentMs - _lastEventMs;
|
||||
if (llmDuration > 100) // 100ms 미만은 노이즈
|
||||
{
|
||||
_waterfallEntries.Add(new WaterfallEntry(
|
||||
$"LLM #{evt.Iteration}", _lastEventMs, llmDuration, "llm"));
|
||||
}
|
||||
}
|
||||
_lastEventMs = currentMs;
|
||||
}
|
||||
else if (evt.Type == AgentEventType.ToolResult && evt.ElapsedMs > 0)
|
||||
{
|
||||
// 도구 실행 시간
|
||||
var toolStart = currentMs - evt.ElapsedMs;
|
||||
_waterfallEntries.Add(new WaterfallEntry(
|
||||
evt.ToolName, toolStart, evt.ElapsedMs, "tool"));
|
||||
|
||||
// 도구별 누적
|
||||
var key = evt.ToolName;
|
||||
_toolTimeAccum[key] = _toolTimeAccum.GetValueOrDefault(key) + evt.ElapsedMs;
|
||||
|
||||
_lastEventMs = currentMs;
|
||||
}
|
||||
else if (evt.Type == AgentEventType.Complete && _lastEventMs > 0)
|
||||
{
|
||||
// 마지막 LLM 사고 시간
|
||||
var llmDuration = currentMs - _lastEventMs;
|
||||
if (llmDuration > 100)
|
||||
{
|
||||
_waterfallEntries.Add(new WaterfallEntry(
|
||||
$"LLM (마무리)", _lastEventMs, llmDuration, "llm"));
|
||||
}
|
||||
}
|
||||
|
||||
// 토큰 추세
|
||||
if ((evt.InputTokens > 0 || evt.OutputTokens > 0) && evt.Iteration > 0)
|
||||
{
|
||||
// 같은 반복의 기존 항목이 있으면 업데이트
|
||||
var idx = _tokenTrend.FindIndex(t => t.Iteration == evt.Iteration);
|
||||
if (idx >= 0)
|
||||
{
|
||||
var old = _tokenTrend[idx];
|
||||
_tokenTrend[idx] = (evt.Iteration,
|
||||
old.Input + evt.InputTokens,
|
||||
old.Output + evt.OutputTokens);
|
||||
}
|
||||
else
|
||||
{
|
||||
_tokenTrend.Add((evt.Iteration, evt.InputTokens, evt.OutputTokens));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 병목 분석 차트 렌더링 ─────────────────────────────────────
|
||||
|
||||
private void ClearBottleneckCharts()
|
||||
{
|
||||
WaterfallCanvas.Children.Clear();
|
||||
ToolTimePanel.Children.Clear();
|
||||
TokenTrendCanvas.Children.Clear();
|
||||
}
|
||||
|
||||
private void RenderBottleneckCharts()
|
||||
{
|
||||
ClearBottleneckCharts();
|
||||
RenderWaterfallChart();
|
||||
RenderToolTimeChart();
|
||||
RenderTokenTrendChart();
|
||||
}
|
||||
|
||||
/// <summary>워터폴 차트: LLM 호출과 도구 실행을 시간 순서로 표시. 병목 구간은 빨간색.</summary>
|
||||
private void RenderWaterfallChart()
|
||||
{
|
||||
if (_waterfallEntries.Count == 0)
|
||||
{
|
||||
WaterfallCanvas.Height = 40;
|
||||
var emptyMsg = new TextBlock
|
||||
{
|
||||
Text = "실행 데이터가 없습니다",
|
||||
FontSize = 11,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
};
|
||||
Canvas.SetLeft(emptyMsg, 10);
|
||||
Canvas.SetTop(emptyMsg, 10);
|
||||
WaterfallCanvas.Children.Add(emptyMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
// 병목 판정: 평균 × 2.0 이상 = 빨강, × 1.5 이상 = 주황
|
||||
var avgDuration = _waterfallEntries.Average(e => e.DurationMs);
|
||||
var maxEndMs = _waterfallEntries.Max(e => e.StartMs + e.DurationMs);
|
||||
if (maxEndMs <= 0) maxEndMs = 1;
|
||||
|
||||
var rowHeight = 22;
|
||||
var labelWidth = 90.0;
|
||||
var chartHeight = _waterfallEntries.Count * rowHeight + 10;
|
||||
WaterfallCanvas.Height = Math.Max(chartHeight, 60);
|
||||
|
||||
var canvasWidth = WaterfallCanvas.ActualWidth > 0 ? WaterfallCanvas.ActualWidth : 480;
|
||||
var barAreaWidth = canvasWidth - labelWidth - 60; // 우측에 시간 텍스트 공간
|
||||
|
||||
for (int i = 0; i < _waterfallEntries.Count; i++)
|
||||
{
|
||||
var entry = _waterfallEntries[i];
|
||||
var y = i * rowHeight + 4;
|
||||
|
||||
// 색상 결정
|
||||
Color barColor;
|
||||
if (entry.DurationMs >= avgDuration * 2.0)
|
||||
barColor = Color.FromRgb(0xEF, 0x44, 0x44); // 빨강 (병목)
|
||||
else if (entry.DurationMs >= avgDuration * 1.5)
|
||||
barColor = Color.FromRgb(0xF9, 0x73, 0x16); // 주황 (주의)
|
||||
else if (entry.Type == "llm")
|
||||
barColor = Color.FromRgb(0x60, 0xA5, 0xFA); // 파랑 (LLM)
|
||||
else
|
||||
barColor = Color.FromRgb(0x34, 0xD3, 0x99); // 녹색 (도구)
|
||||
|
||||
// 라벨
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = Truncate(entry.Label, 12),
|
||||
FontSize = 10,
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
Foreground = new SolidColorBrush(barColor),
|
||||
Width = labelWidth,
|
||||
TextAlignment = TextAlignment.Right,
|
||||
};
|
||||
Canvas.SetLeft(label, 0);
|
||||
Canvas.SetTop(label, y + 2);
|
||||
WaterfallCanvas.Children.Add(label);
|
||||
|
||||
// 바
|
||||
var barStart = (entry.StartMs / (double)maxEndMs) * barAreaWidth;
|
||||
var barWidth = Math.Max((entry.DurationMs / (double)maxEndMs) * barAreaWidth, 3);
|
||||
|
||||
var bar = new Rectangle
|
||||
{
|
||||
Width = barWidth,
|
||||
Height = 14,
|
||||
RadiusX = 3, RadiusY = 3,
|
||||
Fill = new SolidColorBrush(barColor),
|
||||
Opacity = 0.85,
|
||||
ToolTip = $"{entry.Label}: {FormatMs(entry.DurationMs)}",
|
||||
};
|
||||
Canvas.SetLeft(bar, labelWidth + 8 + barStart);
|
||||
Canvas.SetTop(bar, y + 1);
|
||||
WaterfallCanvas.Children.Add(bar);
|
||||
|
||||
// 시간 텍스트
|
||||
var timeText = new TextBlock
|
||||
{
|
||||
Text = FormatMs(entry.DurationMs),
|
||||
FontSize = 9,
|
||||
Foreground = new SolidColorBrush(barColor),
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
};
|
||||
Canvas.SetLeft(timeText, labelWidth + 8 + barStart + barWidth + 4);
|
||||
Canvas.SetTop(timeText, y + 3);
|
||||
WaterfallCanvas.Children.Add(timeText);
|
||||
|
||||
// 병목 아이콘
|
||||
if (entry.DurationMs >= avgDuration * 2.0)
|
||||
{
|
||||
var icon = new TextBlock
|
||||
{
|
||||
Text = "🔴",
|
||||
FontSize = 9,
|
||||
};
|
||||
Canvas.SetLeft(icon, labelWidth + 8 + barStart + barWidth + 44);
|
||||
Canvas.SetTop(icon, y + 2);
|
||||
WaterfallCanvas.Children.Add(icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>도구별 누적 소요시간 수평 바 차트.</summary>
|
||||
private void RenderToolTimeChart()
|
||||
{
|
||||
ToolTimePanel.Children.Clear();
|
||||
|
||||
if (_toolTimeAccum.Count == 0)
|
||||
{
|
||||
ToolTimePanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "도구 실행 데이터가 없습니다",
|
||||
FontSize = 11,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
Margin = new Thickness(0, 4, 0, 4),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// LLM 시간 합산
|
||||
var llmTime = _waterfallEntries
|
||||
.Where(e => e.Type == "llm")
|
||||
.Sum(e => e.DurationMs);
|
||||
var allEntries = _toolTimeAccum
|
||||
.Select(kv => (Name: kv.Key, Ms: kv.Value))
|
||||
.ToList();
|
||||
if (llmTime > 0)
|
||||
allEntries.Add(("LLM 호출", llmTime));
|
||||
|
||||
var sorted = allEntries.OrderByDescending(e => e.Ms).ToList();
|
||||
var maxMs = sorted.Max(e => e.Ms);
|
||||
if (maxMs <= 0) maxMs = 1;
|
||||
|
||||
foreach (var (name, ms) in sorted)
|
||||
{
|
||||
var isBottleneck = ms == sorted[0].Ms;
|
||||
var barColor = name == "LLM 호출"
|
||||
? Color.FromRgb(0x60, 0xA5, 0xFA)
|
||||
: isBottleneck
|
||||
? Color.FromRgb(0xEF, 0x44, 0x44)
|
||||
: Color.FromRgb(0x34, 0xD3, 0x99);
|
||||
|
||||
var row = new Grid { Margin = new Thickness(0, 2, 0, 2) };
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(90) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(60) });
|
||||
|
||||
// 도구명
|
||||
var nameText = new TextBlock
|
||||
{
|
||||
Text = Truncate(name, 12),
|
||||
FontSize = 11,
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
Foreground = new SolidColorBrush(barColor),
|
||||
TextAlignment = TextAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
};
|
||||
Grid.SetColumn(nameText, 0);
|
||||
|
||||
// 바
|
||||
var barWidth = (ms / (double)maxMs);
|
||||
var barBorder = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(barColor),
|
||||
CornerRadius = new CornerRadius(3),
|
||||
Height = 14,
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
Width = 0, // 나중에 SizeChanged에서 설정
|
||||
Opacity = 0.8,
|
||||
};
|
||||
var barContainer = new Border { Margin = new Thickness(0, 2, 0, 2) };
|
||||
barContainer.Child = barBorder;
|
||||
barContainer.SizeChanged += (_, _) =>
|
||||
{
|
||||
barBorder.Width = Math.Max(barContainer.ActualWidth * barWidth, 4);
|
||||
};
|
||||
Grid.SetColumn(barContainer, 1);
|
||||
|
||||
// 시간
|
||||
var timeText = new TextBlock
|
||||
{
|
||||
Text = FormatMs(ms),
|
||||
FontSize = 10,
|
||||
Foreground = new SolidColorBrush(barColor),
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(4, 0, 0, 0),
|
||||
};
|
||||
Grid.SetColumn(timeText, 2);
|
||||
|
||||
row.Children.Add(nameText);
|
||||
row.Children.Add(barContainer);
|
||||
row.Children.Add(timeText);
|
||||
ToolTimePanel.Children.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>반복별 토큰 추세 꺾은선 그래프.</summary>
|
||||
private void RenderTokenTrendChart()
|
||||
{
|
||||
TokenTrendCanvas.Children.Clear();
|
||||
|
||||
if (_tokenTrend.Count < 2)
|
||||
{
|
||||
TokenTrendCanvas.Height = 40;
|
||||
TokenTrendCanvas.Children.Add(new TextBlock
|
||||
{
|
||||
Text = _tokenTrend.Count == 0 ? "토큰 데이터가 없습니다" : "2회 이상 반복이 필요합니다",
|
||||
FontSize = 11,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
});
|
||||
Canvas.SetLeft(TokenTrendCanvas.Children[0], 10);
|
||||
Canvas.SetTop(TokenTrendCanvas.Children[0], 10);
|
||||
return;
|
||||
}
|
||||
|
||||
TokenTrendCanvas.Height = 100;
|
||||
var canvasWidth = TokenTrendCanvas.ActualWidth > 0 ? TokenTrendCanvas.ActualWidth : 480;
|
||||
var canvasHeight = 90.0;
|
||||
var marginLeft = 45.0;
|
||||
var marginRight = 10.0;
|
||||
var chartWidth = canvasWidth - marginLeft - marginRight;
|
||||
var sorted = _tokenTrend.OrderBy(t => t.Iteration).ToList();
|
||||
var maxToken = sorted.Max(t => Math.Max(t.Input, t.Output));
|
||||
if (maxToken <= 0) maxToken = 1;
|
||||
|
||||
// Y축 눈금
|
||||
for (int i = 0; i <= 2; i++)
|
||||
{
|
||||
var y = canvasHeight * (1 - i / 2.0);
|
||||
var val = maxToken * i / 2;
|
||||
var gridLine = new Line
|
||||
{
|
||||
X1 = marginLeft, Y1 = y,
|
||||
X2 = canvasWidth - marginRight, Y2 = y,
|
||||
Stroke = TryFindResource("BorderColor") as Brush ?? Brushes.Gray,
|
||||
StrokeThickness = 0.5,
|
||||
Opacity = 0.4,
|
||||
};
|
||||
TokenTrendCanvas.Children.Add(gridLine);
|
||||
|
||||
var yLabel = new TextBlock
|
||||
{
|
||||
Text = val >= 1000 ? $"{val / 1000.0:F0}k" : val.ToString(),
|
||||
FontSize = 9,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
TextAlignment = TextAlignment.Right,
|
||||
Width = marginLeft - 6,
|
||||
};
|
||||
Canvas.SetLeft(yLabel, 0);
|
||||
Canvas.SetTop(yLabel, y - 6);
|
||||
TokenTrendCanvas.Children.Add(yLabel);
|
||||
}
|
||||
|
||||
// 입력 토큰 선 (파랑)
|
||||
DrawPolyline(sorted.Select((t, i) => new Point(
|
||||
marginLeft + (i / (double)(sorted.Count - 1)) * chartWidth,
|
||||
canvasHeight * (1 - t.Input / (double)maxToken)
|
||||
)).ToList(), "#3B82F6");
|
||||
|
||||
// 출력 토큰 선 (녹색)
|
||||
DrawPolyline(sorted.Select((t, i) => new Point(
|
||||
marginLeft + (i / (double)(sorted.Count - 1)) * chartWidth,
|
||||
canvasHeight * (1 - t.Output / (double)maxToken)
|
||||
)).ToList(), "#10B981");
|
||||
|
||||
// X축 라벨
|
||||
foreach (var (iter, _, _) in sorted)
|
||||
{
|
||||
var idx = sorted.FindIndex(t => t.Iteration == iter);
|
||||
var x = marginLeft + (idx / (double)(sorted.Count - 1)) * chartWidth;
|
||||
var xLabel = new TextBlock
|
||||
{
|
||||
Text = $"#{iter}",
|
||||
FontSize = 9,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
};
|
||||
Canvas.SetLeft(xLabel, x - 8);
|
||||
Canvas.SetTop(xLabel, canvasHeight + 2);
|
||||
TokenTrendCanvas.Children.Add(xLabel);
|
||||
}
|
||||
|
||||
// 범례
|
||||
var legend = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
legend.Children.Add(CreateLegendDot("#3B82F6", "입력"));
|
||||
legend.Children.Add(CreateLegendDot("#10B981", "출력"));
|
||||
Canvas.SetRight(legend, 10);
|
||||
Canvas.SetTop(legend, 0);
|
||||
TokenTrendCanvas.Children.Add(legend);
|
||||
|
||||
// 컨텍스트 폭발 경고
|
||||
if (sorted.Count >= 3)
|
||||
{
|
||||
var inputValues = sorted.Select(t => t.Input).ToList();
|
||||
bool increasing = true;
|
||||
for (int i = 1; i < inputValues.Count; i++)
|
||||
{
|
||||
if (inputValues[i] <= inputValues[i - 1]) { increasing = false; break; }
|
||||
}
|
||||
if (increasing && inputValues[^1] > inputValues[0] * 2)
|
||||
{
|
||||
var warning = new TextBlock
|
||||
{
|
||||
Text = "⚠ 컨텍스트 크기 급증",
|
||||
FontSize = 10,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44)),
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
};
|
||||
Canvas.SetLeft(warning, marginLeft + 4);
|
||||
Canvas.SetTop(warning, 0);
|
||||
TokenTrendCanvas.Children.Add(warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPolyline(List<Point> points, string colorHex)
|
||||
{
|
||||
if (points.Count < 2) return;
|
||||
var color = (Color)ColorConverter.ConvertFromString(colorHex);
|
||||
|
||||
var polyline = new Polyline
|
||||
{
|
||||
Stroke = new SolidColorBrush(color),
|
||||
StrokeThickness = 2,
|
||||
StrokeLineJoin = PenLineJoin.Round,
|
||||
};
|
||||
foreach (var p in points)
|
||||
polyline.Points.Add(p);
|
||||
TokenTrendCanvas.Children.Add(polyline);
|
||||
|
||||
// 점 찍기
|
||||
foreach (var p in points)
|
||||
{
|
||||
var dot = new Ellipse
|
||||
{
|
||||
Width = 6, Height = 6,
|
||||
Fill = new SolidColorBrush(color),
|
||||
};
|
||||
Canvas.SetLeft(dot, p.X - 3);
|
||||
Canvas.SetTop(dot, p.Y - 3);
|
||||
TokenTrendCanvas.Children.Add(dot);
|
||||
}
|
||||
}
|
||||
|
||||
private static StackPanel CreateLegendDot(string colorHex, string text)
|
||||
{
|
||||
var color = (Color)ColorConverter.ConvertFromString(colorHex);
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(8, 0, 0, 0) };
|
||||
sp.Children.Add(new Ellipse
|
||||
{
|
||||
Width = 6, Height = 6,
|
||||
Fill = new SolidColorBrush(color),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 3, 0),
|
||||
});
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
FontSize = 9,
|
||||
Foreground = new SolidColorBrush(color),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
return sp;
|
||||
}
|
||||
|
||||
// ─── 타임라인 노드 생성 ────────────────────────────────────────
|
||||
|
||||
private Border CreateTimelineNode(AgentEvent evt)
|
||||
{
|
||||
var (icon, iconColor, label) = GetEventVisual(evt);
|
||||
|
||||
var node = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
Margin = new Thickness(0, 1, 0, 1),
|
||||
Padding = new Thickness(8, 6, 8, 6),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
Cursor = Cursors.Hand,
|
||||
Tag = evt,
|
||||
};
|
||||
|
||||
var grid = new Grid();
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(28) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(3) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
|
||||
// 아이콘 원
|
||||
var iconBorder = new Border
|
||||
{
|
||||
Width = 22, Height = 22,
|
||||
CornerRadius = new CornerRadius(11),
|
||||
Background = new SolidColorBrush(iconColor),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
};
|
||||
iconBorder.Child = new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 10,
|
||||
Foreground = Brushes.White,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
Grid.SetColumn(iconBorder, 0);
|
||||
grid.Children.Add(iconBorder);
|
||||
|
||||
// 타임라인 세로 선
|
||||
var line = new Rectangle
|
||||
{
|
||||
Width = 2,
|
||||
Fill = TryFindResource("BorderColor") as Brush ?? Brushes.Gray,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Stretch,
|
||||
Opacity = 0.4,
|
||||
};
|
||||
Grid.SetColumn(line, 1);
|
||||
grid.Children.Add(line);
|
||||
|
||||
// 내용
|
||||
var content = new StackPanel { Margin = new Thickness(8, 0, 0, 0) };
|
||||
|
||||
var headerPanel = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
headerPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
|
||||
if (evt.ElapsedMs > 0)
|
||||
{
|
||||
headerPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = evt.ElapsedMs >= 1000 ? $" {evt.ElapsedMs / 1000.0:F1}s" : $" {evt.ElapsedMs}ms",
|
||||
FontSize = 10,
|
||||
Foreground = evt.ElapsedMs > 3000
|
||||
? Brushes.OrangeRed
|
||||
: TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
}
|
||||
|
||||
headerPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $" {evt.Timestamp:HH:mm:ss}",
|
||||
FontSize = 9,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Opacity = 0.6,
|
||||
});
|
||||
content.Children.Add(headerPanel);
|
||||
|
||||
if (!string.IsNullOrEmpty(evt.Summary))
|
||||
{
|
||||
content.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Truncate(evt.Summary, 120),
|
||||
FontSize = 11,
|
||||
Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
LineHeight = 16,
|
||||
});
|
||||
}
|
||||
|
||||
if (evt.InputTokens > 0 || evt.OutputTokens > 0)
|
||||
{
|
||||
var tokenPanel = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 0) };
|
||||
tokenPanel.Children.Add(CreateBadge($"↑{evt.InputTokens:#,0}", "#3B82F6"));
|
||||
tokenPanel.Children.Add(CreateBadge($"↓{evt.OutputTokens:#,0}", "#10B981"));
|
||||
content.Children.Add(tokenPanel);
|
||||
}
|
||||
|
||||
Grid.SetColumn(content, 2);
|
||||
grid.Children.Add(content);
|
||||
node.Child = grid;
|
||||
|
||||
var hoverBrush = TryFindResource("ItemHoverBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
node.MouseEnter += (_, _) => node.Background = hoverBrush;
|
||||
node.MouseLeave += (_, _) => node.Background = Brushes.Transparent;
|
||||
node.MouseLeftButtonUp += (_, _) => ShowDetail(evt);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private static (string Icon, Color Color, string Label) GetEventVisual(AgentEvent evt)
|
||||
{
|
||||
return evt.Type switch
|
||||
{
|
||||
AgentEventType.Thinking => ("\uE7BA", Color.FromRgb(0x60, 0xA5, 0xFA), "사고"),
|
||||
AgentEventType.Planning => ("\uE9D5", Color.FromRgb(0xA7, 0x8B, 0xFA), "계획"),
|
||||
AgentEventType.StepStart => ("\uE72A", Color.FromRgb(0x60, 0xA5, 0xFA), $"단계 {evt.StepCurrent}/{evt.StepTotal}"),
|
||||
AgentEventType.StepDone => ("\uE73E", Color.FromRgb(0x34, 0xD3, 0x99), $"단계 완료"),
|
||||
AgentEventType.ToolCall => ("\uE756", Color.FromRgb(0xFB, 0xBF, 0x24), evt.ToolName),
|
||||
AgentEventType.ToolResult=> ("\uE73E", Color.FromRgb(0x34, 0xD3, 0x99), $"{evt.ToolName} ✓"),
|
||||
AgentEventType.SkillCall => ("\uE768", Color.FromRgb(0xA7, 0x8B, 0xFA), $"스킬: {evt.ToolName}"),
|
||||
AgentEventType.Error => ("\uE783", Color.FromRgb(0xF8, 0x71, 0x71), $"{evt.ToolName} ✗"),
|
||||
AgentEventType.Complete => ("\uE930", Color.FromRgb(0x34, 0xD3, 0x99), "완료"),
|
||||
AgentEventType.Decision => ("\uE8C8", Color.FromRgb(0xFB, 0xBF, 0x24), "의사결정"),
|
||||
_ => ("\uE946", Color.FromRgb(0x6B, 0x72, 0x80), "이벤트"),
|
||||
};
|
||||
}
|
||||
|
||||
private static Border CreateBadge(string text, string colorHex)
|
||||
{
|
||||
var color = (Color)ColorConverter.ConvertFromString(colorHex);
|
||||
return new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(0x30, color.R, color.G, color.B)),
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Padding = new Thickness(5, 1, 5, 1),
|
||||
Margin = new Thickness(0, 0, 4, 0),
|
||||
Child = new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
FontSize = 9,
|
||||
Foreground = new SolidColorBrush(color),
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ─── 상세 패널 ─────────────────────────────────────────────
|
||||
|
||||
private void ShowDetail(AgentEvent evt)
|
||||
{
|
||||
DetailPanel.Visibility = Visibility.Visible;
|
||||
DetailPanel.Tag = evt;
|
||||
var (_, color, label) = GetEventVisual(evt);
|
||||
|
||||
DetailTitle.Text = label;
|
||||
DetailBadge.Text = evt.Success ? "성공" : "실패";
|
||||
DetailBadge.Background = new SolidColorBrush(evt.Success
|
||||
? Color.FromRgb(0x34, 0xD3, 0x99)
|
||||
: Color.FromRgb(0xF8, 0x71, 0x71));
|
||||
|
||||
var meta = $"시간: {evt.Timestamp:HH:mm:ss.fff}";
|
||||
if (evt.ElapsedMs > 0)
|
||||
meta += $" | 소요: {(evt.ElapsedMs >= 1000 ? $"{evt.ElapsedMs / 1000.0:F1}s" : $"{evt.ElapsedMs}ms")}";
|
||||
if (evt.InputTokens > 0 || evt.OutputTokens > 0)
|
||||
meta += $" | 토큰: 입력 {evt.InputTokens:#,0} / 출력 {evt.OutputTokens:#,0}";
|
||||
if (evt.Iteration > 0)
|
||||
meta += $" | 반복 #{evt.Iteration}";
|
||||
DetailMeta.Text = meta;
|
||||
|
||||
var contentText = evt.Summary ?? "";
|
||||
if (!string.IsNullOrEmpty(evt.ToolInput))
|
||||
contentText += $"\n\n파라미터:\n{Truncate(evt.ToolInput, 500)}";
|
||||
if (!string.IsNullOrEmpty(evt.FilePath))
|
||||
contentText += $"\n파일: {evt.FilePath}";
|
||||
DetailContent.Text = contentText;
|
||||
}
|
||||
|
||||
// ─── 요약 카드 업데이트 ──────────────────────────────────────
|
||||
|
||||
private void UpdateSummaryCards()
|
||||
{
|
||||
var elapsed = (DateTime.Now - _startTime).TotalSeconds;
|
||||
CardElapsed.Text = elapsed >= 60 ? $"{elapsed / 60:F0}m {elapsed % 60:F0}s" : $"{elapsed:F1}s";
|
||||
CardIterations.Text = _maxIteration.ToString();
|
||||
var totalTokens = _totalInputTokens + _totalOutputTokens;
|
||||
CardTokens.Text = totalTokens >= 1000 ? $"{totalTokens / 1000.0:F1}k" : totalTokens.ToString();
|
||||
CardInputTokens.Text = _totalInputTokens >= 1000 ? $"{_totalInputTokens / 1000.0:F1}k" : _totalInputTokens.ToString();
|
||||
CardOutputTokens.Text = _totalOutputTokens >= 1000 ? $"{_totalOutputTokens / 1000.0:F1}k" : _totalOutputTokens.ToString();
|
||||
CardToolCalls.Text = _totalToolCalls.ToString();
|
||||
}
|
||||
|
||||
// ─── 유틸리티 ────────────────────────────────────────────────
|
||||
|
||||
private static string FormatMs(long ms)
|
||||
=> ms >= 1000 ? $"{ms / 1000.0:F1}s" : $"{ms}ms";
|
||||
|
||||
// ─── 윈도우 이벤트 ────────────────────────────────────────────
|
||||
|
||||
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 타이틀 바 버튼 영역 클릭 시 드래그 무시 (DragMove가 마우스를 캡처하여 MouseLeftButtonUp 차단 방지)
|
||||
var src = e.OriginalSource as DependencyObject;
|
||||
while (src != null && src != sender)
|
||||
{
|
||||
if (src is Border b && b.Name is "BtnClose" or "BtnMinimize" or "BtnClear")
|
||||
return;
|
||||
src = VisualTreeHelper.GetParent(src);
|
||||
}
|
||||
if (e.ClickCount == 1) DragMove();
|
||||
}
|
||||
|
||||
private void BtnClose_Click(object sender, MouseButtonEventArgs e) => Hide();
|
||||
private void BtnMinimize_Click(object sender, MouseButtonEventArgs e) => WindowState = WindowState.Minimized;
|
||||
private void BtnClear_Click(object sender, MouseButtonEventArgs e) => Reset();
|
||||
|
||||
private void TitleBtn_MouseEnter(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (sender is Border b)
|
||||
b.Background = TryFindResource("ItemHoverBackground") as Brush
|
||||
?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
}
|
||||
|
||||
private void TitleBtn_MouseLeave(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (sender is Border b)
|
||||
b.Background = Brushes.Transparent;
|
||||
}
|
||||
|
||||
private static string Truncate(string text, int maxLen)
|
||||
=> text.Length <= maxLen ? text : text[..maxLen] + "…";
|
||||
|
||||
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||
{
|
||||
if (msg == WM_NCHITTEST)
|
||||
{
|
||||
var pt = PointFromScreen(new Point(
|
||||
(short)(lParam.ToInt32() & 0xFFFF),
|
||||
(short)((lParam.ToInt32() >> 16) & 0xFFFF)));
|
||||
const double grip = 8;
|
||||
var w = ActualWidth;
|
||||
var h = ActualHeight;
|
||||
|
||||
if (pt.X < grip && pt.Y < grip) { handled = true; return (IntPtr)HTTOPLEFT; }
|
||||
if (pt.X > w - grip && pt.Y < grip) { handled = true; return (IntPtr)HTTOPRIGHT; }
|
||||
if (pt.X < grip && pt.Y > h - grip) { handled = true; return (IntPtr)HTBOTTOMLEFT; }
|
||||
if (pt.X > w - grip && pt.Y > h - grip) { handled = true; return (IntPtr)HTBOTTOMRIGHT; }
|
||||
if (pt.X < grip) { handled = true; return (IntPtr)HTLEFT; }
|
||||
if (pt.X > w - grip) { handled = true; return (IntPtr)HTRIGHT; }
|
||||
if (pt.Y < grip) { handled = true; return (IntPtr)HTTOP; }
|
||||
if (pt.Y > h - grip) { handled = true; return (IntPtr)HTBOTTOM; }
|
||||
}
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user