feat: implement dynamic knowledge graph updates and state management services
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
using NexusReader.Application.Queries.Graph;
|
||||
|
||||
namespace NexusReader.UI.Shared.Services;
|
||||
|
||||
public interface IKnowledgeGraphService
|
||||
{
|
||||
GraphDataDto? CurrentGraphData { get; }
|
||||
string? ActiveNodeId { get; }
|
||||
|
||||
event Action? OnGraphUpdated;
|
||||
event Action<string>? OnActiveNodeChanged;
|
||||
|
||||
void UpdateGraph(GraphDataDto newData);
|
||||
void SetActiveNode(string nodeId);
|
||||
}
|
||||
@@ -1,8 +1,19 @@
|
||||
using NexusReader.Application.Queries.Quiz;
|
||||
|
||||
namespace NexusReader.UI.Shared.Services;
|
||||
|
||||
public interface IQuizStateService
|
||||
{
|
||||
string? CurrentQuizBlockId { get; }
|
||||
QuizDto? CurrentQuiz { get; }
|
||||
bool IsHydrating { get; }
|
||||
bool HasNewQuiz { get; }
|
||||
|
||||
event Action<string>? OnQuizRequested;
|
||||
event Action? OnQuizUpdated;
|
||||
|
||||
void RequestQuiz(string blockId);
|
||||
void SetQuiz(string blockId, QuizDto quiz);
|
||||
void SetHydrating(bool hydrating);
|
||||
void MarkQuizAsSeen();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using NexusReader.Application.Queries.Graph;
|
||||
|
||||
namespace NexusReader.UI.Shared.Services;
|
||||
|
||||
public sealed class KnowledgeGraphService : IKnowledgeGraphService
|
||||
{
|
||||
public GraphDataDto? CurrentGraphData { get; private set; }
|
||||
public string? ActiveNodeId { get; private set; }
|
||||
|
||||
public event Action? OnGraphUpdated;
|
||||
public event Action<string>? OnActiveNodeChanged;
|
||||
|
||||
public void UpdateGraph(GraphDataDto newData)
|
||||
{
|
||||
CurrentGraphData = newData;
|
||||
OnGraphUpdated?.Invoke();
|
||||
}
|
||||
|
||||
public void SetActiveNode(string nodeId)
|
||||
{
|
||||
if (ActiveNodeId == nodeId) return;
|
||||
ActiveNodeId = nodeId;
|
||||
OnActiveNodeChanged?.Invoke(nodeId);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,42 @@
|
||||
using NexusReader.Application.Queries.Quiz;
|
||||
|
||||
namespace NexusReader.UI.Shared.Services;
|
||||
|
||||
public sealed class QuizStateService : IQuizStateService
|
||||
{
|
||||
public string? CurrentQuizBlockId { get; private set; }
|
||||
public QuizDto? CurrentQuiz { get; private set; }
|
||||
public bool IsHydrating { get; private set; }
|
||||
public bool HasNewQuiz { get; private set; }
|
||||
|
||||
public event Action<string>? OnQuizRequested;
|
||||
public event Action? OnQuizUpdated;
|
||||
|
||||
public void RequestQuiz(string blockId)
|
||||
{
|
||||
CurrentQuizBlockId = blockId;
|
||||
OnQuizRequested?.Invoke(blockId);
|
||||
}
|
||||
|
||||
public void SetQuiz(string blockId, QuizDto quiz)
|
||||
{
|
||||
CurrentQuizBlockId = blockId;
|
||||
CurrentQuiz = quiz;
|
||||
IsHydrating = false;
|
||||
HasNewQuiz = true;
|
||||
OnQuizUpdated?.Invoke();
|
||||
}
|
||||
|
||||
public void SetHydrating(bool hydrating)
|
||||
{
|
||||
IsHydrating = hydrating;
|
||||
OnQuizUpdated?.Invoke();
|
||||
}
|
||||
|
||||
public void MarkQuizAsSeen()
|
||||
{
|
||||
if (!HasNewQuiz) return;
|
||||
HasNewQuiz = false;
|
||||
OnQuizUpdated?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user