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>
This commit was merged in pull request #44.
This commit is contained in:
2026-05-18 17:53:36 +00:00
committed by Marek Jaisński
parent f808734768
commit 541e9e1fb5
42 changed files with 2351 additions and 155 deletions
@@ -18,13 +18,14 @@
<div class="reader-canvas @(ThemeService.IsLightMode ? "theme-light" : "theme-dark")">
@if (ViewModel == null)
{
<div class="loading-state">
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">@StatusMessage</NexusTypography>
<div class="loading-state full-page">
<div class="spinner-glow"></div>
<NexusTypography Variant="NexusTypography.TypographyVariant.UI" Class="loading-text">@StatusMessage</NexusTypography>
</div>
}
else
{
<div @ref="_containerRef" class="reader-flow-container">
<div @ref="_containerRef" class="reader-flow-container @(_isLoadingChapter ? "content-blurred" : "")">
@foreach (var block in ViewModel.Blocks)
{
<div id="@block.Id" class="block-wrapper @(_highlightedBlockId == block.Id ? "highlighted" : "")">
@@ -35,6 +36,16 @@
</div>
}
</div>
@if (_isLoadingChapter)
{
<div class="chapter-loading-overlay">
<div class="loader-card glass-panel">
<div class="spinner-glow small"></div>
<span class="loader-text">Wczytywanie kolejnego rozdziału...</span>
</div>
</div>
}
}
<SelectionAiPanel
@@ -47,6 +58,7 @@
@code {
private ReaderPageViewModel? ViewModel;
private string StatusMessage = "Loading chapter...";
private bool _isLoadingChapter;
private string _selectedText = string.Empty;
private string _selectedBlockId = string.Empty;
@@ -54,6 +66,7 @@
private string? _highlightedBlockId;
private bool _isJsInitialized;
private ElementReference _containerRef;
private bool _isInteractive;
protected override async Task OnInitializedAsync()
{
@@ -86,6 +99,11 @@
if (firstRender)
{
await SyncService.InitializeAsync();
_isInteractive = true;
if (ViewModel != null)
{
await Coordinator.ProcessFullPageAsync(GetFullPageContent());
}
}
if (ViewModel != null && !_isJsInitialized)
@@ -193,8 +211,9 @@
private async Task LoadChapterAsync(int index)
{
ViewModel = null;
StatusMessage = "Fetching content...";
_isLoadingChapter = true;
StatusMessage = "Wczytywanie treści...";
StateHasChanged();
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var userId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
@@ -202,7 +221,9 @@
var ebookId = NavigationService.CurrentEbookId;
if (ebookId == Guid.Empty)
{
StatusMessage = "No book selected. Please open a book from your library.";
ViewModel = null;
StatusMessage = "Brak wybranej książki. Otwórz książkę z biblioteki.";
_isLoadingChapter = false;
return;
}
@@ -212,13 +233,20 @@
ViewModel = result.Value;
await NavigationService.UpdateMetadataAsync(ViewModel.CurrentChapterIndex, ViewModel.TotalChapters, ViewModel.ChapterTitle);
await Coordinator.ProcessFullPageAsync(GetFullPageContent());
if (_isInteractive)
{
await Coordinator.ProcessFullPageAsync(GetFullPageContent());
}
}
else
{
StatusMessage = $"Error: {result.Errors.FirstOrDefault()?.Message ?? "Failed to load"}";
ViewModel = null;
StatusMessage = $"Błąd: {result.Errors.FirstOrDefault()?.Message ?? "Nie udało się wczytać treści"}";
Logger.LogError("Failed to load chapter {Index} for ebook {EbookId}: {Errors}", index, ebookId, string.Join(", ", result.Errors.Select(e => e.Message)));
}
_isLoadingChapter = false;
StateHasChanged();
}
public async Task ScrollToNodeAsync(string id)