ux(reader): implement scroll-to-top on chapter switch and cancel stale background AI tasks

This commit is contained in:
2026-06-01 18:27:26 +02:00
parent f81b2acc40
commit f4ef7ba906
3 changed files with 81 additions and 6 deletions
@@ -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<IJSObjectReference>("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()
@@ -16,6 +16,9 @@ public sealed partial class KnowledgeCoordinator : IDisposable
private readonly IPlatformService _platformService;
private readonly IReaderInteractionService _interactionService;
private readonly ILogger<KnowledgeCoordinator> _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<Result<KnowledgePacket>> 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}")]
@@ -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;
}