using NexusReader.Application.Abstractions.Services; using FluentResults; using NexusReader.Application.Queries.Graph; using NexusReader.Application.Queries.Quiz; using NexusReader.UI.Shared.Services; using NexusReader.Application.DTOs.AI; using Microsoft.Extensions.Logging; namespace NexusReader.UI.Shared.Services; public sealed partial class KnowledgeCoordinator : IDisposable, IAsyncDisposable { private readonly IKnowledgeService _knowledgeService; private readonly IKnowledgeGraphService _graphService; private readonly IQuizStateService _quizService; private readonly IPlatformService _platformService; private readonly IReaderInteractionService _interactionService; private readonly ILogger _logger; private CancellationTokenSource? _graphCts; private CancellationTokenSource? _quizCts; public string CurrentFullPageContent { get; private set; } = string.Empty; /// /// Raised when the knowledge graph has been updated with new data. /// Subscribers must return a Task to enable proper async handling. /// public event Func? OnGraphUpdated; public KnowledgeCoordinator( IKnowledgeService knowledgeService, IKnowledgeGraphService graphService, IQuizStateService quizService, IPlatformService platformService, IReaderInteractionService interactionService, ILogger logger) { _knowledgeService = knowledgeService; _graphService = graphService; _quizService = quizService; _platformService = platformService; _interactionService = interactionService; _logger = logger; _interactionService.OnNodeSelected += HandleNodeSelected; } private async Task HandleNodeSelected(string nodeId) { string? targetBlockId = nodeId; var graph = _graphService.CurrentGraphData; if (graph != null) { var selectedNode = graph.Nodes.FirstOrDefault(n => n.Id == nodeId); if (selectedNode != null && selectedNode.Group == "concept") { // Look for connected block nodes (group: "current") in the links var connectedLinks = graph.Links.Where(l => l.Source == nodeId || l.Target == nodeId).ToList(); foreach (var link in connectedLinks) { var otherId = link.Source == nodeId ? link.Target : link.Source; var otherNode = graph.Nodes.FirstOrDefault(n => n.Id == otherId); if (otherNode != null && otherNode.Group == "current") { targetBlockId = otherId; break; } } } } if (!string.IsNullOrEmpty(targetBlockId)) { await _interactionService.RequestScrollToBlock(targetBlockId); await _interactionService.RequestHighlightBlock(targetBlockId); } } private void CancelAndDisposeCts(ref CancellationTokenSource? cts) { var localCts = cts; cts = null; if (localCts != null) { try { localCts.Cancel(); } catch (ObjectDisposedException) { } finally { localCts.Dispose(); } } } public async Task ProcessFullPageAsync(string fullContent, string tenantId = "global", Guid? ebookId = null) { if (string.IsNullOrWhiteSpace(fullContent)) { CancelAndDisposeCts(ref _graphCts); await _graphService.Clear(); await _graphService.SetLoading(false); CurrentFullPageContent = string.Empty; return; } CancelAndDisposeCts(ref _graphCts); _graphCts = new CancellationTokenSource(); var token = _graphCts.Token; CurrentFullPageContent = fullContent; LogGeneratingGraph(tenantId); await _graphService.Clear(); await _graphService.SetLoading(true); try { var result = await _knowledgeService.GetGraphDataAsync(fullContent, tenantId, ebookId, token); token.ThrowIfCancellationRequested(); if (result.IsSuccess) { var packet = result.Value; if (packet.Graph != null) { await _graphService.UpdateGraph(packet.Graph); if (OnGraphUpdated != null) await OnGraphUpdated.Invoke(packet.Graph); await _platformService.VibrateSuccessAsync(); return; } } await _graphService.SetLoading(false); } catch (OperationCanceledException) { _logger.LogInformation("[KnowledgeCoordinator] Graph generation task was canceled."); } catch (Exception ex) { if (!token.IsCancellationRequested) { await _graphService.SetLoading(false); LogGraphError(ex, tenantId); } } } public async Task OnBlockReachedAsync(string blockId, string content) { // Only update active node for "TU JESTEĊš" logic, do NOT trigger highlight here await _graphService.SetActiveNode(blockId); } public async Task> RequestSummaryAndQuizAsync(string content, string tenantId = "global") { CancelAndDisposeCts(ref _quizCts); _quizCts = new CancellationTokenSource(); var token = _quizCts.Token; await _quizService.SetHydrating(true); LogRequestingSummary(tenantId); try { var result = await _knowledgeService.GetSummaryAndQuizAsync(content, tenantId, cancellationToken: token); token.ThrowIfCancellationRequested(); if (result.IsSuccess) { var packet = result.Value; var quizQuestions = packet.Quizzes .Select(q => new QuizQuestionDto(q.Question, q.Options, q.CorrectIndex)) .ToList(); await _quizService.SetQuiz(null, new QuizDto(quizQuestions)); await _platformService.VibrateSuccessAsync(); return Result.Ok(packet); } LogSummaryWarning(tenantId); return Result.Fail(result.Errors); } catch (OperationCanceledException) { _logger.LogInformation("[KnowledgeCoordinator] Quiz and summary generation task was canceled."); return Result.Fail("Task canceled"); } catch (Exception ex) { if (!token.IsCancellationRequested) { LogSummaryError(ex, tenantId); return Result.Fail(new Error("Error requesting summary and quiz").CausedBy(ex)); } return Result.Fail("Task canceled"); } finally { await _quizService.SetHydrating(false); } } public async Task ClearAsync() { CancelAndDisposeCts(ref _graphCts); CancelAndDisposeCts(ref _quizCts); CurrentFullPageContent = string.Empty; await _graphService.Clear(); await _quizService.SetQuiz(null, null); } public void Dispose() { _interactionService.OnNodeSelected -= HandleNodeSelected; CancelAndDisposeCts(ref _graphCts); CancelAndDisposeCts(ref _quizCts); } public async ValueTask DisposeAsync() { _interactionService.OnNodeSelected -= HandleNodeSelected; CancelAndDisposeCts(ref _graphCts); CancelAndDisposeCts(ref _quizCts); try { await _graphService.Clear(); await _quizService.SetQuiz(null, null); } catch (Exception ex) { _logger.LogWarning(ex, "Error clearing services during KnowledgeCoordinator disposal."); } } [LoggerMessage(Level = LogLevel.Information, Message = "[KnowledgeCoordinator] Generating full page graph for tenant: {TenantId}")] private partial void LogGeneratingGraph(string tenantId); [LoggerMessage(Level = LogLevel.Error, Message = "[KnowledgeCoordinator] Error generating graph for tenant: {TenantId}")] private partial void LogGraphError(Exception ex, string tenantId); [LoggerMessage(Level = LogLevel.Information, Message = "[KnowledgeCoordinator] Requesting summary and quiz for tenant: {TenantId}")] private partial void LogRequestingSummary(string tenantId); [LoggerMessage(Level = LogLevel.Warning, Message = "[KnowledgeCoordinator] Failed to get summary and quiz for tenant: {TenantId}")] private partial void LogSummaryWarning(string tenantId); [LoggerMessage(Level = LogLevel.Error, Message = "[KnowledgeCoordinator] Error requesting summary and quiz for tenant: {TenantId}")] private partial void LogSummaryError(Exception ex, string tenantId); }