feat: integrate AI-driven selection panel with context-aware text summarization and quiz generation features.

This commit is contained in:
2026-04-26 20:36:08 +02:00
parent 82d726097f
commit 39a9ca5706
25 changed files with 819 additions and 219 deletions
@@ -6,10 +6,14 @@ public interface IKnowledgeGraphService
{
GraphDataDto? CurrentGraphData { get; }
string? ActiveNodeId { get; }
bool IsLoading { get; }
event Action? OnGraphUpdated;
event Action<string>? OnActiveNodeChanged;
event Action<bool>? OnLoadingChanged;
void UpdateGraph(GraphDataDto newData);
void SetActiveNode(string nodeId);
void SetLoading(bool isLoading);
void Clear();
}
@@ -13,7 +13,7 @@ public interface IQuizStateService
event Action? OnQuizUpdated;
void RequestQuiz(string blockId);
void SetQuiz(string blockId, QuizDto quiz);
void SetQuiz(string? blockId, QuizDto quiz);
void SetHydrating(bool hydrating);
void MarkQuizAsSeen();
}
@@ -0,0 +1,16 @@
namespace NexusReader.UI.Shared.Services;
public interface IReaderInteractionService
{
event Action<string>? OnNodeSelected;
event Action<string>? OnScrollToBlockRequested;
event Action<string>? OnHighlightBlockRequested;
event Action<string, string, SelectionCoordinates>? OnTextSelected;
void NotifyNodeSelected(string nodeId);
void RequestScrollToBlock(string blockId);
void RequestHighlightBlock(string blockId);
void NotifyTextSelected(string text, string blockId, SelectionCoordinates coords);
}
public record SelectionCoordinates(double Top, double Left, double Width);
@@ -2,6 +2,7 @@ using NexusReader.Application.Abstractions.Services;
using NexusReader.Application.Queries.Graph;
using NexusReader.Application.Queries.Quiz;
using NexusReader.UI.Shared.Services;
using NexusReader.Application.DTOs.AI;
namespace NexusReader.UI.Shared.Services;
@@ -11,131 +12,94 @@ public sealed class KnowledgeCoordinator : IDisposable
private readonly IKnowledgeGraphService _graphService;
private readonly IQuizStateService _quizService;
private readonly IPlatformService _platformService;
private readonly IReaderInteractionService _interactionService;
private CancellationTokenSource? _debounceCts;
public event Action<GraphDataDto>? OnGraphUpdated;
public KnowledgeCoordinator(
IKnowledgeService knowledgeService,
IKnowledgeGraphService graphService,
IQuizStateService quizService,
IPlatformService platformService)
IPlatformService platformService,
IReaderInteractionService interactionService)
{
_knowledgeService = knowledgeService;
_graphService = graphService;
_quizService = quizService;
_platformService = platformService;
_interactionService = interactionService;
_interactionService.OnNodeSelected += HandleNodeSelected;
}
private void HandleNodeSelected(string nodeId)
{
_interactionService.RequestScrollToBlock(nodeId);
_interactionService.RequestHighlightBlock(nodeId);
}
public async Task ProcessFullPageAsync(string fullContent)
{
if (string.IsNullOrWhiteSpace(fullContent)) return;
Console.WriteLine("[KnowledgeCoordinator] Generating full page graph...");
_graphService.Clear();
_graphService.SetLoading(true);
try
{
var result = await _knowledgeService.GetGraphDataAsync(fullContent);
if (result.IsSuccess)
{
var packet = result.Value;
if (packet.Graph != null)
{
_graphService.UpdateGraph(packet.Graph);
OnGraphUpdated?.Invoke(packet.Graph);
await _platformService.VibrateSuccessAsync();
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[KnowledgeCoordinator] Error generating graph: {ex.Message}");
}
}
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
// Only update active node for "TU JESTEŚ" logic, do NOT trigger highlight here
_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)
public async Task<KnowledgePacket?> RequestSummaryAndQuizAsync(string content)
{
_quizService.SetHydrating(true);
var result = await _knowledgeService.GetKnowledgeAsync(content, ct);
if (result.IsSuccess && !ct.IsCancellationRequested)
try
{
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())
var result = await _knowledgeService.GetSummaryAndQuizAsync(content);
if (result.IsSuccess)
{
// 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"))
var packet = result.Value;
var quizQuestions = packet.Quizzes
.Select(q => new QuizQuestionDto(q.Question, q.Options, q.CorrectIndex))
.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 };
_quizService.SetQuiz(null, new QuizDto(quizQuestions));
await _platformService.VibrateSuccessAsync();
return packet;
}
_graphService.UpdateGraph(graphData);
// Visual/Haptic Feedback
await _platformService.VibrateSuccessAsync();
}
else
finally
{
if (!ct.IsCancellationRequested)
{
Console.WriteLine($"[KnowledgeCoordinator] Extraction failed or returned empty for block: {blockId}. Error: {result.Errors.FirstOrDefault()?.Message}");
}
_quizService.SetHydrating(false);
}
return null;
}
public void Dispose()
{
_debounceCts?.Cancel();
_debounceCts?.Dispose();
_interactionService.OnNodeSelected -= HandleNodeSelected;
}
}
@@ -6,13 +6,17 @@ public sealed class KnowledgeGraphService : IKnowledgeGraphService
{
public GraphDataDto? CurrentGraphData { get; private set; }
public string? ActiveNodeId { get; private set; }
public bool IsLoading { get; private set; }
public event Action? OnGraphUpdated;
public event Action<string>? OnActiveNodeChanged;
public event Action<bool>? OnLoadingChanged;
public void UpdateGraph(GraphDataDto newData)
{
CurrentGraphData = newData;
IsLoading = false;
OnLoadingChanged?.Invoke(false);
OnGraphUpdated?.Invoke();
}
@@ -22,4 +26,18 @@ public sealed class KnowledgeGraphService : IKnowledgeGraphService
ActiveNodeId = nodeId;
OnActiveNodeChanged?.Invoke(nodeId);
}
public void SetLoading(bool isLoading)
{
IsLoading = isLoading;
OnLoadingChanged?.Invoke(isLoading);
}
public void Clear()
{
CurrentGraphData = null;
ActiveNodeId = null;
IsLoading = false;
OnGraphUpdated?.Invoke();
}
}
@@ -18,7 +18,7 @@ public sealed class QuizStateService : IQuizStateService
OnQuizRequested?.Invoke(blockId);
}
public void SetQuiz(string blockId, QuizDto quiz)
public void SetQuiz(string? blockId, QuizDto quiz)
{
CurrentQuizBlockId = blockId;
CurrentQuiz = quiz;
@@ -0,0 +1,29 @@
namespace NexusReader.UI.Shared.Services;
public sealed class ReaderInteractionService : IReaderInteractionService
{
public event Action<string>? OnNodeSelected;
public event Action<string>? OnScrollToBlockRequested;
public event Action<string>? OnHighlightBlockRequested;
public event Action<string, string, SelectionCoordinates>? OnTextSelected;
public void NotifyNodeSelected(string nodeId)
{
OnNodeSelected?.Invoke(nodeId);
}
public void RequestScrollToBlock(string blockId)
{
OnScrollToBlockRequested?.Invoke(blockId);
}
public void RequestHighlightBlock(string blockId)
{
OnHighlightBlockRequested?.Invoke(blockId);
}
public void NotifyTextSelected(string text, string blockId, SelectionCoordinates coords)
{
OnTextSelected?.Invoke(text, blockId, coords);
}
}