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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user