diff --git a/app/MindWork AI Studio/Pages/Plugins.razor.cs b/app/MindWork AI Studio/Pages/Plugins.razor.cs
index 9504c0ff..88c90433 100644
--- a/app/MindWork AI Studio/Pages/Plugins.razor.cs
+++ b/app/MindWork AI Studio/Pages/Plugins.razor.cs
@@ -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
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(this, Event.CONFIGURATION_CHANGED);
}
-
- #region Implementation of IMessageBusReceiver
-
- public string ComponentName => nameof(Plugins);
-
- public Task ProcessMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
- {
- switch (triggeredEvent)
- {
- case Event.PLUGINS_RELOADED:
- this.InvokeAsync(this.StateHasChanged);
- break;
- }
-
- return Task.CompletedTask;
- }
-
- public Task ProcessMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data)
- {
- return Task.FromResult(default);
- }
-
- #endregion
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Pages/Writer.razor.cs b/app/MindWork AI Studio/Pages/Writer.razor.cs
index 42b23abc..a36c0410 100644
--- a/app/MindWork AI Studio/Pages/Writer.razor.cs
+++ b/app/MindWork AI Studio/Pages/Writer.razor.cs
@@ -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(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
- {
- return Task.CompletedTask;
- }
-
- public override Task ProcessMessageWithResult(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;
diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
index 634c8e74..b301dbd7 100644
--- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
@@ -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",
+ }
+ },
+ }
}
diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
index 78f66474..2687f722 100644
--- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
@@ -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",
+ },
+ },
+ }
}
diff --git a/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs b/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs
index 5a2b8f11..a47bde9d 100644
--- a/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs
+++ b/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs
@@ -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(string Name, T Value);
///
public static class ConfigurationSelectDataFactory
{
+ public static IEnumerable> GetLangBehaviorData()
+ {
+ foreach (var behavior in Enum.GetValues())
+ yield return new(behavior.Name(), behavior);
+ }
+
+ public static IEnumerable> GetLanguagesData()
+ {
+ foreach (var runningPlugin in PluginFactory.RunningPlugins)
+ {
+ if(runningPlugin is ILanguagePlugin languagePlugin)
+ yield return new(languagePlugin.LangName, runningPlugin.Id);
+ }
+ }
+
public static IEnumerable> 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);
diff --git a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs
index 7e89404a..af76fa26 100644
--- a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs
+++ b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs
@@ -2,6 +2,16 @@ namespace AIStudio.Settings.DataModel;
public sealed class DataApp
{
+ ///
+ /// The language behavior.
+ ///
+ public LangBehavior LanguageBehavior { get; set; } = LangBehavior.AUTO;
+
+ ///
+ /// The language plugin ID to use.
+ ///
+ public Guid LanguagePluginId { get; set; } = Guid.Empty;
+
///
/// The preferred theme to use.
///
diff --git a/app/MindWork AI Studio/Settings/DataModel/LangBehavior.cs b/app/MindWork AI Studio/Settings/DataModel/LangBehavior.cs
new file mode 100644
index 00000000..492d6bca
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/DataModel/LangBehavior.cs
@@ -0,0 +1,7 @@
+namespace AIStudio.Settings.DataModel;
+
+public enum LangBehavior
+{
+ AUTO,
+ MANUAL,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/DataModel/LangBehaviourExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/LangBehaviourExtensions.cs
new file mode 100644
index 00000000..c71caf92
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/DataModel/LangBehaviourExtensions.cs
@@ -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"
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs
index dcbe48cb..24bd1ded 100644
--- a/app/MindWork AI Studio/Settings/SettingsManager.cs
+++ b/app/MindWork AI Studio/Settings/SettingsManager.cs
@@ -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;
///
/// The settings manager.
///
-public sealed class SettingsManager(ILogger logger)
+public sealed class SettingsManager(ILogger logger, RustService rustService)
{
private const string SETTINGS_FILENAME = "settings.json";
@@ -24,6 +25,7 @@ public sealed class SettingsManager(ILogger logger)
};
private readonly ILogger logger = logger;
+ private readonly RustService rustService = rustService;
///
/// The directory where the configuration files are stored.
@@ -143,8 +145,53 @@ public sealed class SettingsManager(ILogger logger)
return minimumLevel;
}
+ ///
+ /// Checks if the given plugin is enabled.
+ ///
+ /// The plugin to check.
+ /// True, when the plugin is enabled, false otherwise.
public bool IsPluginEnabled(IPluginMetadata plugin) => this.ConfigurationData.EnabledPlugins.Contains(plugin.Id);
+ ///
+ /// Returns the active language plugin.
+ ///
+ /// The active language plugin.
+ public async Task 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)
{
diff --git a/app/MindWork AI Studio/Tools/Event.cs b/app/MindWork AI Studio/Tools/Event.cs
index 57758589..213adf29 100644
--- a/app/MindWork AI Studio/Tools/Event.cs
+++ b/app/MindWork AI Studio/Tools/Event.cs
@@ -8,6 +8,7 @@ public enum Event
STATE_HAS_CHANGED,
CONFIGURATION_CHANGED,
COLOR_THEME_CHANGED,
+ STARTUP_PLUGIN_SYSTEM,
PLUGINS_RELOADED,
SHOW_ERROR,
diff --git a/app/MindWork AI Studio/Tools/FNVHash.cs b/app/MindWork AI Studio/Tools/FNVHash.cs
new file mode 100644
index 00000000..cc47645e
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/FNVHash.cs
@@ -0,0 +1,62 @@
+// ReSharper disable MemberCanBePrivate.Global
+namespace AIStudio.Tools;
+
+///
+/// Implements the Fowler–Noll–Vo hash function for 32-bit and 64-bit hashes.
+///
+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;
+
+ ///
+ /// Computes the 32bit FNV-1a hash of a string.
+ ///
+ /// The string to hash.
+ /// The 32bit FNV-1a hash of the string.
+ public static uint ToFNV32(this string text) => ToFNV32(text.AsSpan());
+
+ ///
+ /// Computes the 32bit FNV-1a hash of a string.
+ ///
+ /// The string to hash.
+ /// The 32bit FNV-1a hash of the string.
+ public static uint ToFNV32(this ReadOnlySpan text)
+ {
+ var hash = FNV_OFFSET_BASIS_32_BIT;
+ foreach (var c in text)
+ {
+ hash ^= c;
+ hash *= FNV_PRIME_32_BIT;
+ }
+
+ return hash;
+ }
+
+ ///
+ /// Computes the 64bit FNV-1a hash of a string.
+ ///
+ /// The string to hash.
+ /// The 64bit FNV-1a hash of the string.
+ public static ulong ToFNV64(this string text) => ToFNV64(text.AsSpan());
+
+ ///
+ /// Computes the 64bit FNV-1a hash of a string.
+ ///
+ /// The string to hash.
+ /// The 64bit FNV-1a hash of the string.
+ public static ulong ToFNV64(this ReadOnlySpan text)
+ {
+ var hash = FNV_OFFSET_BASIS_64_BIT;
+ foreach (var c in text)
+ {
+ hash ^= c;
+ hash *= FNV_PRIME_64_BIT;
+ }
+
+ return hash;
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/IAvailablePlugin.cs b/app/MindWork AI Studio/Tools/PluginSystem/IAvailablePlugin.cs
new file mode 100644
index 00000000..a992d303
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/IAvailablePlugin.cs
@@ -0,0 +1,6 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public interface IAvailablePlugin : IPluginMetadata
+{
+ public string LocalPath { get; }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/ILang.cs b/app/MindWork AI Studio/Tools/PluginSystem/ILang.cs
new file mode 100644
index 00000000..6c5277ea
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/ILang.cs
@@ -0,0 +1,21 @@
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Represents a contract to access text from a language plugin.
+///
+public interface ILang
+{
+ ///
+ /// Tries to get a text from the language plugin.
+ ///
+ ///
+ /// 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.
+ ///
+ /// The fallback text in English (US).
+ /// The text from the language plugin or the fallback text.
+ public string T(string fallbackEN);
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/ILanguagePlugin.cs b/app/MindWork AI Studio/Tools/PluginSystem/ILanguagePlugin.cs
index a33bf3f5..4f214d68 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/ILanguagePlugin.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/ILanguagePlugin.cs
@@ -16,6 +16,17 @@ public interface ILanguagePlugin
///
/// The key to use to get the text.
/// The desired text.
+ /// When true, a warning will be logged if the key does not exist.
/// True if the key exists, false otherwise.
- public bool TryGetText(string key, out string value);
+ public bool TryGetText(string key, out string value, bool logWarning = false);
+
+ ///
+ /// Gets the IETF tag of the language plugin.
+ ///
+ public string IETFTag { get; }
+
+ ///
+ /// Gets the name of the language.
+ ///
+ public string LangName { get; }
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/NoPluginLanguage.cs b/app/MindWork AI Studio/Tools/PluginSystem/NoPluginLanguage.cs
new file mode 100644
index 00000000..26d35849
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/NoPluginLanguage.cs
@@ -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
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs
index c2d75bf3..0a32c17d 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs
@@ -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
{
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs
index 2c14adb6..0a9e9dac 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs
@@ -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())
{
@@ -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);
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs
index 18d2023b..57ac7ea1 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs
@@ -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();
+ private static readonly List AVAILABLE_PLUGINS = [];
+ private static readonly List RUNNING_PLUGINS = [];
- private static readonly FileSystemWatcher HOT_RELOAD_WATCHER;
-
- private static readonly List 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;
///
/// A list of all available plugins.
///
public static IReadOnlyCollection AvailablePlugins => AVAILABLE_PLUGINS;
+
+ ///
+ /// A list of all running plugins.
+ ///
+ public static IReadOnlyCollection RunningPlugins => RUNNING_PLUGINS;
- static PluginFactory()
+ public static ILanguagePlugin BaseLanguage => BASE_LANGUAGE_PLUGIN;
+
+ ///
+ /// 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.
+ ///
+ 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;
}
///
@@ -48,6 +64,12 @@ public static partial class PluginFactory
///
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 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(null, Event.PLUGINS_RELOADED);
+ }
+ }
+
+ private static async Task 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();
}
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs
index 384d81eb..1c201a11 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs
@@ -4,9 +4,12 @@ namespace AIStudio.Tools.PluginSystem;
public sealed class PluginLanguage : PluginBase, ILanguagePlugin
{
+ private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger();
+
private readonly Dictionary content = [];
private readonly List 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
///
/// The language plugin to add.
public void AddOtherLanguagePlugin(ILanguagePlugin languagePlugin) => this.otherLanguagePlugins.Add(languagePlugin);
-
- ///
- /// Tries to get a text from the language plugin.
- ///
- ///
- /// 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 "::".
- ///
- /// The key to use to get the text.
- /// The desired text.
- /// True if the key exists, false otherwise.
- 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;
- }
///
/// 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
+
+ ///
+ /// Tries to get a text from the language plugin.
+ ///
+ ///
+ /// 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 "::".
+ ///
+ /// The key to use to get the text.
+ /// The desired text.
+ /// When true, a warning will be logged if the key does not exist.
+ /// True if the key exists, false otherwise.
+ 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;
+ }
+
+ ///
+ public string IETFTag => this.langCultureTag;
+
+ ///
+ public string LangName => this.langName;
+
+ #endregion
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs
index 2bfdab1e..e98644cb 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs
@@ -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
}
\ No newline at end of file