Files
AX-Copilot-Codex/src/AxCopilot/Views/ChatWindow.FooterPresentation.cs
lacvet b391dfdfb3 하단 안내 카드 가림 문제와 라이브 타이핑 표시를 보정한다
- Cowork·Chat 하단 프리셋 안내 카드가 실제 결과를 가리지 않도록 대화 메시지 존재 시 자동으로 숨기도록 조정

- FooterPresentation에 남아 있던 깨진 한글 워터마크와 상태 문구를 정상 한국어로 복구

- 라이브 타이핑 속도와 최종 프리뷰 deadline을 재조정해 SSE 및 Cowork·Code 최종 응답이 한 번에 붙지 않고 더 눈에 보이게 표시되도록 보정
2026-04-07 09:31:57 +09:00

397 lines
14 KiB
C#

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<string>();
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;
}
}