+ @* One element, since a locked option places it next to its lock icon: *@
+
-
- @this.ResetButtonText
-
-
+
+
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/ConfigurationText.razor.cs b/app/MindWork AI Studio/Components/ConfigurationText.razor.cs
index 3a1f88b18..c2648fb41 100644
--- a/app/MindWork AI Studio/Components/ConfigurationText.razor.cs
+++ b/app/MindWork AI Studio/Components/ConfigurationText.razor.cs
@@ -19,7 +19,7 @@ public partial class ConfigurationText : ConfigurationBaseCore
public Action TextUpdate { get; set; } = _ => { };
///
- /// The icon to display next to the textfield.
+ /// The icon to display next to the textfield, when it has a single line.
///
[Parameter]
public string Icon { get; set; } = Icons.Material.Filled.Info;
@@ -93,6 +93,12 @@ public partial class ConfigurationText : ConfigurationBaseCore
private int GetMaxLines => this.AutoGrow ? this.MaxLines : 1;
+ //
+ // MudBlazor centers the icon vertically, so in a field of several lines it floats in the middle
+ // of the text. Such a field goes without:
+ //
+ private Adornment IconAdornment => this.NumLines == 1 ? Adornment.Start : Adornment.None;
+
private void InternalUpdate(string text)
{
this.timer.Stop();
diff --git a/app/MindWork AI Studio/Components/DataSourceCloudEmbeddingWarning.razor.cs b/app/MindWork AI Studio/Components/DataSourceCloudEmbeddingWarning.razor.cs
index 9f3ae4a2c..584d3dc9c 100644
--- a/app/MindWork AI Studio/Components/DataSourceCloudEmbeddingWarning.razor.cs
+++ b/app/MindWork AI Studio/Components/DataSourceCloudEmbeddingWarning.razor.cs
@@ -32,12 +32,17 @@ public partial class DataSourceCloudEmbeddingWarning : MSGComponentBase
}
}
+ ///
+ /// Which files of a folder are sent depends on its file patterns and switches. The sentence does
+ /// not repeat them: they are right next to it in the dialog, and a sentence per combination would
+ /// have to change with every new setting.
+ ///
private string GetSubjectText()
{
if (string.IsNullOrWhiteSpace(this.SourcePath))
return this.DataSourceType switch
{
- DataSourceType.LOCAL_DIRECTORY => T("All files in this folder and its subfolders"),
+ DataSourceType.LOCAL_DIRECTORY => T("Every file this data source selects from this folder"),
DataSourceType.LOCAL_FILE => T("The selected file"),
DataSourceType.MAILBOX => T("Every mail of this mailbox in the selected folder and period, together with the text of its attachments,"),
_ => T("The selected data")
@@ -45,7 +50,7 @@ public partial class DataSourceCloudEmbeddingWarning : MSGComponentBase
return this.DataSourceType switch
{
- DataSourceType.LOCAL_DIRECTORY => string.Format(T("All files in the folder '{0}' and its subfolders"), this.SourcePath),
+ DataSourceType.LOCAL_DIRECTORY => string.Format(T("Every file this data source selects from the folder '{0}'"), this.SourcePath),
DataSourceType.LOCAL_FILE => string.Format(T("The file '{0}'"), this.SourcePath),
DataSourceType.MAILBOX => string.Format(T("Every mail of the mailbox '{0}' in the selected folder and period, together with the text of its attachments,"), this.SourcePath),
_ => string.Format(T("The data source '{0}'"), this.SourcePath)
diff --git a/app/MindWork AI Studio/Components/DataSourceManagement.razor b/app/MindWork AI Studio/Components/DataSourceManagement.razor
index f31322e8c..57766f135 100644
--- a/app/MindWork AI Studio/Components/DataSourceManagement.razor
+++ b/app/MindWork AI Studio/Components/DataSourceManagement.razor
@@ -42,9 +42,18 @@
@{ var embeddingStatus = embeddingStatuses.GetValueOrDefault(context.Id); }
@context.Num
- @context.Name
- @context.Type.GetDisplayName()
- @this.GetEmbeddingName(context)
+ @*
+ The name gets the room the other columns leave, which in the dialog was next to nothing: it
+ may break anywhere, so a single character was enough for the browser. The minimum width
+ keeps it readable, and the type and the embedding wrap instead, so that the table still fits.
+ The minimum sits on a div, since browsers differ in whether they honor it on a table cell.
+ With "break-word" instead, a long name without spaces would push the table out of the dialog.
+ *@
+
+ @context.Name
+
+ @context.Type.GetDisplayName()
+ @this.GetEmbeddingName(context)
@if (context is IIndexedDataSource)
{
diff --git a/app/MindWork AI Studio/Components/DataSourceManagement.razor.cs b/app/MindWork AI Studio/Components/DataSourceManagement.razor.cs
index 131ce6613..be0ac62fb 100644
--- a/app/MindWork AI Studio/Components/DataSourceManagement.razor.cs
+++ b/app/MindWork AI Studio/Components/DataSourceManagement.razor.cs
@@ -83,17 +83,21 @@ public partial class DataSourceManagement : MSGComponentBase
}
///
- /// Files which were skipped for good are none of the failed ones, so a data source made of
- /// scanned documents stays green: there is nothing here for the user to fix.
+ /// Files which were skipped for good are none of the failed ones, so an otherwise usable data
+ /// source stays green. The shared health policy warns when none of the selected documents could
+ /// be indexed.
///
private static Color GetIndexingStatusColor(DataSourceEmbeddingStatus? status)
{
if (status is null || status.State is DataSourceEmbeddingState.IDLE or DataSourceEmbeddingState.QUEUED or DataSourceEmbeddingState.RUNNING)
return Color.Warning;
- return status.State is DataSourceEmbeddingState.FAILED || status.FailedDocuments > 0
- ? Color.Error
- : Color.Success;
+ return status.Health switch
+ {
+ DataSourceEmbeddingHealth.ERROR => Color.Error,
+ DataSourceEmbeddingHealth.WARNING => Color.Warning,
+ _ => Color.Success,
+ };
}
///
@@ -109,11 +113,11 @@ public partial class DataSourceManagement : MSGComponentBase
if (status is null)
return T("Waiting for indexing status");
- // Only files are skipped for want of readable text. A mail is read from the server, never from a file:
- if (status.PermanentlySkippedDocuments == 0 || status.DataSourceType is DataSourceType.MAILBOX)
+ // Only files have enumeration exclusions. A mail is read from the server, never from a file:
+ if (status.SkippedDocuments == 0 || status.DataSourceType is DataSourceType.MAILBOX)
return status.StateLabel;
- return $"{status.StateLabel} — {string.Format(T("{0} files were skipped because they contain no readable text. AI Studio reads them again once they change."), status.PermanentlySkippedDocuments)}";
+ return $"{status.StateLabel} — {string.Format(T("Skipped files: {0}. Open Background embeddings to see why and what you can do."), status.SkippedDocuments)}";
}
private string GetEmbeddingName(IDataSourceBase dataSource)
diff --git a/app/MindWork AI Studio/Components/RestoreDefaultButton.razor b/app/MindWork AI Studio/Components/RestoreDefaultButton.razor
new file mode 100644
index 000000000..5c64c4dab
--- /dev/null
+++ b/app/MindWork AI Studio/Components/RestoreDefaultButton.razor
@@ -0,0 +1,5 @@
+
+
+ @this.Text
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/RestoreDefaultButton.razor.cs b/app/MindWork AI Studio/Components/RestoreDefaultButton.razor.cs
new file mode 100644
index 000000000..21ba27ea9
--- /dev/null
+++ b/app/MindWork AI Studio/Components/RestoreDefaultButton.razor.cs
@@ -0,0 +1,35 @@
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Components;
+
+///
+/// A small button below a field, which puts the default value back into that field.
+///
+///
+/// The button takes a row of its own, aligned to the right. Next to the field, it took away the width
+/// the field needs, wrapped or got cut off with a longer text, and every field of a form ended at
+/// another place, depending on the length of its button.
+///
+public partial class RestoreDefaultButton : ComponentBase
+{
+ ///
+ /// The text on the button, e.g., "Restore default".
+ ///
+ [Parameter]
+ public string Text { get; set; } = string.Empty;
+
+ ///
+ /// Puts the default value back into the field.
+ ///
+ [Parameter]
+ public EventCallback OnClick { get; set; }
+
+ [Parameter]
+ public bool Disabled { get; set; }
+
+ ///
+ /// The CSS classes of the row, e.g., for the space below it.
+ ///
+ [Parameter]
+ public string Class { get; set; } = string.Empty;
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor
index 64d99b57a..a228980b3 100644
--- a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor
@@ -43,7 +43,7 @@
/>
- @T("Select a root directory for this data source. All data in this directory and all its subdirectories will be processed for this data source.")
+ @T("Select a root directory for this data source.")
@if (!this.CanChangeSource)
{
@@ -71,6 +71,33 @@
/>
}
+
+
+
+ @T("Select the file patterns this data source should index.")
+
+
+
+
@T("In order for the AI to be able to determine the appropriate data at any time, you must choose an embedding provider.")
@@ -121,6 +148,42 @@
+
+
+ @T("Hidden are files and folders whose names start with a dot, such as .git or .env, and those hidden by the operating system. A file pattern which starts with a dot, such as .gitignore, selects such files anyway.")
+
+ @* Without an icon: in a field of several lines, it floats in the middle of the text. *@
+
+
+
@if (!string.IsNullOrWhiteSpace(this.dataEmbeddingId))
{
this.MudDialog.Cancel();
+ private string NormalizeFilePatterns() => FileNamePatterns.Normalize(this.dataFilePatterns);
+
+ private void RestoreDefaultExcludePatterns() => this.dataExcludePatterns = FileExclusions.DEFAULT_PATTERNS;
+
///
/// Gives the fields which are checked against each other a fresh verdict: the embedding provider
/// and the required confidence level, and the token limits, which depend on the embedding provider.
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor
index 0f7abb3be..146f47cd3 100644
--- a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor
@@ -10,6 +10,11 @@
}
+
+
+
+
+
@if (!this.IsDirectoryAvailable)
{
@@ -27,7 +32,7 @@
@if (this.IsCloudEmbedding)
{
- @string.Format(T("The embedding runs in the cloud. All your data from the folder '{0}' and all its subdirectories will be sent to the cloud."), this.DataSource.Path)
+ @string.Format(T("The embedding runs in the cloud. Every file this data source selects from the folder '{0}' will be sent to the cloud."), this.DataSource.Path)
}
else
@@ -40,16 +45,16 @@
-
-
+
+
@if (this.directorySizeNumFiles > 100)
{
- @string.Format(T("For performance reasons, only the first 100 files are shown. The directory contains {0} files in total."), this.NumberFilesInDirectory)
+ @string.Format(T("For performance reasons, only the first 100 matching files are shown. The configured patterns match {0} files in total."), this.NumberFilesInDirectory)
}
-
+
@if (this.IsOperationInProgress)
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs
index 458dbac4f..8eea5a2ce 100644
--- a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryInfoDialog.razor.cs
@@ -32,7 +32,7 @@ public partial class DataSourceLocalDirectoryInfoDialog : MSGComponentBase
if (this.directoryInfo.Exists)
{
- this.directorySizeTask = this.directoryInfo.DetermineContentSize(this.UpdateDirectorySize, this.UpdateDirectoryFiles, this.UpdateFileList, MAX_FILES_TO_SHOW, this.DirectoryOperationDone, this.cts.Token);
+ this.directorySizeTask = this.directoryInfo.DetermineContentSize(this.UpdateDirectorySize, this.UpdateDirectoryFiles, this.UpdateFileList, MAX_FILES_TO_SHOW, this.DirectoryOperationDone, this.cts.Token, FolderFileSelection.Of(this.DataSource));
this.refreshTimer.Elapsed += (_, _) => this.InvokeAsync(this.StateHasChanged);
this.refreshTimer.Start();
}
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor
index c68403b76..ad2379152 100644
--- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogBatchProcessing.razor
@@ -19,8 +19,11 @@
@T("Input")
-
+
+
+
+
@T("Instructions")
diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs
index 410623a47..6e0c33c2a 100644
--- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs
+++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs
@@ -87,7 +87,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
private readonly SemaphoreSlim promptInjectionDialogSemaphore = new(1, 1);
private readonly SemaphoreSlim dataRestorePendingDialogSemaphore = new(1, 1);
- private DataSourceEmbeddingOverview embeddingOverview = new(DataSourceEmbeddingState.COMPLETED, 0, 0, 0);
+ private DataSourceEmbeddingOverview embeddingOverview = DataSourceEmbeddingOverview.Create([]);
private IReadOnlyCollection navItems = [];
private NavBarItem embeddingItem = new (string.Empty, string.Empty, string.Empty, string.Empty, string.Empty, false);
private bool showEmbeddingStatusIcon;
@@ -505,9 +505,10 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
var palette = this.ColorTheme.GetCurrentPalette(this.SettingsManager);
(string icon, string lightcolor, string darkcolor) embeddingIcon = this.embeddingOverview.State switch
{
- DataSourceEmbeddingState.FAILED => (Icons.Material.Filled.Warning, palette.Error.Value, "#d32f2f"),
DataSourceEmbeddingState.QUEUED => (Icons.Material.Filled.Sync, palette.Info.Value, "#1976d2"),
DataSourceEmbeddingState.RUNNING => (Icons.Material.Filled.Sync, palette.Warning.Value, "#d29f00"),
+ _ when this.embeddingOverview.Health is DataSourceEmbeddingHealth.ERROR => (Icons.Material.Filled.Error, palette.Error.Value, "#d32f2f"),
+ _ when this.embeddingOverview.Health is DataSourceEmbeddingHealth.WARNING => (Icons.Material.Filled.Warning, palette.Warning.Value, "#d29f00"),
// Nothing to do: the entry keeps the colors of its neighbors, so a permanently visible
// icon does not draw attention while there is nothing to attend to:
@@ -526,9 +527,13 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
T("Embeddings are running: {0} of {1} are indexed."),
this.embeddingOverview.IndexedDocuments.CompactCount(),
this.embeddingOverview.TotalDocuments.CompactCount()),
- DataSourceEmbeddingState.FAILED => this.embeddingOverview.FailedDocuments > 0
+ _ when this.embeddingOverview.Health is DataSourceEmbeddingHealth.ERROR => this.embeddingOverview.FailedDocuments > 0
? string.Format(T("Some embeddings failed: {0} could not be indexed."), this.embeddingOverview.FailedDocuments.CompactCount())
: T("Some embeddings failed and need attention."),
+ _ when this.embeddingOverview.Health is DataSourceEmbeddingHealth.WARNING && this.embeddingOverview.AttentionDataSources == 1 =>
+ T("One data source needs attention."),
+ _ when this.embeddingOverview.Health is DataSourceEmbeddingHealth.WARNING =>
+ string.Format(T("{0} data sources need attention."), this.embeddingOverview.AttentionDataSources.CompactCount()),
// The entry is always visible, so its resting state needs words as well. An empty tooltip
// would leave the user guessing what the icon is there for:
diff --git a/app/MindWork AI Studio/Pages/Embeddings.razor b/app/MindWork AI Studio/Pages/Embeddings.razor
index 33d60912f..479d4b659 100644
--- a/app/MindWork AI Studio/Pages/Embeddings.razor
+++ b/app/MindWork AI Studio/Pages/Embeddings.razor
@@ -94,7 +94,7 @@
}
- @if (status.PermanentlySkippedDocuments > 0)
+ @if (status.SkippedDocuments > 0)
{
@this.GetSkippedText(status)
@@ -120,7 +120,7 @@
hundred scanned documents were skipped otherwise buries the handful of
files somebody has to look at.
*@
- @if (status.Failures.Count > 0)
+ @if (status.Failures.Count > 0 || status.IgnoredFiles.Count > 0)
{
@foreach (var group in this.GetFailureGroups(status))
@@ -189,9 +189,89 @@
}
+ @foreach (var group in this.GetIgnoredFileGroups(status))
+ {
+