mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-04-27 06:59:47 +00:00
Configure language & start language plugins (#400)
Some checks failed
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, deb) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
Some checks failed
Build and Release / Read metadata (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg updater) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis updater) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, deb) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg updater) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis updater) (push) Has been cancelled
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage deb updater) (push) Has been cancelled
Build and Release / Prepare & create release (push) Has been cancelled
Build and Release / Publish release (push) Has been cancelled
This commit is contained in:
parent
171ed37c27
commit
3fc15d9789
@ -2,6 +2,7 @@
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EDI/@EntryIndexedValue">EDI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERI/@EntryIndexedValue">ERI</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=FNV/@EntryIndexedValue">FNV</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GWDG/@EntryIndexedValue">GWDG</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HF/@EntryIndexedValue">HF</s:String>
|
||||
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LLM/@EntryIndexedValue">LLM</s:String>
|
||||
|
@ -840,8 +840,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
public override string ComponentName => nameof(ChatComponent);
|
||||
|
||||
public override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
|
||||
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
@ -860,7 +860,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
|
||||
protected override Task<TResult?> ProcessIncomingMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
|
@ -46,8 +46,8 @@ public partial class InnerScrolling : MSGComponentBase
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
public override string ComponentName => nameof(InnerScrolling);
|
||||
|
||||
public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
|
||||
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
@ -59,11 +59,6 @@ public partial class InnerScrolling : MSGComponentBase
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
|
||||
{
|
||||
return Task.FromResult(default(TResult));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private string MinWidthStyle => string.IsNullOrWhiteSpace(this.MinWidth) ? string.Empty : $"min-width: {this.MinWidth}; ";
|
||||
|
@ -1,10 +1,11 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBusReceiver
|
||||
public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBusReceiver, ILang
|
||||
{
|
||||
[Inject]
|
||||
protected SettingsManager SettingsManager { get; init; } = null!;
|
||||
@ -12,12 +13,37 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
|
||||
[Inject]
|
||||
protected MessageBus MessageBus { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ILogger<PluginLanguage> Logger { get; init; } = null!;
|
||||
|
||||
private ILanguagePlugin Lang { get; set; } = PluginFactory.BaseLanguage;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override void OnInitialized()
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.Lang = await this.SettingsManager.GetActiveLanguagePlugin();
|
||||
|
||||
this.MessageBus.RegisterComponent(this);
|
||||
base.OnInitialized();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of ILang
|
||||
|
||||
/// <inheritdoc />
|
||||
public string T(string fallbackEN)
|
||||
{
|
||||
var type = this.GetType();
|
||||
var ns = $"{type.Namespace!}::{type.Name}".ToUpperInvariant().Replace(".", "::");
|
||||
var key = $"root::{ns}::T{fallbackEN.ToFNV32()}";
|
||||
|
||||
if(this.Lang.TryGetText(key, out var text, logWarning: false))
|
||||
return text;
|
||||
|
||||
this.Logger.LogWarning($"Missing translation key '{key}' for content '{fallbackEN}'.");
|
||||
return fallbackEN;
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -26,7 +52,7 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
|
||||
|
||||
public abstract string ComponentName { get; }
|
||||
|
||||
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
|
||||
public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
@ -34,19 +60,34 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.PLUGINS_RELOADED:
|
||||
this.Lang = await this.SettingsManager.GetActiveLanguagePlugin();
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
|
||||
default:
|
||||
return this.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
|
||||
await this.ProcessIncomingMessage(sendingComponent, triggeredEvent, data);
|
||||
break;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public abstract Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data);
|
||||
|
||||
public abstract Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data);
|
||||
|
||||
public async Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
|
||||
{
|
||||
return await this.ProcessIncomingMessageWithResult<TPayload, TResult>(sendingComponent, triggeredEvent, data);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected virtual Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected virtual Task<TResult?> ProcessIncomingMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
|
||||
{
|
||||
return Task.FromResult<TResult?>(default);
|
||||
}
|
||||
|
||||
#region Implementation of IDisposable
|
||||
|
||||
public void Dispose()
|
||||
@ -71,7 +112,8 @@ public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBus
|
||||
// Append the color theme changed event to the list of events:
|
||||
var eventsList = new List<Event>(events)
|
||||
{
|
||||
Event.COLOR_THEME_CHANGED
|
||||
Event.COLOR_THEME_CHANGED,
|
||||
Event.PLUGINS_RELOADED,
|
||||
};
|
||||
|
||||
this.MessageBus.ApplyFilters(this, components, eventsList.ToArray());
|
||||
|
@ -3,14 +3,25 @@
|
||||
@inherits SettingsPanelBase
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Apps" HeaderText="App Options">
|
||||
|
||||
@if (PreviewFeatures.PRE_PLUGINS_2025.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<ConfigurationSelect OptionDescription="Language behavior" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.LanguageBehavior)" Data="@ConfigurationSelectDataFactory.GetLangBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.LanguageBehavior = selectedValue)" OptionHelp="Select the language behavior for the app. The default is to use the system language. You might want to choose a language manually?"/>
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.App.LanguageBehavior is LangBehavior.MANUAL)
|
||||
{
|
||||
<ConfigurationSelect OptionDescription="Language" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.LanguagePluginId)" Data="@ConfigurationSelectDataFactory.GetLanguagesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.LanguagePluginId = selectedValue)" OptionHelp="Select the language for the app."/>
|
||||
}
|
||||
}
|
||||
|
||||
<ConfigurationSelect OptionDescription="Color theme" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreferredTheme)" Data="@ConfigurationSelectDataFactory.GetThemesData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreferredTheme = selectedValue)" OptionHelp="Choose the color theme that best suits for you."/>
|
||||
<ConfigurationOption OptionDescription="Save energy?" LabelOn="Energy saving is enabled" LabelOff="Energy saving is disabled" State="@(() => this.SettingsManager.ConfigurationData.App.IsSavingEnergy)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.IsSavingEnergy = updatedState)" OptionHelp="When enabled, streamed content from the AI is updated once every third second. When disabled, streamed content will be updated as soon as it is available."/>
|
||||
<ConfigurationOption OptionDescription="Enable spellchecking?" LabelOn="Spellchecking is enabled" LabelOff="Spellchecking is disabled" State="@(() => this.SettingsManager.ConfigurationData.App.EnableSpellchecking)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.EnableSpellchecking = updatedState)" OptionHelp="When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections." />
|
||||
<ConfigurationOption OptionDescription="Enable spellchecking?" LabelOn="Spellchecking is enabled" LabelOff="Spellchecking is disabled" State="@(() => this.SettingsManager.ConfigurationData.App.EnableSpellchecking)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.App.EnableSpellchecking = updatedState)" OptionHelp="When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections."/>
|
||||
<ConfigurationSelect OptionDescription="Check for updates" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UpdateBehavior)" Data="@ConfigurationSelectDataFactory.GetUpdateBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UpdateBehavior = selectedValue)" OptionHelp="How often should we check for app updates?"/>
|
||||
<ConfigurationSelect OptionDescription="Navigation bar behavior" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.NavigationBehavior)" Data="@ConfigurationSelectDataFactory.GetNavBehaviorData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.NavigationBehavior = selectedValue)" OptionHelp="Select the desired behavior for the navigation bar."/>
|
||||
<ConfigurationSelect OptionDescription="Preview feature visibility" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreviewVisibility)" Data="@ConfigurationSelectDataFactory.GetPreviewVisibility()" SelectionUpdate="@this.UpdatePreviewFeatures" OptionHelp="Do you want to show preview features in the app?"/>
|
||||
|
||||
@if(this.SettingsManager.ConfigurationData.App.PreviewVisibility > PreviewVisibility.NONE)
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.App.PreviewVisibility > PreviewVisibility.NONE)
|
||||
{
|
||||
var availablePreviewFeatures = ConfigurationSelectDataFactory.GetPreviewFeaturesData(this.SettingsManager).ToList();
|
||||
if (availablePreviewFeatures.Count > 0)
|
||||
@ -18,7 +29,7 @@
|
||||
<ConfigurationMultiSelect OptionDescription="Select preview features" SelectedValues="@(() => this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures)" Data="@availablePreviewFeatures" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.EnabledPreviewFeatures = selectedValue)" OptionHelp="Which preview features would you like to enable?"/>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
<ConfigurationProviderSelection Component="Components.APP_SETTINGS" Data="@this.AvailableLLMProvidersFunc()" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProvider = selectedValue)" HelpText="@(() => "Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence.")"/>
|
||||
<ConfigurationSelect OptionDescription="Preselect one of your profiles?" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProfile = selectedValue)" OptionHelp="Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence."/>
|
||||
</ExpansionPanel>
|
@ -83,26 +83,13 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
|
||||
// Ensure that all settings are loaded:
|
||||
await this.SettingsManager.LoadSettings();
|
||||
|
||||
//
|
||||
// We cannot process the plugins before the settings are loaded,
|
||||
// and we know our data directory.
|
||||
//
|
||||
if(PreviewFeatures.PRE_PLUGINS_2025.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
// Ensure that all internal plugins are present:
|
||||
await PluginFactory.EnsureInternalPlugins();
|
||||
|
||||
// Load (but not start) all plugins, without waiting for them:
|
||||
var pluginLoadingTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
_ = PluginFactory.LoadAll(pluginLoadingTimeout.Token);
|
||||
|
||||
// Set up hot reloading for plugins:
|
||||
PluginFactory.SetUpHotReloading();
|
||||
}
|
||||
|
||||
// Register this component with the message bus:
|
||||
this.MessageBus.RegisterComponent(this);
|
||||
this.MessageBus.ApplyFilters(this, [], [ Event.UPDATE_AVAILABLE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED, Event.SHOW_ERROR ]);
|
||||
this.MessageBus.ApplyFilters(this, [],
|
||||
[
|
||||
Event.UPDATE_AVAILABLE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED, Event.SHOW_ERROR,
|
||||
Event.STARTUP_PLUGIN_SYSTEM, Event.PLUGINS_RELOADED
|
||||
]);
|
||||
|
||||
// Set the snackbar for the update service:
|
||||
UpdateService.SetBlazorDependencies(this.Snackbar);
|
||||
@ -115,6 +102,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
|
||||
// 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.WatchSystemPreference(this.SystemeThemeChanged);
|
||||
await this.UpdateThemeConfiguration();
|
||||
this.LoadNavItems();
|
||||
@ -179,6 +169,32 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
|
||||
error.Show(this.Snackbar);
|
||||
|
||||
break;
|
||||
|
||||
case Event.STARTUP_PLUGIN_SYSTEM:
|
||||
if(PreviewFeatures.PRE_PLUGINS_2025.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
// Set up the plugin system:
|
||||
PluginFactory.Setup();
|
||||
|
||||
// Ensure that all internal plugins are present:
|
||||
await PluginFactory.EnsureInternalPlugins();
|
||||
|
||||
// Load (but not start) all plugins, without waiting for them:
|
||||
var pluginLoadingTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
await PluginFactory.LoadAll(pluginLoadingTimeout.Token);
|
||||
|
||||
// Set up hot reloading for plugins:
|
||||
PluginFactory.SetUpHotReloading();
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case Event.PLUGINS_RELOADED:
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,11 +7,11 @@
|
||||
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
|
||||
@if (this.chatThread is not null && this.chatThread.WorkspaceId != Guid.Empty)
|
||||
{
|
||||
@($"Chat in Workspace \"{this.currentWorkspaceName}\"")
|
||||
@(T("Chat in Workspace") + $" \"{this.currentWorkspaceName}\"")
|
||||
}
|
||||
else
|
||||
{
|
||||
@("Temporary Chat")
|
||||
@(T("Short-Term Chat"))
|
||||
}
|
||||
</MudText>
|
||||
|
||||
|
@ -83,8 +83,8 @@ public partial class Chat : MSGComponentBase
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
public override string ComponentName => nameof(Chat);
|
||||
|
||||
public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
|
||||
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
@ -96,10 +96,5 @@ public partial class Chat : MSGComponentBase
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
|
||||
{
|
||||
return Task.FromResult(default(TResult));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
@attribute [Route(Routes.HOME)]
|
||||
@inherits MSGComponentBase
|
||||
|
||||
<div class="inner-scrolling-context">
|
||||
<MudImage Src="svg/banner.svg" Style="max-height: 16em; width: 100%; object-fit: cover;" />
|
||||
<MudText Typo="Typo.h3" Class="mt-2 mb-2">Let's get started</MudText>
|
||||
<MudText Typo="Typo.h3" Class="mt-2 mb-2">
|
||||
@T("Let's get started")
|
||||
</MudText>
|
||||
|
||||
<InnerScrolling>
|
||||
<MudExpansionPanels Class="mb-3" MultiExpansion="@false">
|
||||
|
@ -6,10 +6,10 @@ using Changelog = AIStudio.Components.Changelog;
|
||||
|
||||
namespace AIStudio.Pages;
|
||||
|
||||
public partial class Home : ComponentBase
|
||||
public partial class Home : MSGComponentBase
|
||||
{
|
||||
[Inject]
|
||||
private HttpClient HttpClient { get; set; } = null!;
|
||||
private HttpClient HttpClient { get; init; } = null!;
|
||||
|
||||
private string LastChangeContent { get; set; } = string.Empty;
|
||||
|
||||
@ -22,7 +22,13 @@ public partial class Home : ComponentBase
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
public override string ComponentName => nameof(Home);
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task ReadLastChangeAsync()
|
||||
{
|
||||
var latest = Changelog.LOGS.MaxBy(n => n.Build);
|
||||
|
@ -1,4 +1,5 @@
|
||||
@using AIStudio.Tools.PluginSystem
|
||||
@inherits MSGComponentBase
|
||||
@attribute [Route(Routes.PLUGINS)]
|
||||
|
||||
<div class="inner-scrolling-context">
|
||||
|
@ -1,22 +1,14 @@
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Pages;
|
||||
|
||||
public partial class Plugins : ComponentBase, IMessageBusReceiver
|
||||
public partial class Plugins : MSGComponentBase
|
||||
{
|
||||
private const string GROUP_ENABLED = "Enabled";
|
||||
private const string GROUP_DISABLED = "Disabled";
|
||||
private const string GROUP_INTERNAL = "Internal";
|
||||
|
||||
[Inject]
|
||||
private MessageBus MessageBus { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
private TableGroupDefinition<IPluginMetadata> groupConfig = null!;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
@ -45,7 +37,13 @@ public partial class Plugins : ComponentBase, IMessageBusReceiver
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
public override string ComponentName => nameof(Plugins);
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task PluginActivationStateChanged(IPluginMetadata pluginMeta)
|
||||
{
|
||||
if (this.SettingsManager.IsPluginEnabled(pluginMeta))
|
||||
@ -56,27 +54,4 @@ public partial class Plugins : ComponentBase, IMessageBusReceiver
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED);
|
||||
}
|
||||
|
||||
#region Implementation of IMessageBusReceiver
|
||||
|
||||
public string ComponentName => nameof(Plugins);
|
||||
|
||||
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
|
||||
{
|
||||
switch (triggeredEvent)
|
||||
{
|
||||
case Event.PLUGINS_RELOADED:
|
||||
this.InvokeAsync(this.StateHasChanged);
|
||||
break;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
|
||||
{
|
||||
return Task.FromResult<TResult?>(default);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
@ -24,7 +24,7 @@ public partial class Writer : MSGComponentBase, IAsyncDisposable
|
||||
private string userInput = string.Empty;
|
||||
private string userDirection = string.Empty;
|
||||
private string suggestion = string.Empty;
|
||||
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
@ -43,16 +43,6 @@ public partial class Writer : MSGComponentBase, IAsyncDisposable
|
||||
|
||||
public override string ComponentName => nameof(Writer);
|
||||
|
||||
public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
|
||||
{
|
||||
return Task.FromResult(default(TResult));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private bool IsProviderSelected => this.providerSettings.UsedLLMProvider != LLMProviders.NONE;
|
||||
|
@ -44,6 +44,20 @@ DEPRECATION_MESSAGE = ""
|
||||
-- code followed by the ISO 3166-1 country code:
|
||||
IETF_TAG = "de-DE"
|
||||
|
||||
-- The language name in the user's language:
|
||||
LANG_NAME = "Deutsch (Deutschland)"
|
||||
|
||||
UI_TEXT_CONTENT = {
|
||||
HOME = CONTENT_HOME,
|
||||
AISTUDIO = {
|
||||
PAGES = {
|
||||
HOME = {
|
||||
T2331588413 = "Lass uns anfangen",
|
||||
},
|
||||
|
||||
CHAT = {
|
||||
T3718856736 = "Vorläufiger Chat",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,20 @@ DEPRECATION_MESSAGE = ""
|
||||
-- code followed by the ISO 3166-1 country code:
|
||||
IETF_TAG = "en-US"
|
||||
|
||||
-- The language name in the user's language:
|
||||
LANG_NAME = "English (United States)"
|
||||
|
||||
UI_TEXT_CONTENT = {
|
||||
HOME = CONTENT_HOME,
|
||||
AISTUDIO = {
|
||||
PAGES = {
|
||||
HOME = {
|
||||
T2331588413 = "Let's get started",
|
||||
},
|
||||
|
||||
CHAT = {
|
||||
T3718856736 = "Short-Term Chat",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using AIStudio.Assistants.TextSummarizer;
|
||||
using AIStudio.Assistants.EMail;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using WritingStylesRewrite = AIStudio.Assistants.RewriteImprove.WritingStyles;
|
||||
using WritingStylesEMail = AIStudio.Assistants.EMail.WritingStyles;
|
||||
@ -25,6 +26,21 @@ public readonly record struct ConfigurationSelectData<T>(string Name, T Value);
|
||||
/// </summary>
|
||||
public static class ConfigurationSelectDataFactory
|
||||
{
|
||||
public static IEnumerable<ConfigurationSelectData<LangBehavior>> GetLangBehaviorData()
|
||||
{
|
||||
foreach (var behavior in Enum.GetValues<LangBehavior>())
|
||||
yield return new(behavior.Name(), behavior);
|
||||
}
|
||||
|
||||
public static IEnumerable<ConfigurationSelectData<Guid>> GetLanguagesData()
|
||||
{
|
||||
foreach (var runningPlugin in PluginFactory.RunningPlugins)
|
||||
{
|
||||
if(runningPlugin is ILanguagePlugin languagePlugin)
|
||||
yield return new(languagePlugin.LangName, runningPlugin.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<ConfigurationSelectData<LoadingChatProviderBehavior>> GetLoadingChatProviderBehavior()
|
||||
{
|
||||
yield return new("When possible, use the LLM provider which was used for each chat in the first place", LoadingChatProviderBehavior.USE_CHAT_PROVIDER_IF_AVAILABLE);
|
||||
|
@ -2,6 +2,16 @@ namespace AIStudio.Settings.DataModel;
|
||||
|
||||
public sealed class DataApp
|
||||
{
|
||||
/// <summary>
|
||||
/// The language behavior.
|
||||
/// </summary>
|
||||
public LangBehavior LanguageBehavior { get; set; } = LangBehavior.AUTO;
|
||||
|
||||
/// <summary>
|
||||
/// The language plugin ID to use.
|
||||
/// </summary>
|
||||
public Guid LanguagePluginId { get; set; } = Guid.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The preferred theme to use.
|
||||
/// </summary>
|
||||
|
@ -0,0 +1,7 @@
|
||||
namespace AIStudio.Settings.DataModel;
|
||||
|
||||
public enum LangBehavior
|
||||
{
|
||||
AUTO,
|
||||
MANUAL,
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
namespace AIStudio.Settings.DataModel;
|
||||
|
||||
public static class LangBehaviorExtensions
|
||||
{
|
||||
public static string Name(this LangBehavior langBehavior) => langBehavior switch
|
||||
{
|
||||
LangBehavior.AUTO => "Choose the language automatically, based on your system language.",
|
||||
LangBehavior.MANUAL => "Choose the language manually.",
|
||||
|
||||
_ => "Unknown option"
|
||||
};
|
||||
}
|
@ -5,6 +5,7 @@ using System.Text.Json.Serialization;
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
// ReSharper disable NotAccessedPositionalProperty.Local
|
||||
|
||||
@ -13,7 +14,7 @@ namespace AIStudio.Settings;
|
||||
/// <summary>
|
||||
/// The settings manager.
|
||||
/// </summary>
|
||||
public sealed class SettingsManager(ILogger<SettingsManager> logger)
|
||||
public sealed class SettingsManager(ILogger<SettingsManager> logger, RustService rustService)
|
||||
{
|
||||
private const string SETTINGS_FILENAME = "settings.json";
|
||||
|
||||
@ -24,6 +25,7 @@ public sealed class SettingsManager(ILogger<SettingsManager> logger)
|
||||
};
|
||||
|
||||
private readonly ILogger<SettingsManager> logger = logger;
|
||||
private readonly RustService rustService = rustService;
|
||||
|
||||
/// <summary>
|
||||
/// The directory where the configuration files are stored.
|
||||
@ -143,8 +145,53 @@ public sealed class SettingsManager(ILogger<SettingsManager> logger)
|
||||
return minimumLevel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the given plugin is enabled.
|
||||
/// </summary>
|
||||
/// <param name="plugin">The plugin to check.</param>
|
||||
/// <returns>True, when the plugin is enabled, false otherwise.</returns>
|
||||
public bool IsPluginEnabled(IPluginMetadata plugin) => this.ConfigurationData.EnabledPlugins.Contains(plugin.Id);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the active language plugin.
|
||||
/// </summary>
|
||||
/// <returns>The active language plugin.</returns>
|
||||
public async Task<ILanguagePlugin> GetActiveLanguagePlugin()
|
||||
{
|
||||
switch (this.ConfigurationData.App.LanguageBehavior)
|
||||
{
|
||||
case LangBehavior.AUTO:
|
||||
var languageCode = await this.rustService.ReadUserLanguage();
|
||||
var languagePlugin = PluginFactory.RunningPlugins.FirstOrDefault(x => x is ILanguagePlugin langPlug && langPlug.IETFTag == languageCode);
|
||||
if (languagePlugin is null)
|
||||
return PluginFactory.BaseLanguage;
|
||||
|
||||
if (languagePlugin is ILanguagePlugin langPlugin)
|
||||
return langPlugin;
|
||||
|
||||
this.logger.LogError("The language plugin is not a language plugin.");
|
||||
return PluginFactory.BaseLanguage;
|
||||
|
||||
case LangBehavior.MANUAL:
|
||||
var pluginId = this.ConfigurationData.App.LanguagePluginId;
|
||||
var plugin = PluginFactory.RunningPlugins.FirstOrDefault(x => x.Id == pluginId);
|
||||
if (plugin is null)
|
||||
{
|
||||
this.logger.LogWarning($"The chosen language plugin (id='{pluginId}') is not available.");
|
||||
return PluginFactory.BaseLanguage;
|
||||
}
|
||||
|
||||
if (plugin is ILanguagePlugin chosenLangPlugin)
|
||||
return chosenLangPlugin;
|
||||
|
||||
this.logger.LogError("The chosen language plugin is not a language plugin.");
|
||||
return PluginFactory.BaseLanguage;
|
||||
}
|
||||
|
||||
this.logger.LogError("The language behavior is unknown.");
|
||||
return PluginFactory.BaseLanguage;
|
||||
}
|
||||
|
||||
[SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")]
|
||||
public Provider GetPreselectedProvider(Tools.Components component, string? currentProviderId = null, bool usePreselectionBeforeCurrentProvider = false)
|
||||
{
|
||||
|
@ -8,6 +8,7 @@ public enum Event
|
||||
STATE_HAS_CHANGED,
|
||||
CONFIGURATION_CHANGED,
|
||||
COLOR_THEME_CHANGED,
|
||||
STARTUP_PLUGIN_SYSTEM,
|
||||
PLUGINS_RELOADED,
|
||||
SHOW_ERROR,
|
||||
|
||||
|
62
app/MindWork AI Studio/Tools/FNVHash.cs
Normal file
62
app/MindWork AI Studio/Tools/FNVHash.cs
Normal file
@ -0,0 +1,62 @@
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the Fowler–Noll–Vo hash function for 32-bit and 64-bit hashes.
|
||||
/// </summary>
|
||||
public static class FNVHash
|
||||
{
|
||||
private const uint FNV_OFFSET_BASIS_32_BIT = 2_166_136_261;
|
||||
private const ulong FNV_OFFSET_BASIS_64_BIT = 14_695_981_039_346_656_037;
|
||||
|
||||
private const uint FNV_PRIME_32_BIT = 16_777_619;
|
||||
private const ulong FNV_PRIME_64_BIT = 1_099_511_628_211;
|
||||
|
||||
/// <summary>
|
||||
/// Computes the 32bit FNV-1a hash of a string.
|
||||
/// </summary>
|
||||
/// <param name="text">The string to hash.</param>
|
||||
/// <returns>The 32bit FNV-1a hash of the string.</returns>
|
||||
public static uint ToFNV32(this string text) => ToFNV32(text.AsSpan());
|
||||
|
||||
/// <summary>
|
||||
/// Computes the 32bit FNV-1a hash of a string.
|
||||
/// </summary>
|
||||
/// <param name="text">The string to hash.</param>
|
||||
/// <returns>The 32bit FNV-1a hash of the string.</returns>
|
||||
public static uint ToFNV32(this ReadOnlySpan<char> text)
|
||||
{
|
||||
var hash = FNV_OFFSET_BASIS_32_BIT;
|
||||
foreach (var c in text)
|
||||
{
|
||||
hash ^= c;
|
||||
hash *= FNV_PRIME_32_BIT;
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the 64bit FNV-1a hash of a string.
|
||||
/// </summary>
|
||||
/// <param name="text">The string to hash.</param>
|
||||
/// <returns>The 64bit FNV-1a hash of the string.</returns>
|
||||
public static ulong ToFNV64(this string text) => ToFNV64(text.AsSpan());
|
||||
|
||||
/// <summary>
|
||||
/// Computes the 64bit FNV-1a hash of a string.
|
||||
/// </summary>
|
||||
/// <param name="text">The string to hash.</param>
|
||||
/// <returns>The 64bit FNV-1a hash of the string.</returns>
|
||||
public static ulong ToFNV64(this ReadOnlySpan<char> text)
|
||||
{
|
||||
var hash = FNV_OFFSET_BASIS_64_BIT;
|
||||
foreach (var c in text)
|
||||
{
|
||||
hash ^= c;
|
||||
hash *= FNV_PRIME_64_BIT;
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public interface IAvailablePlugin : IPluginMetadata
|
||||
{
|
||||
public string LocalPath { get; }
|
||||
}
|
21
app/MindWork AI Studio/Tools/PluginSystem/ILang.cs
Normal file
21
app/MindWork AI Studio/Tools/PluginSystem/ILang.cs
Normal file
@ -0,0 +1,21 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a contract to access text from a language plugin.
|
||||
/// </summary>
|
||||
public interface ILang
|
||||
{
|
||||
/// <summary>
|
||||
/// Tries to get a text from the language plugin.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The given fallback text is used to determine the key for
|
||||
/// the language plugin. Base for the key is the namespace of
|
||||
/// the using component and the fallback text in English (US).
|
||||
/// The given text getting hashed. When the key does not exist,
|
||||
/// the fallback text will be returned.
|
||||
/// </remarks>
|
||||
/// <param name="fallbackEN">The fallback text in English (US).</param>
|
||||
/// <returns>The text from the language plugin or the fallback text.</returns>
|
||||
public string T(string fallbackEN);
|
||||
}
|
@ -16,6 +16,17 @@ public interface ILanguagePlugin
|
||||
/// </remarks>
|
||||
/// <param name="key">The key to use to get the text.</param>
|
||||
/// <param name="value">The desired text.</param>
|
||||
/// <param name="logWarning">When true, a warning will be logged if the key does not exist.</param>
|
||||
/// <returns>True if the key exists, false otherwise.</returns>
|
||||
public bool TryGetText(string key, out string value);
|
||||
public bool TryGetText(string key, out string value, bool logWarning = false);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the IETF tag of the language plugin.
|
||||
/// </summary>
|
||||
public string IETFTag { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the language.
|
||||
/// </summary>
|
||||
public string LangName { get; }
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
using Lua;
|
||||
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public sealed class NoPluginLanguage : PluginBase, ILanguagePlugin
|
||||
{
|
||||
public static readonly NoPluginLanguage INSTANCE = new();
|
||||
|
||||
private NoPluginLanguage() : base(true, LuaState.Create(), PluginType.LANGUAGE, string.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
#region Implementation of ILanguagePlugin
|
||||
|
||||
public bool TryGetText(string key, out string value, bool logWarning = false)
|
||||
{
|
||||
value = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
public string IETFTag => string.Empty;
|
||||
|
||||
public string LangName => string.Empty;
|
||||
|
||||
#endregion
|
||||
}
|
@ -4,6 +4,12 @@ public static partial class PluginFactory
|
||||
{
|
||||
public static void SetUpHotReloading()
|
||||
{
|
||||
if (!IS_INITIALIZED)
|
||||
{
|
||||
LOG.LogError("PluginFactory is not initialized. Please call Setup() before using it.");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.LogInformation($"Start hot reloading plugins for path '{HOT_RELOAD_WATCHER.Path}'.");
|
||||
try
|
||||
{
|
||||
|
@ -10,6 +10,12 @@ public static partial class PluginFactory
|
||||
{
|
||||
public static async Task EnsureInternalPlugins()
|
||||
{
|
||||
if (!IS_INITIALIZED)
|
||||
{
|
||||
LOG.LogError("PluginFactory is not initialized. Please call Setup() before using it.");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.LogInformation("Start ensuring internal plugins.");
|
||||
foreach (var plugin in Enum.GetValues<InternalPlugin>())
|
||||
{
|
||||
@ -40,15 +46,15 @@ public static partial class PluginFactory
|
||||
}
|
||||
|
||||
// Ensure that the additional resources exist:
|
||||
foreach (var content in resourceFileProvider.GetDirectoryContents(metaData.ResourcePath))
|
||||
foreach (var contentFilePath in resourceFileProvider.GetDirectoryContents(metaData.ResourcePath))
|
||||
{
|
||||
if(content.IsDirectory)
|
||||
if(contentFilePath.IsDirectory)
|
||||
{
|
||||
LOG.LogError("The plugin contains a directory. This is not allowed.");
|
||||
continue;
|
||||
}
|
||||
|
||||
await CopyInternalPluginFile(content, metaData);
|
||||
await CopyInternalPluginFile(contentFilePath, metaData);
|
||||
}
|
||||
}
|
||||
catch
|
||||
@ -57,9 +63,9 @@ public static partial class PluginFactory
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task CopyInternalPluginFile(IFileInfo resourceInfo, InternalPluginData metaData)
|
||||
private static async Task CopyInternalPluginFile(IFileInfo resourceFilePath, InternalPluginData metaData)
|
||||
{
|
||||
await using var inputStream = resourceInfo.CreateReadStream();
|
||||
await using var inputStream = resourceFilePath.CreateReadStream();
|
||||
|
||||
var pluginTypeBasePath = Path.Join(INTERNAL_PLUGINS_ROOT, metaData.Type.GetDirectory());
|
||||
|
||||
@ -73,7 +79,7 @@ public static partial class PluginFactory
|
||||
if (!Directory.Exists(pluginPath))
|
||||
Directory.CreateDirectory(pluginPath);
|
||||
|
||||
var pluginFilePath = Path.Join(pluginPath, resourceInfo.Name);
|
||||
var pluginFilePath = Path.Join(pluginPath, resourceFilePath.Name);
|
||||
|
||||
await using var outputStream = File.Create(pluginFilePath);
|
||||
await inputStream.CopyToAsync(outputStream);
|
||||
|
@ -9,29 +9,45 @@ namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public static partial class PluginFactory
|
||||
{
|
||||
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger("PluginFactory");
|
||||
|
||||
private static readonly string DATA_DIR = SettingsManager.DataDirectory!;
|
||||
|
||||
private static readonly string PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins");
|
||||
|
||||
private static readonly string INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal");
|
||||
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginFactory));
|
||||
private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
||||
private static readonly List<IAvailablePlugin> AVAILABLE_PLUGINS = [];
|
||||
private static readonly List<PluginBase> RUNNING_PLUGINS = [];
|
||||
|
||||
private static readonly FileSystemWatcher HOT_RELOAD_WATCHER;
|
||||
|
||||
private static readonly List<IPluginMetadata> AVAILABLE_PLUGINS = [];
|
||||
private static bool IS_INITIALIZED;
|
||||
private static string DATA_DIR = string.Empty;
|
||||
private static string PLUGINS_ROOT = string.Empty;
|
||||
private static string INTERNAL_PLUGINS_ROOT = string.Empty;
|
||||
private static FileSystemWatcher HOT_RELOAD_WATCHER = null!;
|
||||
private static ILanguagePlugin BASE_LANGUAGE_PLUGIN = NoPluginLanguage.INSTANCE;
|
||||
|
||||
/// <summary>
|
||||
/// A list of all available plugins.
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<IPluginMetadata> AvailablePlugins => AVAILABLE_PLUGINS;
|
||||
|
||||
/// <summary>
|
||||
/// A list of all running plugins.
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<PluginBase> RunningPlugins => RUNNING_PLUGINS;
|
||||
|
||||
static PluginFactory()
|
||||
public static ILanguagePlugin BaseLanguage => BASE_LANGUAGE_PLUGIN;
|
||||
|
||||
/// <summary>
|
||||
/// Set up the plugin factory. We will read the data directory from the settings manager.
|
||||
/// Afterward, we will create the plugins directory and the internal plugin directory.
|
||||
/// </summary>
|
||||
public static void Setup()
|
||||
{
|
||||
DATA_DIR = SettingsManager.DataDirectory!;
|
||||
PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins");
|
||||
INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal");
|
||||
|
||||
if (!Directory.Exists(PLUGINS_ROOT))
|
||||
Directory.CreateDirectory(PLUGINS_ROOT);
|
||||
|
||||
HOT_RELOAD_WATCHER = new(PLUGINS_ROOT);
|
||||
IS_INITIALIZED = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -48,6 +64,12 @@ public static partial class PluginFactory
|
||||
/// </remarks>
|
||||
public static async Task LoadAll(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!IS_INITIALIZED)
|
||||
{
|
||||
LOG.LogError("PluginFactory is not initialized. Please call Setup() before using it.");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.LogInformation("Start loading plugins.");
|
||||
if (!Directory.Exists(PLUGINS_ROOT))
|
||||
{
|
||||
@ -96,8 +118,11 @@ public static partial class PluginFactory
|
||||
}
|
||||
|
||||
LOG.LogInformation($"Successfully loaded plugin: '{pluginMainFile}' (Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}', Authors='{string.Join(", ", plugin.Authors)}')");
|
||||
AVAILABLE_PLUGINS.Add(new PluginMetadata(plugin));
|
||||
AVAILABLE_PLUGINS.Add(new PluginMetadata(plugin, pluginPath));
|
||||
}
|
||||
|
||||
// Start or restart all plugins:
|
||||
await RestartAllPlugins(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task<PluginBase> Load(string pluginPath, string code, CancellationToken cancellationToken = default)
|
||||
@ -148,9 +173,100 @@ public static partial class PluginFactory
|
||||
_ => new NoPlugin("This plugin type is not supported yet. Please try again with a future version of AI Studio.")
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task RestartAllPlugins(CancellationToken cancellationToken = default)
|
||||
{
|
||||
LOG.LogInformation("Try to start or restart all plugins.");
|
||||
RUNNING_PLUGINS.Clear();
|
||||
|
||||
//
|
||||
// Get the base language plugin. This is the plugin that will be used to fill in missing keys.
|
||||
//
|
||||
var baseLanguagePluginId = InternalPlugin.LANGUAGE_EN_US.MetaData().Id;
|
||||
var baseLanguagePluginMetaData = AVAILABLE_PLUGINS.FirstOrDefault(p => p.Id == baseLanguagePluginId);
|
||||
if (baseLanguagePluginMetaData is null)
|
||||
{
|
||||
LOG.LogError($"Was not able to find the base language plugin: Id='{baseLanguagePluginId}'. Please check your installation.");
|
||||
return;
|
||||
}
|
||||
|
||||
var startedBasePlugin = await Start(baseLanguagePluginMetaData, cancellationToken);
|
||||
if (startedBasePlugin is NoPlugin noPlugin)
|
||||
{
|
||||
LOG.LogError($"Was not able to start the base language plugin: Id='{baseLanguagePluginId}'. Reason: {noPlugin.Issues.First()}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (startedBasePlugin is PluginLanguage languagePlugin)
|
||||
{
|
||||
BASE_LANGUAGE_PLUGIN = languagePlugin;
|
||||
RUNNING_PLUGINS.Add(languagePlugin);
|
||||
LOG.LogInformation($"Successfully started the base language plugin: Id='{languagePlugin.Id}', Type='{languagePlugin.Type}', Name='{languagePlugin.Name}', Version='{languagePlugin.Version}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.LogError($"Was not able to start the base language plugin: Id='{baseLanguagePluginId}'. Reason: {string.Join("; ", startedBasePlugin.Issues)}");
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Iterate over all available plugins and try to start them.
|
||||
//
|
||||
foreach (var availablePlugin in AVAILABLE_PLUGINS)
|
||||
{
|
||||
if(cancellationToken.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
if (availablePlugin.Id == baseLanguagePluginId)
|
||||
continue;
|
||||
|
||||
if (availablePlugin.IsInternal || SETTINGS_MANAGER.IsPluginEnabled(availablePlugin))
|
||||
if(await Start(availablePlugin, cancellationToken) is { IsValid: true } plugin)
|
||||
RUNNING_PLUGINS.Add(plugin);
|
||||
|
||||
// Inform all components that the plugins have been reloaded or started:
|
||||
await MessageBus.INSTANCE.SendMessage<bool>(null, Event.PLUGINS_RELOADED);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<PluginBase> Start(IAvailablePlugin meta, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var pluginMainFile = Path.Join(meta.LocalPath, "plugin.lua");
|
||||
if(!File.Exists(pluginMainFile))
|
||||
{
|
||||
LOG.LogError($"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reason: The plugin file does not exist.");
|
||||
return new NoPlugin($"The plugin file does not exist: {pluginMainFile}");
|
||||
}
|
||||
|
||||
var code = await File.ReadAllTextAsync(pluginMainFile, Encoding.UTF8, cancellationToken);
|
||||
var plugin = await Load(meta.LocalPath, code, cancellationToken);
|
||||
if (plugin is NoPlugin noPlugin)
|
||||
{
|
||||
LOG.LogError($"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reason: {noPlugin.Issues.First()}");
|
||||
return noPlugin;
|
||||
}
|
||||
|
||||
if (plugin.IsValid)
|
||||
{
|
||||
//
|
||||
// When this is a language plugin, we need to set the base language plugin.
|
||||
//
|
||||
if (plugin is PluginLanguage languagePlugin && BASE_LANGUAGE_PLUGIN != NoPluginLanguage.INSTANCE)
|
||||
languagePlugin.SetBaseLanguage(BASE_LANGUAGE_PLUGIN);
|
||||
|
||||
LOG.LogInformation($"Successfully started plugin: Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}'");
|
||||
return plugin;
|
||||
}
|
||||
|
||||
LOG.LogError($"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reasons: {string.Join("; ", plugin.Issues)}");
|
||||
return new NoPlugin($"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reasons: {string.Join("; ", plugin.Issues)}");
|
||||
}
|
||||
|
||||
public static void Dispose()
|
||||
{
|
||||
if(!IS_INITIALIZED)
|
||||
return;
|
||||
|
||||
HOT_RELOAD_WATCHER.Dispose();
|
||||
}
|
||||
}
|
@ -4,9 +4,12 @@ namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public sealed class PluginLanguage : PluginBase, ILanguagePlugin
|
||||
{
|
||||
private static readonly ILogger<PluginLanguage> LOGGER = Program.LOGGER_FACTORY.CreateLogger<PluginLanguage>();
|
||||
|
||||
private readonly Dictionary<string, string> content = [];
|
||||
private readonly List<ILanguagePlugin> otherLanguagePlugins = [];
|
||||
private readonly string langCultureTag;
|
||||
private readonly string langName;
|
||||
|
||||
private ILanguagePlugin? baseLanguage;
|
||||
|
||||
@ -15,6 +18,9 @@ public sealed class PluginLanguage : PluginBase, ILanguagePlugin
|
||||
if(!this.TryInitIETFTag(out var issue, out this.langCultureTag))
|
||||
this.pluginIssues.Add(issue);
|
||||
|
||||
if(!this.TryInitLangName(out issue, out this.langName))
|
||||
this.pluginIssues.Add(issue);
|
||||
|
||||
if (this.TryInitUITextContent(out issue, out var readContent))
|
||||
this.content = readContent;
|
||||
else
|
||||
@ -36,39 +42,6 @@ public sealed class PluginLanguage : PluginBase, ILanguagePlugin
|
||||
/// </remarks>
|
||||
/// <param name="languagePlugin">The language plugin to add.</param>
|
||||
public void AddOtherLanguagePlugin(ILanguagePlugin languagePlugin) => this.otherLanguagePlugins.Add(languagePlugin);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a text from the language plugin.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the key neither in the base language nor in this language exist,
|
||||
/// the value will be an empty string. Please note that the key is case-sensitive.
|
||||
/// Furthermore, the keys are in the format "root::key". That means that
|
||||
/// the keys are hierarchical and separated by "::".
|
||||
/// </remarks>
|
||||
/// <param name="key">The key to use to get the text.</param>
|
||||
/// <param name="value">The desired text.</param>
|
||||
/// <returns>True if the key exists, false otherwise.</returns>
|
||||
public bool TryGetText(string key, out string value)
|
||||
{
|
||||
// First, we check if the key is part of the main language pack:
|
||||
if (this.content.TryGetValue(key, out value!))
|
||||
return true;
|
||||
|
||||
// Second, we check if the key is part of the other language packs, such as the assistant plugins:
|
||||
foreach (var otherLanguagePlugin in this.otherLanguagePlugins)
|
||||
if(otherLanguagePlugin.TryGetText(key, out value))
|
||||
return true;
|
||||
|
||||
// Finally, we check if the key is part of the base language pack. This is the case,
|
||||
// when a language plugin does not cover all keys. In this case, the base language plugin
|
||||
// will be used to fill in the missing keys:
|
||||
if(this.baseLanguage is not null && this.baseLanguage.TryGetText(key, out value))
|
||||
return true;
|
||||
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to initialize the IETF tag.
|
||||
@ -127,4 +100,71 @@ public sealed class PluginLanguage : PluginBase, ILanguagePlugin
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryInitLangName(out string message, out string readLangName)
|
||||
{
|
||||
if (!this.state.Environment["LANG_NAME"].TryRead(out readLangName))
|
||||
{
|
||||
message = "The field LANG_NAME does not exist or is not a valid string.";
|
||||
readLangName = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(readLangName))
|
||||
{
|
||||
message = "The field LANG_NAME is empty. Use a valid language name.";
|
||||
readLangName = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
message = string.Empty;
|
||||
return true;
|
||||
}
|
||||
|
||||
#region Implementation of ILanguagePlugin
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a text from the language plugin.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the key neither in the base language nor in this language exist,
|
||||
/// the value will be an empty string. Please note that the key is case-sensitive.
|
||||
/// Furthermore, the keys are in the format "root::key". That means that
|
||||
/// the keys are hierarchical and separated by "::".
|
||||
/// </remarks>
|
||||
/// <param name="key">The key to use to get the text.</param>
|
||||
/// <param name="value">The desired text.</param>
|
||||
/// <param name="logWarning">When true, a warning will be logged if the key does not exist.</param>
|
||||
/// <returns>True if the key exists, false otherwise.</returns>
|
||||
public bool TryGetText(string key, out string value, bool logWarning = false)
|
||||
{
|
||||
// First, we check if the key is part of the main language pack:
|
||||
if (this.content.TryGetValue(key, out value!))
|
||||
return true;
|
||||
|
||||
// Second, we check if the key is part of the other language packs, such as the assistant plugins:
|
||||
foreach (var otherLanguagePlugin in this.otherLanguagePlugins)
|
||||
if(otherLanguagePlugin.TryGetText(key, out value))
|
||||
return true;
|
||||
|
||||
// Finally, we check if the key is part of the base language pack. This is the case,
|
||||
// when a language plugin does not cover all keys. In this case, the base language plugin
|
||||
// will be used to fill in the missing keys:
|
||||
if(this.baseLanguage is not null && this.baseLanguage.TryGetText(key, out value))
|
||||
return true;
|
||||
|
||||
if(logWarning)
|
||||
LOGGER.LogWarning($"Missing translation key '{key}'.");
|
||||
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string IETFTag => this.langCultureTag;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string LangName => this.langName;
|
||||
|
||||
#endregion
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
namespace AIStudio.Tools.PluginSystem;
|
||||
|
||||
public sealed class PluginMetadata(PluginBase plugin) : IPluginMetadata
|
||||
public sealed class PluginMetadata(PluginBase plugin, string localPath) : IAvailablePlugin
|
||||
{
|
||||
#region Implementation of IPluginMetadata
|
||||
|
||||
@ -47,4 +47,10 @@ public sealed class PluginMetadata(PluginBase plugin) : IPluginMetadata
|
||||
public bool IsInternal { get; } = plugin.IsInternal;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of IAvailablePlugin
|
||||
|
||||
public string LocalPath { get; } = localPath;
|
||||
|
||||
#endregion
|
||||
}
|
Loading…
Reference in New Issue
Block a user