AX Agent 프리뷰 패널 렌더 구조 분리 및 문서 갱신
Some checks failed
Release Gate / gate (push) Has been cancelled

- ChatWindow.PreviewPresentation.cs를 추가해 프리뷰 탭 목록, 헤더, 파일 로드, CSV·텍스트·마크다운·HTML 표시, 컨텍스트 메뉴, 별도 창 미리보기 흐름을 메인 창 코드에서 분리함

- ChatWindow.xaml.cs는 transcript 및 런타임 orchestration 중심으로 더 정리해 claw-code 기준 preview surface 품질 개선을 이어가기 쉬운 구조로 개선함

- README.md와 docs/DEVELOPMENT.md에 2026-04-06 08:47 (KST) 기준 변경 이력을 반영함

- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 기준 경고 0 / 오류 0 확인
This commit is contained in:
2026-04-06 08:48:32 +09:00
parent 2b21e8cdfb
commit 1ce6ccb030
4 changed files with 792 additions and 727 deletions

View File

@@ -7,6 +7,10 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
개발 참고: Claw Code 동등성 작업 추적 문서 개발 참고: Claw Code 동등성 작업 추적 문서
`docs/claw-code-parity-plan.md` `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) - 업데이트: 2026-04-06 08:39 (KST)
- AX Agent 하단 상태바 이벤트 처리와 회전 애니메이션을 `ChatWindow.StatusPresentation.cs`로 옮겼습니다. `UpdateStatusBar`, `StartStatusAnimation`, `StopStatusAnimation`이 상태 표현 파일로 이동해 메인 창 코드의 runtime/status 분기가 더 줄었습니다. - AX Agent 하단 상태바 이벤트 처리와 회전 애니메이션을 `ChatWindow.StatusPresentation.cs`로 옮겼습니다. `UpdateStatusBar`, `StartStatusAnimation`, `StopStatusAnimation`이 상태 표현 파일로 이동해 메인 창 코드의 runtime/status 분기가 더 줄었습니다.
- `ChatWindow.xaml.cs`는 대화 실행 orchestration 중심으로 더 정리됐고, claw-code 기준 status line 정교화와 footer presentation 개선을 계속 이어가기 쉬운 구조로 맞췄습니다. - `ChatWindow.xaml.cs`는 대화 실행 orchestration 중심으로 더 정리됐고, claw-code 기준 status line 정교화와 footer presentation 개선을 계속 이어가기 쉬운 구조로 맞췄습니다.

View File

@@ -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: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) - 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: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.

View File

@@ -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<string> _previewableExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".html", ".htm", ".md", ".txt", ".csv", ".json", ".xml", ".log",
};
/// <summary>열려 있는 프리뷰 탭 목록 (파일 경로 기준).</summary>
private readonly List<string> _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<string>();
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("<html></html>");
}
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();
}
}

View File

@@ -15719,733 +15719,6 @@ public partial class ChatWindow : Window
// ─── 미리보기 패널 (탭 기반) ───────────────────────────────────────────── // ─── 미리보기 패널 (탭 기반) ─────────────────────────────────────────────
private static readonly HashSet<string> _previewableExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".html", ".htm", ".md", ".txt", ".csv", ".json", ".xml", ".log",
};
/// <summary>열려 있는 프리뷰 탭 목록 (파일 경로 기준).</summary>
private readonly List<string> _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);
}
/// <summary>탭 바 UI를 다시 구성합니다.</summary>
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<string>();
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("<html></html>"); } catch { }
}
/// <summary>프리뷰 탭 바 클릭 시 WebView2에서 포커스를 회수 (HWND airspace 문제 방지).</summary>
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}");
}
}
/// <summary>프리뷰 탭 우클릭 컨텍스트 메뉴를 표시합니다.</summary>
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;
}
/// <summary>프리뷰를 별도 팝업 창에서 엽니다.</summary>
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();
}
// ─── 에이전트 스티키 진행률 바 ────────────────────────────────────────── // ─── 에이전트 스티키 진행률 바 ──────────────────────────────────────────