using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Media; using AxCopilot.Models; using AxCopilot.Services; namespace AxCopilot.Views; public partial class ChatWindow { private Popup? _memoryStatusPopup; private string BuildDefaultInputWatermark() { var hasFolder = !string.IsNullOrWhiteSpace(GetCurrentWorkFolder()); return _activeTab switch { "Cowork" => hasFolder ? "문서 작성, 데이터 분석, 파일 작업을 요청하세요. 필요하면 작업 폴더 파일도 함께 참고합니다." : "문서 작성, 데이터 분석, 파일 작업을 요청하세요. 작업 폴더를 선택하면 관련 파일도 함께 참고합니다.", "Code" => hasFolder ? "코드 수정, 원인 분석, 빌드와 테스트를 요청하세요. 작업 폴더 코드와 저장소 상태를 함께 참고합니다." : "작업 폴더를 선택한 뒤 코드 수정, 원인 분석, 빌드와 테스트를 요청하세요.", _ => "질문, 요약, 초안 작성, 아이디어 정리를 요청하세요.", }; } private void RefreshInputWatermarkText() { if (InputWatermark == null) return; InputWatermark.Text = string.IsNullOrWhiteSpace(_promptCardPlaceholder) ? BuildDefaultInputWatermark() : _promptCardPlaceholder; } private string BuildSelectedPresetGuideDescription(TopicPreset preset) { if (!string.IsNullOrWhiteSpace(preset.Description)) return preset.Description.Trim(); if (string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase)) return "선택한 작업 유형에 맞춰 문서·데이터·파일 작업 흐름으로 이어집니다."; return "선택한 대화 주제에 맞춰 응답 방향과 초안 흐름을 정리합니다."; } private void UpdateFolderBar() { if (FolderBar == null) return; if (_activeTab == "Chat") { FolderBar.Visibility = Visibility.Collapsed; UpdateGitBranchUi(null, "", "", "", "", Visibility.Collapsed); RefreshContextUsageVisual(); return; } FolderBar.Visibility = Visibility.Visible; var folder = GetCurrentWorkFolder(); if (!string.IsNullOrEmpty(folder)) { FolderPathLabel.Text = folder; FolderPathLabel.ToolTip = folder; } else { FolderPathLabel.Text = "폴더를 선택하세요"; FolderPathLabel.ToolTip = null; } LoadConversationSettings(); LoadCompactionMetricsFromConversation(); UpdatePermissionUI(); UpdateDataUsageUI(); UpdateMemoryStatusUi(); RefreshContextUsageVisual(); ScheduleGitBranchRefresh(); UpdateGitBranchUi( _currentGitBranchName, GitBranchFilesText?.Text ?? "", GitBranchAddedText?.Text ?? "", GitBranchDeletedText?.Text ?? "", _currentGitTooltip ?? "", BtnGitBranch?.Visibility ?? Visibility.Collapsed); } private void UpdateDataUsageUI() { _folderDataUsage = GetAutomaticFolderDataUsage(); } private void UpdateMemoryStatusUi() { if (BtnMemoryStatus == null || MemoryStatusLabel == null) return; BtnMemoryStatus.Visibility = Visibility.Collapsed; MemoryStatusSeparator.Visibility = Visibility.Collapsed; MemoryStatusLabel.Text = "메모리 없음"; BtnMemoryStatus.ToolTip = null; } private void BtnMemoryStatus_Click(object sender, RoutedEventArgs e) { if (BtnMemoryStatus == null) return; if (BtnMemoryStatus.Visibility != Visibility.Visible) return; _memoryStatusPopup?.SetCurrentValue(Popup.IsOpenProperty, false); var app = System.Windows.Application.Current as App; var memory = app?.MemoryService; if (memory == null) return; var workFolder = GetCurrentWorkFolder(); memory.Load(workFolder); var docs = memory.InstructionDocuments; var learned = memory.All.Count; var includePolicy = _settings.Settings.Llm.AllowExternalMemoryIncludes ? "외부 include 허용" : "외부 include 차단"; var auditEnabled = _settings.Settings.Llm.EnableAuditLog; var recentIncludeEntries = AuditLogService.LoadRecent("MemoryInclude", maxCount: 5, daysBack: 3); var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var accentBrush = TryFindResource("AccentColor") as Brush ?? secondaryText; var okBrush = new SolidColorBrush(Color.FromRgb(0x22, 0x9A, 0x55)); var warnBrush = new SolidColorBrush(Color.FromRgb(0xD9, 0x77, 0x06)); var dangerBrush = new SolidColorBrush(Color.FromRgb(0xDC, 0x26, 0x26)); var panel = new StackPanel { Margin = new Thickness(2) }; panel.Children.Add(new TextBlock { Text = "메모리 상태", FontSize = 13, FontWeight = FontWeights.SemiBold, Foreground = primaryText, Margin = new Thickness(8, 4, 8, 2), }); panel.Children.Add(new TextBlock { Text = $"계층형 규칙 {docs.Count}개 · 학습 메모리 {learned}개 · {includePolicy}", FontSize = 11.5, Foreground = secondaryText, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(8, 0, 8, 6), }); if (docs.Count > 0) { panel.Children.Add(CreateSurfacePopupSeparator()); panel.Children.Add(new TextBlock { Text = "적용 중 규칙", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = primaryText, Margin = new Thickness(8, 6, 8, 4), }); foreach (var doc in docs.Take(6)) panel.Children.Add(BuildMemoryPopupRuleRow(doc, primaryText, secondaryText, accentBrush)); if (docs.Count > 6) { panel.Children.Add(new TextBlock { Text = $"외 {docs.Count - 6}개 규칙", FontSize = 11, Foreground = secondaryText, Margin = new Thickness(8, 2, 8, 4), }); } } panel.Children.Add(CreateSurfacePopupSeparator()); panel.Children.Add(new TextBlock { Text = "최근 include 감사", FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = primaryText, Margin = new Thickness(8, 6, 8, 4), }); if (!auditEnabled) { panel.Children.Add(new TextBlock { Text = "감사 로그가 꺼져 있어 include 이력은 기록되지 않습니다.", FontSize = 11, Foreground = secondaryText, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(8, 0, 8, 6), }); } else if (recentIncludeEntries.Count == 0) { panel.Children.Add(new TextBlock { Text = "최근 3일간 include 감사 기록이 없습니다.", FontSize = 11, Foreground = secondaryText, Margin = new Thickness(8, 0, 8, 6), }); } else { foreach (var entry in recentIncludeEntries) panel.Children.Add(BuildMemoryPopupAuditRow(entry, primaryText, secondaryText, okBrush, warnBrush, dangerBrush)); } var container = CreateSurfacePopupContainer(panel, 340, new Thickness(8)); _memoryStatusPopup = new Popup { Child = container, StaysOpen = false, AllowsTransparency = true, PopupAnimation = PopupAnimation.Fade, Placement = PlacementMode.Top, PlacementTarget = BtnMemoryStatus, VerticalOffset = -6, }; _memoryStatusPopup.IsOpen = true; } private static string ShortenMemoryPath(string path) { try { var fileName = Path.GetFileName(path); if (string.IsNullOrWhiteSpace(fileName)) return path; var directory = Path.GetDirectoryName(path); return string.IsNullOrWhiteSpace(directory) ? fileName : $"{fileName} · {directory}"; } catch { return path; } } private Border BuildMemoryPopupRuleRow(MemoryInstructionDocument doc, Brush primaryText, Brush secondaryText, Brush accentBrush) { var meta = new List(); if (!string.IsNullOrWhiteSpace(doc.Description)) meta.Add(doc.Description.Trim()); if (doc.Tags.Count > 0) meta.Add($"tags: {string.Join(", ", doc.Tags)}"); if (doc.Paths.Count > 0) meta.Add($"paths: {string.Join(", ", doc.Paths.Take(2))}{(doc.Paths.Count > 2 ? "..." : "")}"); var stack = new StackPanel { Margin = new Thickness(8, 2, 8, 4) }; stack.Children.Add(new TextBlock { Text = $"[{doc.Label}] 우선순위 {doc.Priority}", FontSize = 11.5, FontWeight = FontWeights.SemiBold, Foreground = primaryText, }); stack.Children.Add(new TextBlock { Text = meta.Count == 0 ? doc.Layer : $"{doc.Layer} · {string.Join(" · ", meta)}", FontSize = 10.5, Foreground = secondaryText, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 0), }); if (!string.IsNullOrWhiteSpace(doc.Path)) { stack.Children.Add(new TextBlock { Text = ShortenMemoryPath(doc.Path), FontSize = 10.5, Foreground = accentBrush, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 0), }); } return new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(8), Child = stack }; } private Border BuildMemoryPopupAuditRow(AuditEntry entry, Brush primaryText, Brush secondaryText, Brush okBrush, Brush warnBrush, Brush dangerBrush) { var statusBrush = entry.Success ? okBrush : dangerBrush; var statusText = entry.Success ? "허용" : "차단"; var resultBrush = entry.Success ? secondaryText : warnBrush; var stack = new StackPanel { Margin = new Thickness(8, 2, 8, 4) }; stack.Children.Add(new TextBlock { Text = $"{statusText} · {entry.Timestamp:HH:mm:ss}", FontSize = 11.5, FontWeight = FontWeights.SemiBold, Foreground = statusBrush, }); stack.Children.Add(new TextBlock { Text = entry.Parameters, FontSize = 10.5, Foreground = primaryText, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 0), }); stack.Children.Add(new TextBlock { Text = string.IsNullOrWhiteSpace(entry.Result) ? (entry.FilePath ?? "") : entry.Result, FontSize = 10.5, Foreground = resultBrush, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 0), }); return new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(8), Child = stack }; } private string? BuildMemoryContextEvidenceText() { if (_activeTab == "Chat") return null; var app = System.Windows.Application.Current as App; var memory = app?.MemoryService; if (memory == null || !_settings.Settings.Llm.EnableAgentMemory) return null; memory.Load(GetCurrentWorkFolder()); var docs = memory.InstructionDocuments; var learned = memory.All.Count; if (docs.Count == 0 && learned == 0) return null; var labels = docs.Take(2).Select(x => x.Label).ToList(); var labelText = labels.Count == 0 ? "" : $" · {string.Join(", ", labels)}"; return $"메모리 규칙 {docs.Count}개 · 학습 {learned}개 적용 중{labelText}"; } private void UpdateSelectedPresetGuide(ChatConversation? conversation = null) { if (SelectedPresetGuide == null || SelectedPresetGuideTitle == null || SelectedPresetGuideDesc == null) return; conversation ??= _currentConversation; var hasVisibleMessages = conversation?.Messages?.Any(m => !string.IsNullOrWhiteSpace(m.Content) && (string.Equals(m.Role, "user", StringComparison.OrdinalIgnoreCase) || string.Equals(m.Role, "assistant", StringComparison.OrdinalIgnoreCase))) == true; if (string.Equals(_activeTab, "Code", StringComparison.OrdinalIgnoreCase) || hasVisibleMessages || _isStreaming) { SelectedPresetGuide.Visibility = Visibility.Collapsed; SelectedPresetGuideTitle.Text = ""; SelectedPresetGuideDesc.Text = ""; return; } var category = conversation?.Category?.Trim(); if (string.IsNullOrWhiteSpace(category)) { SelectedPresetGuide.Visibility = Visibility.Collapsed; SelectedPresetGuideTitle.Text = ""; SelectedPresetGuideDesc.Text = ""; return; } var preset = PresetService.GetByTabWithCustom(_activeTab, _settings.Settings.Llm.CustomPresets) .FirstOrDefault(p => string.Equals(p.Category?.Trim(), category, StringComparison.OrdinalIgnoreCase)); if (preset == null) { SelectedPresetGuide.Visibility = Visibility.Collapsed; SelectedPresetGuideTitle.Text = ""; SelectedPresetGuideDesc.Text = ""; return; } SelectedPresetGuideTitle.Text = string.Equals(_activeTab, "Cowork", StringComparison.OrdinalIgnoreCase) ? $"선택된 작업 유형 · {preset.Label}" : $"선택된 대화 주제 · {preset.Label}"; SelectedPresetGuideDesc.Text = BuildSelectedPresetGuideDescription(preset); SelectedPresetGuide.Visibility = Visibility.Visible; } }