2024-08-21 06:30:01 +00:00
using AIStudio.Dialogs ;
2024-04-19 19:25:44 +00:00
using AIStudio.Settings ;
2024-07-28 09:20:00 +00:00
using AIStudio.Settings.DataModel ;
2025-03-22 20:12:14 +00:00
using AIStudio.Tools.PluginSystem ;
2024-09-01 18:10:03 +00:00
using AIStudio.Tools.Rust ;
2024-08-21 06:30:01 +00:00
using AIStudio.Tools.Services ;
2024-06-30 13:26:28 +00:00
2024-04-05 20:23:36 +00:00
using Microsoft.AspNetCore.Components ;
2024-07-13 08:37:57 +00:00
using Microsoft.AspNetCore.Components.Routing ;
2024-04-05 14:16:33 +00:00
2024-08-21 06:30:01 +00:00
using DialogOptions = AIStudio . Dialogs . DialogOptions ;
2025-06-02 18:08:25 +00:00
using EnterpriseEnvironment = AIStudio . Tools . EnterpriseEnvironment ;
2024-06-30 13:26:28 +00:00
2024-08-21 06:30:01 +00:00
namespace AIStudio.Layout ;
2024-04-05 20:23:36 +00:00
2025-04-24 11:50:14 +00:00
public partial class MainLayout : LayoutComponentBase , IMessageBusReceiver , ILang , IDisposable
2024-04-05 14:16:33 +00:00
{
2024-06-30 13:26:28 +00:00
[Inject]
private SettingsManager SettingsManager { get ; init ; } = null ! ;
[Inject]
private MessageBus MessageBus { get ; init ; } = null ! ;
[Inject]
2024-06-30 17:11:34 +00:00
private IDialogService DialogService { get ; init ; } = null ! ;
2024-06-30 13:26:28 +00:00
[Inject]
2024-09-01 18:10:03 +00:00
private RustService RustService { get ; init ; } = null ! ;
2024-06-30 17:11:34 +00:00
[Inject]
private ISnackbar Snackbar { get ; init ; } = null ! ;
2024-07-13 08:37:57 +00:00
[Inject]
private NavigationManager NavigationManager { get ; init ; } = null ! ;
2024-09-01 18:10:03 +00:00
[Inject]
private ILogger < MainLayout > Logger { get ; init ; } = null ! ;
2024-09-15 10:30:07 +00:00
[Inject]
private MudTheme ColorTheme { get ; init ; } = null ! ;
2024-06-30 13:26:28 +00:00
2025-04-24 11:50:14 +00:00
private ILanguagePlugin Lang { get ; set ; } = PluginFactory . BaseLanguage ;
2024-07-24 13:17:45 +00: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 13:26:28 +00:00
private bool performingUpdate ;
private UpdateResponse ? currentUpdateResponse ;
2024-09-15 10:30:07 +00:00
private MudThemeProvider themeProvider = null ! ;
private bool useDarkMode ;
private IReadOnlyCollection < NavBarItem > navItems = [ ] ;
2024-07-24 13:17:45 +00:00
2024-04-05 14:16:33 +00:00
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync ( )
{
2024-07-13 08:37:57 +00:00
this . NavigationManager . RegisterLocationChangingHandler ( this . OnLocationChanging ) ;
2024-05-04 08:55:00 +00:00
//
// We use the Tauri API (Rust) to get the data and config directories
// for this app.
//
2024-09-01 18:10:03 +00: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 08:55:00 +00:00
// Store the directories in the settings manager:
2024-04-05 20:23:36 +00:00
SettingsManager . ConfigDirectory = configDir ;
2024-09-01 18:10:03 +00:00
SettingsManager . DataDirectory = dataDir ;
2024-07-13 08:37:57 +00:00
Directory . CreateDirectory ( SettingsManager . DataDirectory ) ;
2024-04-05 20:23:36 +00:00
2025-04-03 12:25:45 +00:00
//
// Read the user language from Rust:
//
var userLanguage = await this . RustService . ReadUserLanguage ( ) ;
2025-04-24 11:50:14 +00:00
this . Logger . LogInformation ( $"The OS says '{userLanguage}' is the user language." ) ;
2025-04-03 12:25:45 +00:00
2024-06-30 13:26:28 +00:00
// Ensure that all settings are loaded:
await this . SettingsManager . LoadSettings ( ) ;
// Register this component with the message bus:
this . MessageBus . RegisterComponent ( this ) ;
2025-04-12 19:13:33 +00:00
this . MessageBus . ApplyFilters ( this , [ ] ,
[
Event . UPDATE_AVAILABLE , Event . CONFIGURATION_CHANGED , Event . COLOR_THEME_CHANGED , Event . SHOW_ERROR ,
2025-05-29 12:01:56 +00:00
Event . SHOW_ERROR , Event . SHOW_WARNING , Event . SHOW_SUCCESS , Event . STARTUP_PLUGIN_SYSTEM ,
2025-08-26 18:06:13 +00:00
Event . PLUGINS_RELOADED , Event . INSTALL_UPDATE ,
2025-04-12 19:13:33 +00:00
] ) ;
2024-06-30 13:26:28 +00:00
2024-09-01 18:10:03 +00:00
// Set the snackbar for the update service:
UpdateService . SetBlazorDependencies ( this . Snackbar ) ;
2026-01-24 19:05:34 +00:00
GlobalShortcutService . Initialize ( ) ;
2024-07-13 08:37:57 +00:00
TemporaryChatService . Initialize ( ) ;
2024-06-30 13:26:28 +00:00
2024-07-24 13:17:45 +00:00
// Should the navigation bar be open by default?
2024-08-05 19:12:52 +00:00
if ( this . SettingsManager . ConfigurationData . App . NavigationBehavior is NavBehavior . ALWAYS_EXPAND )
2024-07-24 13:17:45 +00:00
this . navBarOpen = true ;
2024-09-15 10:30:07 +00:00
2025-04-04 15:44:53 +00:00
// Solve issue https://github.com/MudBlazor/MudBlazor/issues/11133:
MudGlobal . TooltipDefaults . Duration = TimeSpan . Zero ;
2025-04-12 19:13:33 +00:00
// Send a message to start the plugin system:
await this . MessageBus . SendMessage < bool > ( this , Event . STARTUP_PLUGIN_SYSTEM ) ;
2025-06-27 18:03:28 +00:00
await this . themeProvider . WatchSystemDarkModeAsync ( this . SystemeThemeChanged ) ;
2024-09-15 10:30:07 +00:00
await this . UpdateThemeConfiguration ( ) ;
2024-12-03 14:52:45 +00:00
this . LoadNavItems ( ) ;
await base . OnInitializedAsync ( ) ;
}
2024-04-05 14:16:33 +00:00
#endregion
2024-06-30 13:26:28 +00:00
2025-04-24 11:50:14 +00:00
#region Implementation of ILang
2025-04-27 14:13:15 +00:00
/// <inheritdoc />
2025-04-24 11:50:14 +00:00
public string T ( string fallbackEN ) = > this . GetText ( this . Lang , fallbackEN ) ;
2025-04-27 14:13:15 +00:00
/// <inheritdoc />
public string T ( string fallbackEN , string? typeNamespace , string? typeName ) = > this . GetText ( this . Lang , fallbackEN , typeNamespace , typeName ) ;
2025-04-24 11:50:14 +00:00
#endregion
2024-06-30 13:26:28 +00:00
#region Implementation of IMessageBusReceiver
2025-01-21 14:36:22 +00:00
public string ComponentName = > nameof ( MainLayout ) ;
2025-04-24 11:50:14 +00:00
public async Task ProcessMessage < TMessage > ( ComponentBase ? sendingComponent , Event triggeredEvent , TMessage ? data )
2024-06-30 13:26:28 +00:00
{
2025-06-02 18:08:25 +00:00
await this . InvokeAsync ( async ( ) = >
2024-06-30 13:26:28 +00:00
{
2025-06-02 18:08:25 +00:00
switch ( triggeredEvent )
{
2025-08-26 18:06:13 +00:00
case Event . INSTALL_UPDATE :
this . performingUpdate = true ;
this . StateHasChanged ( ) ;
break ;
2025-06-02 18:08:25 +00:00
case Event . UPDATE_AVAILABLE :
if ( data is UpdateResponse updateResponse )
2025-04-04 21:06:42 +00:00
{
2025-06-02 18:08:25 +00: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 21:06:42 +00:00
{
2025-06-02 18:08:25 +00: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 ( ) ;
this . StateHasChanged ( ) ;
break ;
case Event . COLOR_THEME_CHANGED :
this . StateHasChanged ( ) ;
break ;
case Event . SHOW_SUCCESS :
if ( data is DataSuccessMessage success )
success . Show ( this . Snackbar ) ;
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 . STARTUP_PLUGIN_SYSTEM :
_ = Task . Run ( async ( ) = >
2025-04-12 19:13:33 +00:00
{
2025-06-02 18:08:25 +00: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-02-15 17:11:57 +00:00
var enterpriseEnvironments = this . MessageBus
. CheckDeferredMessages < EnterpriseEnvironment > ( Event . STARTUP_ENTERPRISE_ENVIRONMENT )
. Where ( env = > env ! = default )
. ToList ( ) ;
2026-02-19 19:43:47 +00:00
var failedDeferredConfigIds = new HashSet < Guid > ( ) ;
2026-02-15 17:11:57 +00:00
foreach ( var env in enterpriseEnvironments )
2026-02-19 19:43:47 +00: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 18:08:25 +00:00
2026-02-07 21:59:41 +00:00
// Initialize the enterprise encryption service for decrypting API keys:
await PluginFactory . InitializeEnterpriseEncryption ( this . RustService ) ;
2025-06-02 18:08:25 +00:00
// Load (but not start) all plugins without waiting for them:
2025-08-09 17:29:43 +00:00
#if DEBUG
var pluginLoadingTimeout = new CancellationTokenSource ( ) ;
#else
2025-06-02 18:08:25 +00:00
var pluginLoadingTimeout = new CancellationTokenSource ( TimeSpan . FromSeconds ( 5 ) ) ;
2025-08-09 17:29:43 +00:00
#endif
2025-06-02 18:08:25 +00:00
await PluginFactory . LoadAll ( pluginLoadingTimeout . Token ) ;
// Set up hot reloading for plugins:
PluginFactory . SetUpHotReloading ( ) ;
}
} ) ;
break ;
2025-04-24 07:50:03 +00:00
2025-06-02 18:08:25 +00:00
case Event . PLUGINS_RELOADED :
this . Lang = await this . SettingsManager . GetActiveLanguagePlugin ( ) ;
I18N . Init ( this . Lang ) ;
this . LoadNavItems ( ) ;
2025-04-24 07:50:03 +00:00
2025-06-02 18:08:25 +00:00
await this . InvokeAsync ( this . StateHasChanged ) ;
break ;
}
} ) ;
2024-06-30 13:26:28 +00:00
}
2024-07-13 08:37:57 +00:00
public Task < TResult ? > ProcessMessageWithResult < TPayload , TResult > ( ComponentBase ? sendingComponent , Event triggeredEvent , TPayload ? data )
{
return Task . FromResult < TResult ? > ( default ) ;
}
2024-06-30 13:26:28 +00:00
#endregion
2025-03-29 17:40:17 +00:00
2026-01-24 19:05:34 +00:00
private void LoadNavItems ( )
{
this . navItems = new List < NavBarItem > ( this . GetNavItems ( ) ) ;
}
2025-03-29 17:40:17 +00:00
private IEnumerable < NavBarItem > GetNavItems ( )
{
var palette = this . ColorTheme . GetCurrentPalette ( this . SettingsManager ) ;
2025-04-24 11:50:14 +00:00
yield return new ( T ( "Home" ) , Icons . Material . Filled . Home , palette . DarkLighten , palette . GrayLight , Routes . HOME , true ) ;
yield return new ( T ( "Chat" ) , Icons . Material . Filled . Chat , palette . DarkLighten , palette . GrayLight , Routes . CHAT , false ) ;
yield return new ( T ( "Assistants" ) , Icons . Material . Filled . Apps , palette . DarkLighten , palette . GrayLight , Routes . ASSISTANTS , false ) ;
2025-03-29 17:40:17 +00:00
if ( PreviewFeatures . PRE_WRITER_MODE_2024 . IsEnabled ( this . SettingsManager ) )
2025-04-24 11:50:14 +00:00
yield return new ( T ( "Writer" ) , Icons . Material . Filled . Create , palette . DarkLighten , palette . GrayLight , Routes . WRITER , false ) ;
2025-03-29 17:40:17 +00:00
2025-05-31 10:04:58 +00:00
yield return new ( T ( "Plugins" ) , Icons . Material . TwoTone . Extension , palette . DarkLighten , palette . GrayLight , Routes . PLUGINS , false ) ;
2025-04-24 11:50:14 +00:00
yield return new ( T ( "Supporters" ) , Icons . Material . Filled . Favorite , palette . Error . Value , "#801a00" , Routes . SUPPORTERS , false ) ;
2025-12-18 17:10:17 +00:00
yield return new ( T ( "Information" ) , Icons . Material . Filled . Info , palette . DarkLighten , palette . GrayLight , Routes . ABOUT , false ) ;
2025-04-24 11:50:14 +00:00
yield return new ( T ( "Settings" ) , Icons . Material . Filled . Settings , palette . DarkLighten , palette . GrayLight , Routes . SETTINGS , false ) ;
2025-03-29 17:40:17 +00:00
}
2024-06-30 13:26:28 +00:00
private async Task ShowUpdateDialog ( )
{
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 11:50:14 +00:00
var dialogReference = await this . DialogService . ShowAsync < UpdateDialog > ( T ( "Update" ) , dialogParameters , DialogOptions . FULLSCREEN_NO_HEADER ) ;
2024-06-30 13:26:28 +00:00
var dialogResult = await dialogReference . Result ;
2024-07-24 13:17:45 +00:00
if ( dialogResult is null | | dialogResult . Canceled )
2024-06-30 13:26:28 +00:00
return ;
this . performingUpdate = true ;
this . StateHasChanged ( ) ;
2024-09-01 18:10:03 +00:00
await this . RustService . InstallUpdate ( ) ;
2024-06-30 13:26:28 +00:00
}
2024-07-13 08:37:57 +00:00
private async ValueTask OnLocationChanging ( LocationChangingContext context )
{
if ( await MessageBus . INSTANCE . SendMessageUseFirstResult < bool , bool > ( this , Event . HAS_CHAT_UNSAVED_CHANGES ) )
{
2025-08-28 16:51:44 +00:00
var dialogParameters = new DialogParameters < ConfirmDialog >
2024-07-13 08:37:57 +00:00
{
2025-08-28 16:51:44 +00:00
{ x = > x . Message , T ( "Are you sure you want to leave the chat page? All unsaved changes will be lost." ) } ,
2024-07-13 08:37:57 +00:00
} ;
2025-04-24 11:50:14 +00:00
var dialogReference = await this . DialogService . ShowAsync < ConfirmDialog > ( T ( "Leave Chat Page" ) , dialogParameters , DialogOptions . FULLSCREEN ) ;
2024-07-13 08:37:57 +00:00
var dialogResult = await dialogReference . Result ;
2024-07-24 13:17:45 +00:00
if ( dialogResult is null | | dialogResult . Canceled )
2024-07-13 08:37:57 +00: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 10:30:07 +00: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 18:03:28 +00:00
this . useDarkMode = await this . themeProvider . GetSystemDarkModeAsync ( ) ;
2024-09-15 10:30:07 +00: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-01-07 11:56:11 +00:00
2024-07-28 09:20:00 +00:00
#region Implementation of IDisposable
public void Dispose ( )
{
this . MessageBus . Unregister ( this ) ;
}
#endregion
2024-04-05 14:16:33 +00:00
}