using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using AxCopilot.Models;
using AxCopilot.Services;
namespace AxCopilot.Views;
public partial class ChatWindow
{
// ─── 권한 메뉴 ─────────────────────────────────────────────────────────
private void BtnPermission_Click(object sender, RoutedEventArgs e)
{
if (PermissionPopup == null) return;
PermissionItems.Children.Clear();
var levels = new (string Level, string Sym, string Desc, string Color)[] {
("Ask", "\uE8D7", "매번 확인 — 파일 접근 시 사용자에게 묻습니다", "#4B5EFC"),
("Auto", "\uE73E", "자동 허용 — 파일을 자동으로 읽고 씁니다", "#DD6B20"),
("Deny", "\uE711", "접근 차단 — 파일 접근을 허용하지 않습니다", "#C50F1F"),
};
var current = Llm.FilePermission;
foreach (var (level, sym, desc, color) in levels)
{
var isActive = level.Equals(current, StringComparison.OrdinalIgnoreCase);
// 라운드 코너 템플릿 (기본 Button 크롬 제거)
var template = new ControlTemplate(typeof(Button));
var bdFactory = new FrameworkElementFactory(typeof(Border));
bdFactory.SetValue(Border.BackgroundProperty, Brushes.Transparent);
bdFactory.SetValue(Border.CornerRadiusProperty, new CornerRadius(8));
bdFactory.SetValue(Border.PaddingProperty, new Thickness(12, 8, 12, 8));
bdFactory.Name = "Bd";
var cpFactory = new FrameworkElementFactory(typeof(ContentPresenter));
cpFactory.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Left);
cpFactory.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
bdFactory.AppendChild(cpFactory);
template.VisualTree = bdFactory;
// 호버 효과
var hoverTrigger = new Trigger { Property = UIElement.IsMouseOverProperty, Value = true };
hoverTrigger.Setters.Add(new Setter(Border.BackgroundProperty,
new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)), "Bd"));
template.Triggers.Add(hoverTrigger);
var btn = new Button
{
Template = template,
BorderThickness = new Thickness(0),
Cursor = Cursors.Hand,
HorizontalContentAlignment = HorizontalAlignment.Left,
Margin = new Thickness(0, 1, 0, 1),
};
ApplyHoverScaleAnimation(btn, 1.02);
var sp = new StackPanel { Orientation = Orientation.Horizontal };
// 커스텀 체크 아이콘
sp.Children.Add(CreateCheckIcon(isActive));
sp.Children.Add(new TextBlock
{
Text = sym, FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 14,
Foreground = BrushFromHex(color),
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0),
});
var textStack = new StackPanel();
textStack.Children.Add(new TextBlock
{
Text = level, FontSize = 13, FontWeight = FontWeights.Bold,
Foreground = BrushFromHex(color),
});
textStack.Children.Add(new TextBlock
{
Text = desc, FontSize = 11,
Foreground = ThemeResourceHelper.Secondary(this),
TextWrapping = TextWrapping.Wrap,
MaxWidth = 220,
});
sp.Children.Add(textStack);
btn.Content = sp;
var capturedLevel = level;
btn.Click += (_, _) =>
{
Llm.FilePermission = capturedLevel;
UpdatePermissionUI();
SaveConversationSettings();
PermissionPopup.IsOpen = false;
};
PermissionItems.Children.Add(btn);
}
PermissionPopup.IsOpen = true;
}
private bool _autoWarningDismissed; // Auto 경고 배너 사용자가 닫았는지
private void BtnAutoWarningClose_Click(object sender, RoutedEventArgs e)
{
_autoWarningDismissed = true;
if (AutoPermissionWarning != null)
AutoPermissionWarning.Visibility = Visibility.Collapsed;
}
/// Phase 17-UI-B: 헤더 바 권한 칩 클릭 — 권한 팝업을 칩 위치에 표시.
private void PermissionHeaderChip_Click(object sender, RoutedEventArgs e)
{
if (PermissionPopup == null) return;
// 팝업 기준점을 헤더 칩으로 변경
PermissionPopup.PlacementTarget = PermissionHeaderChip;
PermissionPopup.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
BtnPermission_Click(sender, e);
}
private void UpdatePermissionUI()
{
if (PermissionLabel == null || PermissionIcon == null) return;
var perm = Llm.FilePermission;
PermissionLabel.Text = perm;
// Phase 17-UI-B: 헤더 칩 텍스트도 갱신
if (PermissionHeaderLabel != null) PermissionHeaderLabel.Text = perm;
PermissionIcon.Text = perm switch
{
"Auto" => "\uE73E",
"Deny" => "\uE711",
_ => "\uE8D7",
};
// Auto 모드일 때 경고 색상 + 배너 표시
if (perm == "Auto")
{
var warnColor = new SolidColorBrush(Color.FromRgb(0xDD, 0x6B, 0x20));
PermissionLabel.Foreground = warnColor;
PermissionIcon.Foreground = warnColor;
// Auto 전환 시 새 대화에서만 1회 필수 표시 (기존 대화에서 이미 Auto였으면 숨김)
ChatConversation? convForWarn;
lock (_convLock) convForWarn = _currentConversation;
var isExisting = convForWarn != null && convForWarn.Messages.Count > 0 && convForWarn.Permission == "Auto";
if (AutoPermissionWarning != null && !_autoWarningDismissed && !isExisting)
AutoPermissionWarning.Visibility = Visibility.Visible;
}
else
{
_autoWarningDismissed = false; // Auto가 아닌 모드로 전환하면 리셋
var defaultFg = ThemeResourceHelper.Secondary(this);
var iconFg = perm == "Deny" ? new SolidColorBrush(Color.FromRgb(0xC5, 0x0F, 0x1F))
: new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)); // Ask = 파란색
PermissionLabel.Foreground = defaultFg;
PermissionIcon.Foreground = iconFg;
if (AutoPermissionWarning != null)
AutoPermissionWarning.Visibility = Visibility.Collapsed;
}
}
// ──── 데이터 활용 수준 메뉴 ────
private void BtnDataUsage_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (DataUsagePopup == null) return;
DataUsageItems.Children.Clear();
var options = new (string Key, string Sym, string Label, string Desc, string Color)[]
{
("active", "\uE9F5", "적극 활용", "폴더 내 문서를 자동 탐색하여 보고서 작성에 적극 활용합니다", "#107C10"),
("passive", "\uE8FD", "소극 활용", "사용자가 요청할 때만 폴더 데이터를 참조합니다", "#D97706"),
("none", "\uE8D8", "활용하지 않음", "폴더 내 문서를 읽거나 참조하지 않습니다", "#9CA3AF"),
};
foreach (var (key, sym, label, desc, color) in options)
{
var isActive = key.Equals(_folderDataUsage, StringComparison.OrdinalIgnoreCase);
var template = new ControlTemplate(typeof(Button));
var bdFactory = new FrameworkElementFactory(typeof(Border));
bdFactory.SetValue(Border.BackgroundProperty, Brushes.Transparent);
bdFactory.SetValue(Border.CornerRadiusProperty, new CornerRadius(8));
bdFactory.SetValue(Border.PaddingProperty, new Thickness(12, 8, 12, 8));
bdFactory.Name = "Bd";
var cpFactory = new FrameworkElementFactory(typeof(ContentPresenter));
cpFactory.SetValue(ContentPresenter.HorizontalAlignmentProperty, HorizontalAlignment.Left);
cpFactory.SetValue(ContentPresenter.VerticalAlignmentProperty, VerticalAlignment.Center);
bdFactory.AppendChild(cpFactory);
template.VisualTree = bdFactory;
var hoverTrigger = new Trigger { Property = UIElement.IsMouseOverProperty, Value = true };
hoverTrigger.Setters.Add(new Setter(Border.BackgroundProperty,
new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)), "Bd"));
template.Triggers.Add(hoverTrigger);
var btn = new Button
{
Template = template,
BorderThickness = new Thickness(0),
Cursor = Cursors.Hand,
HorizontalContentAlignment = HorizontalAlignment.Left,
Margin = new Thickness(0, 1, 0, 1),
};
ApplyHoverScaleAnimation(btn, 1.02);
var sp = new StackPanel { Orientation = Orientation.Horizontal };
// 커스텀 체크 아이콘
sp.Children.Add(CreateCheckIcon(isActive));
sp.Children.Add(new TextBlock
{
Text = sym, FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 14,
Foreground = BrushFromHex(color),
VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0),
});
var textStack = new StackPanel();
textStack.Children.Add(new TextBlock
{
Text = label, FontSize = 13, FontWeight = FontWeights.Bold,
Foreground = BrushFromHex(color),
});
textStack.Children.Add(new TextBlock
{
Text = desc, FontSize = 11,
Foreground = ThemeResourceHelper.Secondary(this),
TextWrapping = TextWrapping.Wrap,
MaxWidth = 240,
});
sp.Children.Add(textStack);
btn.Content = sp;
var capturedKey = key;
btn.Click += (_, _) =>
{
_folderDataUsage = capturedKey;
UpdateDataUsageUI();
SaveConversationSettings();
DataUsagePopup.IsOpen = false;
};
DataUsageItems.Children.Add(btn);
}
DataUsagePopup.IsOpen = true;
}
private void UpdateDataUsageUI()
{
if (DataUsageLabel == null || DataUsageIcon == null) return;
var (label, icon, color) = _folderDataUsage switch
{
"passive" => ("소극", "\uE8FD", "#D97706"),
"none" => ("미사용", "\uE8D8", "#9CA3AF"),
_ => ("적극", "\uE9F5", "#107C10"),
};
DataUsageLabel.Text = label;
DataUsageIcon.Text = icon;
DataUsageIcon.Foreground = BrushFromHex(color);
}
/// Cowork/Code 탭 진입 시 설정의 기본 권한을 적용.
private void ApplyTabDefaultPermission()
{
if (_activeTab == "Chat")
{
// Chat 탭: 경고 배너 숨기고 기본 Ask 모드로 복원
Llm.FilePermission = "Ask";
UpdatePermissionUI();
return;
}
var defaultPerm = Llm.DefaultAgentPermission;
if (!string.IsNullOrEmpty(defaultPerm))
{
Llm.FilePermission = defaultPerm;
UpdatePermissionUI();
}
}
// ─── 파일 첨부 ─────────────────────────────────────────────────────────
private void BtnAttach_Click(object sender, RoutedEventArgs e)
{
var dlg = new Microsoft.Win32.OpenFileDialog
{
Multiselect = true,
Title = "첨부할 파일을 선택하세요",
Filter = "모든 파일 (*.*)|*.*|텍스트 (*.txt;*.md;*.csv)|*.txt;*.md;*.csv|코드 (*.cs;*.py;*.js;*.ts)|*.cs;*.py;*.js;*.ts",
};
// 작업 폴더가 있으면 초기 경로 설정
var workFolder = GetCurrentWorkFolder();
if (!string.IsNullOrEmpty(workFolder) && System.IO.Directory.Exists(workFolder))
dlg.InitialDirectory = workFolder;
if (dlg.ShowDialog() != true) return;
foreach (var file in dlg.FileNames)
AddAttachedFile(file);
}
private static readonly HashSet ImageExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"
};
private void AddAttachedFile(string filePath)
{
if (_attachedFiles.Contains(filePath)) return;
// 파일 크기 제한 (10MB)
try
{
var fi = new System.IO.FileInfo(filePath);
if (fi.Length > 10 * 1024 * 1024)
{
CustomMessageBox.Show($"파일이 너무 큽니다 (10MB 초과):\n{fi.Name}", "첨부 제한", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
// 이미지 파일 → Vision API용 base64 변환
var ext = fi.Extension.ToLowerInvariant();
if (ImageExtensions.Contains(ext) && Llm.EnableImageInput)
{
var maxKb = Llm.MaxImageSizeKb;
if (maxKb <= 0) maxKb = 5120;
if (fi.Length > maxKb * 1024)
{
CustomMessageBox.Show($"이미지가 너무 큽니다 ({fi.Length / 1024}KB, 최대 {maxKb}KB).",
"이미지 크기 초과", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var bytes = System.IO.File.ReadAllBytes(filePath);
var mimeType = ext switch
{
".jpg" or ".jpeg" => "image/jpeg",
".gif" => "image/gif",
".bmp" => "image/bmp",
".webp" => "image/webp",
_ => "image/png",
};
var attachment = new ImageAttachment
{
Base64 = Convert.ToBase64String(bytes),
MimeType = mimeType,
FileName = fi.Name,
};
// 중복 확인
if (_pendingImages.Any(i => i.FileName == attachment.FileName)) return;
_pendingImages.Add(attachment);
AddImagePreview(attachment);
return;
}
}
catch (Exception) { return; }
_attachedFiles.Add(filePath);
RefreshAttachedFilesUI();
}
private void RemoveAttachedFile(string filePath)
{
_attachedFiles.Remove(filePath);
RefreshAttachedFilesUI();
}
private void RefreshAttachedFilesUI()
{
AttachedFilesPanel.Items.Clear();
if (_attachedFiles.Count == 0)
{
AttachedFilesPanel.Visibility = Visibility.Collapsed;
return;
}
AttachedFilesPanel.Visibility = Visibility.Visible;
var secondaryBrush = ThemeResourceHelper.Secondary(this);
var hintBg = ThemeResourceHelper.Hint(this);
foreach (var file in _attachedFiles.ToList())
{
var fileName = System.IO.Path.GetFileName(file);
var capturedFile = file;
var chip = new Border
{
Background = hintBg,
CornerRadius = new CornerRadius(6),
Padding = new Thickness(8, 4, 4, 4),
Margin = new Thickness(0, 0, 4, 4),
Cursor = Cursors.Hand,
};
var sp = new StackPanel { Orientation = Orientation.Horizontal };
sp.Children.Add(new TextBlock
{
Text = "\uE8A5", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 10,
Foreground = secondaryBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0),
});
sp.Children.Add(new TextBlock
{
Text = fileName, FontSize = 11, Foreground = secondaryBrush,
VerticalAlignment = VerticalAlignment.Center, MaxWidth = 150, TextTrimming = TextTrimming.CharacterEllipsis,
ToolTip = file,
});
var removeBtn = new Button
{
Content = new TextBlock { Text = "\uE711", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 8, Foreground = secondaryBrush },
Background = Brushes.Transparent, BorderThickness = new Thickness(0),
Cursor = Cursors.Hand, Padding = new Thickness(4, 2, 4, 2), Margin = new Thickness(2, 0, 0, 0),
};
removeBtn.Click += (_, _) => RemoveAttachedFile(capturedFile);
sp.Children.Add(removeBtn);
chip.Child = sp;
AttachedFilesPanel.Items.Add(chip);
}
}
/// 첨부 파일 내용을 시스템 메시지로 변환합니다.
private string BuildFileContextPrompt()
{
if (_attachedFiles.Count == 0) return "";
var sb = new System.Text.StringBuilder();
sb.AppendLine("\n[첨부 파일 컨텍스트]");
foreach (var file in _attachedFiles)
{
try
{
var ext = System.IO.Path.GetExtension(file).ToLowerInvariant();
var isBinary = ext is ".exe" or ".dll" or ".zip" or ".7z" or ".rar" or ".tar" or ".gz"
or ".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".ico" or ".webp" or ".svg"
or ".pdf" or ".docx" or ".xlsx" or ".pptx" or ".doc" or ".xls" or ".ppt"
or ".mp3" or ".mp4" or ".avi" or ".mov" or ".mkv" or ".wav" or ".flac"
or ".psd" or ".ai" or ".sketch" or ".fig"
or ".msi" or ".iso" or ".img" or ".bin" or ".dat" or ".db" or ".sqlite";
if (isBinary)
{
sb.AppendLine($"\n--- {System.IO.Path.GetFileName(file)} (바이너리 파일, 내용 생략) ---");
continue;
}
var content = System.IO.File.ReadAllText(file);
// 최대 8000자로 제한
if (content.Length > 8000)
content = content[..8000] + "\n... (이하 생략)";
sb.AppendLine($"\n--- {System.IO.Path.GetFileName(file)} ---");
sb.AppendLine(content);
}
catch (Exception ex)
{
sb.AppendLine($"\n--- {System.IO.Path.GetFileName(file)} (읽기 실패: {ex.Message}) ---");
}
}
return sb.ToString();
}
private void ResizeGrip_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
var newW = Width + e.HorizontalChange;
var newH = Height + e.VerticalChange;
if (newW >= MinWidth) Width = newW;
if (newH >= MinHeight) Height = newH;
}
// ─── 사이드바 토글 ───────────────────────────────────────────────────
private void BtnToggleSidebar_Click(object sender, RoutedEventArgs e)
{
_sidebarVisible = !_sidebarVisible;
if (_sidebarVisible)
{
// 사이드바 열기, 아이콘 바 숨기기
IconBarColumn.Width = new GridLength(0);
IconBarPanel.Visibility = Visibility.Collapsed;
SidebarPanel.Visibility = Visibility.Visible;
ToggleSidebarIcon.Text = "\uE76B";
AnimateSidebar(0, 270, () => SidebarColumn.MinWidth = 200);
}
else
{
// 사이드바 닫기, 아이콘 바 표시
SidebarColumn.MinWidth = 0;
ToggleSidebarIcon.Text = "\uE76C";
AnimateSidebar(270, 0, () =>
{
SidebarPanel.Visibility = Visibility.Collapsed;
IconBarColumn.Width = new GridLength(52);
IconBarPanel.Visibility = Visibility.Visible;
});
}
}
private void AnimateSidebar(double from, double to, Action? onComplete = null)
{
var duration = 200.0;
var start = DateTime.UtcNow;
var timer = new System.Windows.Threading.DispatcherTimer { Interval = TimeSpan.FromMilliseconds(10) };
EventHandler tickHandler = null!;
tickHandler = (_, _) =>
{
var elapsed = (DateTime.UtcNow - start).TotalMilliseconds;
var t = Math.Min(elapsed / duration, 1.0);
t = 1 - (1 - t) * (1 - t);
SidebarColumn.Width = new GridLength(from + (to - from) * t);
if (elapsed >= duration)
{
timer.Stop();
timer.Tick -= tickHandler;
SidebarColumn.Width = new GridLength(to);
onComplete?.Invoke();
}
};
timer.Tick += tickHandler;
timer.Start();
}
}