diff --git a/README.md b/README.md index 7b20319..f5a601f 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저 개발 참고: Claw Code 동등성 작업 추적 문서 `docs/claw-code-parity-plan.md` +- 업데이트: 2026-04-06 08:47 (KST) +- AX Agent 우측 프리뷰 패널 렌더를 `ChatWindow.PreviewPresentation.cs`로 분리했습니다. 프리뷰 탭 목록, 헤더, 파일 로드, CSV/텍스트/마크다운/HTML 표시, 숨김/열기, 우클릭 메뉴, 별도 창 미리보기 흐름이 메인 창 코드 밖으로 이동했습니다. +- `ChatWindow.xaml.cs`는 transcript 및 런타임 orchestration 중심으로 더 정리됐고, claw-code 기준 preview surface 품질 작업을 이어가기 쉬운 구조로 맞췄습니다. + - 업데이트: 2026-04-06 08:39 (KST) - AX Agent 하단 상태바 이벤트 처리와 회전 애니메이션을 `ChatWindow.StatusPresentation.cs`로 옮겼습니다. `UpdateStatusBar`, `StartStatusAnimation`, `StopStatusAnimation`이 상태 표현 파일로 이동해 메인 창 코드의 runtime/status 분기가 더 줄었습니다. - `ChatWindow.xaml.cs`는 대화 실행 orchestration 중심으로 더 정리됐고, claw-code 기준 status line 정교화와 footer presentation 개선을 계속 이어가기 쉬운 구조로 맞췄습니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 4bd4c8d..98cd864 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4899,3 +4899,5 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - Document update: 2026-04-06 08:27 (KST) - This pass further reduces renderer responsibility in the main chat window file and moves AX Agent closer to the `claw-code` structure where transcript orchestration and message interaction presentation are separated. - Document update: 2026-04-06 08:39 (KST) - Moved runtime status bar event handling and spinner animation out of `ChatWindow.xaml.cs` into `ChatWindow.StatusPresentation.cs`. `UpdateStatusBar`, `StartStatusAnimation`, and `StopStatusAnimation` now live alongside the existing operational status presentation helpers. - Document update: 2026-04-06 08:39 (KST) - This pass reduces direct status-line branching in the main chat window file and keeps AX Agent closer to the `claw-code` model where runtime/footer presentation is separated from session orchestration. +- Document update: 2026-04-06 08:47 (KST) - Split preview surface rendering out of `ChatWindow.xaml.cs` into `ChatWindow.PreviewPresentation.cs`. Preview tab tracking, panel open/close, tab bar rebuilding, preview header state, CSV/text/markdown/HTML loaders, context menu actions, and external popup preview are now grouped in a dedicated partial. +- Document update: 2026-04-06 08:47 (KST) - This keeps `ChatWindow.xaml.cs` focused on transcript/runtime orchestration and aligns AX Agent more closely with the `claw-code` model where preview/session surfaces are treated as separate presentation layers. diff --git a/src/AxCopilot/Views/ChatWindow.PreviewPresentation.cs b/src/AxCopilot/Views/ChatWindow.PreviewPresentation.cs new file mode 100644 index 0000000..153ce30 --- /dev/null +++ b/src/AxCopilot/Views/ChatWindow.PreviewPresentation.cs @@ -0,0 +1,786 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Threading; +using AxCopilot.Services; + +namespace AxCopilot.Views; + +public partial class ChatWindow +{ + private static readonly HashSet _previewableExtensions = new(StringComparer.OrdinalIgnoreCase) + { + ".html", ".htm", ".md", ".txt", ".csv", ".json", ".xml", ".log", + }; + + /// 열려 있는 프리뷰 탭 목록 (파일 경로 기준). + private readonly List _previewTabs = new(); + private string? _activePreviewTab; + private bool _webViewInitialized; + private Popup? _previewTabPopup; + + private static readonly string WebView2DataFolder = + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "AxCopilot", "WebView2"); + + private void TryShowPreview(string filePath) + { + if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) + return; + + PreviewWindow.ShowPreview(filePath, _selectedMood); + } + + private void ShowPreviewPanel(string filePath) + { + if (!_previewTabs.Contains(filePath, StringComparer.OrdinalIgnoreCase)) + _previewTabs.Add(filePath); + + _activePreviewTab = filePath; + + if (PreviewColumn.Width.Value < 100) + { + PreviewColumn.Width = new GridLength(420); + SplitterColumn.Width = new GridLength(5); + } + + PreviewPanel.Visibility = Visibility.Visible; + PreviewSplitter.Visibility = Visibility.Visible; + BtnPreviewToggle.Visibility = Visibility.Visible; + + RebuildPreviewTabs(); + LoadPreviewContent(filePath); + } + + private void RebuildPreviewTabs() + { + PreviewTabPanel.Children.Clear(); + + var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; + var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; + var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; + var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; + + foreach (var tabPath in _previewTabs) + { + var fileName = Path.GetFileName(tabPath); + var isActive = string.Equals(tabPath, _activePreviewTab, StringComparison.OrdinalIgnoreCase); + + var tabBorder = new Border + { + Background = isActive + ? new SolidColorBrush(Color.FromArgb(0x15, 0xFF, 0xFF, 0xFF)) + : Brushes.Transparent, + BorderBrush = isActive ? accentBrush : Brushes.Transparent, + BorderThickness = new Thickness(0, 0, 0, isActive ? 2 : 0), + Padding = new Thickness(8, 6, 4, 6), + Cursor = Cursors.Hand, + MaxWidth = _previewTabs.Count <= 3 ? 200 : (_previewTabs.Count <= 5 ? 140 : 100), + }; + + var tabContent = new StackPanel { Orientation = Orientation.Horizontal }; + tabContent.Children.Add(new TextBlock + { + Text = fileName, + FontSize = 11, + Foreground = isActive ? primaryText : secondaryText, + FontWeight = isActive ? FontWeights.SemiBold : FontWeights.Normal, + VerticalAlignment = VerticalAlignment.Center, + TextTrimming = TextTrimming.CharacterEllipsis, + MaxWidth = tabBorder.MaxWidth - 30, + ToolTip = tabPath, + }); + + var closeFg = isActive ? primaryText : secondaryText; + var closeBtnText = new TextBlock + { + Text = "\uE711", + FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontSize = 10, + Foreground = closeFg, + VerticalAlignment = VerticalAlignment.Center, + }; + var closeBtn = new Border + { + Background = Brushes.Transparent, + CornerRadius = new CornerRadius(3), + Padding = new Thickness(3, 2, 3, 2), + Margin = new Thickness(5, 0, 0, 0), + Cursor = Cursors.Hand, + VerticalAlignment = VerticalAlignment.Center, + Visibility = isActive ? Visibility.Visible : Visibility.Hidden, + Child = closeBtnText, + Tag = "close", + }; + + var closePath = tabPath; + closeBtn.MouseEnter += (s, _) => + { + if (s is Border b) + { + b.Background = new SolidColorBrush(Color.FromArgb(0x40, 0xFF, 0x50, 0x50)); + if (b.Child is TextBlock tb) + tb.Foreground = new SolidColorBrush(Color.FromRgb(0xFF, 0x60, 0x60)); + } + }; + closeBtn.MouseLeave += (s, _) => + { + if (s is Border b) + { + b.Background = Brushes.Transparent; + if (b.Child is TextBlock tb) + tb.Foreground = closeFg; + } + }; + closeBtn.MouseLeftButtonUp += (_, e) => + { + e.Handled = true; + ClosePreviewTab(closePath); + }; + + tabContent.Children.Add(closeBtn); + tabBorder.Child = tabContent; + + var clickPath = tabPath; + tabBorder.MouseLeftButtonUp += (_, e) => + { + if (e.Handled) + return; + + e.Handled = true; + _activePreviewTab = clickPath; + RebuildPreviewTabs(); + LoadPreviewContent(clickPath); + }; + + var ctxPath = tabPath; + tabBorder.MouseRightButtonUp += (_, e) => + { + e.Handled = true; + ShowPreviewTabContextMenu(ctxPath); + }; + + var dblPath = tabPath; + tabBorder.MouseLeftButtonDown += (_, e) => + { + if (e.Handled) + return; + + if (e.ClickCount == 2) + { + e.Handled = true; + OpenPreviewPopupWindow(dblPath); + } + }; + + var capturedIsActive = isActive; + var capturedCloseBtn = closeBtn; + tabBorder.MouseEnter += (s, _) => + { + if (s is Border b && !capturedIsActive) + b.Background = new SolidColorBrush(Color.FromArgb(0x10, 0xFF, 0xFF, 0xFF)); + if (!capturedIsActive) + capturedCloseBtn.Visibility = Visibility.Visible; + }; + tabBorder.MouseLeave += (s, _) => + { + if (s is Border b && !capturedIsActive) + b.Background = Brushes.Transparent; + if (!capturedIsActive) + capturedCloseBtn.Visibility = Visibility.Hidden; + }; + + PreviewTabPanel.Children.Add(tabBorder); + + if (!string.Equals(tabPath, _previewTabs[^1], StringComparison.OrdinalIgnoreCase)) + { + PreviewTabPanel.Children.Add(new Border + { + Width = 1, + Height = 14, + Background = borderBrush, + Margin = new Thickness(0, 4, 0, 4), + VerticalAlignment = VerticalAlignment.Center, + }); + } + } + } + + private void ClosePreviewTab(string filePath) + { + _previewTabs.Remove(filePath); + + if (_previewTabs.Count == 0) + { + HidePreviewPanel(); + return; + } + + if (string.Equals(filePath, _activePreviewTab, StringComparison.OrdinalIgnoreCase)) + { + _activePreviewTab = _previewTabs[^1]; + LoadPreviewContent(_activePreviewTab); + } + + RebuildPreviewTabs(); + } + + private async void LoadPreviewContent(string filePath) + { + var ext = Path.GetExtension(filePath).ToLowerInvariant(); + SetPreviewHeader(filePath); + + PreviewWebView.Visibility = Visibility.Collapsed; + PreviewTextScroll.Visibility = Visibility.Collapsed; + PreviewDataGrid.Visibility = Visibility.Collapsed; + PreviewEmpty.Visibility = Visibility.Collapsed; + + if (!File.Exists(filePath)) + { + SetPreviewHeaderState("파일을 찾을 수 없습니다"); + PreviewEmpty.Text = "파일을 찾을 수 없습니다"; + PreviewEmpty.Visibility = Visibility.Visible; + return; + } + + try + { + switch (ext) + { + case ".html": + case ".htm": + await EnsureWebViewInitializedAsync(); + PreviewWebView.Source = new Uri(filePath); + PreviewWebView.Visibility = Visibility.Visible; + break; + + case ".csv": + LoadCsvPreview(filePath); + PreviewDataGrid.Visibility = Visibility.Visible; + break; + + case ".md": + await EnsureWebViewInitializedAsync(); + var mdText = File.ReadAllText(filePath); + if (mdText.Length > 50000) + mdText = mdText[..50000]; + var mdHtml = Services.Agent.TemplateService.RenderMarkdownToHtml(mdText, _selectedMood); + PreviewWebView.NavigateToString(mdHtml); + PreviewWebView.Visibility = Visibility.Visible; + break; + + case ".txt": + case ".json": + case ".xml": + case ".log": + var text = File.ReadAllText(filePath); + if (text.Length > 50000) + text = text[..50000] + "\n\n... (이후 생략)"; + PreviewTextBlock.Text = text; + PreviewTextScroll.Visibility = Visibility.Visible; + break; + + default: + SetPreviewHeaderState("지원되지 않는 형식"); + PreviewEmpty.Text = "미리보기할 수 없는 파일 형식입니다"; + PreviewEmpty.Visibility = Visibility.Visible; + break; + } + } + catch (Exception ex) + { + SetPreviewHeaderState("미리보기 오류"); + PreviewTextBlock.Text = $"미리보기 오류: {ex.Message}"; + PreviewTextScroll.Visibility = Visibility.Visible; + } + } + + private void SetPreviewHeader(string filePath) + { + if (PreviewHeaderTitle == null || PreviewHeaderSubtitle == null || PreviewHeaderMeta == null) + return; + + var fileName = Path.GetFileName(filePath); + var extension = Path.GetExtension(filePath).TrimStart('.').ToUpperInvariant(); + var fileInfo = new FileInfo(filePath); + var sizeText = fileInfo.Exists + ? fileInfo.Length >= 1024 * 1024 + ? $"{fileInfo.Length / 1024d / 1024d:F1} MB" + : $"{Math.Max(1, fileInfo.Length / 1024d):F0} KB" + : "파일 없음"; + + PreviewHeaderTitle.Text = string.IsNullOrWhiteSpace(fileName) ? "미리보기" : fileName; + PreviewHeaderSubtitle.Text = filePath; + PreviewHeaderMeta.Text = string.IsNullOrWhiteSpace(extension) + ? sizeText + : $"{extension} · {sizeText}"; + } + + private void SetPreviewHeaderState(string state) + { + if (PreviewHeaderMeta != null && !string.IsNullOrWhiteSpace(state)) + PreviewHeaderMeta.Text = state; + } + + private async Task EnsureWebViewInitializedAsync() + { + if (_webViewInitialized) + return; + + try + { + var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync( + userDataFolder: WebView2DataFolder); + await PreviewWebView.EnsureCoreWebView2Async(env); + _webViewInitialized = true; + } + catch (Exception ex) + { + LogService.Warn($"WebView2 초기화 실패: {ex.Message}"); + } + } + + private void LoadCsvPreview(string filePath) + { + try + { + var lines = File.ReadAllLines(filePath); + if (lines.Length == 0) + return; + + var dt = new DataTable(); + var headers = ParseCsvLine(lines[0]); + foreach (var h in headers) + dt.Columns.Add(h); + + var maxRows = Math.Min(lines.Length, 501); + for (var i = 1; i < maxRows; i++) + { + var vals = ParseCsvLine(lines[i]); + var row = dt.NewRow(); + for (var j = 0; j < Math.Min(vals.Length, headers.Length); j++) + row[j] = vals[j]; + dt.Rows.Add(row); + } + + PreviewDataGrid.ItemsSource = dt.DefaultView; + } + catch (Exception ex) + { + PreviewTextBlock.Text = $"CSV 로드 오류: {ex.Message}"; + PreviewTextScroll.Visibility = Visibility.Visible; + PreviewDataGrid.Visibility = Visibility.Collapsed; + } + } + + private static string[] ParseCsvLine(string line) + { + var fields = new List(); + var current = new StringBuilder(); + var inQuotes = false; + + for (var i = 0; i < line.Length; i++) + { + var c = line[i]; + if (inQuotes) + { + if (c == '"' && i + 1 < line.Length && line[i + 1] == '"') + { + current.Append('"'); + i++; + } + else if (c == '"') + { + inQuotes = false; + } + else + { + current.Append(c); + } + } + else + { + if (c == '"') + { + inQuotes = true; + } + else if (c == ',') + { + fields.Add(current.ToString()); + current.Clear(); + } + else + { + current.Append(c); + } + } + } + + fields.Add(current.ToString()); + return fields.ToArray(); + } + + private void HidePreviewPanel() + { + _previewTabs.Clear(); + _activePreviewTab = null; + if (PreviewHeaderTitle != null) PreviewHeaderTitle.Text = "미리보기"; + if (PreviewHeaderSubtitle != null) PreviewHeaderSubtitle.Text = "선택한 파일이 여기에 표시됩니다"; + if (PreviewHeaderMeta != null) PreviewHeaderMeta.Text = "파일 메타"; + PreviewColumn.Width = new GridLength(0); + SplitterColumn.Width = new GridLength(0); + PreviewPanel.Visibility = Visibility.Collapsed; + PreviewSplitter.Visibility = Visibility.Collapsed; + PreviewWebView.Visibility = Visibility.Collapsed; + PreviewTextScroll.Visibility = Visibility.Collapsed; + PreviewDataGrid.Visibility = Visibility.Collapsed; + try + { + if (_webViewInitialized) + PreviewWebView.CoreWebView2?.NavigateToString(""); + } + catch + { + } + } + + private void PreviewTabBar_PreviewMouseDown(object sender, MouseButtonEventArgs e) + { + if (PreviewWebView.IsFocused || PreviewWebView.IsKeyboardFocusWithin) + { + var border = sender as Border; + border?.Focus(); + } + } + + private void BtnClosePreview_Click(object sender, RoutedEventArgs e) + { + HidePreviewPanel(); + BtnPreviewToggle.Visibility = Visibility.Collapsed; + } + + private void BtnPreviewToggle_Click(object sender, RoutedEventArgs e) + { + if (PreviewPanel.Visibility == Visibility.Visible) + { + PreviewPanel.Visibility = Visibility.Collapsed; + PreviewSplitter.Visibility = Visibility.Collapsed; + PreviewColumn.Width = new GridLength(0); + SplitterColumn.Width = new GridLength(0); + } + else if (_previewTabs.Count > 0) + { + PreviewPanel.Visibility = Visibility.Visible; + PreviewSplitter.Visibility = Visibility.Visible; + PreviewColumn.Width = new GridLength(420); + SplitterColumn.Width = new GridLength(5); + RebuildPreviewTabs(); + if (_activePreviewTab != null) + LoadPreviewContent(_activePreviewTab); + } + } + + private void BtnOpenExternal_Click(object sender, RoutedEventArgs e) + { + if (string.IsNullOrEmpty(_activePreviewTab) || !File.Exists(_activePreviewTab)) + return; + + try + { + Process.Start(new ProcessStartInfo + { + FileName = _activePreviewTab, + UseShellExecute = true, + }); + } + catch (Exception ex) + { + Debug.WriteLine($"외부 프로그램 실행 오류: {ex.Message}"); + } + } + + private void ShowPreviewTabContextMenu(string filePath) + { + if (_previewTabPopup != null) + _previewTabPopup.IsOpen = false; + + var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); + var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; + var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; + var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; + var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); + + var stack = new StackPanel(); + + void AddItem(string icon, string iconColor, string label, Action action) + { + var itemBorder = new Border + { + Background = Brushes.Transparent, + CornerRadius = new CornerRadius(6), + Padding = new Thickness(10, 7, 16, 7), + Cursor = Cursors.Hand, + }; + var sp = new StackPanel { Orientation = Orientation.Horizontal }; + sp.Children.Add(new TextBlock + { + Text = icon, + FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontSize = 12, + Foreground = string.IsNullOrEmpty(iconColor) + ? secondaryText + : new SolidColorBrush((Color)ColorConverter.ConvertFromString(iconColor)), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 8, 0), + }); + sp.Children.Add(new TextBlock + { + Text = label, + FontSize = 13, + Foreground = primaryText, + VerticalAlignment = VerticalAlignment.Center, + }); + itemBorder.Child = sp; + itemBorder.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; }; + itemBorder.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; + itemBorder.MouseLeftButtonUp += (_, _) => + { + _previewTabPopup!.IsOpen = false; + action(); + }; + stack.Children.Add(itemBorder); + } + + void AddSeparator() + { + stack.Children.Add(new Border + { + Height = 1, + Background = borderBrush, + Margin = new Thickness(8, 3, 8, 3), + }); + } + + AddItem("\uE8A7", "#64B5F6", "외부 프로그램으로 열기", () => + { + try + { + Process.Start(new ProcessStartInfo + { + FileName = filePath, + UseShellExecute = true, + }); + } + catch + { + } + }); + + AddItem("\uE838", "#FFB74D", "파일 위치 열기", () => + { + try + { + Process.Start("explorer.exe", $"/select,\"{filePath}\""); + } + catch + { + } + }); + + AddItem("\uE8A7", "#81C784", "별도 창에서 보기", () => OpenPreviewPopupWindow(filePath)); + + AddSeparator(); + + AddItem("\uE8C8", "", "경로 복사", () => + { + try + { + Clipboard.SetText(filePath); + } + catch + { + } + }); + + AddSeparator(); + + AddItem("\uE711", "#EF5350", "이 탭 닫기", () => ClosePreviewTab(filePath)); + + if (_previewTabs.Count > 1) + { + AddItem("\uE8BB", "#EF5350", "다른 탭 모두 닫기", () => + { + var keep = filePath; + _previewTabs.RemoveAll(p => !string.Equals(p, keep, StringComparison.OrdinalIgnoreCase)); + _activePreviewTab = keep; + RebuildPreviewTabs(); + LoadPreviewContent(keep); + }); + } + + var popupBorder = new Border + { + Background = bg, + BorderBrush = borderBrush, + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(12), + Padding = new Thickness(4, 6, 4, 6), + MinWidth = 180, + Effect = new System.Windows.Media.Effects.DropShadowEffect + { + BlurRadius = 16, + Opacity = 0.4, + ShadowDepth = 4, + Color = Colors.Black, + }, + Child = stack, + }; + + _previewTabPopup = new Popup + { + Child = popupBorder, + Placement = PlacementMode.MousePoint, + StaysOpen = false, + AllowsTransparency = true, + PopupAnimation = PopupAnimation.Fade, + }; + _previewTabPopup.IsOpen = true; + } + + private void OpenPreviewPopupWindow(string filePath) + { + if (!File.Exists(filePath)) + return; + + var ext = Path.GetExtension(filePath).ToLowerInvariant(); + var fileName = Path.GetFileName(filePath); + var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); + var fg = TryFindResource("PrimaryText") as Brush ?? Brushes.White; + + var win = new Window + { + Title = $"미리보기 — {fileName}", + Width = 900, + Height = 700, + WindowStartupLocation = WindowStartupLocation.CenterScreen, + Background = bg, + }; + + FrameworkElement content; + + switch (ext) + { + case ".html": + case ".htm": + var wv = new Microsoft.Web.WebView2.Wpf.WebView2(); + wv.Loaded += async (_, _) => + { + try + { + var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync( + userDataFolder: WebView2DataFolder); + await wv.EnsureCoreWebView2Async(env); + wv.Source = new Uri(filePath); + } + catch + { + } + }; + content = wv; + break; + + case ".md": + var mdWv = new Microsoft.Web.WebView2.Wpf.WebView2(); + var mdMood = _selectedMood; + mdWv.Loaded += async (_, _) => + { + try + { + var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync( + userDataFolder: WebView2DataFolder); + await mdWv.EnsureCoreWebView2Async(env); + var mdSrc = File.ReadAllText(filePath); + if (mdSrc.Length > 100000) + mdSrc = mdSrc[..100000]; + var html = Services.Agent.TemplateService.RenderMarkdownToHtml(mdSrc, mdMood); + mdWv.NavigateToString(html); + } + catch + { + } + }; + content = mdWv; + break; + + case ".csv": + var dg = new DataGrid + { + AutoGenerateColumns = true, + IsReadOnly = true, + Background = Brushes.Transparent, + Foreground = Brushes.White, + BorderThickness = new Thickness(0), + FontSize = 12, + }; + try + { + var lines = File.ReadAllLines(filePath); + if (lines.Length > 0) + { + var dt = new DataTable(); + var headers = ParseCsvLine(lines[0]); + foreach (var h in headers) + dt.Columns.Add(h); + for (var i = 1; i < Math.Min(lines.Length, 1001); i++) + { + var vals = ParseCsvLine(lines[i]); + var row = dt.NewRow(); + for (var j = 0; j < Math.Min(vals.Length, dt.Columns.Count); j++) + row[j] = vals[j]; + dt.Rows.Add(row); + } + + dg.ItemsSource = dt.DefaultView; + } + } + catch + { + } + content = dg; + break; + + default: + var text = File.ReadAllText(filePath); + if (text.Length > 100000) + text = text[..100000] + "\n\n... (이후 생략)"; + var sv = new ScrollViewer + { + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + Padding = new Thickness(20), + Content = new TextBlock + { + Text = text, + TextWrapping = TextWrapping.Wrap, + FontFamily = new FontFamily("Consolas"), + FontSize = 13, + Foreground = fg, + }, + }; + content = sv; + break; + } + + win.Content = content; + win.Show(); + } +} diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index 8a8d6ff..905c89d 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -15719,733 +15719,6 @@ public partial class ChatWindow : Window // ─── 미리보기 패널 (탭 기반) ───────────────────────────────────────────── - private static readonly HashSet _previewableExtensions = new(StringComparer.OrdinalIgnoreCase) - { - ".html", ".htm", ".md", ".txt", ".csv", ".json", ".xml", ".log", - }; - - /// 열려 있는 프리뷰 탭 목록 (파일 경로 기준). - private readonly List _previewTabs = new(); - private string? _activePreviewTab; - - private void TryShowPreview(string filePath) - { - if (string.IsNullOrEmpty(filePath) || !System.IO.File.Exists(filePath)) - return; - - // 별도 커스텀 창으로 미리보기 (WebView2 HWND airspace 문제 근본 해결) - PreviewWindow.ShowPreview(filePath, _selectedMood); - } - - private void ShowPreviewPanel(string filePath) - { - // 탭에 없으면 추가 - if (!_previewTabs.Contains(filePath, StringComparer.OrdinalIgnoreCase)) - _previewTabs.Add(filePath); - - _activePreviewTab = filePath; - - // 패널 열기 - if (PreviewColumn.Width.Value < 100) - { - PreviewColumn.Width = new GridLength(420); - SplitterColumn.Width = new GridLength(5); - } - PreviewPanel.Visibility = Visibility.Visible; - PreviewSplitter.Visibility = Visibility.Visible; - BtnPreviewToggle.Visibility = Visibility.Visible; - - RebuildPreviewTabs(); - LoadPreviewContent(filePath); - } - - /// 탭 바 UI를 다시 구성합니다. - private void RebuildPreviewTabs() - { - PreviewTabPanel.Children.Clear(); - - var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; - var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; - var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; - var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; - - foreach (var tabPath in _previewTabs) - { - var fileName = System.IO.Path.GetFileName(tabPath); - var isActive = string.Equals(tabPath, _activePreviewTab, StringComparison.OrdinalIgnoreCase); - - var tabBorder = new Border - { - Background = isActive - ? new SolidColorBrush(Color.FromArgb(0x15, 0xFF, 0xFF, 0xFF)) - : Brushes.Transparent, - BorderBrush = isActive ? accentBrush : Brushes.Transparent, - BorderThickness = new Thickness(0, 0, 0, isActive ? 2 : 0), - Padding = new Thickness(8, 6, 4, 6), - Cursor = Cursors.Hand, - MaxWidth = _previewTabs.Count <= 3 ? 200 : (_previewTabs.Count <= 5 ? 140 : 100), - }; - - var tabContent = new StackPanel { Orientation = Orientation.Horizontal }; - - // 파일명 - tabContent.Children.Add(new TextBlock - { - Text = fileName, - FontSize = 11, - Foreground = isActive ? primaryText : secondaryText, - FontWeight = isActive ? FontWeights.SemiBold : FontWeights.Normal, - VerticalAlignment = VerticalAlignment.Center, - TextTrimming = TextTrimming.CharacterEllipsis, - MaxWidth = tabBorder.MaxWidth - 30, - ToolTip = tabPath, - }); - - // 닫기 버튼 (x) — 활성 탭은 항상 표시, 비활성 탭은 호버 시에만 표시 - var closeFg = isActive ? primaryText : secondaryText; - var closeBtnText = new TextBlock - { - Text = "\uE711", - FontFamily = new FontFamily("Segoe MDL2 Assets"), - FontSize = 10, - Foreground = closeFg, - VerticalAlignment = VerticalAlignment.Center, - }; - var closeBtn = new Border - { - Background = Brushes.Transparent, - CornerRadius = new CornerRadius(3), - Padding = new Thickness(3, 2, 3, 2), - Margin = new Thickness(5, 0, 0, 0), - Cursor = Cursors.Hand, - VerticalAlignment = VerticalAlignment.Center, - // 비활성 탭은 초기에 숨김, 활성 탭은 항상 표시 - Visibility = isActive ? Visibility.Visible : Visibility.Hidden, - Child = closeBtnText, - }; - - var closePath = tabPath; - closeBtn.MouseEnter += (s, _) => - { - if (s is Border b) - { - b.Background = new SolidColorBrush(Color.FromArgb(0x40, 0xFF, 0x50, 0x50)); - if (b.Child is TextBlock tb) tb.Foreground = new SolidColorBrush(Color.FromRgb(0xFF, 0x60, 0x60)); - } - }; - closeBtn.MouseLeave += (s, _) => - { - if (s is Border b) - { - b.Background = Brushes.Transparent; - if (b.Child is TextBlock tb) tb.Foreground = closeFg; - } - }; - closeBtn.Tag = "close"; // 닫기 버튼 식별용 - closeBtn.MouseLeftButtonUp += (_, e) => - { - e.Handled = true; // 부모 탭 클릭 이벤트 차단 - ClosePreviewTab(closePath); - }; - - tabContent.Children.Add(closeBtn); - tabBorder.Child = tabContent; - - // 탭 클릭 → 활성화 (MouseLeftButtonUp 사용: 닫기 버튼의 PreviewMouseLeftButtonDown보다 늦게 실행되어 충돌 방지) - var clickPath = tabPath; - tabBorder.MouseLeftButtonUp += (_, e) => - { - if (e.Handled) return; - e.Handled = true; - _activePreviewTab = clickPath; - RebuildPreviewTabs(); - LoadPreviewContent(clickPath); - }; - - // 우클릭 → 컨텍스트 메뉴 - var ctxPath = tabPath; - tabBorder.MouseRightButtonUp += (_, e) => - { - e.Handled = true; - ShowPreviewTabContextMenu(ctxPath); - }; - - // 더블클릭 → 별도 창에서 보기 - var dblPath = tabPath; - tabBorder.MouseLeftButtonDown += (_, e) => - { - if (e.Handled) return; - if (e.ClickCount == 2) - { - e.Handled = true; - OpenPreviewPopupWindow(dblPath); - } - }; - - // 호버 효과 — 비활성 탭에서 배경 강조 + 닫기 버튼 표시 - var capturedIsActive = isActive; - var capturedCloseBtn = closeBtn; - tabBorder.MouseEnter += (s, _) => - { - if (s is Border b && !capturedIsActive) - b.Background = new SolidColorBrush(Color.FromArgb(0x10, 0xFF, 0xFF, 0xFF)); - // 비활성 탭도 호버 시 닫기 버튼 표시 - if (!capturedIsActive) - capturedCloseBtn.Visibility = Visibility.Visible; - }; - tabBorder.MouseLeave += (s, _) => - { - if (s is Border b && !capturedIsActive) - b.Background = Brushes.Transparent; - // 비활성 탭 호버 해제 시 닫기 버튼 숨김 - if (!capturedIsActive) - capturedCloseBtn.Visibility = Visibility.Hidden; - }; - - PreviewTabPanel.Children.Add(tabBorder); - - // 탭 사이 구분선 - if (tabPath != _previewTabs[^1]) - { - PreviewTabPanel.Children.Add(new Border - { - Width = 1, Height = 14, - Background = borderBrush, - Margin = new Thickness(0, 4, 0, 4), - VerticalAlignment = VerticalAlignment.Center, - }); - } - } - } - - private void ClosePreviewTab(string filePath) - { - _previewTabs.Remove(filePath); - - if (_previewTabs.Count == 0) - { - HidePreviewPanel(); - return; - } - - // 닫힌 탭이 활성 탭이면 마지막 탭으로 전환 - if (string.Equals(filePath, _activePreviewTab, StringComparison.OrdinalIgnoreCase)) - { - _activePreviewTab = _previewTabs[^1]; - LoadPreviewContent(_activePreviewTab); - } - - RebuildPreviewTabs(); - } - - private async void LoadPreviewContent(string filePath) - { - var ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant(); - SetPreviewHeader(filePath); - - // 모든 콘텐츠 숨기기 - PreviewWebView.Visibility = Visibility.Collapsed; - PreviewTextScroll.Visibility = Visibility.Collapsed; - PreviewDataGrid.Visibility = Visibility.Collapsed; - PreviewEmpty.Visibility = Visibility.Collapsed; - - if (!System.IO.File.Exists(filePath)) - { - SetPreviewHeaderState("파일을 찾을 수 없습니다"); - PreviewEmpty.Text = "파일을 찾을 수 없습니다"; - PreviewEmpty.Visibility = Visibility.Visible; - return; - } - - try - { - switch (ext) - { - case ".html": - case ".htm": - await EnsureWebViewInitializedAsync(); - PreviewWebView.Source = new Uri(filePath); - PreviewWebView.Visibility = Visibility.Visible; - break; - - case ".csv": - LoadCsvPreview(filePath); - PreviewDataGrid.Visibility = Visibility.Visible; - break; - - case ".md": - await EnsureWebViewInitializedAsync(); - var mdText = System.IO.File.ReadAllText(filePath); - if (mdText.Length > 50000) mdText = mdText[..50000]; - var mdHtml = Services.Agent.TemplateService.RenderMarkdownToHtml(mdText, _selectedMood); - PreviewWebView.NavigateToString(mdHtml); - PreviewWebView.Visibility = Visibility.Visible; - break; - - case ".txt": - case ".json": - case ".xml": - case ".log": - var text = System.IO.File.ReadAllText(filePath); - if (text.Length > 50000) text = text[..50000] + "\n\n... (이후 생략)"; - PreviewTextBlock.Text = text; - PreviewTextScroll.Visibility = Visibility.Visible; - break; - - default: - SetPreviewHeaderState("지원되지 않는 형식"); - PreviewEmpty.Text = "미리보기할 수 없는 파일 형식입니다"; - PreviewEmpty.Visibility = Visibility.Visible; - break; - } - } - catch (Exception ex) - { - SetPreviewHeaderState("미리보기 오류"); - PreviewTextBlock.Text = $"미리보기 오류: {ex.Message}"; - PreviewTextScroll.Visibility = Visibility.Visible; - } - } - - private void SetPreviewHeader(string filePath) - { - if (PreviewHeaderTitle == null || PreviewHeaderSubtitle == null || PreviewHeaderMeta == null) - return; - - var fileName = System.IO.Path.GetFileName(filePath); - var extension = System.IO.Path.GetExtension(filePath).TrimStart('.').ToUpperInvariant(); - var fileInfo = new System.IO.FileInfo(filePath); - var sizeText = fileInfo.Exists - ? fileInfo.Length >= 1024 * 1024 - ? $"{fileInfo.Length / 1024d / 1024d:F1} MB" - : $"{Math.Max(1, fileInfo.Length / 1024d):F0} KB" - : "파일 없음"; - - PreviewHeaderTitle.Text = string.IsNullOrWhiteSpace(fileName) ? "미리보기" : fileName; - PreviewHeaderSubtitle.Text = filePath; - PreviewHeaderMeta.Text = string.IsNullOrWhiteSpace(extension) - ? sizeText - : $"{extension} · {sizeText}"; - } - - private void SetPreviewHeaderState(string state) - { - if (PreviewHeaderMeta != null && !string.IsNullOrWhiteSpace(state)) - PreviewHeaderMeta.Text = state; - } - - private bool _webViewInitialized; - private static readonly string WebView2DataFolder = - System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "AxCopilot", "WebView2"); - - private async Task EnsureWebViewInitializedAsync() - { - if (_webViewInitialized) return; - try - { - var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync( - userDataFolder: WebView2DataFolder); - await PreviewWebView.EnsureCoreWebView2Async(env); - _webViewInitialized = true; - } - catch (Exception ex) - { - Services.LogService.Warn($"WebView2 초기화 실패: {ex.Message}"); - } - } - - private void LoadCsvPreview(string filePath) - { - try - { - var lines = System.IO.File.ReadAllLines(filePath); - if (lines.Length == 0) return; - - var dt = new System.Data.DataTable(); - var headers = ParseCsvLine(lines[0]); - foreach (var h in headers) - dt.Columns.Add(h); - - var maxRows = Math.Min(lines.Length, 501); - for (int i = 1; i < maxRows; i++) - { - var vals = ParseCsvLine(lines[i]); - var row = dt.NewRow(); - for (int j = 0; j < Math.Min(vals.Length, headers.Length); j++) - row[j] = vals[j]; - dt.Rows.Add(row); - } - - PreviewDataGrid.ItemsSource = dt.DefaultView; - } - catch (Exception ex) - { - PreviewTextBlock.Text = $"CSV 로드 오류: {ex.Message}"; - PreviewTextScroll.Visibility = Visibility.Visible; - PreviewDataGrid.Visibility = Visibility.Collapsed; - } - } - - private static string[] ParseCsvLine(string line) - { - var fields = new System.Collections.Generic.List(); - var current = new System.Text.StringBuilder(); - bool inQuotes = false; - - for (int i = 0; i < line.Length; i++) - { - char c = line[i]; - if (inQuotes) - { - if (c == '"' && i + 1 < line.Length && line[i + 1] == '"') - { - current.Append('"'); - i++; - } - else if (c == '"') - inQuotes = false; - else - current.Append(c); - } - else - { - if (c == '"') - inQuotes = true; - else if (c == ',') - { - fields.Add(current.ToString()); - current.Clear(); - } - else - current.Append(c); - } - } - fields.Add(current.ToString()); - return fields.ToArray(); - } - - private void HidePreviewPanel() - { - _previewTabs.Clear(); - _activePreviewTab = null; - if (PreviewHeaderTitle != null) PreviewHeaderTitle.Text = "미리보기"; - if (PreviewHeaderSubtitle != null) PreviewHeaderSubtitle.Text = "선택한 파일이 여기에 표시됩니다"; - if (PreviewHeaderMeta != null) PreviewHeaderMeta.Text = "파일 메타"; - PreviewColumn.Width = new GridLength(0); - SplitterColumn.Width = new GridLength(0); - PreviewPanel.Visibility = Visibility.Collapsed; - PreviewSplitter.Visibility = Visibility.Collapsed; - PreviewWebView.Visibility = Visibility.Collapsed; - PreviewTextScroll.Visibility = Visibility.Collapsed; - PreviewDataGrid.Visibility = Visibility.Collapsed; - try { if (_webViewInitialized) PreviewWebView.CoreWebView2?.NavigateToString(""); } catch { } - } - - /// 프리뷰 탭 바 클릭 시 WebView2에서 포커스를 회수 (HWND airspace 문제 방지). - private void PreviewTabBar_PreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) - { - // WebView2가 포커스를 잡고 있으면 WPF 버튼 클릭이 무시될 수 있으므로 포커스를 강제 이동 - if (PreviewWebView.IsFocused || PreviewWebView.IsKeyboardFocusWithin) - { - var border = sender as Border; - border?.Focus(); - } - } - - private void BtnClosePreview_Click(object sender, RoutedEventArgs e) - { - HidePreviewPanel(); - BtnPreviewToggle.Visibility = Visibility.Collapsed; - } - - private void BtnPreviewToggle_Click(object sender, RoutedEventArgs e) - { - if (PreviewPanel.Visibility == Visibility.Visible) - { - // 숨기기 (탭은 유지) - PreviewPanel.Visibility = Visibility.Collapsed; - PreviewSplitter.Visibility = Visibility.Collapsed; - PreviewColumn.Width = new GridLength(0); - SplitterColumn.Width = new GridLength(0); - } - else if (_previewTabs.Count > 0) - { - // 다시 열기 - PreviewPanel.Visibility = Visibility.Visible; - PreviewSplitter.Visibility = Visibility.Visible; - PreviewColumn.Width = new GridLength(420); - SplitterColumn.Width = new GridLength(5); - RebuildPreviewTabs(); - if (_activePreviewTab != null) LoadPreviewContent(_activePreviewTab); - } - } - - private void BtnOpenExternal_Click(object sender, RoutedEventArgs e) - { - if (string.IsNullOrEmpty(_activePreviewTab) || !System.IO.File.Exists(_activePreviewTab)) - return; - - try - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo - { - FileName = _activePreviewTab, - UseShellExecute = true, - }); - } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine($"외부 프로그램 실행 오류: {ex.Message}"); - } - } - - /// 프리뷰 탭 우클릭 컨텍스트 메뉴를 표시합니다. - private Popup? _previewTabPopup; - - private void ShowPreviewTabContextMenu(string filePath) - { - // 기존 팝업 닫기 - if (_previewTabPopup != null) _previewTabPopup.IsOpen = false; - - var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); - var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; - var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; - var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; - var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); - - var stack = new StackPanel(); - - void AddItem(string icon, string iconColor, string label, Action action) - { - var itemBorder = new Border - { - Background = Brushes.Transparent, - CornerRadius = new CornerRadius(6), - Padding = new Thickness(10, 7, 16, 7), - Cursor = Cursors.Hand, - }; - var sp = new StackPanel { Orientation = Orientation.Horizontal }; - sp.Children.Add(new TextBlock - { - Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), - FontSize = 12, Foreground = string.IsNullOrEmpty(iconColor) - ? secondaryText - : new SolidColorBrush((Color)ColorConverter.ConvertFromString(iconColor)), - VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0), - }); - sp.Children.Add(new TextBlock - { - Text = label, FontSize = 13, Foreground = primaryText, - VerticalAlignment = VerticalAlignment.Center, - }); - itemBorder.Child = sp; - itemBorder.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; }; - itemBorder.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; - itemBorder.MouseLeftButtonUp += (_, _) => - { - _previewTabPopup!.IsOpen = false; - action(); - }; - stack.Children.Add(itemBorder); - } - - void AddSeparator() - { - stack.Children.Add(new Border - { - Height = 1, - Background = borderBrush, - Margin = new Thickness(8, 3, 8, 3), - }); - } - - AddItem("\uE8A7", "#64B5F6", "외부 프로그램으로 열기", () => - { - try - { - System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo - { - FileName = filePath, UseShellExecute = true, - }); - } - catch { } - }); - - AddItem("\uE838", "#FFB74D", "파일 위치 열기", () => - { - try { System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{filePath}\""); } - catch { } - }); - - AddItem("\uE8A7", "#81C784", "별도 창에서 보기", () => OpenPreviewPopupWindow(filePath)); - - AddSeparator(); - - AddItem("\uE8C8", "", "경로 복사", () => - { - try { Clipboard.SetText(filePath); } catch { } - }); - - AddSeparator(); - - AddItem("\uE711", "#EF5350", "이 탭 닫기", () => ClosePreviewTab(filePath)); - - if (_previewTabs.Count > 1) - { - AddItem("\uE8BB", "#EF5350", "다른 탭 모두 닫기", () => - { - var keep = filePath; - _previewTabs.RemoveAll(p => !string.Equals(p, keep, StringComparison.OrdinalIgnoreCase)); - _activePreviewTab = keep; - RebuildPreviewTabs(); - LoadPreviewContent(keep); - }); - } - - var popupBorder = new Border - { - Background = bg, - BorderBrush = borderBrush, - BorderThickness = new Thickness(1), - CornerRadius = new CornerRadius(12), - Padding = new Thickness(4, 6, 4, 6), - MinWidth = 180, - Effect = new System.Windows.Media.Effects.DropShadowEffect - { - BlurRadius = 16, Opacity = 0.4, ShadowDepth = 4, - Color = Colors.Black, - }, - Child = stack, - }; - - _previewTabPopup = new Popup - { - Child = popupBorder, - Placement = PlacementMode.MousePoint, - StaysOpen = false, - AllowsTransparency = true, - PopupAnimation = PopupAnimation.Fade, - }; - _previewTabPopup.IsOpen = true; - } - - /// 프리뷰를 별도 팝업 창에서 엽니다. - private void OpenPreviewPopupWindow(string filePath) - { - if (!System.IO.File.Exists(filePath)) return; - - var ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant(); - var fileName = System.IO.Path.GetFileName(filePath); - var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); - var fg = TryFindResource("PrimaryText") as Brush ?? Brushes.White; - - var win = new Window - { - Title = $"미리보기 — {fileName}", - Width = 900, - Height = 700, - WindowStartupLocation = WindowStartupLocation.CenterScreen, - Background = bg, - }; - - FrameworkElement content; - - switch (ext) - { - case ".html": - case ".htm": - var wv = new Microsoft.Web.WebView2.Wpf.WebView2(); - wv.Loaded += async (_, _) => - { - try - { - var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync( - userDataFolder: WebView2DataFolder); - await wv.EnsureCoreWebView2Async(env); - wv.Source = new Uri(filePath); - } - catch { } - }; - content = wv; - break; - - case ".md": - var mdWv = new Microsoft.Web.WebView2.Wpf.WebView2(); - var mdMood = _selectedMood; - mdWv.Loaded += async (_, _) => - { - try - { - var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync( - userDataFolder: WebView2DataFolder); - await mdWv.EnsureCoreWebView2Async(env); - var mdSrc = System.IO.File.ReadAllText(filePath); - if (mdSrc.Length > 100000) mdSrc = mdSrc[..100000]; - var html = Services.Agent.TemplateService.RenderMarkdownToHtml(mdSrc, mdMood); - mdWv.NavigateToString(html); - } - catch { } - }; - content = mdWv; - break; - - case ".csv": - var dg = new System.Windows.Controls.DataGrid - { - AutoGenerateColumns = true, - IsReadOnly = true, - Background = Brushes.Transparent, - Foreground = Brushes.White, - BorderThickness = new Thickness(0), - FontSize = 12, - }; - try - { - var lines = System.IO.File.ReadAllLines(filePath); - if (lines.Length > 0) - { - var dt = new System.Data.DataTable(); - var headers = ParseCsvLine(lines[0]); - foreach (var h in headers) dt.Columns.Add(h); - for (int i = 1; i < Math.Min(lines.Length, 1001); i++) - { - var vals = ParseCsvLine(lines[i]); - var row = dt.NewRow(); - for (int j = 0; j < Math.Min(vals.Length, dt.Columns.Count); j++) - row[j] = vals[j]; - dt.Rows.Add(row); - } - dg.ItemsSource = dt.DefaultView; - } - } - catch { } - content = dg; - break; - - default: - var text = System.IO.File.ReadAllText(filePath); - if (text.Length > 100000) text = text[..100000] + "\n\n... (이후 생략)"; - var sv = new ScrollViewer - { - VerticalScrollBarVisibility = ScrollBarVisibility.Auto, - Padding = new Thickness(20), - Content = new TextBlock - { - Text = text, - TextWrapping = TextWrapping.Wrap, - FontFamily = new FontFamily("Consolas"), - FontSize = 13, - Foreground = fg, - }, - }; - content = sv; - break; - } - - win.Content = content; - win.Show(); - } // ─── 에이전트 스티키 진행률 바 ──────────────────────────────────────────