From 408e06e9c09413042924f5ebf640b4101d418285 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Tue, 15 Sep 2026 17:38:36 +0200 Subject: [PATCH] Fixed the source counter of an answer doing nothing when clicked --- .../Chat/ContentBlockComponent.razor | 4 ++-- .../Chat/ContentBlockComponent.razor.cs | 20 ++++++++++++++++++ .../Components/SourcesList.razor | 2 +- .../Components/SourcesList.razor.cs | 21 +++++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index 51f46c48..d1868ed2 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -48,7 +48,7 @@ { - + } @@ -223,7 +223,7 @@ } @if (textContent.Sources.Count > 0) { - + } } diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 9ee23715..170d90f8 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -123,6 +123,7 @@ public partial class ContentBlockComponent : MSGComponentBase private IReadOnlyList cachedMessageTables = []; private char csvSeparator = ','; private ElementReference mathContentContainer; + private SourcesList? sourcesList; private string lastMathRenderSignature = string.Empty; private bool hasActiveMathContainer; private bool isDisposed; @@ -815,6 +816,25 @@ public partial class ContentBlockComponent : MSGComponentBase this.Content.FileAttachments = [.. result]; } + /// + /// Whether the sources of this block stand below the answer, where the counter can take the reader. + /// + /// + /// The same condition the block itself renders the list under. While an answer is still coming + /// in, its sources may already be known, but there is nothing on the page yet to scroll to -- + /// so the counter says it cannot do anything rather than doing nothing when clicked. + /// + private bool HasSourcesToShow => this.Content is { InitialRemoteWait: false, IsStreaming: false, Sources.Count: > 0 }; + + /// + /// Takes the reader from the source counter down to the sources themselves. + /// + private async Task ShowSources() + { + if (this.sourcesList is not null) + await this.sourcesList.ScrollIntoViewAsync(); + } + protected override async ValueTask DisposeResourcesAsync() { if (this.isDisposed) diff --git a/app/MindWork AI Studio/Components/SourcesList.razor b/app/MindWork AI Studio/Components/SourcesList.razor index 131d8159..9a2ce75b 100644 --- a/app/MindWork AI Studio/Components/SourcesList.razor +++ b/app/MindWork AI Studio/Components/SourcesList.razor @@ -2,7 +2,7 @@ @* The class is what the Markdown renderer wraps its own output in, so the headings and the list keep the look they had while this list was Markdown. *@ -
+
@foreach (var group in this.groups) { @* A level-two heading was shown as h5 while this list was Markdown, because that is what diff --git a/app/MindWork AI Studio/Components/SourcesList.razor.cs b/app/MindWork AI Studio/Components/SourcesList.razor.cs index 531a13da..b7d24d5f 100644 --- a/app/MindWork AI Studio/Components/SourcesList.razor.cs +++ b/app/MindWork AI Studio/Components/SourcesList.razor.cs @@ -17,6 +17,12 @@ namespace AIStudio.Components; /// public partial class SourcesList : MSGComponentBase { + // + // The name is about the alignment the function uses, not about the page: it brings the element + // into view with its end at the bottom, which for a list at the end of an answer shows all of it. + // + private const string SCROLL_INTO_VIEW_FUNCTION = "scrollToBottom"; + /// /// The sources to show. /// @@ -26,11 +32,26 @@ public partial class SourcesList : MSGComponentBase [Inject] private RustService RustService { get; init; } = null!; + [Inject] + private IJSRuntime JsRuntime { get; init; } = null!; + [Inject] private ILogger Logger { get; init; } = null!; private readonly List groups = []; + private ElementReference listElement; + + /// + /// Brings this list into view. + /// + /// + /// The counter above an answer says how many sources it rests on; this is how it takes the + /// reader to them. The element stays here, where it is rendered, rather than being handed to + /// whoever wants to scroll to it. + /// + public async Task ScrollIntoViewAsync() => await this.JsRuntime.TryInvokeVoidAsync(this.CircuitState, SCROLL_INTO_VIEW_FUNCTION, this.listElement); + #region Overrides of ComponentBase protected override async Task OnParametersSetAsync()