Add the question naming what a re-index would cost

This commit is contained in:
Thorsten Sommer 2026-09-18 15:42:55 +02:00
parent 545bf76f32
commit 4cf33e807e
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108

View File

@ -0,0 +1,147 @@
using System.Text;
using AIStudio.Dialogs;
using AIStudio.Settings;
using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Services;
namespace AIStudio.Tools;
/// <summary>
/// Asks before an edit makes the prepared documents of data sources useless.
/// </summary>
/// <remarks>
/// Kept here rather than in the dialogs which ask -- the embedding provider dialog and the two data
/// source dialogs -- so the sentence naming what a rebuild costs cannot drift apart between them.
/// That is the same reason DataSourceRepair sits next to it, and both name the same two costs.
///
/// Nothing is asked when nothing is lost. A data source only reaches the question when the edit
/// really changes its embedding signature and when the index already holds something for it, so
/// renaming an embedding provider or editing a data source nobody has indexed yet stays silent.
/// </remarks>
public static class DataSourceReindexWarning
{
/// <summary>
/// How many data sources are named before the rest is only counted.
/// </summary>
private const int MAX_NAMED_DATA_SOURCES = 10;
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(DataSourceReindexWarning).Namespace, nameof(DataSourceReindexWarning));
/// <summary>
/// Asks before an edited embedding provider is saved.
/// </summary>
/// <param name="dialogService">The dialog service to ask with.</param>
/// <param name="settingsManager">The settings, read for the data sources behind the provider.</param>
/// <param name="embeddingService">The service which knows what the index holds.</param>
/// <param name="before">The embedding provider as it is stored.</param>
/// <param name="after">The embedding provider as it would be stored.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>True when the edit may be saved.</returns>
public static async Task<bool> ConfirmEmbeddingProviderChangeAsync(IDialogService dialogService, SettingsManager settingsManager, DataSourceEmbeddingService embeddingService,
EmbeddingProvider before, EmbeddingProvider after, CancellationToken token = default)
{
// Nothing was stored under this id, so no data source can point at it:
if (before == EmbeddingProvider.NONE)
return true;
var candidates = GetDataSourcesUsing(settingsManager, before.Id)
.Where(dataSource => EmbeddingChangeImpact.AffectsStoredIndex(dataSource, before, after))
.Cast<IDataSource>()
.ToList();
if (candidates.Count == 0)
return true;
var affected = await embeddingService.GetDataSourcesWithStoredIndexAsync(candidates, token);
return await ConfirmAsync(dialogService, affected, !after.IsSelfHosted);
}
/// <summary>
/// Asks before an edited data source is saved.
/// </summary>
/// <param name="dialogService">The dialog service to ask with.</param>
/// <param name="settingsManager">The settings, read for the embedding provider of the data source.</param>
/// <param name="embeddingService">The service which knows what the index holds.</param>
/// <param name="before">The data source as it is stored.</param>
/// <param name="after">The data source as it would be stored.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>True when the edit may be saved.</returns>
public static async Task<bool> ConfirmDataSourceChangeAsync(IDialogService dialogService, SettingsManager settingsManager, DataSourceEmbeddingService embeddingService,
IInternalDataSource before, IInternalDataSource after, CancellationToken token = default)
{
// Without a provider nothing is embedded at all, so nothing can be lost:
if (!DataSourceEmbeddingProviders.TryResolve(settingsManager, after, out var embeddingProvider))
return true;
if (!EmbeddingChangeImpact.AffectsStoredIndex(embeddingProvider, before, after))
return true;
var affected = await embeddingService.GetDataSourcesWithStoredIndexAsync([after], token);
return await ConfirmAsync(dialogService, affected, !embeddingProvider.IsSelfHosted);
}
/// <summary>
/// The data sources which are indexed with a given embedding provider.
/// </summary>
/// <param name="settingsManager">The settings holding the data sources.</param>
/// <param name="embeddingProviderId">The id of the embedding provider.</param>
/// <returns>The data sources pointing at that embedding provider.</returns>
private static IReadOnlyList<IInternalDataSource> GetDataSourcesUsing(SettingsManager settingsManager, string embeddingProviderId) =>
settingsManager.ConfigurationData.DataSources
.OfType<IInternalDataSource>()
.Where(dataSource => embeddingProviderId.Equals(dataSource.EmbeddingId, StringComparison.OrdinalIgnoreCase))
.ToList();
/// <summary>
/// Names data sources as a Markdown list, counting the rest when there are too many to name.
/// </summary>
/// <param name="dataSources">The data sources to name.</param>
/// <returns>The Markdown list.</returns>
private static string FormatDataSourceNames(IReadOnlyList<IDataSource> dataSources)
{
var names = dataSources
.Select(dataSource => dataSource.Name)
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
.ToList();
var lines = names.Take(MAX_NAMED_DATA_SOURCES).Select(name => $"- {name}").ToList();
if (names.Count > MAX_NAMED_DATA_SOURCES)
lines.Add($"- {string.Format(TB("and {0} more."), (names.Count - MAX_NAMED_DATA_SOURCES).CompactCount())}");
return string.Join(Environment.NewLine, lines);
}
private static async Task<bool> ConfirmAsync(IDialogService dialogService, IReadOnlyList<IDataSource> affected, bool usesCloudEmbedding)
{
if (affected.Count == 0)
return true;
var body = new StringBuilder();
// Counted rather than put into a plural form: the I18N has no mechanism for one.
body.AppendLine(string.Format(TB("This change makes the prepared documents of the following data sources unusable ({0}):"), affected.Count.CompactCount()));
body.AppendLine();
body.AppendLine(FormatDataSourceNames(affected));
body.AppendLine();
body.AppendLine(TB("Everything prepared for them is thrown away, and every one of their documents goes to your embedding provider once more. With a large data source, this takes a while."));
if (usesCloudEmbedding)
{
body.AppendLine();
body.AppendLine(TB("Your embedding provider runs in the cloud, so preparing everything again costs money."));
}
body.AppendLine();
body.AppendLine(TB("Do you want to apply this change anyway?"));
var dialogParameters = new DialogParameters<ConfirmDialog>
{
{ x => x.MarkdownBody, body.ToString() },
};
var dialogReference = await dialogService.ShowAsync<ConfirmDialog>(TB("Documents Will Be Prepared Again"), dialogParameters, Dialogs.DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
return dialogResult is not null && !dialogResult.Canceled;
}
}