From f81b2acc40b24d38467c8593b5e4056fc03415ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Jasi=C5=84ski?= Date: Mon, 1 Jun 2026 18:18:19 +0200 Subject: [PATCH 1/4] fix(ingest): implement beautiful upload loading state and fix button loading spinner visibility --- .../Components/Organisms/BookIngestionModal.razor | 15 +++++++++++---- .../Organisms/BookIngestionModal.razor.css | 13 ++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor b/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor index bb04130..68baf6c 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor +++ b/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor @@ -33,8 +33,15 @@

Scanning metadata...

+ +
+
+
+

Saving book to library...

+
+
-
+
@if (Metadata != null) {
@@ -70,8 +77,8 @@
Back + OnClick="SaveToLibrary" + Disabled="IsIngesting"> @(IsIngesting ? "" : "Save to Library")
@@ -79,7 +86,7 @@
diff --git a/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor.css b/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor.css index 519639b..d92d068 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor.css +++ b/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor.css @@ -118,8 +118,9 @@ z-index: 10; } -/* Parsing State */ -.parsing-state { +/* Parsing and Ingesting States */ +.parsing-state, +.ingesting-state { flex: 1; display: flex; justify-content: center; @@ -158,7 +159,8 @@ filter: drop-shadow(0 0 8px rgba(0, 255, 153, 0.3)); } -.parsing-state p { +.parsing-state p, +.ingesting-state p { color: var(--nexus-text); font-family: var(--nexus-font-mono, monospace); font-size: 0.9rem; @@ -371,10 +373,11 @@ position: absolute; width: 20px; height: 20px; - border: 2px solid rgba(0, 0, 0, 0.1); - border-top-color: #000; + border: 2px solid rgba(255, 255, 255, 0.2); + border-top-color: var(--nexus-neon, #00ffaa); border-radius: 50%; animation: spin 0.8s linear infinite; + filter: drop-shadow(0 0 4px var(--nexus-neon, #00ffaa)); } /* Indexing State */ -- 2.52.0 From f4ef7ba906dc49cd6a370da93a57770893503fa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Jasi=C5=84ski?= Date: Mon, 1 Jun 2026 18:27:26 +0200 Subject: [PATCH 2/4] ux(reader): implement scroll-to-top on chapter switch and cancel stale background AI tasks --- .../Components/Organisms/ReaderCanvas.razor | 18 ++++++ .../Services/KnowledgeCoordinator.cs | 59 +++++++++++++++++-- .../wwwroot/js/viewport.js | 10 ++++ 3 files changed, 81 insertions(+), 6 deletions(-) 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; +} + -- 2.52.0 From bd9e9bd03688d97427e29d40bb0ef5c1d153e291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Jasi=C5=84ski?= Date: Mon, 1 Jun 2026 18:33:38 +0200 Subject: [PATCH 3/4] docs: update nexus-code-review guidelines with detailed requirements for posting and resolving comments --- .agent/skills/nexus-code-review/SKILL.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.agent/skills/nexus-code-review/SKILL.md b/.agent/skills/nexus-code-review/SKILL.md index ee10dae..2dbd164 100644 --- a/.agent/skills/nexus-code-review/SKILL.md +++ b/.agent/skills/nexus-code-review/SKILL.md @@ -30,4 +30,14 @@ When conducting or receiving a code review for NexusReader, ensure the implement - [ ] **AI Prompts**: Ensure changes to AI logic do not bypass the `PromptRegistry` or token estimation limits defined in `AiSettings`. ## 6. Code Review Comments -- [ ] **Specific Linking**: Comments should be linked to specific code. Try to avoid general comments about the entire pull request. + +### 6.1 Posting Comments +- [ ] **Code-Linked Comments**: Every review comment **must** be anchored to a specific file and line range using the Gitea inline comment API (`path` + `new_line_num`/`old_line_num`). Free-floating general comments are only acceptable for summary notes that cannot be attributed to a single location. +- [ ] **Severity Prefix**: Prefix each comment with its severity so the author can prioritize: `🔴 Blocking`, `🟡 Design/Architecture`, or `🟢 Minor/Suggestion`. +- [ ] **Actionable Guidance**: Each comment must include a concrete, actionable suggestion — not just a description of the problem. Where applicable, provide a corrected code snippet. + +### 6.2 Resolving Comments (Author Responsibility) +- [ ] **Reply Before Resolving**: When a review comment has been addressed, the author **must** reply to the specific thread explaining *how* the issue was resolved (e.g., commit SHA, approach taken, or a reasoned rejection with justification). Do not close a thread without a reply. +- [ ] **Link to Fix**: If the resolution is a code change, include the commit SHA or a reference to the changed line in the reply (e.g., `Fixed in abc1234 — moved the guard before CTS allocation`). +- [ ] **Close Only After Reply**: Mark a thread as **Resolved** only after posting the reply. A thread with no reply must remain open, even if the underlying code has changed. +- [ ] **Rejection Must Be Justified**: If the author disagrees with a comment and chooses not to act on it, they must reply with a clear technical justification. The reviewer then decides whether to accept the reasoning and close the thread, or escalate it. -- 2.52.0 From f0662537017bcb8c77c93606b04e310be56ce55d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Jasi=C5=84ski?= Date: Mon, 1 Jun 2026 18:41:00 +0200 Subject: [PATCH 4/4] fix(reader): resolve PR #66 code review issues - Shifted ScrollToTopAsync timing to post-render inside LoadChapterAsync. - Refactored viewport module loading using EnsureViewportModuleAsync to prevent leaks. - Added selector parameter to scrollToTop in viewport.js. - Implemented IAsyncDisposable and thread-safe CTS management in KnowledgeCoordinator. - Fixed early-return loading status and memory leaks in KnowledgeCoordinator. - Simplified BookIngestionModal state rendering with mutual exclusivity. - Documented thread-affinity dispatching (InvokeAsync) in BookIngestionModal. --- .../Organisms/BookIngestionModal.razor | 17 +++-- .../Components/Organisms/ReaderCanvas.razor | 51 +++++++++----- .../Services/KnowledgeCoordinator.cs | 69 ++++++++++++++----- .../wwwroot/js/viewport.js | 7 +- 4 files changed, 99 insertions(+), 45 deletions(-) diff --git a/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor b/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor index 68baf6c..d2abe30 100644 --- a/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor +++ b/src/NexusReader.UI.Shared/Components/Organisms/BookIngestionModal.razor @@ -27,21 +27,21 @@