using System.Text; using AIStudio.Dialogs; using AIStudio.Settings; using AIStudio.Tools.PluginSystem; using AIStudio.Tools.Services; namespace AIStudio.Tools; /// /// Asks before an edit makes the prepared documents of data sources useless. /// /// /// 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. /// public static class DataSourceReindexWarning { /// /// How many data sources are named before the rest is only counted. /// private const int MAX_NAMED_DATA_SOURCES = 10; private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(DataSourceReindexWarning).Namespace, nameof(DataSourceReindexWarning)); /// /// Asks before an edited embedding provider is saved. /// /// The dialog service to ask with. /// The settings, read for the data sources behind the provider. /// The service which knows what the index holds. /// The embedding provider as it is stored. /// The embedding provider as it would be stored. /// The cancellation token. /// True when the edit may be saved. public static async Task 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() .ToList(); if (candidates.Count == 0) return true; var affected = await embeddingService.GetDataSourcesWithStoredIndexAsync(candidates, token); return await ConfirmAsync(dialogService, affected, !after.IsSelfHosted); } /// /// Asks before an edited data source is saved. /// /// The dialog service to ask with. /// The settings, read for the embedding provider of the data source. /// The service which knows what the index holds. /// The data source as it is stored. /// The data source as it would be stored. /// The cancellation token. /// True when the edit may be saved. public static async Task 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); } /// /// The data sources which are indexed with a given embedding provider. /// /// The settings holding the data sources. /// The id of the embedding provider. /// The data sources pointing at that embedding provider. private static IReadOnlyList GetDataSourcesUsing(SettingsManager settingsManager, string embeddingProviderId) => settingsManager.ConfigurationData.DataSources .OfType() .Where(dataSource => embeddingProviderId.Equals(dataSource.EmbeddingId, StringComparison.OrdinalIgnoreCase)) .ToList(); /// /// Names data sources as a Markdown list, counting the rest when there are too many to name. /// /// The data sources to name. /// The Markdown list. private static string FormatDataSourceNames(IReadOnlyList 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 ConfirmAsync(IDialogService dialogService, IReadOnlyList 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 { { x => x.MarkdownBody, body.ToString() }, }; var dialogReference = await dialogService.ShowAsync(TB("Documents Will Be Prepared Again"), dialogParameters, Dialogs.DialogOptions.FULLSCREEN); var dialogResult = await dialogReference.Result; return dialogResult is not null && !dialogResult.Canceled; } }