using System.Collections.ObjectModel; using System.ComponentModel; using AxCopilot.Services.Agent; namespace AxCopilot.ViewModels; /// Phase 18-B: 에이전트 이벤트 타임라인 재생 ViewModel. public class ReplayTimelineViewModel : INotifyPropertyChanged { private readonly AgentReplayService _replayService; private CancellationTokenSource? _replayCts; public ObservableCollection Events { get; } = new(); public ObservableCollection Sessions { get; } = new(); private ReplaySessionInfo? _selectedSession; public ReplaySessionInfo? SelectedSession { get => _selectedSession; set { _selectedSession = value; OnPropertyChanged(); OnPropertyChanged(nameof(CanReplay)); } } private bool _isReplaying; public bool IsReplaying { get => _isReplaying; set { _isReplaying = value; OnPropertyChanged(); OnPropertyChanged(nameof(CanReplay)); } } public bool CanReplay => SelectedSession != null && !IsReplaying; private int _replaySpeedMs = 200; public int ReplaySpeedMs { get => _replaySpeedMs; set { _replaySpeedMs = Math.Max(0, value); OnPropertyChanged(); } } private int _progressValue; public int ProgressValue { get => _progressValue; set { _progressValue = value; OnPropertyChanged(); } } private int _progressMax = 100; public int ProgressMax { get => _progressMax; set { _progressMax = value; OnPropertyChanged(); } } public ReplayTimelineViewModel() { _replayService = new AgentReplayService(); } public void LoadSessions() { Sessions.Clear(); foreach (var s in _replayService.GetSessions()) Sessions.Add(s); if (Sessions.Count > 0) SelectedSession = Sessions[0]; } public async Task StartReplayAsync() { if (SelectedSession == null || IsReplaying) return; Events.Clear(); IsReplaying = true; ProgressValue = 0; _replayCts = new CancellationTokenSource(); try { // 전체 이벤트 수 먼저 파악 var allEvents = await _replayService.LoadAllEventsAsync( SelectedSession.SessionId, _replayCts.Token); ProgressMax = Math.Max(1, allEvents.Count); int i = 0; await _replayService.ReplayAsync( SelectedSession.SessionId, async record => { var item = new ReplayEventItem(record); // Dispatcher needed — raise event for UI thread ReplayEventReceived?.Invoke(item); i++; ProgressValue = i; await Task.CompletedTask; }, ReplaySpeedMs, _replayCts.Token); } catch (OperationCanceledException) { } finally { IsReplaying = false; _replayCts?.Dispose(); _replayCts = null; } } public void StopReplay() { _replayCts?.Cancel(); } /// UI 스레드에서 이벤트 항목을 추가하기 위한 이벤트. public event Action? ReplayEventReceived; public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } /// 타임라인에 표시할 단일 이벤트 항목. public class ReplayEventItem { public AgentEventRecord Record { get; } public string TypeLabel { get; } public string Icon { get; } public string TimeLabel { get; } public ReplayEventItem(AgentEventRecord record) { Record = record; TimeLabel = record.Timestamp.ToString("HH:mm:ss"); (TypeLabel, Icon) = record.Type switch { AgentEventLogType.SessionStart => ("세션 시작", "\uE768"), AgentEventLogType.SessionEnd => ("세션 종료", "\uE711"), AgentEventLogType.UserMessage => ("사용자 메시지", "\uE8BD"), AgentEventLogType.AssistantMessage => ("AI 응답", "\uE8D4"), AgentEventLogType.ToolRequest => ("도구 요청", "\uE756"), AgentEventLogType.ToolResult => ("도구 결과", "\uE73E"), AgentEventLogType.HookFired => ("훅 실행", "\uE81C"), AgentEventLogType.HookResult => ("훅 결과", "\uE73E"), AgentEventLogType.SkillActivated => ("스킬 활성화", "\uE82D"), AgentEventLogType.SkillCompleted => ("스킬 완료", "\uE930"), AgentEventLogType.CompactionTriggered => ("컨텍스트 압축", "\uE8EC"), AgentEventLogType.CompactionCompleted => ("압축 완료", "\uE930"), AgentEventLogType.SubagentSpawned => ("서브에이전트 생성", "\uE718"), AgentEventLogType.SubagentCompleted => ("서브에이전트 완료", "\uE930"), AgentEventLogType.ReflexionSaved => ("반성 저장", "\uE90F"), AgentEventLogType.Error => ("오류", "\uE783"), _ => (record.Type.ToString(), "\uE946") }; } }