[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"; // 복원/최대화 아이콘
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// ─── 드래그 앤 드롭 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();
|
||||
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;
|
||||
}
|
||||
|
||||
// ─── 타임라인 노드 생성 ────────────────────────────────────────
|
||||
|
||||
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