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 primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var stack = new StackPanel(); void AddItem(string icon, string iconColor, string label, Action action) { var iconBrush = string.IsNullOrEmpty(iconColor) ? secondaryText : new SolidColorBrush((Color)ColorConverter.ConvertFromString(iconColor)); var itemBorder = CreateSurfacePopupMenuItem(icon, iconBrush, label, () => { _previewTabPopup!.IsOpen = false; action(); }, primaryText, 13); stack.Children.Add(itemBorder); } void AddSeparator() { stack.Children.Add(CreateSurfacePopupSeparator()); } 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 = CreateSurfacePopupContainer(stack, 180, new Thickness(4, 6, 4, 6)); _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(); } }