diff --git a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor index e9a919d..e45c1ee 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor +++ b/src/NexusReader.UI.Shared/Components/Organisms/ReaderCanvas.razor @@ -308,6 +308,11 @@ StatusMessage = "Wczytywanie treści..."; StateHasChanged(); + if (string.IsNullOrEmpty(NavigationService.PendingScrollBlockId)) + { + await ScrollToTopAsync(); + } + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; @@ -374,6 +379,19 @@ } } + public async Task ScrollToTopAsync() + { + try + { + var module = _viewportModule ?? await JS.InvokeAsync("import", "./_content/NexusReader.UI.Shared/js/viewport.js"); + await module.InvokeVoidAsync("scrollToTop"); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Failed to scroll reader canvas to top."); + } + } + private Task HandleUpdate() => InvokeAsync(StateHasChanged); private void HandleEscape() diff --git a/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs b/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs index 1986436..dd34ba1 100644 --- a/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs +++ b/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs @@ -16,6 +16,9 @@ public sealed partial class KnowledgeCoordinator : IDisposable private readonly IPlatformService _platformService; private readonly IReaderInteractionService _interactionService; private readonly ILogger _logger; + + private CancellationTokenSource? _graphCts; + private CancellationTokenSource? _quizCts; public string CurrentFullPageContent { get; private set; } = string.Empty; @@ -77,6 +80,11 @@ public sealed partial class KnowledgeCoordinator : IDisposable public async Task ProcessFullPageAsync(string fullContent, string tenantId = "global", Guid? ebookId = null) { + _graphCts?.Cancel(); + _graphCts?.Dispose(); + _graphCts = new CancellationTokenSource(); + var token = _graphCts.Token; + if (string.IsNullOrWhiteSpace(fullContent)) return; CurrentFullPageContent = fullContent; @@ -87,7 +95,9 @@ public sealed partial class KnowledgeCoordinator : IDisposable try { - var result = await _knowledgeService.GetGraphDataAsync(fullContent, tenantId, ebookId); + var result = await _knowledgeService.GetGraphDataAsync(fullContent, tenantId, ebookId, token); + token.ThrowIfCancellationRequested(); + if (result.IsSuccess) { var packet = result.Value; @@ -103,10 +113,17 @@ public sealed partial class KnowledgeCoordinator : IDisposable await _graphService.SetLoading(false); } + catch (OperationCanceledException) + { + _logger.LogInformation("[KnowledgeCoordinator] Graph generation task was canceled."); + } catch (Exception ex) { - await _graphService.SetLoading(false); - LogGraphError(ex, tenantId); + if (!token.IsCancellationRequested) + { + await _graphService.SetLoading(false); + LogGraphError(ex, tenantId); + } } } @@ -118,11 +135,18 @@ public sealed partial class KnowledgeCoordinator : IDisposable public async Task> RequestSummaryAndQuizAsync(string content, string tenantId = "global") { + _quizCts?.Cancel(); + _quizCts?.Dispose(); + _quizCts = new CancellationTokenSource(); + var token = _quizCts.Token; + await _quizService.SetHydrating(true); LogRequestingSummary(tenantId); try { - var result = await _knowledgeService.GetSummaryAndQuizAsync(content, tenantId); + var result = await _knowledgeService.GetSummaryAndQuizAsync(content, tenantId, cancellationToken: token); + token.ThrowIfCancellationRequested(); + if (result.IsSuccess) { var packet = result.Value; @@ -138,10 +162,19 @@ public sealed partial class KnowledgeCoordinator : IDisposable 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) { - LogSummaryError(ex, tenantId); - return Result.Fail(new Error("Error requesting summary and quiz").CausedBy(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 { @@ -151,6 +184,14 @@ public sealed partial class KnowledgeCoordinator : IDisposable public async Task ClearAsync() { + _graphCts?.Cancel(); + _graphCts?.Dispose(); + _graphCts = null; + + _quizCts?.Cancel(); + _quizCts?.Dispose(); + _quizCts = null; + CurrentFullPageContent = string.Empty; await _graphService.Clear(); await _quizService.SetQuiz(null, null); @@ -159,6 +200,12 @@ public sealed partial class KnowledgeCoordinator : IDisposable public void Dispose() { _interactionService.OnNodeSelected -= HandleNodeSelected; + + _graphCts?.Cancel(); + _graphCts?.Dispose(); + + _quizCts?.Cancel(); + _quizCts?.Dispose(); } [LoggerMessage(Level = LogLevel.Information, Message = "[KnowledgeCoordinator] Generating full page graph for tenant: {TenantId}")] diff --git a/src/NexusReader.UI.Shared/wwwroot/js/viewport.js b/src/NexusReader.UI.Shared/wwwroot/js/viewport.js index 8c02aaf..d4cb965 100644 --- a/src/NexusReader.UI.Shared/wwwroot/js/viewport.js +++ b/src/NexusReader.UI.Shared/wwwroot/js/viewport.js @@ -38,3 +38,13 @@ export function scrollIntoView(id) { } return false; } + +export function scrollToTop() { + const el = document.querySelector('.reader-canvas'); + if (el) { + el.scrollTop = 0; + return true; + } + return false; +} +