541e9e1fb5
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>
145 lines
4.5 KiB
Plaintext
145 lines
4.5 KiB
Plaintext
@using MediatR
|
||
@using NexusReader.Application.Queries.Graph
|
||
@using Microsoft.JSInterop
|
||
@using NexusReader.UI.Shared.Services
|
||
@implements IAsyncDisposable
|
||
@inject IMediator Mediator
|
||
@inject IJSRuntime JS
|
||
@inject IFocusModeService FocusMode
|
||
@inject IKnowledgeGraphService GraphService
|
||
@inject IReaderInteractionService InteractionService
|
||
|
||
<div class="knowledge-graph-container @(GraphService.IsLoading ? "loading" : "")" id="@ContainerId">
|
||
@if (GraphService.IsLoading || GraphService.CurrentGraphData == null)
|
||
{
|
||
<div class="loading-state">
|
||
<div class="preloader-robot">
|
||
<NexusIcon Name="robot" Size="64" Class="neon-pulse" />
|
||
<div class="scan-line"></div>
|
||
</div>
|
||
<NexusTypography Variant="NexusTypography.TypographyVariant.UI">Mapowanie relacji rozdziału...</NexusTypography>
|
||
</div>
|
||
}
|
||
else
|
||
{
|
||
<div class="graph-controls">
|
||
<button class="zoom-btn" @onclick="ZoomIn" title="Zoom In">+</button>
|
||
<button class="zoom-btn" @onclick="ZoomOut" title="Zoom Out">−</button>
|
||
<button class="zoom-btn reset" @onclick="ZoomReset" title="Reset">⟲</button>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
|
||
@code {
|
||
[Parameter] public EventCallback<string> OnNodeSelected { get; set; }
|
||
|
||
private string ContainerId = "d3-graph-container";
|
||
private IJSObjectReference? _module;
|
||
private DotNetObjectReference<KnowledgeGraph>? _dotNetHelper;
|
||
|
||
protected override void OnInitialized()
|
||
{
|
||
FocusMode.OnFocusModeChanged += HandleFocusSimulation;
|
||
GraphService.OnGraphUpdated += HandleGraphUpdate;
|
||
GraphService.OnActiveNodeChanged += HandleActiveNodeChange;
|
||
GraphService.OnLoadingChanged += HandleLoadingChange;
|
||
}
|
||
|
||
private async Task HandleGraphUpdate()
|
||
{
|
||
if (_module == null) return;
|
||
|
||
if (GraphService.CurrentGraphData == null)
|
||
{
|
||
await _module.InvokeVoidAsync("clear");
|
||
}
|
||
else
|
||
{
|
||
await _module.InvokeVoidAsync("updateData", GraphService.CurrentGraphData);
|
||
}
|
||
|
||
await InvokeAsync(StateHasChanged);
|
||
}
|
||
|
||
private async Task HandleActiveNodeChange(string nodeId)
|
||
{
|
||
if (_module == null) return;
|
||
await _module.InvokeVoidAsync("setActiveNode", nodeId);
|
||
}
|
||
|
||
private async Task HandleLoadingChange(bool isLoading)
|
||
{
|
||
await InvokeAsync(StateHasChanged);
|
||
}
|
||
|
||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||
{
|
||
if (firstRender)
|
||
{
|
||
await InitializeGraphAsync();
|
||
|
||
if (GraphService.CurrentGraphData != null)
|
||
{
|
||
await HandleGraphUpdate();
|
||
}
|
||
}
|
||
}
|
||
|
||
private async Task InitializeGraphAsync()
|
||
{
|
||
_module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/knowledgeGraph.js");
|
||
_dotNetHelper = DotNetObjectReference.Create(this);
|
||
await _module.InvokeVoidAsync("mount", ContainerId, GraphService.CurrentGraphData, _dotNetHelper);
|
||
}
|
||
|
||
private async Task ZoomIn() => await (_module?.InvokeVoidAsync("zoomIn") ?? ValueTask.CompletedTask);
|
||
private async Task ZoomOut() => await (_module?.InvokeVoidAsync("zoomOut") ?? ValueTask.CompletedTask);
|
||
private async Task ZoomReset() => await (_module?.InvokeVoidAsync("zoomReset") ?? ValueTask.CompletedTask);
|
||
|
||
[JSInvokable]
|
||
public async Task OnNodeClicked(string nodeId)
|
||
{
|
||
await InteractionService.NotifyNodeSelected(nodeId);
|
||
|
||
if (OnNodeSelected.HasDelegate)
|
||
{
|
||
await OnNodeSelected.InvokeAsync(nodeId);
|
||
}
|
||
}
|
||
|
||
|
||
private async Task HandleFocusSimulation()
|
||
{
|
||
if (_module == null) return;
|
||
try
|
||
{
|
||
if (FocusMode.IsFocusModeActive)
|
||
await _module.InvokeVoidAsync("pause");
|
||
else
|
||
await _module.InvokeVoidAsync("resume");
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
public async ValueTask DisposeAsync()
|
||
{
|
||
FocusMode.OnFocusModeChanged -= HandleFocusSimulation;
|
||
GraphService.OnGraphUpdated -= HandleGraphUpdate;
|
||
GraphService.OnActiveNodeChanged -= HandleActiveNodeChange;
|
||
GraphService.OnLoadingChanged -= HandleLoadingChange;
|
||
|
||
try
|
||
{
|
||
if (_module is not null)
|
||
{
|
||
await _module.InvokeVoidAsync("unmount", ContainerId);
|
||
await _module.DisposeAsync();
|
||
}
|
||
}
|
||
catch { }
|
||
|
||
_dotNetHelper?.Dispose();
|
||
}
|
||
}
|