feat: implement dynamic knowledge graph updates and state management services

This commit is contained in:
2026-04-26 14:53:48 +02:00
parent 412320980f
commit 7859c9806f
30 changed files with 668 additions and 153 deletions
@@ -0,0 +1,141 @@
using NexusReader.Application.Abstractions.Services;
using NexusReader.Application.Queries.Graph;
using NexusReader.Application.Queries.Quiz;
using NexusReader.UI.Shared.Services;
namespace NexusReader.UI.Shared.Services;
public sealed class KnowledgeCoordinator : IDisposable
{
private readonly IKnowledgeService _knowledgeService;
private readonly IKnowledgeGraphService _graphService;
private readonly IQuizStateService _quizService;
private readonly IPlatformService _platformService;
private CancellationTokenSource? _debounceCts;
public KnowledgeCoordinator(
IKnowledgeService knowledgeService,
IKnowledgeGraphService graphService,
IQuizStateService quizService,
IPlatformService platformService)
{
_knowledgeService = knowledgeService;
_graphService = graphService;
_quizService = quizService;
_platformService = platformService;
}
public void OnBlockReached(string blockId, string content)
{
Console.WriteLine($"[KnowledgeCoordinator] Block reached: {blockId}");
// 1. Skip extraction for the title page (usually the first block or contains 'title')
if (blockId.Equals("seg-0", StringComparison.OrdinalIgnoreCase) ||
blockId.Contains("title", StringComparison.OrdinalIgnoreCase) ||
content.Length < 50) // Title pages are usually short
{
Console.WriteLine($"[KnowledgeCoordinator] Skipping extraction for title page/short block: {blockId}");
_graphService.SetActiveNode(blockId);
return;
}
// 2. Update active node immediately for "TU JESTEŚ" logic
_graphService.SetActiveNode(blockId);
// 3. Debounce the AI extraction to prevent spamming while scrolling
_debounceCts?.Cancel();
_debounceCts = new CancellationTokenSource();
var token = _debounceCts.Token;
_ = DebounceAndExtractAsync(blockId, content, token);
}
private async Task DebounceAndExtractAsync(string blockId, string content, CancellationToken token)
{
try
{
await Task.Delay(1000, token);
if (token.IsCancellationRequested) return;
Console.WriteLine($"[KnowledgeCoordinator] Triggering extraction for block: {blockId}");
await ProcessKnowledgeExtractionAsync(blockId, content, token);
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
Console.WriteLine($"[KnowledgeCoordinator] Unexpected error in task: {ex.Message}");
}
}
private async Task ProcessKnowledgeExtractionAsync(string blockId, string content, CancellationToken ct)
{
_quizService.SetHydrating(true);
var result = await _knowledgeService.GetKnowledgeAsync(content, ct);
if (result.IsSuccess && !ct.IsCancellationRequested)
{
Console.WriteLine($"[KnowledgeCoordinator] Extraction success for block: {blockId}. Updating state...");
var packet = result.Value;
// Update Quiz State
var quizQuestions = packet.Quizzes
.Select(q => new QuizQuestionDto(q.Question, q.Options, q.CorrectIndex))
.ToList();
_quizService.SetQuiz(blockId, new QuizDto(quizQuestions));
// Update Graph State
GraphDataDto graphData;
if (packet.Graph != null && packet.Graph.Nodes != null && packet.Graph.Nodes.Any())
{
// Use AI-generated graph
graphData = packet.Graph;
// Ensure current block is linked to the first concept or added if missing
if (!graphData.Nodes.Any(n => n.Id == blockId))
{
graphData.Nodes.Add(new GraphNodeDto(blockId, "TU JESTEŚ", "current"));
if (graphData.Nodes.Count > 1)
{
graphData.Links.Add(new GraphLinkDto(blockId, graphData.Nodes[0].Id, 1));
}
}
}
else
{
// Fallback: Transform Concepts to GraphData if AI didn't provide a graph
var nodes = packet.Concepts
.Select(c => new GraphNodeDto(c.Title.ToLowerInvariant(), c.Title, "concept"))
.ToList();
nodes.Add(new GraphNodeDto(blockId, "TU JESTEŚ", "current"));
var links = packet.Concepts
.Select(c => new GraphLinkDto(blockId, c.Title.ToLowerInvariant(), 1))
.ToList();
graphData = new GraphDataDto { Nodes = nodes, Links = links };
}
_graphService.UpdateGraph(graphData);
// Visual/Haptic Feedback
await _platformService.VibrateSuccessAsync();
}
else
{
if (!ct.IsCancellationRequested)
{
Console.WriteLine($"[KnowledgeCoordinator] Extraction failed or returned empty for block: {blockId}. Error: {result.Errors.FirstOrDefault()?.Message}");
}
_quizService.SetHydrating(false);
}
}
public void Dispose()
{
_debounceCts?.Cancel();
_debounceCts?.Dispose();
}
}