2024-08-21 08:30:01 +02:00
using AIStudio.Dialogs ;
2024-04-19 21:25:44 +02:00
using AIStudio.Settings ;
2024-07-28 11:20:00 +02:00
using AIStudio.Settings.DataModel ;
2026-05-24 13:50:42 +02:00
using AIStudio.Tools.AIJobs ;
2026-07-03 15:02:20 +02:00
using AIStudio.Tools.AssistantSessions ;
2026-07-15 12:53:30 +02:00
using AIStudio.Tools.Media ;
2025-03-22 21:12:14 +01:00
using AIStudio.Tools.PluginSystem ;
2026-08-23 11:09:11 +02:00
using AIStudio.Tools.Security ;
2024-09-01 20:10:03 +02:00
using AIStudio.Tools.Rust ;
2024-08-21 08:30:01 +02:00
using AIStudio.Tools.Services ;
2024-06-30 15:26:28 +02:00
2024-04-05 22:23:36 +02:00
using Microsoft.AspNetCore.Components ;
2024-07-13 10:37:57 +02:00
using Microsoft.AspNetCore.Components.Routing ;
2024-04-05 16:16:33 +02:00
2024-08-21 08:30:01 +02:00
using DialogOptions = AIStudio . Dialogs . DialogOptions ;
2025-06-02 20:08:25 +02:00
using EnterpriseEnvironment = AIStudio . Tools . EnterpriseEnvironment ;
2024-06-30 15:26:28 +02:00
2024-08-21 08:30:01 +02:00
namespace AIStudio.Layout ;
2024-04-05 22:23:36 +02:00
2025-04-24 13:50:14 +02:00
public partial class MainLayout : LayoutComponentBase , IMessageBusReceiver , ILang , IDisposable
2024-04-05 16:16:33 +02:00
{
2024-06-30 15:26:28 +02:00
[Inject]
private SettingsManager SettingsManager { get ; init ; } = null !;
[Inject]
private MessageBus MessageBus { get ; init ; } = null !;
[Inject]
2024-06-30 19:11:34 +02:00
private IDialogService DialogService { get ; init ; } = null !;
2024-06-30 15:26:28 +02:00
[Inject]
2024-09-01 20:10:03 +02:00
private RustService RustService { get ; init ; } = null !;
2026-05-24 13:50:42 +02:00
2026-07-13 13:35:24 +02:00
[Inject]
private UpdatePolicy UpdatePolicy { get ; init ; } = null !;
2026-05-24 13:50:42 +02:00
[Inject]
private AIJobService AIJobService { get ; init ; } = null !;
2026-07-03 15:02:20 +02:00
[Inject]
private AssistantSessionService AssistantSessionService { get ; init ; } = null !;
2026-07-15 12:53:30 +02:00
[Inject]
private MediaTranscriptionService MediaTranscriptionService { get ; init ; } = null !;
2024-06-30 19:11:34 +02:00
[Inject]
private ISnackbar Snackbar { get ; init ; } = null !;
2024-07-13 10:37:57 +02:00
[Inject]
private NavigationManager NavigationManager { get ; init ; } = null !;
2024-09-01 20:10:03 +02:00
[Inject]
private ILogger < MainLayout > Logger { get ; init ; } = null !;
2024-09-15 12:30:07 +02:00
[Inject]
private MudTheme ColorTheme { get ; init ; } = null !;
2026-08-25 18:53:09 +02:00
2026-09-09 18:43:37 +02:00
[Inject]
private DataSourceEmbeddingService DataSourceEmbeddingService { get ; init ; } = null !;
2026-08-25 18:53:09 +02:00
[Inject]
private CircuitStateService CircuitState { get ; init ; } = null !;
2024-06-30 15:26:28 +02:00
2025-04-24 13:50:14 +02:00
private ILanguagePlugin Lang { get ; set ; } = PluginFactory . BaseLanguage ;
2024-07-24 15:17:45 +02:00
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 ;
2024-06-30 15:26:28 +02:00
private bool performingUpdate ;
private UpdateResponse ? currentUpdateResponse ;
2024-09-15 12:30:07 +02:00
private MudThemeProvider themeProvider = null !;
private bool useDarkMode ;
2026-04-10 17:11:05 +02:00
private bool startupCompleted ;
2026-06-21 18:46:21 +02:00
private bool settingsWriteProtectionWarningShown ;
2026-04-10 17:11:05 +02:00
private readonly SemaphoreSlim mandatoryInfoDialogSemaphore = new ( 1 , 1 );
2026-08-23 11:09:11 +02:00
private readonly SemaphoreSlim promptInjectionDialogSemaphore = new ( 1 , 1 );
2024-09-15 12:30:07 +02:00
2026-09-09 18:43:37 +02:00
private DataSourceEmbeddingOverview embeddingOverview = new ( DataSourceEmbeddingState . COMPLETED , 0 , 0 , 0 );
2024-09-15 12:30:07 +02:00
private IReadOnlyCollection < NavBarItem > navItems = [];
2026-09-09 18:43:37 +02:00
private NavBarItem embeddingItem = new ( string . Empty , string . Empty , string . Empty , string . Empty , string . Empty , false );
private bool showEmbeddingStatusIcon ;
2024-07-24 15:17:45 +02:00
2024-04-05 16:16:33 +02:00
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync ()
{
2024-07-13 10:37:57 +02:00
this . NavigationManager . RegisterLocationChangingHandler ( this . OnLocationChanging );
2026-07-15 12:53:30 +02:00
this . MediaTranscriptionService . StateChanged += this . OnMediaImportStateChanged ;
2024-07-13 10:37:57 +02:00
2024-05-04 10:55:00 +02:00
//
// We use the Tauri API (Rust) to get the data and config directories
// for this app.
//
2024-09-01 20:10:03 +02:00
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}'" );
2024-05-04 10:55:00 +02:00
// Store the directories in the settings manager:
2024-04-05 22:23:36 +02:00
SettingsManager . ConfigDirectory = configDir ;
2024-09-01 20:10:03 +02:00
SettingsManager . DataDirectory = dataDir ;
2024-07-13 10:37:57 +02:00
Directory . CreateDirectory ( SettingsManager . DataDirectory );
2024-04-05 22:23:36 +02:00
2025-04-03 14:25:45 +02:00
//
// Read the user language from Rust:
//
var userLanguage = await this . RustService . ReadUserLanguage ();
2026-05-18 16:26:51 +02:00
var userName = await this . RustService . ReadUserName ();
2025-04-24 13:50:14 +02:00
this . Logger . LogInformation ( $"The OS says '{userLanguage}' is the user language." );
2026-05-18 16:26:51 +02:00
this . Logger . LogInformation ( $"The OS says '{userName}' is the username." );
2025-04-03 14:25:45 +02:00
2024-06-30 15:26:28 +02:00
// Ensure that all settings are loaded:
await this . SettingsManager . LoadSettings ();
2026-09-09 18:43:37 +02:00
await this . DataSourceEmbeddingService . QueueAllInternalDataSourcesIfAutomaticRefreshAsync ();
2024-06-30 15:26:28 +02:00
// Register this component with the message bus:
2026-08-25 18:53:09 +02:00
this . MessageBus . RegisterComponent ( this , this . CircuitState );
2025-04-12 21:13:33 +02:00
this . MessageBus . ApplyFilters ( this , [],
[
Event.UPDATE_AVAILABLE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED, Event.SHOW_ERROR,
2026-08-23 11:09:11 +02:00
Event.SHOW_WARNING, Event.SHOW_SUCCESS, Event.SHOW_INFO, Event.SHOW_PROMPT_INJECTION_ALERT, Event.STARTUP_PLUGIN_SYSTEM, Event.PLUGINS_RELOADED,
2026-05-24 13:50:42 +02:00
Event.INSTALL_UPDATE, Event.STARTUP_COMPLETED, Event.AI_JOB_CHANGED, Event.AI_JOB_FINISHED,
2026-09-09 18:43:37 +02:00
Event.CHAT_GENERATION_CHANGED, Event.RAG_EMBEDDING_STATUS_CHANGED,Event.ASSISTANT_SESSION_CHANGED,
Event.ASSISTANT_SESSION_FINISHED,
2025-04-12 21:13:33 +02:00
] );
2024-06-30 15:26:28 +02:00
2024-09-01 20:10:03 +02:00
// Set the snackbar for the update service:
2026-08-02 21:41:12 +02:00
UpdateService . MarkBlazorReady ();
2024-07-13 10:37:57 +02:00
TemporaryChatService . Initialize ();
2024-06-30 15:26:28 +02:00
2024-07-24 15:17:45 +02:00
// Should the navigation bar be open by default?
2024-08-05 21:12:52 +02:00
if ( this . SettingsManager . ConfigurationData . App . NavigationBehavior is NavBehavior . ALWAYS_EXPAND )
2024-07-24 15:17:45 +02:00
this . navBarOpen = true ;
2024-09-15 12:30:07 +02:00
2025-04-04 17:44:53 +02:00
// Solve issue https://github.com/MudBlazor/MudBlazor/issues/11133:
MudGlobal . TooltipDefaults . Duration = TimeSpan . Zero ;
2025-04-12 21:13:33 +02:00
// Send a message to start the plugin system:
await this . MessageBus . SendMessage < bool >( this , Event . STARTUP_PLUGIN_SYSTEM );
2025-06-27 20:03:28 +02:00
await this . themeProvider . WatchSystemDarkModeAsync ( this . SystemeThemeChanged );
2024-09-15 12:30:07 +02:00
await this . UpdateThemeConfiguration ();
2024-12-03 15:52:45 +01:00
this . LoadNavItems ();
2026-09-09 18:43:37 +02:00
this . LoadEmbeddingItem ();
2024-12-03 15:52:45 +01:00
await base . OnInitializedAsync ();
}
2024-04-05 16:16:33 +02:00
#endregion
2024-06-30 15:26:28 +02:00
2026-06-21 18:46:21 +02:00
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 );
};
});
}
2025-04-24 13:50:14 +02:00
#region Implementation of ILang
2025-04-27 16:13:15 +02:00
/// <inheritdoc />
2025-04-24 13:50:14 +02:00
public string T ( string fallbackEN ) => this . GetText ( this . Lang , fallbackEN );
2025-04-27 16:13:15 +02:00
/// <inheritdoc />
public string T ( string fallbackEN , string? typeNamespace , string? typeName ) => this . GetText ( this . Lang , fallbackEN , typeNamespace , typeName );
2025-04-24 13:50:14 +02:00
#endregion
2024-06-30 15:26:28 +02:00
#region Implementation of IMessageBusReceiver
2025-01-21 15:36:22 +01:00
public string ComponentName => nameof ( MainLayout );
2025-04-24 13:50:14 +02:00
public async Task ProcessMessage < TMessage >( ComponentBase ? sendingComponent , Event triggeredEvent , TMessage ? data )
2024-06-30 15:26:28 +02:00
{
2025-06-02 20:08:25 +02:00
await this . InvokeAsync ( async () =>
2024-06-30 15:26:28 +02:00
{
2025-06-02 20:08:25 +02:00
switch ( triggeredEvent )
{
2025-08-26 20:06:13 +02:00
case Event . INSTALL_UPDATE :
2026-07-13 13:35:24 +02:00
if (! this . UpdatePolicy . AllowsInstallations )
break ;
2025-08-26 20:06:13 +02:00
this . performingUpdate = true ;
this . StateHasChanged ();
break ;
2025-06-02 20:08:25 +02:00
case Event . UPDATE_AVAILABLE :
if ( data is UpdateResponse updateResponse )
2025-04-04 23:06:42 +02:00
{
2025-06-02 20:08:25 +02:00
this . currentUpdateResponse = updateResponse ;
var message = string . Format ( T ( "An update to version {0} is available." ), updateResponse . NewVersion );
this . Snackbar . Add ( message , Severity . Info , config =>
2025-04-04 23:06:42 +02:00
{
2025-06-02 20:08:25 +02:00
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 ();
2026-09-09 18:43:37 +02:00
this . LoadEmbeddingItem ();
2025-06-02 20:08:25 +02:00
this . StateHasChanged ();
2026-04-10 17:11:05 +02:00
if ( this . startupCompleted )
2026-08-25 18:53:09 +02:00
this . EnsureMandatoryInfosAcceptedAsync (). Observe ( $"{nameof(MainLayout)}: mandatory infos after a configuration change" );
2025-06-02 20:08:25 +02:00
break ;
case Event . COLOR_THEME_CHANGED :
this . StateHasChanged ();
break ;
2026-05-24 13:50:42 +02:00
case Event . AI_JOB_CHANGED :
case Event . AI_JOB_FINISHED :
case Event . CHAT_GENERATION_CHANGED :
2026-07-03 15:02:20 +02:00
case Event . ASSISTANT_SESSION_CHANGED :
case Event . ASSISTANT_SESSION_FINISHED :
2026-05-24 13:50:42 +02:00
this . LoadNavItems ();
this . StateHasChanged ();
break ;
2025-06-02 20:08:25 +02:00
case Event . SHOW_SUCCESS :
if ( data is DataSuccessMessage success )
success . Show ( this . Snackbar );
break ;
2026-08-23 11:09:11 +02:00
case Event . SHOW_PROMPT_INJECTION_ALERT :
if ( data is PromptInjectionAlertMessage promptInjectionAlert )
await this . ShowPromptInjectionAlertAsync ( promptInjectionAlert );
break ;
2025-06-02 20:08:25 +02:00
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 ;
2026-08-02 21:41:12 +02:00
case Event . SHOW_INFO :
if ( data is DataInfoMessage info )
info . Show ( this . Snackbar );
break ;
2025-06-02 20:08:25 +02:00
case Event . STARTUP_PLUGIN_SYSTEM :
2026-08-25 18:53:09 +02:00
Task . Run ( async () =>
2025-04-12 21:13:33 +02:00
{
2025-06-02 20:08:25 +02:00
// 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:
//
2026-08-23 21:15:37 +02:00
// Every deferred environment matters here: each one is a configuration
// to download, so this is the one place which uses all of them.
2026-02-15 18:11:57 +01:00
var enterpriseEnvironments = this . MessageBus
2026-08-23 21:15:37 +02:00
. TakeDeferredMessages < EnterpriseEnvironment >( Event . STARTUP_ENTERPRISE_ENVIRONMENT )
2026-02-15 18:11:57 +01:00
. Where ( env => env != default )
. ToList ();
2026-02-19 20:43:47 +01:00
var failedDeferredConfigIds = new HashSet < Guid >();
2026-02-15 18:11:57 +01:00
foreach ( var env in enterpriseEnvironments )
2026-02-19 20:43:47 +01:00
{
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 );
}
2025-06-02 20:08:25 +02:00
2026-02-07 22:59:41 +01:00
// Initialize the enterprise encryption service for decrypting API keys:
await PluginFactory . InitializeEnterpriseEncryption ( this . RustService );
2025-06-02 20:08:25 +02:00
// Load (but not start) all plugins without waiting for them:
2025-08-09 19:29:43 +02:00
#if DEBUG
var pluginLoadingTimeout = new CancellationTokenSource ();
#else
2025-06-02 20:08:25 +02:00
var pluginLoadingTimeout = new CancellationTokenSource ( TimeSpan . FromSeconds ( 5 ));
2025-08-09 19:29:43 +02:00
#endif
2025-06-02 20:08:25 +02:00
await PluginFactory . LoadAll ( pluginLoadingTimeout . Token );
// Set up hot reloading for plugins:
PluginFactory . SetUpHotReloading ();
2026-03-10 20:50:45 +01:00
await this . MessageBus . SendMessage < bool >( this , Event . STARTUP_COMPLETED );
2025-06-02 20:08:25 +02:00
}
2026-08-25 18:53:09 +02:00
}). Observe ( $"{nameof(MainLayout)}: setting up the plugin system" );
2025-06-02 20:08:25 +02:00
break ;
2025-04-24 09:50:03 +02:00
2025-06-02 20:08:25 +02:00
case Event . PLUGINS_RELOADED :
this . Lang = await this . SettingsManager . GetActiveLanguagePlugin ();
I18N . Init ( this . Lang );
2026-06-21 18:46:21 +02:00
this . ShowSettingsWriteProtectionWarning ();
2025-06-02 20:08:25 +02:00
this . LoadNavItems ();
2026-09-09 18:43:37 +02:00
this . LoadEmbeddingItem ();
2025-04-24 09:50:03 +02:00
2025-06-02 20:08:25 +02:00
await this . InvokeAsync ( this . StateHasChanged );
2026-04-10 17:11:05 +02:00
if ( this . startupCompleted )
2026-08-25 18:53:09 +02:00
this . EnsureMandatoryInfosAcceptedAsync (). Observe ( $"{nameof(MainLayout)}: mandatory infos after a plugin reload" );
2026-04-10 17:11:05 +02:00
break ;
case Event . STARTUP_COMPLETED :
this . startupCompleted = true ;
2026-08-25 18:53:09 +02:00
this . EnsureMandatoryInfosAcceptedAsync (). Observe ( $"{nameof(MainLayout)}: mandatory infos after the startup" );
2025-06-02 20:08:25 +02:00
break ;
2026-09-09 18:43:37 +02:00
case Event . RAG_EMBEDDING_STATUS_CHANGED :
this . LoadNavItems ();
this . LoadEmbeddingItem ();
this . StateHasChanged ();
break ;
2025-06-02 20:08:25 +02:00
}
});
2024-06-30 15:26:28 +02:00
}
2026-08-23 11:09:11 +02:00
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 ();
}
}
2024-07-13 10:37:57 +02:00
public Task < TResult ?> ProcessMessageWithResult < TPayload , TResult >( ComponentBase ? sendingComponent , Event triggeredEvent , TPayload ? data )
{
return Task . FromResult < TResult ?>( default );
}
2024-06-30 15:26:28 +02:00
#endregion
2025-03-29 18:40:17 +01:00
2026-01-24 20:05:34 +01:00
private void LoadNavItems ()
{
this . navItems = new List < NavBarItem >( this . GetNavItems ());
}
2026-07-15 12:53:30 +02:00
/// <summary>Refreshes navigation activity colors when a media import changes state.</summary>
private void OnMediaImportStateChanged ( MediaImportOwner owner )
{
2026-08-25 18:53:09 +02:00
this . InvokeAsync (() =>
2026-07-15 12:53:30 +02:00
{
this . LoadNavItems ();
this . StateHasChanged ();
2026-08-25 18:53:09 +02:00
}). Observe ( $"{nameof(MainLayout)}: refreshing the navigation after a media import change" );
2026-07-15 12:53:30 +02:00
}
2026-01-24 20:05:34 +01:00
2025-03-29 18:40:17 +01:00
private IEnumerable < NavBarItem > GetNavItems ()
{
var palette = this . ColorTheme . GetCurrentPalette ( this . SettingsManager );
2026-07-03 15:02:20 +02:00
var activityIndicatorLightColor = this . ColorTheme . GetActivityIndicatorLightColor ();
var activityIndicatorDarkColor = this . ColorTheme . GetActivityIndicatorDarkColor ();
var defaultLightColor = palette . DarkLighten ;
var defaultDarkColor = palette . GrayLight ;
2026-07-15 12:53:30 +02:00
var mediaSnapshots = this . MediaTranscriptionService . GetSnapshots ();
2026-08-02 21:41:12 +02:00
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 });
2026-07-15 12:53:30 +02:00
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 ;
2025-03-29 18:40:17 +01:00
2026-07-03 15:02:20 +02:00
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 );
2025-03-29 18:40:17 +01:00
if ( PreviewFeatures . PRE_WRITER_MODE_2024 . IsEnabled ( this . SettingsManager ))
2026-07-03 15:02:20 +02:00
yield return new ( T ( "Writer" ), Icons . Material . Filled . Create , defaultLightColor , defaultDarkColor , Routes . WRITER , false );
2025-03-29 18:40:17 +01:00
2026-07-03 15:02:20 +02:00
yield return new ( T ( "Plugins" ), Icons . Material . TwoTone . Extension , defaultLightColor , defaultDarkColor , Routes . PLUGINS , false );
2025-04-24 13:50:14 +02:00
yield return new ( T ( "Supporters" ), Icons . Material . Filled . Favorite , palette . Error . Value , "#801a00" , Routes . SUPPORTERS , false );
2026-07-03 15:02:20 +02:00
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 );
2025-03-29 18:40:17 +01:00
}
2024-06-30 15:26:28 +02:00
2026-09-09 18:43:37 +02:00
private void LoadEmbeddingItem ()
{
this . embeddingOverview = this . DataSourceEmbeddingService . GetOverview ();
//
// The entry is shown whenever local RAG is available, in every state. Hiding it while
// nothing was running looked tidier, but a data source which was just added has no status
// yet: the service creates one when the run begins. The icon was therefore missing during
// the very moment the user was waiting for it. What the entry does communicate is its
// state, through the icon below.
//
// The preview feature is what gates it now. The route itself is not gated, so without this
// check, users who have no RAG at all would get a navigation entry for it.
//
this . showEmbeddingStatusIcon = PreviewFeatures . PRE_RAG_2024 . IsEnabled ( this . SettingsManager );
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" ),
// 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:
_ => ( Icons . Material . Filled . CloudDone , palette . DarkLighten , palette . GrayLight ),
};
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." ),
// 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:
_ => T ( "All data sources are up to date." )
};
2024-06-30 15:26:28 +02:00
private async Task ShowUpdateDialog ()
{
2026-07-13 13:35:24 +02:00
if (! this . UpdatePolicy . AllowsInstallations )
return ;
2024-06-30 15:26:28 +02:00
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 }
};
2025-04-24 13:50:14 +02:00
var dialogReference = await this . DialogService . ShowAsync < UpdateDialog >( T ( "Update" ), dialogParameters , DialogOptions . FULLSCREEN_NO_HEADER );
2024-06-30 15:26:28 +02:00
var dialogResult = await dialogReference . Result ;
2024-07-24 15:17:45 +02:00
if ( dialogResult is null || dialogResult . Canceled )
2024-06-30 15:26:28 +02:00
return ;
2026-07-13 13:35:24 +02:00
if (! this . UpdatePolicy . AllowsInstallations )
return ;
2024-06-30 15:26:28 +02:00
this . performingUpdate = true ;
this . StateHasChanged ();
2024-09-01 20:10:03 +02:00
await this . RustService . InstallUpdate ();
2024-06-30 15:26:28 +02:00
}
2024-07-13 10:37:57 +02:00
private async ValueTask OnLocationChanging ( LocationChangingContext context )
{
if ( await MessageBus . INSTANCE . SendMessageUseFirstResult < bool , bool >( this , Event . HAS_CHAT_UNSAVED_CHANGES ))
{
2025-08-28 18:51:44 +02:00
var dialogParameters = new DialogParameters < ConfirmDialog >
2024-07-13 10:37:57 +02:00
{
2025-08-28 18:51:44 +02:00
{ x => x . Message , T ( "Are you sure you want to leave the chat page? All unsaved changes will be lost." ) },
2024-07-13 10:37:57 +02:00
};
2025-04-24 13:50:14 +02:00
var dialogReference = await this . DialogService . ShowAsync < ConfirmDialog >( T ( "Leave Chat Page" ), dialogParameters , DialogOptions . FULLSCREEN );
2024-07-13 10:37:57 +02:00
var dialogResult = await dialogReference . Result ;
2024-07-24 15:17:45 +02:00
if ( dialogResult is null || dialogResult . Canceled )
2024-07-13 10:37:57 +02:00
{
context . PreventNavigation ();
return ;
}
// User accepted to leave the chat page, reset the chat state:
await MessageBus . INSTANCE . SendMessage < bool >( this , Event . RESET_CHAT_STATE );
}
}
2024-09-15 12:30:07 +02:00
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 )
2025-06-27 20:03:28 +02:00
this . useDarkMode = await this . themeProvider . GetSystemDarkModeAsync ();
2024-09-15 12:30:07 +02:00
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 ();
}
2026-04-10 17:11:05 +02:00
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 ();
}
2026-01-07 12:56:11 +01:00
2024-07-28 11:20:00 +02:00
#region Implementation of IDisposable
public void Dispose ()
{
2026-07-15 12:53:30 +02:00
this . MediaTranscriptionService . StateChanged -= this . OnMediaImportStateChanged ;
2024-07-28 11:20:00 +02:00
this . MessageBus . Unregister ( this );
2026-04-10 17:11:05 +02:00
this . mandatoryInfoDialogSemaphore . Dispose ();
2024-07-28 11:20:00 +02:00
}
#endregion
2024-04-05 16:16:33 +02:00
}