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..."; StatusMessage = "Wczytywanie treści...";
StateHasChanged(); StateHasChanged();
if (string.IsNullOrEmpty(NavigationService.PendingScrollBlockId))
{
await ScrollToTopAsync();
}
var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; 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 Task HandleUpdate() => InvokeAsync(StateHasChanged);
private void HandleEscape() private void HandleEscape()
@@ -17,6 +17,9 @@ public sealed partial class KnowledgeCoordinator : IDisposable
private readonly IReaderInteractionService _interactionService; private readonly IReaderInteractionService _interactionService;
private readonly ILogger<KnowledgeCoordinator> _logger; private readonly ILogger<KnowledgeCoordinator> _logger;
private CancellationTokenSource? _graphCts;
private CancellationTokenSource? _quizCts;
public string CurrentFullPageContent { get; private set; } = string.Empty; public string CurrentFullPageContent { get; private set; } = string.Empty;
/// <summary> /// <summary>
@@ -77,6 +80,11 @@ public sealed partial class KnowledgeCoordinator : IDisposable
public async Task ProcessFullPageAsync(string fullContent, string tenantId = "global", Guid? ebookId = null) 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; if (string.IsNullOrWhiteSpace(fullContent)) return;
CurrentFullPageContent = fullContent; CurrentFullPageContent = fullContent;
@@ -87,7 +95,9 @@ public sealed partial class KnowledgeCoordinator : IDisposable
try try
{ {
var result = await _knowledgeService.GetGraphDataAsync(fullContent, tenantId, ebookId); var result = await _knowledgeService.GetGraphDataAsync(fullContent, tenantId, ebookId, token);
token.ThrowIfCancellationRequested();
if (result.IsSuccess) if (result.IsSuccess)
{ {
var packet = result.Value; var packet = result.Value;
@@ -103,12 +113,19 @@ public sealed partial class KnowledgeCoordinator : IDisposable
await _graphService.SetLoading(false); await _graphService.SetLoading(false);
} }
catch (OperationCanceledException)
{
_logger.LogInformation("[KnowledgeCoordinator] Graph generation task was canceled.");
}
catch (Exception ex) catch (Exception ex)
{
if (!token.IsCancellationRequested)
{ {
await _graphService.SetLoading(false); await _graphService.SetLoading(false);
LogGraphError(ex, tenantId); LogGraphError(ex, tenantId);
} }
} }
}
public async Task OnBlockReachedAsync(string blockId, string content) public async Task OnBlockReachedAsync(string blockId, string content)
{ {
@@ -118,11 +135,18 @@ public sealed partial class KnowledgeCoordinator : IDisposable
public async Task<Result<KnowledgePacket>> RequestSummaryAndQuizAsync(string content, string tenantId = "global") 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); await _quizService.SetHydrating(true);
LogRequestingSummary(tenantId); LogRequestingSummary(tenantId);
try try
{ {
var result = await _knowledgeService.GetSummaryAndQuizAsync(content, tenantId); var result = await _knowledgeService.GetSummaryAndQuizAsync(content, tenantId, cancellationToken: token);
token.ThrowIfCancellationRequested();
if (result.IsSuccess) if (result.IsSuccess)
{ {
var packet = result.Value; var packet = result.Value;
@@ -138,11 +162,20 @@ public sealed partial class KnowledgeCoordinator : IDisposable
LogSummaryWarning(tenantId); LogSummaryWarning(tenantId);
return Result.Fail(result.Errors); 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) catch (Exception ex)
{
if (!token.IsCancellationRequested)
{ {
LogSummaryError(ex, tenantId); LogSummaryError(ex, tenantId);
return Result.Fail(new Error("Error requesting summary and quiz").CausedBy(ex)); return Result.Fail(new Error("Error requesting summary and quiz").CausedBy(ex));
} }
return Result.Fail("Task canceled");
}
finally finally
{ {
await _quizService.SetHydrating(false); await _quizService.SetHydrating(false);
@@ -151,6 +184,14 @@ public sealed partial class KnowledgeCoordinator : IDisposable
public async Task ClearAsync() public async Task ClearAsync()
{ {
_graphCts?.Cancel();
_graphCts?.Dispose();
_graphCts = null;
_quizCts?.Cancel();
_quizCts?.Dispose();
_quizCts = null;
CurrentFullPageContent = string.Empty; CurrentFullPageContent = string.Empty;
await _graphService.Clear(); await _graphService.Clear();
await _quizService.SetQuiz(null, null); await _quizService.SetQuiz(null, null);
@@ -159,6 +200,12 @@ public sealed partial class KnowledgeCoordinator : IDisposable
public void Dispose() public void Dispose()
{ {
_interactionService.OnNodeSelected -= HandleNodeSelected; _interactionService.OnNodeSelected -= HandleNodeSelected;
_graphCts?.Cancel();
_graphCts?.Dispose();
_quizCts?.Cancel();
_quizCts?.Dispose();
} }
[LoggerMessage(Level = LogLevel.Information, Message = "[KnowledgeCoordinator] Generating full page graph for tenant: {TenantId}")] [LoggerMessage(Level = LogLevel.Information, Message = "[KnowledgeCoordinator] Generating full page graph for tenant: {TenantId}")]
@@ -38,3 +38,13 @@ export function scrollIntoView(id) {
} }
return false; return false;
} }
export function scrollToTop() {
const el = document.querySelector('.reader-canvas');
if (el) {
el.scrollTop = 0;
return true;
}
return false;
}