[Phase48] 대형 파일 분할 리팩터링 4차 — 4개 신규 파셜 파일 생성
## 분할 대상 및 결과 ### ChatWindow.Controls.cs (595줄 → 372줄) - ChatWindow.TabSwitching.cs (232줄, 신규): _activeTab 필드, _tabConversationId 필드 TabChat/Cowork/Code_Checked, UpdateTabUI, BtnPlanMode_Click, UpdatePlanModeUI, SwitchToTabConversation, SaveCurrentTabConversationId, StopStreamingIfActive ### ChatWindow.SlashCommands.cs (579줄 → 406줄) - ChatWindow.DropActions.cs (160줄, 신규): DropActions 딕셔너리, CodeExtensions, DataExtensions, _dropActionPopup 필드, ShowDropActionMenu() 메서드 ### WorkflowAnalyzerWindow.Charts.cs (667줄 → 397줄) - WorkflowAnalyzerWindow.Timeline.cs (281줄, 신규): CreateTimelineNode, GetEventVisual, CreateBadge, ShowDetail, UpdateSummaryCards, FormatMs, Truncate, 윈도우 이벤트 핸들러, WndProc ### SkillGalleryWindow.xaml.cs (631줄 → ~430줄) - SkillGalleryWindow.SkillDetail.cs (197줄, 신규): ShowSkillDetail() 메서드 전체 (스킬 상세 보기 팝업 — 메타정보·프롬프트 미리보기·Action 버튼) ## 빌드 결과: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -369,227 +369,4 @@ public partial class ChatWindow
|
|||||||
MaximizeIcon.Text = WindowState == WindowState.Maximized ? "\uE923" : "\uE739"; // 복원/최대화 아이콘
|
MaximizeIcon.Text = WindowState == WindowState.Maximized ? "\uE923" : "\uE739"; // 복원/최대화 아이콘
|
||||||
}
|
}
|
||||||
private void BtnClose_Click(object sender, RoutedEventArgs e) => Close();
|
private void BtnClose_Click(object sender, RoutedEventArgs e) => Close();
|
||||||
|
|
||||||
// ─── 탭 전환 ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private string _activeTab = "Chat";
|
|
||||||
|
|
||||||
private void SaveCurrentTabConversationId()
|
|
||||||
{
|
|
||||||
lock (_convLock)
|
|
||||||
{
|
|
||||||
if (_currentConversation != null && _currentConversation.Messages.Count > 0)
|
|
||||||
{
|
|
||||||
_tabConversationId[_activeTab] = _currentConversation.Id;
|
|
||||||
// 탭 전환 시 현재 대화를 즉시 저장 (스트리밍 중이어도 진행 중인 내용 보존)
|
|
||||||
try { _storage.Save(_currentConversation); } catch (Exception) { /* 대화 저장 실패 — UI 차단 방지 */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 탭별 마지막 대화 ID를 설정에 영속 저장 (앱 재시작 시 복원용)
|
|
||||||
SaveLastConversations();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>탭 전환 전 스트리밍 중이면 즉시 중단합니다.</summary>
|
|
||||||
private void StopStreamingIfActive()
|
|
||||||
{
|
|
||||||
if (!_isStreaming) return;
|
|
||||||
// 스트리밍 중단
|
|
||||||
_streamCts?.Cancel();
|
|
||||||
_cursorTimer.Stop();
|
|
||||||
_elapsedTimer.Stop();
|
|
||||||
_typingTimer.Stop();
|
|
||||||
StopRainbowGlow();
|
|
||||||
HideStickyProgress();
|
|
||||||
_activeStreamText = null;
|
|
||||||
_elapsedLabel = null;
|
|
||||||
_cachedStreamContent = "";
|
|
||||||
_isStreaming = false;
|
|
||||||
BtnSend.IsEnabled = true;
|
|
||||||
BtnStop.Visibility = Visibility.Collapsed;
|
|
||||||
BtnPause.Visibility = Visibility.Collapsed;
|
|
||||||
PauseIcon.Text = "\uE769"; // 리셋
|
|
||||||
BtnSend.Visibility = Visibility.Visible;
|
|
||||||
_streamCts?.Dispose();
|
|
||||||
_streamCts = null;
|
|
||||||
SetStatusIdle();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TabChat_Checked(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_activeTab == "Chat") return;
|
|
||||||
StopStreamingIfActive();
|
|
||||||
SaveCurrentTabConversationId();
|
|
||||||
_activeTab = "Chat";
|
|
||||||
_selectedCategory = ""; UpdateCategoryLabel();
|
|
||||||
UpdateTabUI();
|
|
||||||
UpdatePlanModeUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TabCowork_Checked(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_activeTab == "Cowork") return;
|
|
||||||
StopStreamingIfActive();
|
|
||||||
SaveCurrentTabConversationId();
|
|
||||||
_activeTab = "Cowork";
|
|
||||||
_selectedCategory = ""; UpdateCategoryLabel();
|
|
||||||
UpdateTabUI();
|
|
||||||
UpdatePlanModeUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TabCode_Checked(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (_activeTab == "Code") return;
|
|
||||||
StopStreamingIfActive();
|
|
||||||
SaveCurrentTabConversationId();
|
|
||||||
_activeTab = "Code";
|
|
||||||
_selectedCategory = ""; UpdateCategoryLabel();
|
|
||||||
UpdateTabUI();
|
|
||||||
UpdatePlanModeUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>탭별로 마지막으로 활성화된 대화 ID를 기억.</summary>
|
|
||||||
private readonly Dictionary<string, string?> _tabConversationId = new()
|
|
||||||
{
|
|
||||||
["Chat"] = null, ["Cowork"] = null, ["Code"] = null,
|
|
||||||
};
|
|
||||||
|
|
||||||
private void UpdateTabUI()
|
|
||||||
{
|
|
||||||
// 폴더 바는 Cowork/Code 탭에서만 표시
|
|
||||||
if (FolderBar != null)
|
|
||||||
FolderBar.Visibility = _activeTab != "Chat" ? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
|
|
||||||
// 탭별 입력 안내 문구
|
|
||||||
if (InputWatermark != null)
|
|
||||||
{
|
|
||||||
InputWatermark.Text = _activeTab switch
|
|
||||||
{
|
|
||||||
"Cowork" => "에이전트에게 작업을 요청하세요 (파일 읽기/쓰기, 문서 생성...)",
|
|
||||||
"Code" => "코드 관련 작업을 요청하세요...",
|
|
||||||
_ => _promptCardPlaceholder,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 권한 기본값 적용 (Cowork/Code 탭은 설정의 기본값 사용)
|
|
||||||
ApplyTabDefaultPermission();
|
|
||||||
|
|
||||||
// 포맷/디자인 드롭다운은 Cowork 탭에서만 표시
|
|
||||||
if (_activeTab == "Cowork")
|
|
||||||
{
|
|
||||||
BuildBottomBar();
|
|
||||||
if (Llm.ShowFileBrowser && FileBrowserPanel != null)
|
|
||||||
{
|
|
||||||
FileBrowserPanel.Visibility = Visibility.Visible;
|
|
||||||
BuildFileTree();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (_activeTab == "Code")
|
|
||||||
{
|
|
||||||
// Code 탭: 언어 선택기 + 파일 탐색기
|
|
||||||
BuildCodeBottomBar();
|
|
||||||
if (Llm.ShowFileBrowser && FileBrowserPanel != null)
|
|
||||||
{
|
|
||||||
FileBrowserPanel.Visibility = Visibility.Visible;
|
|
||||||
BuildFileTree();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
MoodIconPanel.Children.Clear();
|
|
||||||
if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Collapsed;
|
|
||||||
if (FileBrowserPanel != null) FileBrowserPanel.Visibility = Visibility.Collapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 탭별 프리셋 버튼 재구성
|
|
||||||
BuildTopicButtons();
|
|
||||||
|
|
||||||
// 현재 대화를 해당 탭 대화로 전환
|
|
||||||
SwitchToTabConversation();
|
|
||||||
|
|
||||||
// Cowork/Code 탭 전환 시 팁 표시
|
|
||||||
ShowRandomTip();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BtnPlanMode_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
|
||||||
{
|
|
||||||
// 3단 순환: off → auto → always → off
|
|
||||||
Llm.PlanMode = Llm.PlanMode switch
|
|
||||||
{
|
|
||||||
"auto" => "always",
|
|
||||||
"always" => "off",
|
|
||||||
_ => "auto"
|
|
||||||
};
|
|
||||||
_settings.Save();
|
|
||||||
UpdatePlanModeUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdatePlanModeUI()
|
|
||||||
{
|
|
||||||
var planMode = Llm.PlanMode ?? "off";
|
|
||||||
if (PlanModeValue == null) return;
|
|
||||||
|
|
||||||
PlanModeValue.Text = planMode switch
|
|
||||||
{
|
|
||||||
"auto" => "Auto",
|
|
||||||
"always" => "Always",
|
|
||||||
_ => "Off"
|
|
||||||
};
|
|
||||||
var isActive = planMode != "off";
|
|
||||||
var activeBrush = ThemeResourceHelper.Accent(this);
|
|
||||||
var secondaryBrush = ThemeResourceHelper.Secondary(this);
|
|
||||||
if (PlanModeIcon != null) PlanModeIcon.Foreground = isActive ? activeBrush : secondaryBrush;
|
|
||||||
if (PlanModeLabel != null) PlanModeLabel.Foreground = isActive ? activeBrush : secondaryBrush;
|
|
||||||
if (BtnPlanMode != null)
|
|
||||||
BtnPlanMode.Background = isActive
|
|
||||||
? new SolidColorBrush(Color.FromArgb(0x1A, 0x4B, 0x5E, 0xFC))
|
|
||||||
: Brushes.Transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SwitchToTabConversation()
|
|
||||||
{
|
|
||||||
// 이전 탭의 대화 저장
|
|
||||||
lock (_convLock)
|
|
||||||
{
|
|
||||||
if (_currentConversation != null && _currentConversation.Messages.Count > 0)
|
|
||||||
{
|
|
||||||
try { _storage.Save(_currentConversation); } catch (Exception) { /* 대화 저장 실패 — UI 차단 방지 */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 현재 탭에 기억된 대화가 있으면 복원
|
|
||||||
var savedId = _tabConversationId.GetValueOrDefault(_activeTab);
|
|
||||||
if (!string.IsNullOrEmpty(savedId))
|
|
||||||
{
|
|
||||||
var conv = _storage.Load(savedId);
|
|
||||||
if (conv != null)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(conv.Tab)) conv.Tab = _activeTab;
|
|
||||||
lock (_convLock) _currentConversation = conv;
|
|
||||||
MessagePanel.Children.Clear();
|
|
||||||
foreach (var msg in conv.Messages)
|
|
||||||
AddMessageBubble(msg.Role, msg.Content, animate: false, message: msg);
|
|
||||||
EmptyState.Visibility = conv.Messages.Count > 0 ? Visibility.Collapsed : Visibility.Visible;
|
|
||||||
UpdateChatTitle();
|
|
||||||
RefreshConversationList();
|
|
||||||
UpdateFolderBar();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 기억된 대화가 없으면 새 대화
|
|
||||||
lock (_convLock)
|
|
||||||
{
|
|
||||||
_currentConversation = new ChatConversation { Tab = _activeTab };
|
|
||||||
var workFolder = Llm.WorkFolder;
|
|
||||||
if (!string.IsNullOrEmpty(workFolder) && _activeTab != "Chat")
|
|
||||||
_currentConversation.WorkFolder = workFolder;
|
|
||||||
}
|
|
||||||
MessagePanel.Children.Clear();
|
|
||||||
EmptyState.Visibility = Visibility.Visible;
|
|
||||||
_attachedFiles.Clear();
|
|
||||||
RefreshAttachedFilesUI();
|
|
||||||
UpdateChatTitle();
|
|
||||||
RefreshConversationList();
|
|
||||||
UpdateFolderBar();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
183
src/AxCopilot/Views/ChatWindow.DropActions.cs
Normal file
183
src/AxCopilot/Views/ChatWindow.DropActions.cs
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Controls.Primitives;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace AxCopilot.Views;
|
||||||
|
|
||||||
|
public partial class ChatWindow
|
||||||
|
{
|
||||||
|
// ─── 드래그 앤 드롭 AI 액션 팝업 ─────────────────────────────────────
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, List<(string Label, string Icon, string Prompt)>> DropActions = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["code"] =
|
||||||
|
[
|
||||||
|
("코드 리뷰", "\uE943", "첨부된 코드를 리뷰해 주세요. 버그, 성능 이슈, 보안 취약점, 개선점을 찾아 구체적으로 제안하세요."),
|
||||||
|
("코드 설명", "\uE946", "첨부된 코드를 상세히 설명해 주세요. 주요 함수, 데이터 흐름, 설계 패턴을 포함하세요."),
|
||||||
|
("리팩토링 제안", "\uE70F", "첨부된 코드의 리팩토링 방안을 제안해 주세요. 가독성, 유지보수성, 성능을 고려하세요."),
|
||||||
|
("테스트 생성", "\uE9D5", "첨부된 코드에 대한 단위 테스트 코드를 생성해 주세요."),
|
||||||
|
],
|
||||||
|
["document"] =
|
||||||
|
[
|
||||||
|
("요약", "\uE8AB", "첨부된 문서를 핵심 포인트 위주로 간결하게 요약해 주세요."),
|
||||||
|
("분석", "\uE9D9", "첨부된 문서의 내용을 분석하고 주요 인사이트를 도출해 주세요."),
|
||||||
|
("번역", "\uE8C1", "첨부된 문서를 영어로 번역해 주세요. 원문의 톤과 뉘앙스를 유지하세요."),
|
||||||
|
],
|
||||||
|
["data"] =
|
||||||
|
[
|
||||||
|
("데이터 분석", "\uE9D9", "첨부된 데이터를 분석해 주세요. 통계, 추세, 이상치를 찾아 보고하세요."),
|
||||||
|
("시각화 제안", "\uE9D9", "첨부된 데이터를 시각화할 최적의 차트 유형을 제안하고 chart_create로 생성해 주세요."),
|
||||||
|
("포맷 변환", "\uE8AB", "첨부된 데이터를 다른 형식으로 변환해 주세요. (CSV↔JSON↔Excel 등)"),
|
||||||
|
],
|
||||||
|
["image"] =
|
||||||
|
[
|
||||||
|
("이미지 설명", "\uE946", "첨부된 이미지를 자세히 설명해 주세요. 내용, 레이아웃, 텍스트를 분석하세요."),
|
||||||
|
("UI 리뷰", "\uE70F", "첨부된 UI 스크린샷을 리뷰해 주세요. UX 개선점, 접근성, 디자인 일관성을 평가하세요."),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly HashSet<string> CodeExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{ ".cs", ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".cpp", ".c", ".h", ".go", ".rs", ".rb", ".php", ".swift", ".kt", ".scala", ".sh", ".ps1", ".bat", ".cmd", ".sql", ".xaml", ".vue" };
|
||||||
|
private static readonly HashSet<string> DataExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{ ".csv", ".json", ".xml", ".yaml", ".yml", ".tsv" };
|
||||||
|
// ImageExtensions는 이미지 첨부 영역에서 정의됨 — 재사용
|
||||||
|
|
||||||
|
private Popup? _dropActionPopup;
|
||||||
|
|
||||||
|
private void ShowDropActionMenu(string[] files)
|
||||||
|
{
|
||||||
|
// 파일 유형 판별
|
||||||
|
var ext = System.IO.Path.GetExtension(files[0]).ToLowerInvariant();
|
||||||
|
string category;
|
||||||
|
if (CodeExtensions.Contains(ext)) category = "code";
|
||||||
|
else if (DataExtensions.Contains(ext)) category = "data";
|
||||||
|
else if (ImageExtensions.Contains(ext)) category = "image";
|
||||||
|
else category = "document";
|
||||||
|
|
||||||
|
var actions = DropActions.GetValueOrDefault(category) ?? DropActions["document"];
|
||||||
|
|
||||||
|
// 팝업 생성
|
||||||
|
_dropActionPopup?.SetValue(Popup.IsOpenProperty, false);
|
||||||
|
|
||||||
|
var panel = new StackPanel();
|
||||||
|
// 헤더
|
||||||
|
var header = new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"📎 {System.IO.Path.GetFileName(files[0])}{(files.Length > 1 ? $" 외 {files.Length - 1}개" : "")}",
|
||||||
|
FontSize = 11,
|
||||||
|
Foreground = ThemeResourceHelper.Secondary(this),
|
||||||
|
Margin = new Thickness(12, 8, 12, 6),
|
||||||
|
};
|
||||||
|
panel.Children.Add(header);
|
||||||
|
|
||||||
|
// 액션 항목
|
||||||
|
var hoverBrush = ThemeResourceHelper.HoverBg(this);
|
||||||
|
var accentBrush = ThemeResourceHelper.Accent(this);
|
||||||
|
var textBrush = ThemeResourceHelper.Primary(this);
|
||||||
|
|
||||||
|
foreach (var (label, icon, prompt) in actions)
|
||||||
|
{
|
||||||
|
var capturedPrompt = prompt;
|
||||||
|
var row = new Border
|
||||||
|
{
|
||||||
|
Background = Brushes.Transparent,
|
||||||
|
CornerRadius = new CornerRadius(6),
|
||||||
|
Padding = new Thickness(12, 7, 12, 7),
|
||||||
|
Margin = new Thickness(4, 1, 4, 1),
|
||||||
|
Cursor = Cursors.Hand,
|
||||||
|
};
|
||||||
|
var stack = new StackPanel { Orientation = Orientation.Horizontal };
|
||||||
|
stack.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = icon,
|
||||||
|
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||||
|
FontSize = 13, Foreground = accentBrush,
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = new Thickness(0, 0, 8, 0),
|
||||||
|
});
|
||||||
|
stack.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = label, FontSize = 13, FontWeight = FontWeights.SemiBold,
|
||||||
|
Foreground = textBrush, VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
});
|
||||||
|
row.Child = stack;
|
||||||
|
|
||||||
|
row.MouseEnter += (_, _) => row.Background = hoverBrush;
|
||||||
|
row.MouseLeave += (_, _) => row.Background = Brushes.Transparent;
|
||||||
|
row.MouseLeftButtonUp += (_, _) =>
|
||||||
|
{
|
||||||
|
if (_dropActionPopup != null) _dropActionPopup.IsOpen = false;
|
||||||
|
foreach (var f in files) AddAttachedFile(f);
|
||||||
|
InputBox.Text = capturedPrompt;
|
||||||
|
InputBox.CaretIndex = InputBox.Text.Length;
|
||||||
|
InputBox.Focus();
|
||||||
|
if (Llm.DragDropAutoSend)
|
||||||
|
_ = SendMessageAsync();
|
||||||
|
};
|
||||||
|
panel.Children.Add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// "첨부만" 항목
|
||||||
|
var attachOnly = new Border
|
||||||
|
{
|
||||||
|
Background = Brushes.Transparent,
|
||||||
|
CornerRadius = new CornerRadius(6),
|
||||||
|
Padding = new Thickness(12, 7, 12, 7),
|
||||||
|
Margin = new Thickness(4, 1, 4, 1),
|
||||||
|
Cursor = Cursors.Hand,
|
||||||
|
};
|
||||||
|
var attachStack = new StackPanel { Orientation = Orientation.Horizontal };
|
||||||
|
attachStack.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "\uE723",
|
||||||
|
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
||||||
|
FontSize = 13, Foreground = ThemeResourceHelper.Secondary(this),
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
Margin = new Thickness(0, 0, 8, 0),
|
||||||
|
});
|
||||||
|
attachStack.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = "첨부만", FontSize = 13,
|
||||||
|
Foreground = ThemeResourceHelper.Secondary(this),
|
||||||
|
VerticalAlignment = VerticalAlignment.Center,
|
||||||
|
});
|
||||||
|
attachOnly.Child = attachStack;
|
||||||
|
attachOnly.MouseEnter += (_, _) => attachOnly.Background = hoverBrush;
|
||||||
|
attachOnly.MouseLeave += (_, _) => attachOnly.Background = Brushes.Transparent;
|
||||||
|
attachOnly.MouseLeftButtonUp += (_, _) =>
|
||||||
|
{
|
||||||
|
if (_dropActionPopup != null) _dropActionPopup.IsOpen = false;
|
||||||
|
foreach (var f in files) AddAttachedFile(f);
|
||||||
|
InputBox.Focus();
|
||||||
|
};
|
||||||
|
panel.Children.Add(attachOnly);
|
||||||
|
|
||||||
|
var container = new Border
|
||||||
|
{
|
||||||
|
Background = ThemeResourceHelper.Background(this),
|
||||||
|
CornerRadius = new CornerRadius(12),
|
||||||
|
BorderBrush = ThemeResourceHelper.Border(this),
|
||||||
|
BorderThickness = new Thickness(1),
|
||||||
|
Padding = new Thickness(4, 4, 4, 6),
|
||||||
|
Child = panel,
|
||||||
|
MinWidth = 200,
|
||||||
|
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
||||||
|
{
|
||||||
|
Color = Colors.Black, BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
_dropActionPopup = new Popup
|
||||||
|
{
|
||||||
|
PlacementTarget = InputBorder,
|
||||||
|
Placement = PlacementMode.Top,
|
||||||
|
StaysOpen = false,
|
||||||
|
AllowsTransparency = true,
|
||||||
|
PopupAnimation = PopupAnimation.Fade,
|
||||||
|
Child = container,
|
||||||
|
};
|
||||||
|
_dropActionPopup.IsOpen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -403,177 +403,4 @@ public partial class ChatWindow
|
|||||||
|
|
||||||
return (null, input);
|
return (null, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 드래그 앤 드롭 AI 액션 팝업 ─────────────────────────────────────
|
|
||||||
|
|
||||||
private static readonly Dictionary<string, List<(string Label, string Icon, string Prompt)>> DropActions = new(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
["code"] =
|
|
||||||
[
|
|
||||||
("코드 리뷰", "\uE943", "첨부된 코드를 리뷰해 주세요. 버그, 성능 이슈, 보안 취약점, 개선점을 찾아 구체적으로 제안하세요."),
|
|
||||||
("코드 설명", "\uE946", "첨부된 코드를 상세히 설명해 주세요. 주요 함수, 데이터 흐름, 설계 패턴을 포함하세요."),
|
|
||||||
("리팩토링 제안", "\uE70F", "첨부된 코드의 리팩토링 방안을 제안해 주세요. 가독성, 유지보수성, 성능을 고려하세요."),
|
|
||||||
("테스트 생성", "\uE9D5", "첨부된 코드에 대한 단위 테스트 코드를 생성해 주세요."),
|
|
||||||
],
|
|
||||||
["document"] =
|
|
||||||
[
|
|
||||||
("요약", "\uE8AB", "첨부된 문서를 핵심 포인트 위주로 간결하게 요약해 주세요."),
|
|
||||||
("분석", "\uE9D9", "첨부된 문서의 내용을 분석하고 주요 인사이트를 도출해 주세요."),
|
|
||||||
("번역", "\uE8C1", "첨부된 문서를 영어로 번역해 주세요. 원문의 톤과 뉘앙스를 유지하세요."),
|
|
||||||
],
|
|
||||||
["data"] =
|
|
||||||
[
|
|
||||||
("데이터 분석", "\uE9D9", "첨부된 데이터를 분석해 주세요. 통계, 추세, 이상치를 찾아 보고하세요."),
|
|
||||||
("시각화 제안", "\uE9D9", "첨부된 데이터를 시각화할 최적의 차트 유형을 제안하고 chart_create로 생성해 주세요."),
|
|
||||||
("포맷 변환", "\uE8AB", "첨부된 데이터를 다른 형식으로 변환해 주세요. (CSV↔JSON↔Excel 등)"),
|
|
||||||
],
|
|
||||||
["image"] =
|
|
||||||
[
|
|
||||||
("이미지 설명", "\uE946", "첨부된 이미지를 자세히 설명해 주세요. 내용, 레이아웃, 텍스트를 분석하세요."),
|
|
||||||
("UI 리뷰", "\uE70F", "첨부된 UI 스크린샷을 리뷰해 주세요. UX 개선점, 접근성, 디자인 일관성을 평가하세요."),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly HashSet<string> CodeExtensions = new(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{ ".cs", ".py", ".js", ".ts", ".tsx", ".jsx", ".java", ".cpp", ".c", ".h", ".go", ".rs", ".rb", ".php", ".swift", ".kt", ".scala", ".sh", ".ps1", ".bat", ".cmd", ".sql", ".xaml", ".vue" };
|
|
||||||
private static readonly HashSet<string> DataExtensions = new(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{ ".csv", ".json", ".xml", ".yaml", ".yml", ".tsv" };
|
|
||||||
// ImageExtensions는 이미지 첨부 영역에서 정의됨 — 재사용
|
|
||||||
|
|
||||||
private Popup? _dropActionPopup;
|
|
||||||
|
|
||||||
private void ShowDropActionMenu(string[] files)
|
|
||||||
{
|
|
||||||
// 파일 유형 판별
|
|
||||||
var ext = System.IO.Path.GetExtension(files[0]).ToLowerInvariant();
|
|
||||||
string category;
|
|
||||||
if (CodeExtensions.Contains(ext)) category = "code";
|
|
||||||
else if (DataExtensions.Contains(ext)) category = "data";
|
|
||||||
else if (ImageExtensions.Contains(ext)) category = "image";
|
|
||||||
else category = "document";
|
|
||||||
|
|
||||||
var actions = DropActions.GetValueOrDefault(category) ?? DropActions["document"];
|
|
||||||
|
|
||||||
// 팝업 생성
|
|
||||||
_dropActionPopup?.SetValue(Popup.IsOpenProperty, false);
|
|
||||||
|
|
||||||
var panel = new StackPanel();
|
|
||||||
// 헤더
|
|
||||||
var header = new TextBlock
|
|
||||||
{
|
|
||||||
Text = $"📎 {System.IO.Path.GetFileName(files[0])}{(files.Length > 1 ? $" 외 {files.Length - 1}개" : "")}",
|
|
||||||
FontSize = 11,
|
|
||||||
Foreground = ThemeResourceHelper.Secondary(this),
|
|
||||||
Margin = new Thickness(12, 8, 12, 6),
|
|
||||||
};
|
|
||||||
panel.Children.Add(header);
|
|
||||||
|
|
||||||
// 액션 항목
|
|
||||||
var hoverBrush = ThemeResourceHelper.HoverBg(this);
|
|
||||||
var accentBrush = ThemeResourceHelper.Accent(this);
|
|
||||||
var textBrush = ThemeResourceHelper.Primary(this);
|
|
||||||
|
|
||||||
foreach (var (label, icon, prompt) in actions)
|
|
||||||
{
|
|
||||||
var capturedPrompt = prompt;
|
|
||||||
var row = new Border
|
|
||||||
{
|
|
||||||
Background = Brushes.Transparent,
|
|
||||||
CornerRadius = new CornerRadius(6),
|
|
||||||
Padding = new Thickness(12, 7, 12, 7),
|
|
||||||
Margin = new Thickness(4, 1, 4, 1),
|
|
||||||
Cursor = Cursors.Hand,
|
|
||||||
};
|
|
||||||
var stack = new StackPanel { Orientation = Orientation.Horizontal };
|
|
||||||
stack.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = icon,
|
|
||||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
|
||||||
FontSize = 13, Foreground = accentBrush,
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
Margin = new Thickness(0, 0, 8, 0),
|
|
||||||
});
|
|
||||||
stack.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = label, FontSize = 13, FontWeight = FontWeights.SemiBold,
|
|
||||||
Foreground = textBrush, VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
});
|
|
||||||
row.Child = stack;
|
|
||||||
|
|
||||||
row.MouseEnter += (_, _) => row.Background = hoverBrush;
|
|
||||||
row.MouseLeave += (_, _) => row.Background = Brushes.Transparent;
|
|
||||||
row.MouseLeftButtonUp += (_, _) =>
|
|
||||||
{
|
|
||||||
if (_dropActionPopup != null) _dropActionPopup.IsOpen = false;
|
|
||||||
foreach (var f in files) AddAttachedFile(f);
|
|
||||||
InputBox.Text = capturedPrompt;
|
|
||||||
InputBox.CaretIndex = InputBox.Text.Length;
|
|
||||||
InputBox.Focus();
|
|
||||||
if (Llm.DragDropAutoSend)
|
|
||||||
_ = SendMessageAsync();
|
|
||||||
};
|
|
||||||
panel.Children.Add(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
// "첨부만" 항목
|
|
||||||
var attachOnly = new Border
|
|
||||||
{
|
|
||||||
Background = Brushes.Transparent,
|
|
||||||
CornerRadius = new CornerRadius(6),
|
|
||||||
Padding = new Thickness(12, 7, 12, 7),
|
|
||||||
Margin = new Thickness(4, 1, 4, 1),
|
|
||||||
Cursor = Cursors.Hand,
|
|
||||||
};
|
|
||||||
var attachStack = new StackPanel { Orientation = Orientation.Horizontal };
|
|
||||||
attachStack.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = "\uE723",
|
|
||||||
FontFamily = ThemeResourceHelper.SegoeMdl2,
|
|
||||||
FontSize = 13, Foreground = ThemeResourceHelper.Secondary(this),
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
Margin = new Thickness(0, 0, 8, 0),
|
|
||||||
});
|
|
||||||
attachStack.Children.Add(new TextBlock
|
|
||||||
{
|
|
||||||
Text = "첨부만", FontSize = 13,
|
|
||||||
Foreground = ThemeResourceHelper.Secondary(this),
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
});
|
|
||||||
attachOnly.Child = attachStack;
|
|
||||||
attachOnly.MouseEnter += (_, _) => attachOnly.Background = hoverBrush;
|
|
||||||
attachOnly.MouseLeave += (_, _) => attachOnly.Background = Brushes.Transparent;
|
|
||||||
attachOnly.MouseLeftButtonUp += (_, _) =>
|
|
||||||
{
|
|
||||||
if (_dropActionPopup != null) _dropActionPopup.IsOpen = false;
|
|
||||||
foreach (var f in files) AddAttachedFile(f);
|
|
||||||
InputBox.Focus();
|
|
||||||
};
|
|
||||||
panel.Children.Add(attachOnly);
|
|
||||||
|
|
||||||
var container = new Border
|
|
||||||
{
|
|
||||||
Background = ThemeResourceHelper.Background(this),
|
|
||||||
CornerRadius = new CornerRadius(12),
|
|
||||||
BorderBrush = ThemeResourceHelper.Border(this),
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
Padding = new Thickness(4, 4, 4, 6),
|
|
||||||
Child = panel,
|
|
||||||
MinWidth = 200,
|
|
||||||
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
|
||||||
{
|
|
||||||
Color = Colors.Black, BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
_dropActionPopup = new Popup
|
|
||||||
{
|
|
||||||
PlacementTarget = InputBorder,
|
|
||||||
Placement = PlacementMode.Top,
|
|
||||||
StaysOpen = false,
|
|
||||||
AllowsTransparency = true,
|
|
||||||
PopupAnimation = PopupAnimation.Fade,
|
|
||||||
Child = container,
|
|
||||||
};
|
|
||||||
_dropActionPopup.IsOpen = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
232
src/AxCopilot/Views/ChatWindow.TabSwitching.cs
Normal file
232
src/AxCopilot/Views/ChatWindow.TabSwitching.cs
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
|
||||||
|
namespace AxCopilot.Views;
|
||||||
|
|
||||||
|
public partial class ChatWindow
|
||||||
|
{
|
||||||
|
// ─── 탭 전환 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private string _activeTab = "Chat";
|
||||||
|
|
||||||
|
/// <summary>탭별로 마지막으로 활성화된 대화 ID를 기억.</summary>
|
||||||
|
private readonly Dictionary<string, string?> _tabConversationId = new()
|
||||||
|
{
|
||||||
|
["Chat"] = null, ["Cowork"] = null, ["Code"] = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
private void SaveCurrentTabConversationId()
|
||||||
|
{
|
||||||
|
lock (_convLock)
|
||||||
|
{
|
||||||
|
if (_currentConversation != null && _currentConversation.Messages.Count > 0)
|
||||||
|
{
|
||||||
|
_tabConversationId[_activeTab] = _currentConversation.Id;
|
||||||
|
// 탭 전환 시 현재 대화를 즉시 저장 (스트리밍 중이어도 진행 중인 내용 보존)
|
||||||
|
try { _storage.Save(_currentConversation); } catch (Exception) { /* 대화 저장 실패 — UI 차단 방지 */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 탭별 마지막 대화 ID를 설정에 영속 저장 (앱 재시작 시 복원용)
|
||||||
|
SaveLastConversations();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>탭 전환 전 스트리밍 중이면 즉시 중단합니다.</summary>
|
||||||
|
private void StopStreamingIfActive()
|
||||||
|
{
|
||||||
|
if (!_isStreaming) return;
|
||||||
|
// 스트리밍 중단
|
||||||
|
_streamCts?.Cancel();
|
||||||
|
_cursorTimer.Stop();
|
||||||
|
_elapsedTimer.Stop();
|
||||||
|
_typingTimer.Stop();
|
||||||
|
StopRainbowGlow();
|
||||||
|
HideStickyProgress();
|
||||||
|
_activeStreamText = null;
|
||||||
|
_elapsedLabel = null;
|
||||||
|
_cachedStreamContent = "";
|
||||||
|
_isStreaming = false;
|
||||||
|
BtnSend.IsEnabled = true;
|
||||||
|
BtnStop.Visibility = Visibility.Collapsed;
|
||||||
|
BtnPause.Visibility = Visibility.Collapsed;
|
||||||
|
PauseIcon.Text = "\uE769"; // 리셋
|
||||||
|
BtnSend.Visibility = Visibility.Visible;
|
||||||
|
_streamCts?.Dispose();
|
||||||
|
_streamCts = null;
|
||||||
|
SetStatusIdle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TabChat_Checked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_activeTab == "Chat") return;
|
||||||
|
StopStreamingIfActive();
|
||||||
|
SaveCurrentTabConversationId();
|
||||||
|
_activeTab = "Chat";
|
||||||
|
_selectedCategory = ""; UpdateCategoryLabel();
|
||||||
|
UpdateTabUI();
|
||||||
|
UpdatePlanModeUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TabCowork_Checked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_activeTab == "Cowork") return;
|
||||||
|
StopStreamingIfActive();
|
||||||
|
SaveCurrentTabConversationId();
|
||||||
|
_activeTab = "Cowork";
|
||||||
|
_selectedCategory = ""; UpdateCategoryLabel();
|
||||||
|
UpdateTabUI();
|
||||||
|
UpdatePlanModeUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TabCode_Checked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_activeTab == "Code") return;
|
||||||
|
StopStreamingIfActive();
|
||||||
|
SaveCurrentTabConversationId();
|
||||||
|
_activeTab = "Code";
|
||||||
|
_selectedCategory = ""; UpdateCategoryLabel();
|
||||||
|
UpdateTabUI();
|
||||||
|
UpdatePlanModeUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTabUI()
|
||||||
|
{
|
||||||
|
// 폴더 바는 Cowork/Code 탭에서만 표시
|
||||||
|
if (FolderBar != null)
|
||||||
|
FolderBar.Visibility = _activeTab != "Chat" ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
|
||||||
|
// 탭별 입력 안내 문구
|
||||||
|
if (InputWatermark != null)
|
||||||
|
{
|
||||||
|
InputWatermark.Text = _activeTab switch
|
||||||
|
{
|
||||||
|
"Cowork" => "에이전트에게 작업을 요청하세요 (파일 읽기/쓰기, 문서 생성...)",
|
||||||
|
"Code" => "코드 관련 작업을 요청하세요...",
|
||||||
|
_ => _promptCardPlaceholder,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 권한 기본값 적용 (Cowork/Code 탭은 설정의 기본값 사용)
|
||||||
|
ApplyTabDefaultPermission();
|
||||||
|
|
||||||
|
// 포맷/디자인 드롭다운은 Cowork 탭에서만 표시
|
||||||
|
if (_activeTab == "Cowork")
|
||||||
|
{
|
||||||
|
BuildBottomBar();
|
||||||
|
if (Llm.ShowFileBrowser && FileBrowserPanel != null)
|
||||||
|
{
|
||||||
|
FileBrowserPanel.Visibility = Visibility.Visible;
|
||||||
|
BuildFileTree();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (_activeTab == "Code")
|
||||||
|
{
|
||||||
|
// Code 탭: 언어 선택기 + 파일 탐색기
|
||||||
|
BuildCodeBottomBar();
|
||||||
|
if (Llm.ShowFileBrowser && FileBrowserPanel != null)
|
||||||
|
{
|
||||||
|
FileBrowserPanel.Visibility = Visibility.Visible;
|
||||||
|
BuildFileTree();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MoodIconPanel.Children.Clear();
|
||||||
|
if (FormatMoodSeparator != null) FormatMoodSeparator.Visibility = Visibility.Collapsed;
|
||||||
|
if (FileBrowserPanel != null) FileBrowserPanel.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 탭별 프리셋 버튼 재구성
|
||||||
|
BuildTopicButtons();
|
||||||
|
|
||||||
|
// 현재 대화를 해당 탭 대화로 전환
|
||||||
|
SwitchToTabConversation();
|
||||||
|
|
||||||
|
// Cowork/Code 탭 전환 시 팁 표시
|
||||||
|
ShowRandomTip();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BtnPlanMode_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
// 3단 순환: off → auto → always → off
|
||||||
|
Llm.PlanMode = Llm.PlanMode switch
|
||||||
|
{
|
||||||
|
"auto" => "always",
|
||||||
|
"always" => "off",
|
||||||
|
_ => "auto"
|
||||||
|
};
|
||||||
|
_settings.Save();
|
||||||
|
UpdatePlanModeUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePlanModeUI()
|
||||||
|
{
|
||||||
|
var planMode = Llm.PlanMode ?? "off";
|
||||||
|
if (PlanModeValue == null) return;
|
||||||
|
|
||||||
|
PlanModeValue.Text = planMode switch
|
||||||
|
{
|
||||||
|
"auto" => "Auto",
|
||||||
|
"always" => "Always",
|
||||||
|
_ => "Off"
|
||||||
|
};
|
||||||
|
var isActive = planMode != "off";
|
||||||
|
var activeBrush = ThemeResourceHelper.Accent(this);
|
||||||
|
var secondaryBrush = ThemeResourceHelper.Secondary(this);
|
||||||
|
if (PlanModeIcon != null) PlanModeIcon.Foreground = isActive ? activeBrush : secondaryBrush;
|
||||||
|
if (PlanModeLabel != null) PlanModeLabel.Foreground = isActive ? activeBrush : secondaryBrush;
|
||||||
|
if (BtnPlanMode != null)
|
||||||
|
BtnPlanMode.Background = isActive
|
||||||
|
? new SolidColorBrush(Color.FromArgb(0x1A, 0x4B, 0x5E, 0xFC))
|
||||||
|
: Brushes.Transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SwitchToTabConversation()
|
||||||
|
{
|
||||||
|
// 이전 탭의 대화 저장
|
||||||
|
lock (_convLock)
|
||||||
|
{
|
||||||
|
if (_currentConversation != null && _currentConversation.Messages.Count > 0)
|
||||||
|
{
|
||||||
|
try { _storage.Save(_currentConversation); } catch (Exception) { /* 대화 저장 실패 — UI 차단 방지 */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 현재 탭에 기억된 대화가 있으면 복원
|
||||||
|
var savedId = _tabConversationId.GetValueOrDefault(_activeTab);
|
||||||
|
if (!string.IsNullOrEmpty(savedId))
|
||||||
|
{
|
||||||
|
var conv = _storage.Load(savedId);
|
||||||
|
if (conv != null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(conv.Tab)) conv.Tab = _activeTab;
|
||||||
|
lock (_convLock) _currentConversation = conv;
|
||||||
|
MessagePanel.Children.Clear();
|
||||||
|
foreach (var msg in conv.Messages)
|
||||||
|
AddMessageBubble(msg.Role, msg.Content, animate: false, message: msg);
|
||||||
|
EmptyState.Visibility = conv.Messages.Count > 0 ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
UpdateChatTitle();
|
||||||
|
RefreshConversationList();
|
||||||
|
UpdateFolderBar();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기억된 대화가 없으면 새 대화
|
||||||
|
lock (_convLock)
|
||||||
|
{
|
||||||
|
_currentConversation = new Models.ChatConversation { Tab = _activeTab };
|
||||||
|
var workFolder = Llm.WorkFolder;
|
||||||
|
if (!string.IsNullOrEmpty(workFolder) && _activeTab != "Chat")
|
||||||
|
_currentConversation.WorkFolder = workFolder;
|
||||||
|
}
|
||||||
|
MessagePanel.Children.Clear();
|
||||||
|
EmptyState.Visibility = Visibility.Visible;
|
||||||
|
_attachedFiles.Clear();
|
||||||
|
RefreshAttachedFilesUI();
|
||||||
|
UpdateChatTitle();
|
||||||
|
RefreshConversationList();
|
||||||
|
UpdateFolderBar();
|
||||||
|
}
|
||||||
|
}
|
||||||
210
src/AxCopilot/Views/SkillGalleryWindow.SkillDetail.cs
Normal file
210
src/AxCopilot/Views/SkillGalleryWindow.SkillDetail.cs
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using AxCopilot.Services.Agent;
|
||||||
|
|
||||||
|
namespace AxCopilot.Views;
|
||||||
|
|
||||||
|
public partial class SkillGalleryWindow
|
||||||
|
{
|
||||||
|
// ─── 스킬 상세 보기 팝업 ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
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 = ThemeResourceHelper.SegoeMdl2,
|
||||||
|
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 = ThemeResourceHelper.SegoeMdl2,
|
||||||
|
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++);
|
||||||
|
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 = ThemeResourceHelper.ConsolasCode,
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -428,204 +428,4 @@ public partial class SkillGalleryWindow : Window
|
|||||||
btn.MouseLeftButtonUp += (_, _) => action();
|
btn.MouseLeftButtonUp += (_, _) => action();
|
||||||
return btn;
|
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 = ThemeResourceHelper.SegoeMdl2,
|
|
||||||
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 = ThemeResourceHelper.SegoeMdl2,
|
|
||||||
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++);
|
|
||||||
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 = ThemeResourceHelper.ConsolasCode,
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -394,274 +394,4 @@ public partial class WorkflowAnalyzerWindow
|
|||||||
});
|
});
|
||||||
return sp;
|
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 = ThemeResourceHelper.SegoeMdl2,
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
281
src/AxCopilot/Views/WorkflowAnalyzerWindow.Timeline.cs
Normal file
281
src/AxCopilot/Views/WorkflowAnalyzerWindow.Timeline.cs
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
using AxCopilot.Services.Agent;
|
||||||
|
|
||||||
|
namespace AxCopilot.Views;
|
||||||
|
|
||||||
|
public partial class WorkflowAnalyzerWindow
|
||||||
|
{
|
||||||
|
// ─── 타임라인 노드 생성 ────────────────────────────────────────
|
||||||
|
|
||||||
|
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 = ThemeResourceHelper.SegoeMdl2,
|
||||||
|
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 static string Truncate(string text, int maxLen)
|
||||||
|
=> text.Length <= maxLen ? text : text[..maxLen] + "…";
|
||||||
|
|
||||||
|
// ─── 윈도우 이벤트 ────────────────────────────────────────────
|
||||||
|
|
||||||
|
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 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