From 2f7f513677654f71e766413ac1d165b2b0656b8f Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 9 Sep 2026 18:24:25 +0200 Subject: [PATCH] Show data sources as collapsible panels and reach their settings --- .../Assistants/I18N/allTexts.lua | 6 + .../Components/ExpansionPanel.razor | 4 +- .../Components/ExpansionPanel.razor.cs | 22 +- app/MindWork AI Studio/Pages/Embeddings.razor | 255 ++++++++++-------- .../Pages/Embeddings.razor.cs | 70 +++++ .../plugin.lua | 8 +- .../plugin.lua | 6 + app/MindWork AI Studio/wwwroot/app.css | 13 + 8 files changed, 263 insertions(+), 121 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 003ff2a0..82e9864f 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -8959,6 +8959,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1946414905"] = "Tried again durin -- Skipped files: {0} UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T196379388"] = "Skipped files: {0}" +-- Manage your data sources +UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T2149927097"] = "Manage your data sources" + -- Noticed UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T2367007983"] = "Noticed" @@ -8980,6 +8983,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T2547971789"] = "Background embedd -- Refresh this data source UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T2901874229"] = "Refresh this data source" +-- Data source: {0} +UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T2945218010"] = "Data source: {0}" + -- Embedding provider: {0} UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T300213237"] = "Embedding provider: {0}" diff --git a/app/MindWork AI Studio/Components/ExpansionPanel.razor b/app/MindWork AI Studio/Components/ExpansionPanel.razor index 329bfd08..67acc05d 100644 --- a/app/MindWork AI Studio/Components/ExpansionPanel.razor +++ b/app/MindWork AI Studio/Components/ExpansionPanel.razor @@ -1,8 +1,8 @@ - +
- + @this.HeaderText @if (this.ShowEndButton) diff --git a/app/MindWork AI Studio/Components/ExpansionPanel.razor.cs b/app/MindWork AI Studio/Components/ExpansionPanel.razor.cs index a8ba6a2f..e57487e8 100644 --- a/app/MindWork AI Studio/Components/ExpansionPanel.razor.cs +++ b/app/MindWork AI Studio/Components/ExpansionPanel.razor.cs @@ -15,7 +15,27 @@ public partial class ExpansionPanel : ComponentBase [Parameter] public string HeaderText { get; set; } = "n/a"; - + + /// + /// The typography of the header text. + /// + /// + /// Worth lowering for a panel which sits inside another one, together with the compact header + /// class below: two headers of the same size give no clue about which one contains the other. + /// + [Parameter] + public Typo HeaderTypo { get; set; } = Typo.h6; + + /// + /// Additional class names for the header, separated by space. + /// + /// + /// The one this exists for is expansion-panel-header-compact, which takes the height of the + /// header down for a nested panel. + /// + [Parameter] + public string HeaderClass { get; set; } = string.Empty; + [Parameter] public int? MaxHeight { get; set; } diff --git a/app/MindWork AI Studio/Pages/Embeddings.razor b/app/MindWork AI Studio/Pages/Embeddings.razor index 6022b4aa..2a496f67 100644 --- a/app/MindWork AI Studio/Pages/Embeddings.razor +++ b/app/MindWork AI Studio/Pages/Embeddings.razor @@ -3,7 +3,13 @@ - @T("Background embeddings") + + @T("Background embeddings") + + + + + @T("AI Studio indexes local RAG data sources in the background. Finished files stay recorded so unchanged files can be skipped after a restart, while added or deleted files are detected during the next run. The same applies to documents without readable text, such as scanned pages: AI Studio remembers them and reads them again only once they change.") @@ -23,133 +29,148 @@ } else { - @foreach (var status in this.Statuses) - { - - - - @status.DataSourceName - + @* + One panel per data source, and only one of them open: a folder with thousands of files + fills the page by itself. What a closed panel still has to say stays in its header, so + nobody has to open every one of them to see how their data sources are doing. + *@ + + @foreach (var status in this.Statuses) + { + + +
+ @string.Format(T("Data source: {0}"), status.DataSourceName) + @status.StateLabel @if (CanRefresh(status)) { + @* + This opens or closes the panel along the way. The header is what + toggles it, and neither stopping the event nor swallowing the + click works in this project — see the end button of our own + ExpansionPanel component, which behaves the same way. + *@ } - - +
+
+ + + - + + @string.Format(T("{0} of {1} files are indexed."), status.IndexedFiles, status.TotalFiles) + - - @string.Format(T("{0} of {1} files are indexed."), status.IndexedFiles, status.TotalFiles) - - - @if (status.PermanentlySkippedFiles > 0) - { - - @string.Format(T("Skipped files: {0}. AI Studio reads them again once they change."), status.PermanentlySkippedFiles) - - } - - @if (status.FailedFiles > 0) - { - - @string.Format(T("Failed files: {0}"), status.FailedFiles) - - } - - @if (!string.IsNullOrWhiteSpace(status.CurrentFile)) - { - - @string.Format(T("Current file: {0}"), status.CurrentFile) - - } - - @* - One panel per cause, the most pressing one first. A folder in which nine - hundred scanned documents were skipped otherwise buries the handful of - files somebody has to look at. - *@ - @if (status.Failures.Count > 0) - { - - @foreach (var group in this.GetFailureGroups(status)) + @if (status.PermanentlySkippedFiles > 0) { - - - - - @(group.Cause.IsPermanent ? T("Skipped until the file changes") : T("Tried again during the next run")) - - @if (!string.IsNullOrWhiteSpace(group.EmbeddingProviderName)) - { - @string.Format(T("Embedding provider: {0}"), group.EmbeddingProviderName) - } - @if (group.Cause.NeedsProviderSettings) - { - - @T("Open the settings") - - } - - - - - - - - - @T("File") - @T("Noticed") - @T("Actions") - - - - @* Captions render as spans, so each of them needs to be told to take its own line. *@ - @GetFileName(context.FilePath) - @context.FilePath - @if (group.Cause.ShowsMessagePerFile) - { - @context.Reason - } - - - @GetOccurrenceText(context) - - - @if (CanShowInFileManager(context)) - { - - - - } - - - - - - - - + + @string.Format(T("Skipped files: {0}. AI Studio reads them again once they change."), status.PermanentlySkippedFiles) + } - - } - @* - Shown next to the list, not instead of it: the list says which files failed, - while this is the one sentence about the data source as a whole. Hiding it - as soon as a single file failed is what made it invisible in practice. - *@ - @if (!string.IsNullOrWhiteSpace(status.LastError)) - { - - @status.LastError - - } - -
- } + @if (status.FailedFiles > 0) + { + + @string.Format(T("Failed files: {0}"), status.FailedFiles) + + } + + @if (!string.IsNullOrWhiteSpace(status.CurrentFile)) + { + + @string.Format(T("Current file: {0}"), status.CurrentFile) + + } + + @* + One panel per cause, the most pressing one first. A folder in which nine + hundred scanned documents were skipped otherwise buries the handful of + files somebody has to look at. + *@ + @if (status.Failures.Count > 0) + { + + @foreach (var group in this.GetFailureGroups(status)) + { + + + + + @(group.Cause.IsPermanent ? T("Skipped until the file changes") : T("Tried again during the next run")) + + @if (!string.IsNullOrWhiteSpace(group.EmbeddingProviderName)) + { + @string.Format(T("Embedding provider: {0}"), group.EmbeddingProviderName) + } + @if (group.Cause.NeedsProviderSettings) + { + + @T("Open the settings") + + } + + + + + + + + + @T("File") + @T("Noticed") + @T("Actions") + + + + @* Captions render as spans, so each of them needs to be told to take its own line. *@ + @GetFileName(context.FilePath) + @context.FilePath + @if (group.Cause.ShowsMessagePerFile) + { + @context.Reason + } + + + @GetOccurrenceText(context) + + + @if (CanShowInFileManager(context)) + { + + + + } + + + + + + + + + } + + } + + @* + Shown next to the list, not instead of it: the list says which files failed, + while this is the one sentence about the data source as a whole. Hiding it + as soon as a single file failed is what made it invisible in practice. + *@ + @if (!string.IsNullOrWhiteSpace(status.LastError)) + { + + @status.LastError + + } +
+ + + } + } diff --git a/app/MindWork AI Studio/Pages/Embeddings.razor.cs b/app/MindWork AI Studio/Pages/Embeddings.razor.cs index 84214645..95bfb528 100644 --- a/app/MindWork AI Studio/Pages/Embeddings.razor.cs +++ b/app/MindWork AI Studio/Pages/Embeddings.razor.cs @@ -1,4 +1,5 @@ using AIStudio.Components; +using AIStudio.Dialogs.Settings; using AIStudio.Provider; using AIStudio.Settings.DataModel; using AIStudio.Tools.Rust; @@ -6,6 +7,8 @@ using AIStudio.Tools.Services; using Microsoft.AspNetCore.Components; +using DialogOptions = AIStudio.Dialogs.DialogOptions; + namespace AIStudio.Pages; public partial class Embeddings : MSGComponentBase @@ -21,11 +24,17 @@ public partial class Embeddings : MSGComponentBase [Inject] private RustService RustService { get; init; } = null!; + [Inject] + private IDialogService DialogService { get; init; } = null!; + [Inject] private ILogger Logger { get; init; } = null!; private IReadOnlyList Statuses { get; set; } = []; + private string? expandedDataSourceId; + private bool userChoseExpansion; + private int TotalIndexedFiles => this.Statuses.Sum(status => status.IndexedFiles); private int TotalPendingFiles => this.Statuses.Sum(status => Math.Max(0, status.TotalFiles - status.IndexedFiles - status.FailedFiles - status.PermanentlySkippedFiles)); @@ -70,6 +79,67 @@ public partial class Embeddings : MSGComponentBase .OrderBy(status => status.SortOrder) .ThenBy(status => status.DataSourceName, StringComparer.OrdinalIgnoreCase) .ToList(); + + this.UpdateAutoExpansion(); + } + + /// + /// Opens the data source which is worth reading, as long as the user has not chosen one. + /// + /// + /// It never closes what is open. A data source which finishes its run while somebody is reading + /// it would otherwise fold up at the very moment its result becomes interesting. From the first + /// click on, the page stops rearranging itself at all. + /// + private void UpdateAutoExpansion() + { + if (this.userChoseExpansion) + return; + + // The list is sorted by state, so the first match is the most pressing one: a running data + // source before a queued one, and a failed one before a completed one: + var worthOpening = this.Statuses.FirstOrDefault(IsWorthOpening); + if (worthOpening is null) + return; + + this.expandedDataSourceId = worthOpening.DataSourceId; + } + + /// + /// Whoever opens this page does so because a run is under way or because something went wrong. + /// Meeting nothing but closed panels would be a step back from the version which showed every + /// data source at once. + /// + private static bool IsWorthOpening(DataSourceEmbeddingStatus status) => + status.State is DataSourceEmbeddingState.RUNNING or DataSourceEmbeddingState.QUEUED or DataSourceEmbeddingState.FAILED || status.FailedFiles > 0; + + /// + /// MudBlazor keeps track of which panel is open on its own, so this only records the decision. + /// Both events of a switch arrive, in either order — the one closing the old panel and the one + /// opening the new one — which is why the closing event only clears what it actually named. + /// + private void DataSourcePanelExpandedChanged(DataSourceEmbeddingStatus status, bool isExpanded) + { + this.userChoseExpansion = true; + + if (isExpanded) + this.expandedDataSourceId = status.DataSourceId; + else if (this.expandedDataSourceId == status.DataSourceId) + this.expandedDataSourceId = null; + } + + /// + /// Opens the data source settings, the same dialog the chat offers next to its data source selection. + /// + /// + /// Nothing is left to do once it closes: the dialog writes the settings itself and publishes + /// CONFIGURATION_CHANGED, which this page already listens to. + /// + private async Task OpenDataSourceSettings() + { + var dialogParameters = new DialogParameters(); + var dialogReference = await this.DialogService.ShowAsync(null, dialogParameters, DialogOptions.FULLSCREEN); + await dialogReference.Result; } private static Color GetStatusColor(DataSourceEmbeddingStatus status) => status.State switch diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index abf0e349..ab41464e 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -3871,7 +3871,7 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T2113594442"] = "Sie UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T21181525"] = "Wählen Sie hier die Daten aus, die Sie verwenden möchten." -- Manage your data sources -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T2149927097"] = "Verwalten Sie ihre Datenquellen" +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T2149927097"] = "Ihre Datenquellen verwalten" -- Select data UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T274155039"] = "Daten auswählen" @@ -8961,6 +8961,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1946414905"] = "Beim nächsten Du -- Skipped files: {0} UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T196379388"] = "Übersprungene Dateien: {0}" +-- Manage your data sources +UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T2149927097"] = "Ihre Datenquellen verwalten" + -- Noticed UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T2367007983"] = "Zur Kenntnis genommen" @@ -8982,6 +8985,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T2547971789"] = "Einbettungen im H -- Refresh this data source UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T2901874229"] = "Diese Datenquelle aktualisieren" +-- Data source: {0} +UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T2945218010"] = "Datenquelle: {0}" + -- Embedding provider: {0} UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T300213237"] = "Einbettungsanbieter: {0}" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 00fd7658..13998081 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -8961,6 +8961,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T1946414905"] = "Tried again durin -- Skipped files: {0} UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T196379388"] = "Skipped files: {0}" +-- Manage your data sources +UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T2149927097"] = "Manage your data sources" + -- Noticed UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T2367007983"] = "Noticed" @@ -8982,6 +8985,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T2547971789"] = "Background embedd -- Refresh this data source UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T2901874229"] = "Refresh this data source" +-- Data source: {0} +UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T2945218010"] = "Data source: {0}" + -- Embedding provider: {0} UI_TEXT_CONTENT["AISTUDIO::PAGES::EMBEDDINGS::T300213237"] = "Embedding provider: {0}" diff --git a/app/MindWork AI Studio/wwwroot/app.css b/app/MindWork AI Studio/wwwroot/app.css index af0ea4c0..5c83cd77 100644 --- a/app/MindWork AI Studio/wwwroot/app.css +++ b/app/MindWork AI Studio/wwwroot/app.css @@ -444,3 +444,16 @@ tr:has(> .provider-group-header) .mud-icon-button { .provider-group-header { font-weight: 600; } + +/* + * Headers of an expansion panel which sits inside another one, such as the failure groups of a + * data source on the embeddings page. Two headers of the same size give no clue about which one + * contains the other. MudBlazor's own Dense is no help here: it takes the padding off the content, + * not off the header, whose height is fixed in the framework. Hence this rule — and it has to name + * the MudBlazor classes to outweigh their specificity. + */ +.mud-expand-panel .mud-expand-panel-header.expansion-panel-header-compact { + min-height: 2rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; +}