\ No newline at end of file
diff --git a/app/MindWork AI Studio/Pages/Plugins.razor.cs b/app/MindWork AI Studio/Pages/Plugins.razor.cs
new file mode 100644
index 00000000..f2dc83d9
--- /dev/null
+++ b/app/MindWork AI Studio/Pages/Plugins.razor.cs
@@ -0,0 +1,56 @@
+using AIStudio.Settings;
+using AIStudio.Tools.PluginSystem;
+
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Pages;
+
+public partial class Plugins : ComponentBase
+{
+ 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
+
+ protected override async Task OnInitializedAsync()
+ {
+ this.groupConfig = new TableGroupDefinition
+ {
+ Expandable = true,
+ IsInitiallyExpanded = true,
+ Selector = pluginMeta =>
+ {
+ if (pluginMeta.IsInternal)
+ return GROUP_INTERNAL;
+
+ return this.SettingsManager.IsPluginEnabled(pluginMeta)
+ ? GROUP_ENABLED
+ : GROUP_DISABLED;
+ }
+ };
+
+ await base.OnInitializedAsync();
+ }
+
+ #endregion
+
+ private async Task PluginActivationStateChanged(IPluginMetadata pluginMeta)
+ {
+ if (this.SettingsManager.IsPluginEnabled(pluginMeta))
+ this.SettingsManager.ConfigurationData.EnabledPlugins.Remove(pluginMeta.Id);
+ else
+ this.SettingsManager.ConfigurationData.EnabledPlugins.Add(pluginMeta.Id);
+
+ await this.SettingsManager.StoreSettings();
+ await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED);
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Pages/Writer.razor b/app/MindWork AI Studio/Pages/Writer.razor
index 6108647b..790e3e07 100644
--- a/app/MindWork AI Studio/Pages/Writer.razor
+++ b/app/MindWork AI Studio/Pages/Writer.razor
@@ -2,11 +2,10 @@
@inherits MSGComponentBase
-
+
Writer
-
-
+
diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/icon.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/icon.lua
new file mode 100644
index 00000000..c10dd294
--- /dev/null
+++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/icon.lua
@@ -0,0 +1,11 @@
+SVG = [[]]
\ No newline at end of file
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 4801b782..ab05f0bc 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
@@ -1,6 +1,12 @@
+require("contentHome")
+require("icon")
+
-- The ID for this plugin:
ID = "43065dbc-78d0-45b7-92be-f14c2926e2dc"
+-- The icon for the plugin:
+ICON_SVG = SVG
+
-- The name of the plugin:
NAME = "MindWork AI Studio - German / Deutsch"
@@ -32,8 +38,8 @@ TARGET_GROUPS = { "EVERYONE" }
IS_MAINTAINED = true
-- When the plugin is deprecated, this message will be shown to users:
-DEPRECATION_MESSAGE = nil
+DEPRECATION_MESSAGE = ""
UI_TEXT_CONTENT = {
HOME = CONTENT_HOME,
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/icon.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/icon.lua
new file mode 100644
index 00000000..75320473
--- /dev/null
+++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/icon.lua
@@ -0,0 +1,12 @@
+SVG = [[]]
\ No newline at end of file
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 6e2fea50..d6c98d49 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
@@ -1,8 +1,12 @@
require("contentHome")
+require("icon")
-- The ID for this plugin:
ID = "97dfb1ba-50c4-4440-8dfa-6575daf543c8"
+-- The icon for the plugin:
+ICON_SVG = SVG
+
-- The name of the plugin:
NAME = "MindWork AI Studio - US English"
@@ -34,8 +38,8 @@ TARGET_GROUPS = { "EVERYONE" }
IS_MAINTAINED = true
-- When the plugin is deprecated, this message will be shown to users:
-DEPRECATION_MESSAGE = nil
+DEPRECATION_MESSAGE = ""
UI_TEXT_CONTENT = {
HOME = CONTENT_HOME,
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Routes.razor.cs b/app/MindWork AI Studio/Routes.razor.cs
index 98326821..b6318820 100644
--- a/app/MindWork AI Studio/Routes.razor.cs
+++ b/app/MindWork AI Studio/Routes.razor.cs
@@ -9,6 +9,7 @@ public sealed partial class Routes
public const string SETTINGS = "/settings";
public const string SUPPORTERS = "/supporters";
public const string WRITER = "/writer";
+ public const string PLUGINS = "/plugins";
// ReSharper disable InconsistentNaming
public const string ASSISTANT_TRANSLATION = "/assistant/translation";
diff --git a/app/MindWork AI Studio/Settings/DataModel/Data.cs b/app/MindWork AI Studio/Settings/DataModel/Data.cs
index b7139c72..b47eba49 100644
--- a/app/MindWork AI Studio/Settings/DataModel/Data.cs
+++ b/app/MindWork AI Studio/Settings/DataModel/Data.cs
@@ -36,6 +36,11 @@ public sealed class Data
///
public List Profiles { get; init; } = [];
+ ///
+ /// List of enabled plugins.
+ ///
+ public List EnabledPlugins { get; set; } = [];
+
///
/// The next provider number to use.
///
diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs
index 0930c9a5..ec74cab8 100644
--- a/app/MindWork AI Studio/Settings/SettingsManager.cs
+++ b/app/MindWork AI Studio/Settings/SettingsManager.cs
@@ -4,6 +4,7 @@ using System.Text.Json.Serialization;
using AIStudio.Provider;
using AIStudio.Settings.DataModel;
+using AIStudio.Tools.PluginSystem;
// ReSharper disable NotAccessedPositionalProperty.Local
@@ -142,6 +143,8 @@ public sealed class SettingsManager(ILogger logger)
return minimumLevel;
}
+ public bool IsPluginEnabled(IPluginMetadata plugin) => this.ConfigurationData.EnabledPlugins.Contains(plugin.Id);
+
[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/PluginSystem/IPluginMetadata.cs b/app/MindWork AI Studio/Tools/PluginSystem/IPluginMetadata.cs
new file mode 100644
index 00000000..95d26b34
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/IPluginMetadata.cs
@@ -0,0 +1,74 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public interface IPluginMetadata
+{
+ ///
+ /// The icon of this plugin.
+ ///
+ public string IconSVG { get; }
+
+ ///
+ /// The type of this plugin.
+ ///
+ public PluginType Type { get; }
+
+ ///
+ /// The ID of this plugin.
+ ///
+ public Guid Id { get; }
+
+ ///
+ /// The name of this plugin.
+ ///
+ public string Name { get; }
+
+ ///
+ /// The description of this plugin.
+ ///
+ public string Description { get; }
+
+ ///
+ /// The version of this plugin.
+ ///
+ public PluginVersion Version { get; }
+
+ ///
+ /// The authors of this plugin.
+ ///
+ public string[] Authors { get; }
+
+ ///
+ /// The support contact for this plugin.
+ ///
+ public string SupportContact { get; }
+
+ ///
+ /// The source URL of this plugin.
+ ///
+ public string SourceURL { get; }
+
+ ///
+ /// The categories of this plugin.
+ ///
+ public PluginCategory[] Categories { get; }
+
+ ///
+ /// The target groups of this plugin.
+ ///
+ public PluginTargetGroup[] TargetGroups { get; }
+
+ ///
+ /// True, when the plugin is maintained.
+ ///
+ public bool IsMaintained { get; }
+
+ ///
+ /// The message that should be displayed when the plugin is deprecated.
+ ///
+ public string DeprecationMessage { get; }
+
+ ///
+ /// True, when the plugin is AI Studio internal.
+ ///
+ public bool IsInternal { get; }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/NoPlugin.cs b/app/MindWork AI Studio/Tools/PluginSystem/NoPlugin.cs
index a21ab819..3d9b74d1 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/NoPlugin.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/NoPlugin.cs
@@ -6,4 +6,4 @@ namespace AIStudio.Tools.PluginSystem;
/// Represents a plugin that could not be loaded.
///
/// The error message that occurred while parsing the plugin.
-public sealed class NoPlugin(string parsingError) : PluginBase(string.Empty, LuaState.Create(), PluginType.NONE, parsingError);
\ No newline at end of file
+public sealed class NoPlugin(string parsingError) : PluginBase(false, LuaState.Create(), PluginType.NONE, parsingError);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.Icon.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.Icon.cs
new file mode 100644
index 00000000..5c6140c8
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.Icon.cs
@@ -0,0 +1,45 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public abstract partial class PluginBase
+{
+ private const string DEFAULT_ICON_SVG =
+ """
+
+ """;
+
+ #region Initialization-related methods
+
+ ///
+ /// Tries to initialize the icon of the plugin.
+ ///
+ ///
+ /// When no icon is specified, the default icon will be used.
+ ///
+ /// The error message, when the icon could not be read.
+ /// The read icon as SVG.
+ /// True, when the icon could be read successfully.
+
+ // ReSharper disable once OutParameterValueIsAlwaysDiscarded.Local
+ // ReSharper disable once UnusedMethodReturnValue.Local
+ private bool TryInitIconSVG(out string message, out string iconSVG)
+ {
+ if (!this.state.Environment["ICON_SVG"].TryRead(out iconSVG))
+ {
+ iconSVG = DEFAULT_ICON_SVG;
+ message = "The field ICON_SVG does not exist or is not a valid string.";
+ return true;
+ }
+
+ if (string.IsNullOrWhiteSpace(iconSVG))
+ {
+ iconSVG = DEFAULT_ICON_SVG;
+ message = "The field ICON_SVG is empty. The icon must be a non-empty string.";
+ return true;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs
index 4f719d18..2674d4db 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs
@@ -1,5 +1,4 @@
using Lua;
-using Lua.Standard;
// ReSharper disable MemberCanBePrivate.Global
namespace AIStudio.Tools.PluginSystem;
@@ -7,73 +6,55 @@ namespace AIStudio.Tools.PluginSystem;
///
/// Represents the base of any AI Studio plugin.
///
-public abstract class PluginBase
+public abstract partial class PluginBase : IPluginMetadata
{
private readonly IReadOnlyCollection baseIssues;
protected readonly LuaState state;
protected readonly List pluginIssues = [];
+
+ ///
+ public string IconSVG { get; }
- ///
- /// The type of this plugin.
- ///
+ ///
public PluginType Type { get; }
- ///
- /// The ID of this plugin.
- ///
+ ///
public Guid Id { get; }
- ///
- /// The name of this plugin.
- ///
+ ///
public string Name { get; } = string.Empty;
- ///
- /// The description of this plugin.
- ///
+ ///
public string Description { get; } = string.Empty;
- ///
- /// The version of this plugin.
- ///
+ ///
public PluginVersion Version { get; }
- ///
- /// The authors of this plugin.
- ///
+ ///
public string[] Authors { get; } = [];
- ///
- /// The support contact for this plugin.
- ///
+ ///
public string SupportContact { get; } = string.Empty;
- ///
- /// The source URL of this plugin.
- ///
+ ///
public string SourceURL { get; } = string.Empty;
- ///
- /// The categories of this plugin.
- ///
+ ///
public PluginCategory[] Categories { get; } = [];
- ///
- /// The target groups of this plugin.
- ///
+ ///
public PluginTargetGroup[] TargetGroups { get; } = [];
- ///
- /// True, when the plugin is maintained.
- ///
+ ///
public bool IsMaintained { get; }
- ///
- /// The message that should be displayed when the plugin is deprecated.
- ///
- public string? DeprecationMessage { get; }
-
+ ///
+ public string DeprecationMessage { get; } = string.Empty;
+
+ ///
+ public bool IsInternal { get; }
+
///
/// The issues that occurred during the initialization of this plugin.
///
@@ -88,31 +69,24 @@ public abstract class PluginBase
///
public bool IsValid => this is not NoPlugin && this.baseIssues.Count == 0 && this.pluginIssues.Count == 0;
- protected PluginBase(string path, LuaState state, PluginType type, string parseError = "")
+ protected PluginBase(bool isInternal, LuaState state, PluginType type, string parseError = "")
{
this.state = state;
this.Type = type;
-
- // For security reasons, we don't want to allow the plugin to load modules:
- this.state.ModuleLoader = new NoModuleLoader();
-
- // Add some useful libraries:
- this.state.OpenModuleLibrary();
- this.state.OpenStringLibrary();
- this.state.OpenTableLibrary();
- this.state.OpenMathLibrary();
- this.state.OpenBitwiseLibrary();
- this.state.OpenCoroutineLibrary();
-
- // Add the module loader so that the plugin can load other Lua modules:
- this.state.ModuleLoader = new PluginLoader(path);
var issues = new List();
if(!string.IsNullOrWhiteSpace(parseError))
issues.Add(parseError);
+
+ // Notice: when no icon is specified, the default icon will be used.
+ this.TryInitIconSVG(out _, out var iconSVG);
+ this.IconSVG = iconSVG;
if(this.TryInitId(out var issue, out var id))
+ {
this.Id = id;
+ this.IsInternal = isInternal;
+ }
else if(this is not NoPlugin)
issues.Add(issue);
@@ -456,19 +430,19 @@ public abstract class PluginBase
/// The error message, when the deprecation message could not be read.
/// The read deprecation message.
/// True, when the deprecation message could be read successfully.
- private bool TryInitDeprecationMessage(out string message, out string? deprecationMessage)
+ private bool TryInitDeprecationMessage(out string message, out string deprecationMessage)
{
if (!this.state.Environment["DEPRECATION_MESSAGE"].TryRead(out deprecationMessage))
{
- deprecationMessage = null;
- message = "The field DEPRECATION_MESSAGE does not exist, is not a valid string. This field is optional: use nil to indicate that the plugin is not deprecated.";
+ deprecationMessage = string.Empty;
+ message = "The field DEPRECATION_MESSAGE does not exist, is not a valid string. This message is optional: use an empty string to indicate that the plugin is not deprecated.";
return false;
}
message = string.Empty;
return true;
}
-
+
///
/// Tries to initialize the UI text content of the plugin.
///
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs
new file mode 100644
index 00000000..316376f6
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Internal.cs
@@ -0,0 +1,79 @@
+using System.Reflection;
+
+using Microsoft.Extensions.FileProviders;
+
+namespace AIStudio.Tools.PluginSystem;
+
+public static partial class PluginFactory
+{
+ public static async Task EnsureInternalPlugins()
+ {
+ LOG.LogInformation("Start ensuring internal plugins.");
+ foreach (var plugin in Enum.GetValues())
+ {
+ LOG.LogInformation($"Ensure plugin: {plugin}");
+ await EnsurePlugin(plugin);
+ }
+ }
+
+ private static async Task EnsurePlugin(InternalPlugin plugin)
+ {
+ try
+ {
+ #if DEBUG
+ var basePath = Path.Join(Environment.CurrentDirectory, "Plugins");
+ var resourceFileProvider = new PhysicalFileProvider(basePath);
+ #else
+ var resourceFileProvider = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "Plugins");
+ #endif
+
+ var metaData = plugin.MetaData();
+ var mainResourcePath = $"{metaData.ResourcePath}/plugin.lua";
+ var resourceInfo = resourceFileProvider.GetFileInfo(mainResourcePath);
+
+ if(!resourceInfo.Exists)
+ {
+ LOG.LogError($"The plugin {plugin} does not exist. This should not happen, since the plugin is an integral part of AI Studio.");
+ return;
+ }
+
+ // Ensure that the additional resources exist:
+ foreach (var content in resourceFileProvider.GetDirectoryContents(metaData.ResourcePath))
+ {
+ if(content.IsDirectory)
+ {
+ LOG.LogError("The plugin contains a directory. This is not allowed.");
+ continue;
+ }
+
+ await CopyInternalPluginFile(content, metaData);
+ }
+ }
+ catch
+ {
+ LOG.LogError($"Was not able to ensure the plugin: {plugin}");
+ }
+ }
+
+ private static async Task CopyInternalPluginFile(IFileInfo resourceInfo, InternalPluginData metaData)
+ {
+ await using var inputStream = resourceInfo.CreateReadStream();
+
+ var pluginTypeBasePath = Path.Join(INTERNAL_PLUGINS_ROOT, metaData.Type.GetDirectory());
+
+ if (!Directory.Exists(INTERNAL_PLUGINS_ROOT))
+ Directory.CreateDirectory(INTERNAL_PLUGINS_ROOT);
+
+ if (!Directory.Exists(pluginTypeBasePath))
+ Directory.CreateDirectory(pluginTypeBasePath);
+
+ var pluginPath = Path.Join(pluginTypeBasePath, metaData.ResourceName);
+ if (!Directory.Exists(pluginPath))
+ Directory.CreateDirectory(pluginPath);
+
+ var pluginFilePath = Path.Join(pluginPath, resourceInfo.Name);
+
+ await using var outputStream = File.Create(pluginFilePath);
+ await inputStream.CopyToAsync(outputStream);
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs
index e20d80f4..81837f73 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs
@@ -1,101 +1,110 @@
-using System.Reflection;
+using System.Text;
using AIStudio.Settings;
using Lua;
-
-using Microsoft.Extensions.FileProviders;
+using Lua.Standard;
namespace AIStudio.Tools.PluginSystem;
-public static class PluginFactory
+public static partial class PluginFactory
{
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger("PluginFactory");
+
private static readonly string DATA_DIR = SettingsManager.DataDirectory!;
- public static async Task EnsureInternalPlugins()
- {
- LOG.LogInformation("Start ensuring internal plugins.");
- foreach (var plugin in Enum.GetValues())
- {
- LOG.LogInformation($"Ensure plugin: {plugin}");
- await EnsurePlugin(plugin);
- }
- }
+ private static readonly string PLUGINS_ROOT = Path.Join(DATA_DIR, "plugins");
- private static async Task EnsurePlugin(InternalPlugin plugin)
+ private static readonly string INTERNAL_PLUGINS_ROOT = Path.Join(PLUGINS_ROOT, ".internal");
+
+ private static readonly List AVAILABLE_PLUGINS = [];
+
+ ///
+ /// A list of all available plugins.
+ ///
+ public static IReadOnlyCollection AvailablePlugins => AVAILABLE_PLUGINS;
+
+ ///
+ /// Try to load all plugins from the plugins directory.
+ ///
+ ///
+ /// Loading plugins means:
+ /// - Parsing and checking the plugin code
+ /// - Check for forbidden plugins
+ /// - Creating a new instance of the allowed plugin
+ /// - Read the plugin metadata
+ ///
+ /// Loading a plugin does not mean to start the plugin, though.
+ ///
+ public static async Task LoadAll(CancellationToken cancellationToken = default)
{
- try
+ LOG.LogInformation("Start loading plugins.");
+ if (!Directory.Exists(PLUGINS_ROOT))
{
- #if DEBUG
- var basePath = Path.Join(Environment.CurrentDirectory, "Plugins");
- var resourceFileProvider = new PhysicalFileProvider(basePath);
- #else
- var resourceFileProvider = new ManifestEmbeddedFileProvider(Assembly.GetAssembly(type: typeof(Program))!, "Plugins");
- #endif
+ LOG.LogInformation("No plugins found.");
+ return;
+ }
+
+ //
+ // The easiest way to load all plugins is to find all `plugin.lua` files and load them.
+ // By convention, each plugin is enforced to have a `plugin.lua` file.
+ //
+ var pluginMainFiles = Directory.EnumerateFiles(PLUGINS_ROOT, "plugin.lua", SearchOption.AllDirectories);
+ foreach (var pluginMainFile in pluginMainFiles)
+ {
+ if (cancellationToken.IsCancellationRequested)
+ break;
- var metaData = plugin.MetaData();
- var mainResourcePath = $"{metaData.ResourcePath}/plugin.lua";
- var resourceInfo = resourceFileProvider.GetFileInfo(mainResourcePath);
+ LOG.LogInformation($"Try to load plugin: {pluginMainFile}");
+ var code = await File.ReadAllTextAsync(pluginMainFile, Encoding.UTF8, cancellationToken);
+ var pluginPath = Path.GetDirectoryName(pluginMainFile)!;
+ var plugin = await Load(pluginPath, code, cancellationToken);
- if(!resourceInfo.Exists)
+ switch (plugin)
{
- LOG.LogError($"The plugin {plugin} does not exist. This should not happen, since the plugin is an integral part of AI Studio.");
- return;
- }
-
- // Ensure that the additional resources exist:
- foreach (var content in resourceFileProvider.GetDirectoryContents(metaData.ResourcePath))
- {
- if(content.IsDirectory)
- {
- LOG.LogError("The plugin contains a directory. This is not allowed.");
+ case NoPlugin noPlugin when noPlugin.Issues.Any():
+ LOG.LogError($"Was not able to load plugin: '{pluginMainFile}'. Reason: {noPlugin.Issues.First()}");
continue;
- }
- await CopyPluginFile(content, metaData);
+ case NoPlugin:
+ LOG.LogError($"Was not able to load plugin: '{pluginMainFile}'. Reason: Unknown.");
+ continue;
+
+ case { IsValid: false }:
+ LOG.LogError($"Was not able to load plugin '{pluginMainFile}', because the Lua code is not a valid AI Studio plugin. There are {plugin.Issues.Count()} issues to fix.");
+ #if DEBUG
+ foreach (var pluginIssue in plugin.Issues)
+ LOG.LogError($"Plugin issue: {pluginIssue}");
+ #endif
+ continue;
+
+ case { IsMaintained: false }:
+ LOG.LogWarning($"The plugin '{pluginMainFile}' is not maintained anymore. Please consider to disable it.");
+ break;
}
- }
- catch
- {
- LOG.LogError($"Was not able to ensure the plugin: {plugin}");
- }
- }
-
- private static async Task CopyPluginFile(IFileInfo resourceInfo, InternalPluginData metaData)
- {
- await using var inputStream = resourceInfo.CreateReadStream();
-
- var pluginsRoot = Path.Join(DATA_DIR, "plugins");
- var pluginTypeBasePath = Path.Join(pluginsRoot, metaData.Type.GetDirectory());
-
- if (!Directory.Exists(pluginsRoot))
- Directory.CreateDirectory(pluginsRoot);
-
- if (!Directory.Exists(pluginTypeBasePath))
- Directory.CreateDirectory(pluginTypeBasePath);
-
- var pluginPath = Path.Join(pluginTypeBasePath, metaData.ResourceName);
- if (!Directory.Exists(pluginPath))
- Directory.CreateDirectory(pluginPath);
-
- var pluginFilePath = Path.Join(pluginPath, resourceInfo.Name);
- await using var outputStream = File.Create(pluginFilePath);
- await inputStream.CopyToAsync(outputStream);
+ 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));
+ }
}
- public static async Task LoadAll()
- {
-
- }
-
- public static async Task Load(string path, string code, CancellationToken cancellationToken = default)
+ private static async Task Load(string pluginPath, string code, CancellationToken cancellationToken = default)
{
if(ForbiddenPlugins.Check(code) is { IsForbidden: true } forbiddenState)
return new NoPlugin($"This plugin is forbidden: {forbiddenState.Message}");
var state = LuaState.Create();
+
+ // Add the module loader so that the plugin can load other Lua modules:
+ state.ModuleLoader = new PluginLoader(pluginPath);
+
+ // Add some useful libraries:
+ state.OpenModuleLibrary();
+ state.OpenStringLibrary();
+ state.OpenTableLibrary();
+ state.OpenMathLibrary();
+ state.OpenBitwiseLibrary();
+ state.OpenCoroutineLibrary();
try
{
@@ -105,6 +114,10 @@ public static class PluginFactory
{
return new NoPlugin($"Was not able to parse the plugin: {e.Message}");
}
+ catch (LuaRuntimeException e)
+ {
+ return new NoPlugin($"Was not able to run the plugin: {e.Message}");
+ }
if (!state.Environment["TYPE"].TryRead(out var typeText))
return new NoPlugin("TYPE does not exist or is not a valid string.");
@@ -115,9 +128,10 @@ public static class PluginFactory
if(type is PluginType.NONE)
return new NoPlugin($"TYPE is not a valid plugin type. Valid types are: {CommonTools.GetAllEnumValues()}");
+ var isInternal = pluginPath.StartsWith(INTERNAL_PLUGINS_ROOT, StringComparison.OrdinalIgnoreCase);
return type switch
{
- PluginType.LANGUAGE => new PluginLanguage(path, state, type),
+ PluginType.LANGUAGE => new PluginLanguage(isInternal, state, type),
_ => new NoPlugin("This plugin type is not supported yet. Please try again with a future version of AI Studio.")
};
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs
index e1676e49..4c8cf30a 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs
@@ -8,7 +8,7 @@ public sealed class PluginLanguage : PluginBase, ILanguagePlugin
private ILanguagePlugin? baseLanguage;
- public PluginLanguage(string path, LuaState state, PluginType type) : base(path, state, type)
+ public PluginLanguage(bool isInternal, LuaState state, PluginType type) : base(isInternal, state, type)
{
if (this.TryInitUITextContent(out var issue, out var readContent))
this.content = readContent;
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs
new file mode 100644
index 00000000..2bfdab1e
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs
@@ -0,0 +1,50 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public sealed class PluginMetadata(PluginBase plugin) : IPluginMetadata
+{
+ #region Implementation of IPluginMetadata
+
+ ///
+ public string IconSVG { get; } = plugin.IconSVG;
+
+ ///
+ public PluginType Type { get; } = plugin.Type;
+
+ ///
+ public Guid Id { get; } = plugin.Id;
+
+ ///
+ public string Name { get; } = plugin.Name;
+
+ ///
+ public string Description { get; } = plugin.Description;
+
+ ///
+ public PluginVersion Version { get; } = plugin.Version;
+
+ ///
+ public string[] Authors { get; } = plugin.Authors;
+
+ ///
+ public string SupportContact { get; } = plugin.SupportContact;
+
+ ///
+ public string SourceURL { get; } = plugin.SourceURL;
+
+ ///
+ public PluginCategory[] Categories { get; } = plugin.Categories;
+
+ ///
+ public PluginTargetGroup[] TargetGroups { get; } = plugin.TargetGroups;
+
+ ///
+ public bool IsMaintained { get; } = plugin.IsMaintained;
+
+ ///
+ public string DeprecationMessage { get; } = plugin.DeprecationMessage;
+
+ ///
+ public bool IsInternal { get; } = plugin.IsInternal;
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/wwwroot/app.css b/app/MindWork AI Studio/wwwroot/app.css
index e62b4f83..af0e139a 100644
--- a/app/MindWork AI Studio/wwwroot/app.css
+++ b/app/MindWork AI Studio/wwwroot/app.css
@@ -35,6 +35,20 @@
margin-top: 4px;
}
+.plugin-icon-container {
+ width: var(--mud-icon-size-large);
+ height: var(--mud-icon-size-large);
+}
+
+.plugin-icon-container svg {
+ width: 100%;
+ height: 100%;
+}
+
+.mud-popover-open.InnerScrollingFix {
+ left: 0 !important;
+}
+
:root {
--confidence-color: #000000;
}
diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.39.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.39.md
index a3f49df3..94f9958e 100644
--- a/app/MindWork AI Studio/wwwroot/changelog/v0.9.39.md
+++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.39.md
@@ -1,4 +1,6 @@
# v0.9.39, build 214 (2025-03-xx xx:xx UTC)
- Added a feature flag for the plugin system. This flag is disabled by default and can be enabled inside the app settings. Please note that this feature is still in development; there are no plugins available yet.
- Added the Lua library we use for the plugin system to the about page.
+- Added the plugin overview page. This page shows all installed plugins and allows you to enable or disable them. It is only available when the plugin preview feature is enabled.
+- Fixed the preview tooltip component not showing the correct position when used inside a scrollable container.
- Upgraded to Rust 1.85.1
\ No newline at end of file