From a2a09b5e916677157cbdaa18e263ccd3ace0aa63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Jasi=C5=84ski?= Date: Mon, 18 May 2026 20:07:32 +0200 Subject: [PATCH] fix(ui): enforce idempotent AI data fetching and JSRuntime guards --- .../Molecules/AiAssistantBubble.razor | 17 ++++++++++++ .../Molecules/GroundednessBadge.razor | 27 ++++++++++++++++++- src/NexusReader.UI.Shared/Pages/Home.razor | 5 ++-- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/NexusReader.UI.Shared/Components/Molecules/AiAssistantBubble.razor b/src/NexusReader.UI.Shared/Components/Molecules/AiAssistantBubble.razor index 3b4f4ca..ba4a703 100644 --- a/src/NexusReader.UI.Shared/Components/Molecules/AiAssistantBubble.razor +++ b/src/NexusReader.UI.Shared/Components/Molecules/AiAssistantBubble.razor @@ -51,9 +51,13 @@ private string _lastFetchedBlockId = string.Empty; private KnowledgePacket? _packet; private CancellationTokenSource? _streamCts; + private bool _isInteractive; protected override async Task OnParametersSetAsync() { + if (!_isInteractive) + return; + // Only re-fetch when the block context actually changes if (string.IsNullOrEmpty(ContextBlockId) || ContextBlockId == _lastFetchedBlockId) return; @@ -62,6 +66,19 @@ await FetchAndStreamAsync(); } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _isInteractive = true; + if (!string.IsNullOrEmpty(ContextBlockId)) + { + _lastFetchedBlockId = ContextBlockId; + await FetchAndStreamAsync(); + } + } + } + private async Task FetchAndStreamAsync() { // Cancel any in-progress stream diff --git a/src/NexusReader.UI.Shared/Components/Molecules/GroundednessBadge.razor b/src/NexusReader.UI.Shared/Components/Molecules/GroundednessBadge.razor index 723b394..79dd733 100644 --- a/src/NexusReader.UI.Shared/Components/Molecules/GroundednessBadge.razor +++ b/src/NexusReader.UI.Shared/Components/Molecules/GroundednessBadge.razor @@ -26,15 +26,40 @@ private GroundednessResult? _result; private bool _isChecking; + private bool _isInteractive; + private string _previousAnswer = string.Empty; + private string _previousContext = string.Empty; + + protected override void OnParametersSet() + { + if (Answer != _previousAnswer || Context != _previousContext) + { + _result = null; + _previousAnswer = Answer; + _previousContext = Context; + } + } protected override async Task OnParametersSetAsync() { - if (!string.IsNullOrEmpty(Answer) && !string.IsNullOrEmpty(Context) && _result == null) + if (_isInteractive && !string.IsNullOrEmpty(Answer) && !string.IsNullOrEmpty(Context) && _result == null) { await RunCheck(); } } + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + _isInteractive = true; + if (!string.IsNullOrEmpty(Answer) && !string.IsNullOrEmpty(Context) && _result == null) + { + await RunCheck(); + } + } + } + private async Task RunCheck() { _isChecking = true; diff --git a/src/NexusReader.UI.Shared/Pages/Home.razor b/src/NexusReader.UI.Shared/Pages/Home.razor index bf31e07..d9d8c3e 100644 --- a/src/NexusReader.UI.Shared/Pages/Home.razor +++ b/src/NexusReader.UI.Shared/Pages/Home.razor @@ -27,11 +27,10 @@ private IJSObjectReference? _keydownHandler; private DotNetObjectReference? _dotNetRef; - protected override async Task OnInitializedAsync() + protected override void OnInitialized() { QuizState.OnQuizRequested += HandleQuizRequestedAsync; FocusMode.OnFocusModeChanged += HandleUpdate; - await FocusMode.InitializeAsync(); } protected override async Task OnParametersSetAsync() @@ -65,11 +64,13 @@ { if (firstRender) { + await FocusMode.InitializeAsync(); try { _interopModule = await JS.InvokeAsync("import", "./_content/NexusReader.UI.Shared/js/focusInterop.js"); _dotNetRef = DotNetObjectReference.Create(this); _keydownHandler = await _interopModule.InvokeAsync("attachKeyboardListener", _dotNetRef); } catch { } /* ignored dynamically */ + StateHasChanged(); } }