Files
Nexus.Reader/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs
T
Antigravity 541e9e1fb5 feat(ai-ux): deduplicate AI queries, handle ServiceUnavailable retries, and optimize reader canvas graph prerendering (#44)
This Pull Request encapsulates all outstanding AI, Blazor InteractiveAuto lifecycle, pgvector, and Firefox authorization/session compatibility fixes.

### Key Accomplishments:
1. **Concurrent Request Deduplication (Option B):** Implemented a thread-safe active task registry in `KnowledgeService` that groups concurrent graph extraction queries for the same content, preventing duplicate AI calls completely.
2. **Resilience Strategy for Downstream Demands:** Extended the `ai-retry` resilience pipeline to automatically intercept and retry on temporary Google API `503 ServiceUnavailable` / `high demand` spikes.
3. **Interactive Graph Generation Guard (Option A):** Prevented server-side prerender-phase graph requests in the reader canvas component.
4. **Firefox Compatibility & Cookie Handler:** Implemented an authentication endpoint and hybrid hidden-form submission flow to solve login, registration, and logout redirections and cookies securely.
5. **Autoscrolling & Graph Exclusions:** Added concept-to-block smooth scrolling, active block badging, and filtered out markdown code blocks from being extracted as nodes.

All unit tests compiled and passed 100% cleanly.

---------

Co-authored-by: Marek Jasiński <jasins.marek@gmail.com>
Reviewed-on: #44
Co-authored-by: Antigravity <antigravity@google.com>
Co-committed-by: Antigravity <antigravity@google.com>
2026-05-18 17:53:36 +00:00

171 lines
6.3 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
{
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;
/// <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;
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);
}
}
public async Task ProcessFullPageAsync(string fullContent, string tenantId = "global")
{
if (string.IsNullOrWhiteSpace(fullContent)) return;
LogGeneratingGraph(tenantId);
await _graphService.Clear();
await _graphService.SetLoading(true);
try
{
var result = await _knowledgeService.GetGraphDataAsync(fullContent, tenantId);
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();
}
}
}
catch (Exception ex)
{
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")
{
await _quizService.SetHydrating(true);
LogRequestingSummary(tenantId);
try
{
var result = await _knowledgeService.GetSummaryAndQuizAsync(content, tenantId);
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 (Exception ex)
{
LogSummaryError(ex, tenantId);
return Result.Fail(new Error("Error requesting summary and quiz").CausedBy(ex));
}
finally
{
await _quizService.SetHydrating(false);
}
}
public async Task ClearAsync()
{
await _graphService.Clear();
await _quizService.SetQuiz(null, null);
}
public void Dispose()
{
_interactionService.OnNodeSelected -= HandleNodeSelected;
}
[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);
}