mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-09-28 20:03:36 +00:00
Resolved 29 conflicting files. The notable decisions: Confidence: main's tool-calling gate (RequiredProviderConfidence) and this branch's local-RAG gate (DataConfidenceLevel) turned out to be the same rule on the same axis, so they are now one field. Both tool results and data sources raise it through RequireProviderConfidence(). The gate checks the level strictly and no longer exempts providers trusted by configuration: TrustedProviderIds is documented as applying to data-source security checks only, and organizations set confidence through DataConfidence .CustomConfidenceScheme instead. The security axis (DataSecurity, ERI, IsTrustedForDataSourceSecurityChecks) is unchanged. Provider creation: main's CreateProvider signature won (hfEndpointKind, capabilityOverrides, no model parameter); tokenizerPath was added to it and is set for every provider, including the new Hetzner, IONOS and LiteLLM. Provider and EmbeddingProvider combine the record parameters, Lua parsing and Lua serialization of both sides. File types: main's hierarchy (ODT leaf, WORD parent, PowerPoint without the legacy .ppt, TABULAR instead of DELIMITED_TABLE) plus this branch's SPREADSHEET parent with ODS and the xlsm/xlsb/xla/xlam extensions, which the runtime already reads. Both sides had added a conflicting HTML filter; the reading family keeps the name, and the export path uses a narrow HTML_DOCUMENT, following the existing LATEX/TEX split. Runtime: main's file_data.rs is the base, including the prompt-injection sanitizer and the extraction routes. Token counting and chunk segmentation moved into take_released, so they act on the text the filter has released rather than on text it is still holding. A failed count is logged and left out instead of ending the extraction, because the app counts such a segment itself. Data sources: the participating-provider checks of this branch are kept, and main's GetAllowedDataSources overload now builds on them. DirectChatService resolves the launched chat's data source options before the check, so filter and chat see the same options. .NET and Rust both build clean; I18N regenerated to 4060 keys.
654 lines
29 KiB
C#
654 lines
29 KiB
C#
using System.Runtime.CompilerServices;
|
|
using AIStudio.Dialogs;
|
|
using AIStudio.Settings;
|
|
using AIStudio.Settings.DataModel;
|
|
using AIStudio.Tools.AIJobs;
|
|
using AIStudio.Tools.AssistantSessions;
|
|
using AIStudio.Tools.Media;
|
|
using AIStudio.Tools.PluginSystem;
|
|
using AIStudio.Tools.Security;
|
|
using AIStudio.Tools.Rust;
|
|
using AIStudio.Tools.Services;
|
|
|
|
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.AspNetCore.Components.Routing;
|
|
|
|
using DialogOptions = AIStudio.Dialogs.DialogOptions;
|
|
using EnterpriseEnvironment = AIStudio.Tools.EnterpriseEnvironment;
|
|
|
|
namespace AIStudio.Layout;
|
|
|
|
public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILang, IDisposable
|
|
{
|
|
[Inject]
|
|
private SettingsManager SettingsManager { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private MessageBus MessageBus { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private IDialogService DialogService { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private RustService RustService { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private UpdatePolicy UpdatePolicy { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private AIJobService AIJobService { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private AssistantSessionService AssistantSessionService { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private MediaTranscriptionService MediaTranscriptionService { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private ISnackbar Snackbar { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private NavigationManager NavigationManager { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private ILogger<MainLayout> Logger { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private MudTheme ColorTheme { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private DataSourceEmbeddingService DataSourceEmbeddingService { get; init; } = null!;
|
|
|
|
[Inject]
|
|
private CircuitStateService CircuitState { get; init; } = null!;
|
|
|
|
private ILanguagePlugin Lang { get; set; } = PluginFactory.BaseLanguage;
|
|
|
|
private string PaddingLeft => this.navBarOpen ? $"padding-left: {NAVBAR_EXPANDED_WIDTH_INT - NAVBAR_COLLAPSED_WIDTH_INT}em;" : "padding-left: 0em;";
|
|
|
|
private const int NAVBAR_COLLAPSED_WIDTH_INT = 4;
|
|
private const int NAVBAR_EXPANDED_WIDTH_INT = 10;
|
|
private static readonly string NAVBAR_COLLAPSED_WIDTH = $"{NAVBAR_COLLAPSED_WIDTH_INT}em";
|
|
private static readonly string NAVBAR_EXPANDED_WIDTH = $"{NAVBAR_EXPANDED_WIDTH_INT}em";
|
|
|
|
private bool navBarOpen;
|
|
private bool performingUpdate;
|
|
private UpdateResponse? currentUpdateResponse;
|
|
private MudThemeProvider themeProvider = null!;
|
|
private bool useDarkMode;
|
|
private bool startupCompleted;
|
|
private bool settingsWriteProtectionWarningShown;
|
|
private readonly SemaphoreSlim mandatoryInfoDialogSemaphore = new(1, 1);
|
|
private readonly SemaphoreSlim promptInjectionDialogSemaphore = new(1, 1);
|
|
|
|
private DataSourceEmbeddingOverview embeddingOverview = new(false, DataSourceEmbeddingState.COMPLETED, 0, 0, 0);
|
|
private IReadOnlyCollection<NavBarItem> navItems = [];
|
|
private NavBarItem embeddingItem = new (string.Empty, string.Empty, string.Empty, string.Empty, string.Empty, false);
|
|
private bool showEmbeddingStatusIcon = false;
|
|
|
|
#region Overrides of ComponentBase
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
this.NavigationManager.RegisterLocationChangingHandler(this.OnLocationChanging);
|
|
this.MediaTranscriptionService.StateChanged += this.OnMediaImportStateChanged;
|
|
|
|
//
|
|
// We use the Tauri API (Rust) to get the data and config directories
|
|
// for this app.
|
|
//
|
|
var dataDir = await this.RustService.GetDataDirectory();
|
|
var configDir = await this.RustService.GetConfigDirectory();
|
|
|
|
this.Logger.LogInformation($"The data directory is: '{dataDir}'");
|
|
this.Logger.LogInformation($"The config directory is: '{configDir}'");
|
|
|
|
// Store the directories in the settings manager:
|
|
SettingsManager.ConfigDirectory = configDir;
|
|
SettingsManager.DataDirectory = dataDir;
|
|
Directory.CreateDirectory(SettingsManager.DataDirectory);
|
|
|
|
//
|
|
// Read the user language from Rust:
|
|
//
|
|
var userLanguage = await this.RustService.ReadUserLanguage();
|
|
var userName = await this.RustService.ReadUserName();
|
|
this.Logger.LogInformation($"The OS says '{userLanguage}' is the user language.");
|
|
this.Logger.LogInformation($"The OS says '{userName}' is the username.");
|
|
|
|
// Ensure that all settings are loaded:
|
|
await this.SettingsManager.LoadSettings();
|
|
await this.DataSourceEmbeddingService.QueueAllInternalDataSourcesIfAutomaticRefreshAsync();
|
|
|
|
// Register this component with the message bus:
|
|
this.MessageBus.RegisterComponent(this, this.CircuitState);
|
|
this.MessageBus.ApplyFilters(this, [],
|
|
[
|
|
Event.UPDATE_AVAILABLE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED, Event.SHOW_ERROR,
|
|
Event.SHOW_WARNING, Event.SHOW_SUCCESS, Event.SHOW_INFO, Event.SHOW_PROMPT_INJECTION_ALERT, Event.STARTUP_PLUGIN_SYSTEM, Event.PLUGINS_RELOADED,
|
|
Event.INSTALL_UPDATE, Event.STARTUP_COMPLETED, Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED,
|
|
Event.CHAT_GENERATION_CHANGED, Event.RAG_EMBEDDING_STATUS_CHANGED,Event.ASSISTANT_SESSION_CHANGED,
|
|
Event.ASSISTANT_SESSION_FINISHED,
|
|
]);
|
|
|
|
// Set the snackbar for the update service:
|
|
UpdateService.MarkBlazorReady();
|
|
TemporaryChatService.Initialize();
|
|
|
|
// Should the navigation bar be open by default?
|
|
if(this.SettingsManager.ConfigurationData.App.NavigationBehavior is NavBehavior.ALWAYS_EXPAND)
|
|
this.navBarOpen = true;
|
|
|
|
// Solve issue https://github.com/MudBlazor/MudBlazor/issues/11133:
|
|
MudGlobal.TooltipDefaults.Duration = TimeSpan.Zero;
|
|
|
|
// Send a message to start the plugin system:
|
|
await this.MessageBus.SendMessage<bool>(this, Event.STARTUP_PLUGIN_SYSTEM);
|
|
|
|
await this.themeProvider.WatchSystemDarkModeAsync(this.SystemeThemeChanged);
|
|
await this.UpdateThemeConfiguration();
|
|
this.LoadNavItems();
|
|
this.LoadEmbeddingItem();
|
|
|
|
await base.OnInitializedAsync();
|
|
}
|
|
|
|
#endregion
|
|
|
|
private void ShowSettingsWriteProtectionWarning()
|
|
{
|
|
if(!this.SettingsManager.SettingsWriteBlocked || this.settingsWriteProtectionWarningShown)
|
|
return;
|
|
|
|
this.settingsWriteProtectionWarningShown = true;
|
|
var reason = this.SettingsManager.SettingsWriteBlockReason;
|
|
var message = reason switch
|
|
{
|
|
SettingsWriteBlockReason.VERSION_NEWER_THAN_APP => T("Your settings were created by a newer AI Studio version. Changes in this session will not be saved. Please install or start the latest available update."),
|
|
SettingsWriteBlockReason.VERSION_MISSING => T("Your settings file does not contain a settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support."),
|
|
SettingsWriteBlockReason.VERSION_UNKNOWN => T("AI Studio does not recognize your settings-format version. Changes in this session will not be saved to avoid overwriting your settings. Please check for updates or contact support."),
|
|
SettingsWriteBlockReason.FILE_UNREADABLE => T("AI Studio could not read your settings file. Changes in this session will not be saved to avoid overwriting recoverable settings. Please check for updates or contact support."),
|
|
SettingsWriteBlockReason.CURRENT_VERSION_INVALID => T("AI Studio found the current settings format but could not load it safely. Changes in this session will not be saved. Please check for updates or contact support."),
|
|
_ => T("AI Studio cannot safely save settings in this session. Please check for updates or contact support."),
|
|
};
|
|
message = $"{message} {T("Reason")}: {reason}";
|
|
|
|
this.Snackbar.Add(message, Severity.Warning, config =>
|
|
{
|
|
config.Icon = Icons.Material.Filled.WarningAmber;
|
|
config.IconSize = Size.Large;
|
|
config.VisibleStateDuration = 32_000;
|
|
config.HideTransitionDuration = 600;
|
|
config.Action = T("Check for updates");
|
|
config.ActionVariant = Variant.Filled;
|
|
config.OnClick = async _ =>
|
|
{
|
|
await this.MessageBus.SendMessage<bool>(this, Event.USER_SEARCH_FOR_UPDATE);
|
|
};
|
|
});
|
|
}
|
|
|
|
#region Implementation of ILang
|
|
|
|
/// <inheritdoc />
|
|
public string T(string fallbackEN) => this.GetText(this.Lang, fallbackEN);
|
|
|
|
/// <inheritdoc />
|
|
public string T(string fallbackEN, string? typeNamespace, string? typeName) => this.GetText(this.Lang, fallbackEN, typeNamespace, typeName);
|
|
|
|
#endregion
|
|
|
|
#region Implementation of IMessageBusReceiver
|
|
|
|
public string ComponentName => nameof(MainLayout);
|
|
|
|
public async Task ProcessMessage<TMessage>(ComponentBase? sendingComponent, Event triggeredEvent, TMessage? data)
|
|
{
|
|
await this.InvokeAsync(async () =>
|
|
{
|
|
switch (triggeredEvent)
|
|
{
|
|
case Event.INSTALL_UPDATE:
|
|
if (!this.UpdatePolicy.AllowsInstallations)
|
|
break;
|
|
this.performingUpdate = true;
|
|
this.StateHasChanged();
|
|
break;
|
|
|
|
case Event.UPDATE_AVAILABLE:
|
|
if (data is UpdateResponse updateResponse)
|
|
{
|
|
this.currentUpdateResponse = updateResponse;
|
|
var message = string.Format(T("An update to version {0} is available."), updateResponse.NewVersion);
|
|
this.Snackbar.Add(message, Severity.Info, config =>
|
|
{
|
|
config.Icon = Icons.Material.Filled.Update;
|
|
config.IconSize = Size.Large;
|
|
config.HideTransitionDuration = 600;
|
|
config.VisibleStateDuration = 32_000;
|
|
config.OnClick = async _ =>
|
|
{
|
|
await this.ShowUpdateDialog();
|
|
};
|
|
config.Action = T("Show details");
|
|
config.ActionVariant = Variant.Filled;
|
|
});
|
|
}
|
|
|
|
break;
|
|
|
|
case Event.CONFIGURATION_CHANGED:
|
|
if (this.SettingsManager.ConfigurationData.App.NavigationBehavior is NavBehavior.ALWAYS_EXPAND)
|
|
this.navBarOpen = true;
|
|
else
|
|
this.navBarOpen = false;
|
|
|
|
await this.UpdateThemeConfiguration();
|
|
this.LoadNavItems();
|
|
this.LoadEmbeddingItem();
|
|
this.StateHasChanged();
|
|
if (this.startupCompleted)
|
|
this.EnsureMandatoryInfosAcceptedAsync().Observe($"{nameof(MainLayout)}: mandatory infos after a configuration change");
|
|
break;
|
|
|
|
case Event.COLOR_THEME_CHANGED:
|
|
this.StateHasChanged();
|
|
break;
|
|
|
|
case Event.AI_JOB_CHANGED:
|
|
case Event.AI_JOB_FINISHED:
|
|
case Event.CHAT_GENERATION_CHANGED:
|
|
case Event.ASSISTANT_SESSION_CHANGED:
|
|
case Event.ASSISTANT_SESSION_FINISHED:
|
|
this.LoadNavItems();
|
|
this.StateHasChanged();
|
|
break;
|
|
|
|
case Event.SHOW_SUCCESS:
|
|
if (data is DataSuccessMessage success)
|
|
success.Show(this.Snackbar);
|
|
|
|
break;
|
|
|
|
case Event.SHOW_PROMPT_INJECTION_ALERT:
|
|
if (data is PromptInjectionAlertMessage promptInjectionAlert)
|
|
await this.ShowPromptInjectionAlertAsync(promptInjectionAlert);
|
|
|
|
break;
|
|
|
|
case Event.SHOW_ERROR:
|
|
if (data is DataErrorMessage error)
|
|
error.Show(this.Snackbar);
|
|
|
|
break;
|
|
|
|
case Event.SHOW_WARNING:
|
|
if (data is DataWarningMessage warning)
|
|
warning.Show(this.Snackbar);
|
|
|
|
break;
|
|
|
|
case Event.SHOW_INFO:
|
|
if (data is DataInfoMessage info)
|
|
info.Show(this.Snackbar);
|
|
|
|
break;
|
|
|
|
case Event.STARTUP_PLUGIN_SYSTEM:
|
|
Task.Run(async () =>
|
|
{
|
|
// Set up the plugin system:
|
|
if (PluginFactory.Setup())
|
|
{
|
|
// Ensure that all internal plugins are present:
|
|
await PluginFactory.EnsureInternalPlugins();
|
|
|
|
//
|
|
// Check if there is an enterprise configuration plugin to download:
|
|
//
|
|
// Every deferred environment matters here: each one is a configuration
|
|
// to download, so this is the one place which uses all of them.
|
|
var enterpriseEnvironments = this.MessageBus
|
|
.TakeDeferredMessages<EnterpriseEnvironment>(Event.STARTUP_ENTERPRISE_ENVIRONMENT)
|
|
.Where(env => env != default)
|
|
.ToList();
|
|
|
|
var failedDeferredConfigIds = new HashSet<Guid>();
|
|
foreach (var env in enterpriseEnvironments)
|
|
{
|
|
var wasDownloadSuccessful = await PluginFactory.TryDownloadingConfigPluginAsync(env.ConfigurationId, env.ConfigurationServerUrl);
|
|
if (!wasDownloadSuccessful)
|
|
{
|
|
failedDeferredConfigIds.Add(env.ConfigurationId);
|
|
this.Logger.LogWarning("Failed to download deferred enterprise configuration '{ConfigId}' during startup. Keeping managed plugins unchanged.", env.ConfigurationId);
|
|
}
|
|
}
|
|
|
|
if (EnterpriseEnvironmentService.HasValidEnterpriseSnapshot)
|
|
{
|
|
var activeConfigIds = EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS
|
|
.Select(env => env.ConfigurationId)
|
|
.ToHashSet();
|
|
|
|
PluginFactory.RemoveUnreferencedManagedConfigurationPlugins(activeConfigIds);
|
|
if (failedDeferredConfigIds.Count > 0)
|
|
this.Logger.LogWarning("Deferred startup updates failed for {FailedCount} enterprise configuration(s). Those configurations were kept unchanged.", failedDeferredConfigIds.Count);
|
|
}
|
|
|
|
// Initialize the enterprise encryption service for decrypting API keys:
|
|
await PluginFactory.InitializeEnterpriseEncryption(this.RustService);
|
|
|
|
// Load (but not start) all plugins without waiting for them:
|
|
#if DEBUG
|
|
var pluginLoadingTimeout = new CancellationTokenSource();
|
|
#else
|
|
var pluginLoadingTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
|
#endif
|
|
await PluginFactory.LoadAll(pluginLoadingTimeout.Token);
|
|
|
|
// Set up hot reloading for plugins:
|
|
PluginFactory.SetUpHotReloading();
|
|
await this.MessageBus.SendMessage<bool>(this, Event.STARTUP_COMPLETED);
|
|
}
|
|
}).Observe($"{nameof(MainLayout)}: setting up the plugin system");
|
|
break;
|
|
|
|
case Event.PLUGINS_RELOADED:
|
|
this.Lang = await this.SettingsManager.GetActiveLanguagePlugin();
|
|
I18N.Init(this.Lang);
|
|
this.ShowSettingsWriteProtectionWarning();
|
|
this.LoadNavItems();
|
|
this.LoadEmbeddingItem();
|
|
|
|
await this.InvokeAsync(this.StateHasChanged);
|
|
if (this.startupCompleted)
|
|
this.EnsureMandatoryInfosAcceptedAsync().Observe($"{nameof(MainLayout)}: mandatory infos after a plugin reload");
|
|
break;
|
|
|
|
case Event.STARTUP_COMPLETED:
|
|
this.startupCompleted = true;
|
|
this.EnsureMandatoryInfosAcceptedAsync().Observe($"{nameof(MainLayout)}: mandatory infos after the startup");
|
|
break;
|
|
|
|
case Event.RAG_EMBEDDING_STATUS_CHANGED:
|
|
this.LoadNavItems();
|
|
this.LoadEmbeddingItem();
|
|
this.StateHasChanged();
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
private async Task ShowPromptInjectionAlertAsync(PromptInjectionAlertMessage alert)
|
|
{
|
|
await this.promptInjectionDialogSemaphore.WaitAsync();
|
|
try
|
|
{
|
|
if (!this.SettingsManager.ConfigurationData.App.ShowPromptInjectionAlert)
|
|
return;
|
|
|
|
var dialogParameters = new DialogParameters<PromptInjectionAlertDialog>
|
|
{
|
|
{ x => x.Alert, alert },
|
|
};
|
|
|
|
var dialogReference = await this.DialogService.ShowAsync<PromptInjectionAlertDialog>(
|
|
T("Security notice"),
|
|
dialogParameters,
|
|
DialogOptions.FULLSCREEN);
|
|
|
|
await dialogReference.Result;
|
|
}
|
|
finally
|
|
{
|
|
this.promptInjectionDialogSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
|
|
{
|
|
return Task.FromResult<TResult?>(default);
|
|
}
|
|
|
|
#endregion
|
|
|
|
private void LoadNavItems()
|
|
{
|
|
this.navItems = new List<NavBarItem>(this.GetNavItems());
|
|
}
|
|
|
|
/// <summary>Refreshes navigation activity colors when a media import changes state.</summary>
|
|
private void OnMediaImportStateChanged(MediaImportOwner owner)
|
|
{
|
|
this.InvokeAsync(() =>
|
|
{
|
|
this.LoadNavItems();
|
|
this.StateHasChanged();
|
|
}).Observe($"{nameof(MainLayout)}: refreshing the navigation after a media import change");
|
|
}
|
|
|
|
private IEnumerable<NavBarItem> GetNavItems()
|
|
{
|
|
var palette = this.ColorTheme.GetCurrentPalette(this.SettingsManager);
|
|
var activityIndicatorLightColor = this.ColorTheme.GetActivityIndicatorLightColor();
|
|
var activityIndicatorDarkColor = this.ColorTheme.GetActivityIndicatorDarkColor();
|
|
var defaultLightColor = palette.DarkLighten;
|
|
var defaultDarkColor = palette.GrayLight;
|
|
var mediaSnapshots = this.MediaTranscriptionService.GetSnapshots();
|
|
var hasActiveChatMedia = mediaSnapshots.Any(snapshot => snapshot is { IsBusy: true, Owner.Kind: MediaImportOwnerKind.CHAT });
|
|
var hasActiveAssistantMedia = mediaSnapshots.Any(snapshot => snapshot is { IsBusy: true, Owner.Kind: MediaImportOwnerKind.ASSISTANT or MediaImportOwnerKind.VISUAL_BRIEFING });
|
|
var hasActiveChatWork = this.AIJobService.HasActiveJobs || hasActiveChatMedia;
|
|
var hasActiveAssistantWork = this.AssistantSessionService.HasActiveSessions || hasActiveAssistantMedia;
|
|
var chatLightColor = hasActiveChatWork ? activityIndicatorLightColor : defaultLightColor;
|
|
var chatDarkColor = hasActiveChatWork ? activityIndicatorDarkColor : defaultDarkColor;
|
|
var assistantsLightColor = hasActiveAssistantWork ? activityIndicatorLightColor : defaultLightColor;
|
|
var assistantsDarkColor = hasActiveAssistantWork ? activityIndicatorDarkColor : defaultDarkColor;
|
|
|
|
yield return new(T("Home"), Icons.Material.Filled.Home, defaultLightColor, defaultDarkColor, Routes.HOME, true);
|
|
yield return new(T("Chat"), Icons.Material.Filled.Chat, chatLightColor, chatDarkColor, Routes.CHAT, false);
|
|
yield return new(T("Assistants"), Icons.Material.Filled.Apps, assistantsLightColor, assistantsDarkColor, Routes.ASSISTANTS, false);
|
|
|
|
if (PreviewFeatures.PRE_WRITER_MODE_2024.IsEnabled(this.SettingsManager))
|
|
yield return new(T("Writer"), Icons.Material.Filled.Create, defaultLightColor, defaultDarkColor, Routes.WRITER, false);
|
|
|
|
yield return new(T("Plugins"), Icons.Material.TwoTone.Extension, defaultLightColor, defaultDarkColor, Routes.PLUGINS, false);
|
|
yield return new(T("Supporters"), Icons.Material.Filled.Favorite, palette.Error.Value, "#801a00", Routes.SUPPORTERS, false);
|
|
yield return new(T("Information"), Icons.Material.Filled.Info, defaultLightColor, defaultDarkColor, Routes.ABOUT, false);
|
|
yield return new(T("Settings"), Icons.Material.Filled.Settings, defaultLightColor, defaultDarkColor, Routes.SETTINGS, false);
|
|
}
|
|
|
|
private void LoadEmbeddingItem()
|
|
{
|
|
this.embeddingOverview = this.DataSourceEmbeddingService.GetOverview();
|
|
this.showEmbeddingStatusIcon = this.embeddingOverview.IsVisible;
|
|
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"),
|
|
_ => (Icons.Material.Filled.Sync, palette.Warning.Value, "#d29f00"),
|
|
};
|
|
this.embeddingItem = new NavBarItem(T("Embeddings"), embeddingIcon.icon, embeddingIcon.lightcolor, embeddingIcon.darkcolor, Routes.EMBEDDINGS, false);
|
|
}
|
|
|
|
private string EmbeddingNavigationTooltip => this.embeddingOverview.State switch
|
|
{
|
|
DataSourceEmbeddingState.QUEUED => T("Embeddings are waiting to be processed."),
|
|
DataSourceEmbeddingState.RUNNING => string.Format(
|
|
T("Embeddings are running: {0} of {1} files are indexed."),
|
|
this.embeddingOverview.IndexedFiles,
|
|
this.embeddingOverview.TotalFiles),
|
|
DataSourceEmbeddingState.FAILED => this.embeddingOverview.FailedFiles > 0
|
|
? string.Format(T("Some embeddings failed. {0} file(s) need attention."), this.embeddingOverview.FailedFiles)
|
|
: T("Some embeddings failed and need attention."),
|
|
_ => string.Empty
|
|
};
|
|
|
|
private async Task ShowUpdateDialog()
|
|
{
|
|
if (!this.UpdatePolicy.AllowsInstallations)
|
|
return;
|
|
|
|
if(this.currentUpdateResponse is null)
|
|
return;
|
|
|
|
//
|
|
// Replace the fir line with `# Changelog`:
|
|
//
|
|
var changelog = this.currentUpdateResponse.Value.Changelog;
|
|
if (!string.IsNullOrWhiteSpace(changelog))
|
|
{
|
|
var lines = changelog.Split('\n');
|
|
if (lines.Length > 0)
|
|
lines[0] = "# Changelog";
|
|
|
|
changelog = string.Join('\n', lines);
|
|
}
|
|
|
|
var updatedResponse = this.currentUpdateResponse.Value with { Changelog = changelog };
|
|
var dialogParameters = new DialogParameters<UpdateDialog>
|
|
{
|
|
{ x => x.UpdateResponse, updatedResponse }
|
|
};
|
|
|
|
var dialogReference = await this.DialogService.ShowAsync<UpdateDialog>(T("Update"), dialogParameters, DialogOptions.FULLSCREEN_NO_HEADER);
|
|
var dialogResult = await dialogReference.Result;
|
|
if (dialogResult is null || dialogResult.Canceled)
|
|
return;
|
|
|
|
if (!this.UpdatePolicy.AllowsInstallations)
|
|
return;
|
|
|
|
this.performingUpdate = true;
|
|
this.StateHasChanged();
|
|
await this.RustService.InstallUpdate();
|
|
}
|
|
|
|
private async ValueTask OnLocationChanging(LocationChangingContext context)
|
|
{
|
|
if (await MessageBus.INSTANCE.SendMessageUseFirstResult<bool, bool>(this, Event.HAS_CHAT_UNSAVED_CHANGES))
|
|
{
|
|
var dialogParameters = new DialogParameters<ConfirmDialog>
|
|
{
|
|
{ x => x.Message, T("Are you sure you want to leave the chat page? All unsaved changes will be lost.") },
|
|
};
|
|
|
|
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Leave Chat Page"), dialogParameters, DialogOptions.FULLSCREEN);
|
|
var dialogResult = await dialogReference.Result;
|
|
if (dialogResult is null || dialogResult.Canceled)
|
|
{
|
|
context.PreventNavigation();
|
|
return;
|
|
}
|
|
|
|
// User accepted to leave the chat page, reset the chat state:
|
|
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.RESET_CHAT_STATE);
|
|
}
|
|
}
|
|
|
|
private async Task SystemeThemeChanged(bool isDark)
|
|
{
|
|
this.Logger.LogInformation($"The system theme changed to {(isDark ? "dark" : "light")}.");
|
|
await this.UpdateThemeConfiguration();
|
|
}
|
|
|
|
private async Task UpdateThemeConfiguration()
|
|
{
|
|
if (this.SettingsManager.ConfigurationData.App.PreferredTheme is Themes.SYSTEM)
|
|
this.useDarkMode = await this.themeProvider.GetSystemDarkModeAsync();
|
|
else
|
|
this.useDarkMode = this.SettingsManager.ConfigurationData.App.PreferredTheme == Themes.DARK;
|
|
|
|
this.SettingsManager.IsDarkMode = this.useDarkMode;
|
|
await this.MessageBus.SendMessage<bool>(this, Event.COLOR_THEME_CHANGED);
|
|
this.StateHasChanged();
|
|
}
|
|
|
|
private async Task EnsureMandatoryInfosAcceptedAsync()
|
|
{
|
|
if (!await this.mandatoryInfoDialogSemaphore.WaitAsync(0))
|
|
return;
|
|
|
|
try
|
|
{
|
|
while (true)
|
|
{
|
|
var pendingInfos = this.GetPendingMandatoryInfos().ToList();
|
|
if (pendingInfos.Count == 0)
|
|
return;
|
|
|
|
foreach (var info in pendingInfos)
|
|
{
|
|
var wasAccepted = await this.ShowMandatoryInfoDialog(info);
|
|
if (!wasAccepted)
|
|
{
|
|
await this.RustService.ExitApplication();
|
|
return;
|
|
}
|
|
|
|
await this.StoreMandatoryInfoAcceptance(info);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
this.mandatoryInfoDialogSemaphore.Release();
|
|
}
|
|
}
|
|
|
|
private IEnumerable<DataMandatoryInfo> GetPendingMandatoryInfos()
|
|
{
|
|
return PluginFactory.GetMandatoryInfos()
|
|
.Where(info =>
|
|
{
|
|
var acceptance = this.SettingsManager.ConfigurationData.MandatoryInformation.FindAcceptance(info.Id);
|
|
return acceptance is null || !string.Equals(acceptance.AcceptedHash, info.AcceptanceHash, StringComparison.Ordinal);
|
|
});
|
|
}
|
|
|
|
private async Task<bool> ShowMandatoryInfoDialog(DataMandatoryInfo info)
|
|
{
|
|
var acceptance = this.SettingsManager.ConfigurationData.MandatoryInformation.FindAcceptance(info.Id);
|
|
var dialogParameters = new DialogParameters<MandatoryInfoDialog>
|
|
{
|
|
{ x => x.Info, info },
|
|
{ x => x.Acceptance, acceptance },
|
|
};
|
|
|
|
var dialogReference = await this.DialogService.ShowAsync<MandatoryInfoDialog>(info.Title, dialogParameters, DialogOptions.BLOCKING_FULLSCREEN);
|
|
var dialogResult = await dialogReference.Result;
|
|
return dialogResult is { Canceled: false, Data: true };
|
|
}
|
|
|
|
private async Task StoreMandatoryInfoAcceptance(DataMandatoryInfo info)
|
|
{
|
|
var acceptances = this.SettingsManager.ConfigurationData.MandatoryInformation.Acceptances;
|
|
var acceptance = new DataMandatoryInfoAcceptance
|
|
{
|
|
InfoId = info.Id,
|
|
AcceptedVersion = info.VersionText,
|
|
AcceptedHash = info.AcceptanceHash,
|
|
AcceptedAtUtc = DateTimeOffset.UtcNow,
|
|
EnterpriseConfigurationPluginId = info.EnterpriseConfigurationPluginId,
|
|
};
|
|
|
|
var existingIndex = acceptances.FindIndex(item => item.InfoId == info.Id);
|
|
if (existingIndex >= 0)
|
|
acceptances[existingIndex] = acceptance;
|
|
else
|
|
acceptances.Add(acceptance);
|
|
|
|
await this.SettingsManager.StoreSettings();
|
|
}
|
|
|
|
#region Implementation of IDisposable
|
|
|
|
public void Dispose()
|
|
{
|
|
this.MediaTranscriptionService.StateChanged -= this.OnMediaImportStateChanged;
|
|
this.MessageBus.Unregister(this);
|
|
this.mandatoryInfoDialogSemaphore.Dispose();
|
|
}
|
|
|
|
#endregion
|
|
} |