Files
Nexus.Reader/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs
T

314 lines
11 KiB
C#

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<KnowledgeCoordinator> _logger;
private CancellationTokenSource? _graphCts;
private CancellationTokenSource? _quizCts;
public string CurrentFullPageContent { get; private set; } = string.Empty;
public bool IsLoadingSelectionSummary { get; private set; }
public string? SelectionSummary { get; private set; }
public string? SelectedTextContext { get; private set; }
/// <summary>
/// Raised when the knowledge graph has been updated with new data.
/// Subscribers must return a Task to enable proper async handling.
/// </summary>
public event Func<GraphDataDto, Task>? OnGraphUpdated;
/// <summary>
/// Raised when the selection summary state has changed (loading started, finished, or cleared).
/// </summary>
public event Func<Task>? OnSelectionSummaryStateChanged;
public KnowledgeCoordinator(
IKnowledgeService knowledgeService,
IKnowledgeGraphService graphService,
IQuizStateService quizService,
IPlatformService platformService,
IReaderInteractionService interactionService,
ILogger<KnowledgeCoordinator> 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<Result<KnowledgePacket>> 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 StartSelectionSummaryAsync(string text, string tenantId = "global")
{
if (string.IsNullOrWhiteSpace(text)) return;
IsLoadingSelectionSummary = true;
SelectionSummary = null;
SelectedTextContext = text;
if (OnSelectionSummaryStateChanged != null)
{
await OnSelectionSummaryStateChanged.Invoke();
}
try
{
var result = await RequestSummaryAndQuizAsync(text, tenantId);
if (result.IsSuccess)
{
SelectionSummary = result.Value.Summary;
}
else
{
_logger.LogWarning("[KnowledgeCoordinator] Selection summary request failed: {Errors}", string.Join(", ", result.Errors.Select(e => e.Message)));
}
}
finally
{
IsLoadingSelectionSummary = false;
if (OnSelectionSummaryStateChanged != null)
{
await OnSelectionSummaryStateChanged.Invoke();
}
}
}
public async Task ClearSelectionSummaryAsync()
{
SelectionSummary = null;
SelectedTextContext = null;
IsLoadingSelectionSummary = false;
if (OnSelectionSummaryStateChanged != null)
{
await OnSelectionSummaryStateChanged.Invoke();
}
}
public async Task ClearAsync()
{
CancelAndDisposeCts(ref _graphCts);
CancelAndDisposeCts(ref _quizCts);
CurrentFullPageContent = string.Empty;
await _graphService.Clear();
await _quizService.SetQuiz(null, null);
await ClearSelectionSummaryAsync();
}
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);
}