diff --git a/app/MindWork AI Studio/Chat/Workspace.cs b/app/MindWork AI Studio/Chat/Workspace.cs
deleted file mode 100644
index 0dad2b9b..00000000
--- a/app/MindWork AI Studio/Chat/Workspace.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace AIStudio.Chat;
-
-///
-/// Data about a workspace.
-///
-/// The name of the workspace.
-public sealed class Workspace(string name)
-{
- public string Name { get; set; } = name;
-
- public List Threads { get; set; } = new();
-}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs
index b302b23e..32d9e981 100644
--- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs
+++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs
@@ -1,6 +1,7 @@
using AIStudio.Dialogs;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
+using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
@@ -81,6 +82,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, IDis
// Ensure that all settings are loaded:
await this.SettingsManager.LoadSettings();
+ // Ensure that all internal plugins are present:
+ await PluginFactory.EnsureInternalPlugins();
+
// Register this component with the message bus:
this.MessageBus.RegisterComponent(this);
this.MessageBus.ApplyFilters(this, [], [ Event.UPDATE_AVAILABLE, Event.USER_SEARCH_FOR_UPDATE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED ]);
diff --git a/app/MindWork AI Studio/MindWork AI Studio.csproj b/app/MindWork AI Studio/MindWork AI Studio.csproj
index bdfe8287..90069618 100644
--- a/app/MindWork AI Studio/MindWork AI Studio.csproj
+++ b/app/MindWork AI Studio/MindWork AI Studio.csproj
@@ -41,8 +41,8 @@
-
+
@@ -52,6 +52,7 @@
+
diff --git a/app/MindWork AI Studio/Pages/About.razor b/app/MindWork AI Studio/Pages/About.razor
index 74492c4a..282e5e72 100644
--- a/app/MindWork AI Studio/Pages/About.razor
+++ b/app/MindWork AI Studio/Pages/About.razor
@@ -108,6 +108,7 @@
+
diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/contentHome.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/contentHome.lua
new file mode 100644
index 00000000..f26d9aff
--- /dev/null
+++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/contentHome.lua
@@ -0,0 +1,3 @@
+CONTENT_HOME = {
+ LetsGetStarted = "Lass uns anfangen",
+}
\ 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
new file mode 100644
index 00000000..4801b782
--- /dev/null
+++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
@@ -0,0 +1,39 @@
+-- The ID for this plugin:
+ID = "43065dbc-78d0-45b7-92be-f14c2926e2dc"
+
+-- The name of the plugin:
+NAME = "MindWork AI Studio - German / Deutsch"
+
+-- The description of the plugin:
+DESCRIPTION = "Dieses Plugin bietet deutsche Sprachunterstützung für MindWork AI Studio."
+
+-- The version of the plugin:
+VERSION = "1.0.0"
+
+-- The type of the plugin:
+TYPE = "LANGUAGE"
+
+-- The authors of the plugin:
+AUTHORS = {"MindWork AI Community"}
+
+-- The support contact for the plugin:
+SUPPORT_CONTACT = "MindWork AI Community"
+
+-- The source URL for the plugin:
+SOURCE_URL = "https://github.com/MindWorkAI/AI-Studio"
+
+-- The categories for the plugin:
+CATEGORIES = { "CORE" }
+
+-- The target groups for the plugin:
+TARGET_GROUPS = { "EVERYONE" }
+
+-- The flag for whether the plugin is maintained:
+IS_MAINTAINED = true
+
+-- When the plugin is deprecated, this message will be shown to users:
+DEPRECATION_MESSAGE = nil
+
+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/contentHome.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/contentHome.lua
new file mode 100644
index 00000000..d1805fd0
--- /dev/null
+++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/contentHome.lua
@@ -0,0 +1,3 @@
+CONTENT_HOME = {
+ LetsGetStarted = "Let's get started",
+}
\ 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
new file mode 100644
index 00000000..6e2fea50
--- /dev/null
+++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
@@ -0,0 +1,41 @@
+require("contentHome")
+
+-- The ID for this plugin:
+ID = "97dfb1ba-50c4-4440-8dfa-6575daf543c8"
+
+-- The name of the plugin:
+NAME = "MindWork AI Studio - US English"
+
+-- The description of the plugin:
+DESCRIPTION = "This plugin provides US English language support for MindWork AI Studio."
+
+-- The version of the plugin:
+VERSION = "1.0.0"
+
+-- The type of the plugin:
+TYPE = "LANGUAGE"
+
+-- The authors of the plugin:
+AUTHORS = {"MindWork AI Community"}
+
+-- The support contact for the plugin:
+SUPPORT_CONTACT = "MindWork AI Community"
+
+-- The source URL for the plugin:
+SOURCE_URL = "https://github.com/MindWorkAI/AI-Studio"
+
+-- The categories for the plugin:
+CATEGORIES = { "CORE" }
+
+-- The target groups for the plugin:
+TARGET_GROUPS = { "EVERYONE" }
+
+-- The flag for whether the plugin is maintained:
+IS_MAINTAINED = true
+
+-- When the plugin is deprecated, this message will be shown to users:
+DEPRECATION_MESSAGE = nil
+
+UI_TEXT_CONTENT = {
+ HOME = CONTENT_HOME,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs
index 61261b16..cc37d9cd 100644
--- a/app/MindWork AI Studio/Program.cs
+++ b/app/MindWork AI Studio/Program.cs
@@ -22,6 +22,7 @@ internal sealed class Program
public static Encryption ENCRYPTION = null!;
public static string API_TOKEN = null!;
public static IServiceProvider SERVICE_PROVIDER = null!;
+ public static ILoggerFactory LOGGER_FACTORY = null!;
public static async Task Main(string[] args)
{
@@ -147,6 +148,9 @@ internal sealed class Program
// Execute the builder to get the app:
var app = builder.Build();
+ // Get the logging factory for e.g., static classes:
+ LOGGER_FACTORY = app.Services.GetRequiredService();
+
// Get a program logger:
var programLogger = app.Services.GetRequiredService>();
programLogger.LogInformation("Starting the AI Studio server.");
diff --git a/app/MindWork AI Studio/Settings/DataModel/PreviewFeatures.cs b/app/MindWork AI Studio/Settings/DataModel/PreviewFeatures.cs
index 825e8037..ff642a0a 100644
--- a/app/MindWork AI Studio/Settings/DataModel/PreviewFeatures.cs
+++ b/app/MindWork AI Studio/Settings/DataModel/PreviewFeatures.cs
@@ -8,4 +8,6 @@ public enum PreviewFeatures
//
PRE_WRITER_MODE_2024,
PRE_RAG_2024,
+
+ PRE_PLUGINS_2025,
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs
index 133d6538..2eb25587 100644
--- a/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs
+++ b/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs
@@ -6,6 +6,7 @@ public static class PreviewFeaturesExtensions
{
PreviewFeatures.PRE_WRITER_MODE_2024 => "Writer Mode: Experiments about how to write long texts using AI",
PreviewFeatures.PRE_RAG_2024 => "RAG: Preview of our RAG implementation where you can refer your files or integrate enterprise data within your company",
+ PreviewFeatures.PRE_PLUGINS_2025 => "Plugins: Preview of our plugin system where you can extend the functionality of the app",
_ => "Unknown preview feature"
};
diff --git a/app/MindWork AI Studio/Settings/DataModel/PreviewVisibilityExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/PreviewVisibilityExtensions.cs
index f80939f6..b0f07716 100644
--- a/app/MindWork AI Studio/Settings/DataModel/PreviewVisibilityExtensions.cs
+++ b/app/MindWork AI Studio/Settings/DataModel/PreviewVisibilityExtensions.cs
@@ -25,6 +25,7 @@ public static class PreviewVisibilityExtensions
if (visibility >= PreviewVisibility.EXPERIMENTAL)
{
features.Add(PreviewFeatures.PRE_WRITER_MODE_2024);
+ features.Add(PreviewFeatures.PRE_PLUGINS_2025);
}
return features;
diff --git a/app/MindWork AI Studio/Tools/CommonTools.cs b/app/MindWork AI Studio/Tools/CommonTools.cs
new file mode 100644
index 00000000..26150880
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/CommonTools.cs
@@ -0,0 +1,22 @@
+using System.Text;
+
+namespace AIStudio.Tools;
+
+public static class CommonTools
+{
+ ///
+ /// Get all the values (the names) of an enum as a string, separated by commas.
+ ///
+ /// The enum type to get the values of.
+ /// The values to exclude from the result.
+ /// The values of the enum as a string, separated by commas.
+ public static string GetAllEnumValues(params TEnum[] exceptions) where TEnum : struct, Enum
+ {
+ var sb = new StringBuilder();
+ foreach (var value in Enum.GetValues())
+ if(!exceptions.Contains(value))
+ sb.Append(value).Append(", ");
+
+ return sb.ToString();
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/ForbiddenPlugins.cs b/app/MindWork AI Studio/Tools/PluginSystem/ForbiddenPlugins.cs
new file mode 100644
index 00000000..b38459d6
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/ForbiddenPlugins.cs
@@ -0,0 +1,99 @@
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Checks if a plugin is forbidden.
+///
+public static class ForbiddenPlugins
+{
+ private const string ID_PATTERN = "ID = \"";
+ private static readonly int ID_PATTERN_LEN = ID_PATTERN.Length;
+
+ ///
+ /// Checks if the given code represents a forbidden plugin.
+ ///
+ /// The code to check.
+ /// The result of the check.
+ public static PluginCheckResult Check(ReadOnlySpan code)
+ {
+ var endIndex = 0;
+ var foundAnyId = false;
+ var id = ReadOnlySpan.Empty;
+ while (true)
+ {
+ // Create a slice of the code starting at the end index.
+ // This way we can search for all IDs in the code:
+ code = code[endIndex..];
+
+ // Read the next ID as a string:
+ if (!TryGetId(code, out id, out endIndex))
+ {
+ // When no ID was found at all, we block this plugin.
+ // When another ID was found previously, we allow this plugin.
+ if(foundAnyId)
+ return new PluginCheckResult(false, null);
+
+ return new PluginCheckResult(true, "No ID was found.");
+ }
+
+ // Try to parse the ID as a GUID:
+ if (!Guid.TryParse(id, out var parsedGuid))
+ {
+ // Again, when no ID was found at all, we block this plugin.
+ if(foundAnyId)
+ return new PluginCheckResult(false, null);
+
+ return new PluginCheckResult(true, "The ID is not a valid GUID.");
+ }
+
+ // Check if the GUID is forbidden:
+ if (FORBIDDEN_PLUGINS.TryGetValue(parsedGuid, out var reason))
+ return new PluginCheckResult(true, reason);
+
+ foundAnyId = true;
+ }
+ }
+
+ private static bool TryGetId(ReadOnlySpan code, out ReadOnlySpan id, out int endIndex)
+ {
+ //
+ // Please note: the code variable is a slice of the original code.
+ // That means the indices are relative to the slice, not the original code.
+ //
+
+ id = ReadOnlySpan.Empty;
+ endIndex = 0;
+
+ // Find the next ID:
+ var idStartIndex = code.IndexOf(ID_PATTERN);
+ if (idStartIndex < 0)
+ return false;
+
+ // Find the start index of the value (Guid):
+ var valueStartIndex = idStartIndex + ID_PATTERN_LEN;
+
+ // Find the end index of the value. In order to do that,
+ // we create a slice of the code starting at the value
+ // start index. That means that the end index is relative
+ // to the inner slice, not the original code nor the outer slice.
+ var valueEndIndex = code[valueStartIndex..].IndexOf('"');
+ if (valueEndIndex < 0)
+ return false;
+
+ // From the perspective of the start index is the end index
+ // the length of the value:
+ endIndex = valueStartIndex + valueEndIndex;
+ id = code.Slice(valueStartIndex, valueEndIndex);
+ return true;
+ }
+
+ ///
+ /// The forbidden plugins.
+ ///
+ ///
+ /// A dictionary that maps the GUID of a plugin to the reason why it is forbidden.
+ ///
+ // ReSharper disable once CollectionNeverUpdated.Local
+ private static readonly Dictionary FORBIDDEN_PLUGINS =
+ [
+ ];
+}
\ 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
new file mode 100644
index 00000000..a33bf3f5
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/ILanguagePlugin.cs
@@ -0,0 +1,21 @@
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Represents a contract for a language plugin.
+///
+public interface ILanguagePlugin
+{
+ ///
+ /// Tries to get a text from the language plugin.
+ ///
+ ///
+ /// When the key does not 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);
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/InternalPlugin.cs b/app/MindWork AI Studio/Tools/PluginSystem/InternalPlugin.cs
new file mode 100644
index 00000000..1cbd8ca7
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/InternalPlugin.cs
@@ -0,0 +1,7 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public enum InternalPlugin
+{
+ LANGUAGE_EN_US,
+ LANGUAGE_DE_DE,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/InternalPluginData.cs b/app/MindWork AI Studio/Tools/PluginSystem/InternalPluginData.cs
new file mode 100644
index 00000000..d7595f2a
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/InternalPluginData.cs
@@ -0,0 +1,8 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public readonly record struct InternalPluginData(PluginType Type, Guid Id, string ShortName)
+{
+ public string ResourcePath => $"{this.Type.GetDirectory()}/{this.ShortName.ToLowerInvariant()}-{this.Id}";
+
+ public string ResourceName => $"{this.ShortName.ToLowerInvariant()}-{this.Id}";
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/InternalPluginExtensions.cs b/app/MindWork AI Studio/Tools/PluginSystem/InternalPluginExtensions.cs
new file mode 100644
index 00000000..3a25aa18
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/InternalPluginExtensions.cs
@@ -0,0 +1,12 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public static class InternalPluginExtensions
+{
+ public static InternalPluginData MetaData(this InternalPlugin plugin) => plugin switch
+ {
+ InternalPlugin.LANGUAGE_EN_US => new (PluginType.LANGUAGE, new("97dfb1ba-50c4-4440-8dfa-6575daf543c8"), "en-us"),
+ InternalPlugin.LANGUAGE_DE_DE => new(PluginType.LANGUAGE, new("43065dbc-78d0-45b7-92be-f14c2926e2dc"), "de-de"),
+
+ _ => new InternalPluginData(PluginType.NONE, Guid.Empty, "unknown")
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/NoModuleLoader.cs b/app/MindWork AI Studio/Tools/PluginSystem/NoModuleLoader.cs
new file mode 100644
index 00000000..d40d2237
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/NoModuleLoader.cs
@@ -0,0 +1,20 @@
+using Lua;
+
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// This Lua module loader does not load any modules.
+///
+public sealed class NoModuleLoader : ILuaModuleLoader
+{
+ #region Implementation of ILuaModuleLoader
+
+ public bool Exists(string moduleName) => false;
+
+ public ValueTask LoadAsync(string moduleName, CancellationToken cancellationToken = default)
+ {
+ return ValueTask.FromResult(new LuaModule());
+ }
+
+ #endregion
+}
\ 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
new file mode 100644
index 00000000..a21ab819
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/NoPlugin.cs
@@ -0,0 +1,9 @@
+using Lua;
+
+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
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs
new file mode 100644
index 00000000..4f719d18
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs
@@ -0,0 +1,521 @@
+using Lua;
+using Lua.Standard;
+
+// ReSharper disable MemberCanBePrivate.Global
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Represents the base of any AI Studio plugin.
+///
+public abstract class PluginBase
+{
+ private readonly IReadOnlyCollection baseIssues;
+ protected readonly LuaState state;
+
+ protected readonly List pluginIssues = [];
+
+ ///
+ /// 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; }
+
+ ///
+ /// The issues that occurred during the initialization of this plugin.
+ ///
+ public IEnumerable Issues => this.baseIssues.Concat(this.pluginIssues);
+
+ ///
+ /// True, when the plugin is valid.
+ ///
+ ///
+ /// False means that there were issues during the initialization of the plugin.
+ /// Please check the Issues property for more information.
+ ///
+ 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 = "")
+ {
+ 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);
+
+ if(this.TryInitId(out var issue, out var id))
+ this.Id = id;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitName(out issue, out var name))
+ this.Name = name;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitDescription(out issue, out var description))
+ this.Description = description;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitVersion(out issue, out var version))
+ this.Version = version;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitAuthors(out issue, out var authors))
+ this.Authors = authors;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitSupportContact(out issue, out var contact))
+ this.SupportContact = contact;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitSourceURL(out issue, out var url))
+ this.SourceURL = url;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitCategories(out issue, out var categories))
+ this.Categories = categories;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitTargetGroups(out issue, out var targetGroups))
+ this.TargetGroups = targetGroups;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitIsMaintained(out issue, out var isMaintained))
+ this.IsMaintained = isMaintained;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ if(this.TryInitDeprecationMessage(out issue, out var deprecationMessage))
+ this.DeprecationMessage = deprecationMessage;
+ else if(this is not NoPlugin)
+ issues.Add(issue);
+
+ this.baseIssues = issues;
+ }
+
+ #region Initialization-related methods
+
+ ///
+ /// Tries to read the ID of the plugin.
+ ///
+ /// The error message, when the ID could not be read.
+ /// The read ID.
+ /// True, when the ID could be read successfully.
+ private bool TryInitId(out string message, out Guid id)
+ {
+ if (!this.state.Environment["ID"].TryRead(out var idText))
+ {
+ message = "The field ID does not exist or is not a valid string.";
+ id = Guid.Empty;
+ return false;
+ }
+
+ if (!Guid.TryParse(idText, out id))
+ {
+ message = "The field ID is not a valid GUID / UUID. The ID must be formatted in the 8-4-4-4-12 format (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX).";
+ id = Guid.Empty;
+ return false;
+ }
+
+ if(id == Guid.Empty)
+ {
+ message = "The field ID is empty. The ID must be formatted in the 8-4-4-4-12 format (XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX).";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to read the name of the plugin.
+ ///
+ /// The error message, when the name could not be read.
+ /// The read name.
+ /// True, when the name could be read successfully.
+ private bool TryInitName(out string message, out string name)
+ {
+ if (!this.state.Environment["NAME"].TryRead(out name))
+ {
+ message = "The field NAME does not exist or is not a valid string.";
+ name = string.Empty;
+ return false;
+ }
+
+ if(string.IsNullOrWhiteSpace(name))
+ {
+ message = "The field NAME is empty. The name must be a non-empty string.";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to read the description of the plugin.
+ ///
+ /// The error message, when the description could not be read.
+ /// The read description.
+ /// True, when the description could be read successfully.
+ private bool TryInitDescription(out string message, out string description)
+ {
+ if (!this.state.Environment["DESCRIPTION"].TryRead(out description))
+ {
+ message = "The field DESCRIPTION does not exist or is not a valid string.";
+ description = string.Empty;
+ return false;
+ }
+
+ if(string.IsNullOrWhiteSpace(description))
+ {
+ message = "The field DESCRIPTION is empty. The description must be a non-empty string.";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to read the version of the plugin.
+ ///
+ /// The error message, when the version could not be read.
+ /// The read version.
+ /// True, when the version could be read successfully.
+ private bool TryInitVersion(out string message, out PluginVersion version)
+ {
+ if (!this.state.Environment["VERSION"].TryRead(out var versionText))
+ {
+ message = "The field VERSION does not exist or is not a valid string.";
+ version = PluginVersion.NONE;
+ return false;
+ }
+
+ if (!PluginVersion.TryParse(versionText, out version))
+ {
+ message = "The field VERSION is not a valid version number. The version number must be formatted as string in the major.minor.patch format (X.X.X).";
+ version = PluginVersion.NONE;
+ return false;
+ }
+
+ if(version == PluginVersion.NONE)
+ {
+ message = "The field VERSION is empty. The version number must be formatted as string in the major.minor.patch format (X.X.X).";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to read the authors of the plugin.
+ ///
+ /// The error message, when the authors could not be read.
+ /// The read authors.
+ /// True, when the authors could be read successfully.
+ private bool TryInitAuthors(out string message, out string[] authors)
+ {
+ if (!this.state.Environment["AUTHORS"].TryRead(out var authorsTable))
+ {
+ authors = [];
+ message = "The table AUTHORS does not exist or is using an invalid syntax.";
+ return false;
+ }
+
+ var authorList = new List();
+ foreach(var author in authorsTable.GetArraySpan())
+ if(author.TryRead(out var authorName))
+ authorList.Add(authorName);
+
+ authors = authorList.ToArray();
+ if(authorList.Count == 0)
+ {
+ message = "The table AUTHORS is empty. At least one author must be specified.";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to read the support contact for the plugin.
+ ///
+ /// The error message, when the support contact could not be read.
+ /// The read support contact.
+ /// True, when the support contact could be read successfully.
+ private bool TryInitSupportContact(out string message, out string contact)
+ {
+ if (!this.state.Environment["SUPPORT_CONTACT"].TryRead(out contact))
+ {
+ contact = string.Empty;
+ message = "The field SUPPORT_CONTACT does not exist or is not a valid string.";
+ return false;
+ }
+
+ if(string.IsNullOrWhiteSpace(contact))
+ {
+ message = "The field SUPPORT_CONTACT is empty. The support contact must be a non-empty string.";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Try to read the source URL of the plugin.
+ ///
+ /// The error message, when the source URL could not be read.
+ /// The read source URL.
+ /// True, when the source URL could be read successfully.
+ private bool TryInitSourceURL(out string message, out string url)
+ {
+ if (!this.state.Environment["SOURCE_URL"].TryRead(out url))
+ {
+ url = string.Empty;
+ message = "The field SOURCE_URL does not exist or is not a valid string.";
+ return false;
+ }
+
+ if (!url.StartsWith("http://", StringComparison.InvariantCultureIgnoreCase) && !url.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase))
+ {
+ url = string.Empty;
+ message = "The field SOURCE_URL is not a valid URL. The URL must start with 'http://' or 'https://'.";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to read the categories of the plugin.
+ ///
+ /// The error message, when the categories could not be read.
+ /// The read categories.
+ /// True, when the categories could be read successfully.
+ private bool TryInitCategories(out string message, out PluginCategory[] categories)
+ {
+ if (!this.state.Environment["CATEGORIES"].TryRead(out var categoriesTable))
+ {
+ categories = [];
+ message = "The table CATEGORIES does not exist or is using an invalid syntax.";
+ return false;
+ }
+
+ var categoryList = new List();
+ foreach(var luaCategory in categoriesTable.GetArraySpan())
+ if(luaCategory.TryRead(out var categoryName))
+ if(Enum.TryParse(categoryName, out var category) && category != PluginCategory.NONE)
+ categoryList.Add(category);
+
+ categories = categoryList.ToArray();
+ if(categoryList.Count == 0)
+ {
+ message = $"The table CATEGORIES is empty. At least one category is necessary. Valid categories are: {CommonTools.GetAllEnumValues(PluginCategory.NONE)}.";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to read the intended target groups for the plugin.
+ ///
+ /// The error message, when the target groups could not be read.
+ /// The read target groups.
+ /// True, when the target groups could be read successfully.
+ private bool TryInitTargetGroups(out string message, out PluginTargetGroup[] targetGroups)
+ {
+ if (!this.state.Environment["TARGET_GROUPS"].TryRead(out var targetGroupsTable))
+ {
+ targetGroups = [];
+ message = "The table TARGET_GROUPS does not exist or is using an invalid syntax.";
+ return false;
+ }
+
+ var targetGroupList = new List();
+ foreach(var luaTargetGroup in targetGroupsTable.GetArraySpan())
+ if(luaTargetGroup.TryRead(out var targetGroupName))
+ if(Enum.TryParse(targetGroupName, out var targetGroup) && targetGroup != PluginTargetGroup.NONE)
+ targetGroupList.Add(targetGroup);
+
+ targetGroups = targetGroupList.ToArray();
+ if(targetGroups.Length == 0)
+ {
+ message = "The table TARGET_GROUPS is empty or is not a valid table of strings. Valid target groups are: {CommonTools.GetAllEnumValues(PluginTargetGroup.NONE)}.";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to read the maintenance status of the plugin.
+ ///
+ /// The error message, when the maintenance status could not be read.
+ /// The read maintenance status.
+ /// True, when the maintenance status could be read successfully.
+ private bool TryInitIsMaintained(out string message, out bool isMaintained)
+ {
+ if (!this.state.Environment["IS_MAINTAINED"].TryRead(out isMaintained))
+ {
+ isMaintained = false;
+ message = "The field IS_MAINTAINED does not exist or is not a valid boolean.";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to read the deprecation message of the plugin.
+ ///
+ /// 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)
+ {
+ 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.";
+ return false;
+ }
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Tries to initialize the UI text content of the plugin.
+ ///
+ /// The error message, when the UI text content could not be read.
+ /// The read UI text content.
+ /// True, when the UI text content could be read successfully.
+ protected bool TryInitUITextContent(out string message, out Dictionary pluginContent)
+ {
+ if (!this.state.Environment["UI_TEXT_CONTENT"].TryRead(out var textTable))
+ {
+ message = "The UI_TEXT_CONTENT table does not exist or is not a valid table.";
+ pluginContent = [];
+ return false;
+ }
+
+ this.ReadTextTable("root", textTable, out pluginContent);
+
+ message = string.Empty;
+ return true;
+ }
+
+ ///
+ /// Reads a flat or hierarchical text table.
+ ///
+ /// The parent key(s).
+ /// The table to read.
+ /// The read table content.
+ protected void ReadTextTable(string parent, LuaTable table, out Dictionary tableContent)
+ {
+ tableContent = [];
+ var lastKey = LuaValue.Nil;
+ while (table.TryGetNext(lastKey, out var pair))
+ {
+ var keyText = pair.Key.ToString();
+ if (pair.Value.TryRead(out var value))
+ tableContent[$"{parent}::{keyText}"] = value;
+
+ else if (pair.Value.TryRead(out var t))
+ {
+ this.ReadTextTable($"{parent}::{keyText}", t, out var subContent);
+ foreach (var (k, v) in subContent)
+ tableContent[k] = v;
+ }
+
+ lastKey = pair.Key;
+ }
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginCategory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginCategory.cs
new file mode 100644
index 00000000..00afcd0e
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginCategory.cs
@@ -0,0 +1,33 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public enum PluginCategory
+{
+ NONE,
+ CORE,
+
+ BUSINESS,
+ INDUSTRY,
+ UTILITY,
+ SOFTWARE_DEVELOPMENT,
+ GAMING,
+ EDUCATION,
+ ENTERTAINMENT,
+ SOCIAL,
+ SHOPPING,
+ TRAVEL,
+ HEALTH,
+ FITNESS,
+ FOOD,
+ PARTY,
+ SPORTS,
+ NEWS,
+ WEATHER,
+ MUSIC,
+ POLITICAL,
+ SCIENCE,
+ TECHNOLOGY,
+ ART,
+ FICTION,
+ WRITING,
+ CONTENT_CREATION,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginCategoryExtensions.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginCategoryExtensions.cs
new file mode 100644
index 00000000..35303c06
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginCategoryExtensions.cs
@@ -0,0 +1,38 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public static class PluginCategoryExtensions
+{
+ public static string GetName(this PluginCategory type) => type switch
+ {
+ PluginCategory.NONE => "None",
+ PluginCategory.CORE => "AI Studio Core",
+
+ PluginCategory.BUSINESS => "Business",
+ PluginCategory.INDUSTRY => "Industry",
+ PluginCategory.UTILITY => "Utility",
+ PluginCategory.SOFTWARE_DEVELOPMENT => "Software Development",
+ PluginCategory.GAMING => "Gaming",
+ PluginCategory.EDUCATION => "Education",
+ PluginCategory.ENTERTAINMENT => "Entertainment",
+ PluginCategory.SOCIAL => "Social",
+ PluginCategory.SHOPPING => "Shopping",
+ PluginCategory.TRAVEL => "Travel",
+ PluginCategory.HEALTH => "Health",
+ PluginCategory.FITNESS => "Fitness",
+ PluginCategory.FOOD => "Food",
+ PluginCategory.PARTY => "Party",
+ PluginCategory.SPORTS => "Sports",
+ PluginCategory.NEWS => "News",
+ PluginCategory.WEATHER => "Weather",
+ PluginCategory.MUSIC => "Music",
+ PluginCategory.POLITICAL => "Political",
+ PluginCategory.SCIENCE => "Science",
+ PluginCategory.TECHNOLOGY => "Technology",
+ PluginCategory.ART => "Art",
+ PluginCategory.FICTION => "Fiction",
+ PluginCategory.WRITING => "Writing",
+ PluginCategory.CONTENT_CREATION => "Content Creation",
+
+ _ => "Unknown plugin category",
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginCheckResult.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginCheckResult.cs
new file mode 100644
index 00000000..f390a47d
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginCheckResult.cs
@@ -0,0 +1,8 @@
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Represents the result of a plugin check.
+///
+/// In case the plugin is forbidden, this is true.
+/// The message that describes why the plugin is forbidden.
+public readonly record struct PluginCheckResult(bool IsForbidden, string? Message);
\ 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
new file mode 100644
index 00000000..e20d80f4
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs
@@ -0,0 +1,125 @@
+using System.Reflection;
+
+using AIStudio.Settings;
+
+using Lua;
+
+using Microsoft.Extensions.FileProviders;
+
+namespace AIStudio.Tools.PluginSystem;
+
+public static 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 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 CopyPluginFile(content, metaData);
+ }
+ }
+ 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);
+ }
+
+ public static async Task LoadAll()
+ {
+
+ }
+
+ public static async Task Load(string path, 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();
+
+ try
+ {
+ await state.DoStringAsync(code, cancellationToken: cancellationToken);
+ }
+ catch (LuaParseException e)
+ {
+ return new NoPlugin($"Was not able to parse 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.");
+
+ if (!Enum.TryParse(typeText, out var type))
+ return new NoPlugin($"TYPE is not a valid plugin type. Valid types are: {CommonTools.GetAllEnumValues()}");
+
+ if(type is PluginType.NONE)
+ return new NoPlugin($"TYPE is not a valid plugin type. Valid types are: {CommonTools.GetAllEnumValues()}");
+
+ return type switch
+ {
+ PluginType.LANGUAGE => new PluginLanguage(path, state, type),
+
+ _ => new NoPlugin("This plugin type is not supported yet. Please try again with a future version of AI Studio.")
+ };
+ }
+}
\ 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
new file mode 100644
index 00000000..e1676e49
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginLanguage.cs
@@ -0,0 +1,48 @@
+using Lua;
+
+namespace AIStudio.Tools.PluginSystem;
+
+public sealed class PluginLanguage : PluginBase, ILanguagePlugin
+{
+ private readonly Dictionary content = [];
+
+ private ILanguagePlugin? baseLanguage;
+
+ public PluginLanguage(string path, LuaState state, PluginType type) : base(path, state, type)
+ {
+ if (this.TryInitUITextContent(out var issue, out var readContent))
+ this.content = readContent;
+ else
+ this.pluginIssues.Add(issue);
+ }
+
+ ///
+ /// Sets the base language plugin. This plugin will be used to fill in missing keys.
+ ///
+ /// The base language plugin to use.
+ public void SetBaseLanguage(ILanguagePlugin baseLanguagePlugin) => this.baseLanguage = baseLanguagePlugin;
+
+ ///
+ /// 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)
+ {
+ if (this.content.TryGetValue(key, out value!))
+ return true;
+
+ if(this.baseLanguage is not null && this.baseLanguage.TryGetText(key, out value))
+ return true;
+
+ value = string.Empty;
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginLoader.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginLoader.cs
new file mode 100644
index 00000000..ec81f73c
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginLoader.cs
@@ -0,0 +1,48 @@
+using System.Text;
+
+using AIStudio.Settings;
+
+using Lua;
+
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Loads Lua modules from a plugin directory.
+///
+///
+/// Any plugin can load Lua modules from its own directory. This class is used to load these modules.
+/// Loading other modules outside the plugin directory is not allowed.
+///
+/// The directory where the plugin is located.
+public sealed class PluginLoader(string pluginDirectory) : ILuaModuleLoader
+{
+ private static readonly string PLUGIN_BASE_PATH = Path.Join(SettingsManager.DataDirectory, "plugins");
+
+ #region Implementation of ILuaModuleLoader
+
+ ///
+ public bool Exists(string moduleName)
+ {
+ // Ensure that the user doesn't try to escape the plugin directory:
+ if (moduleName.Contains("..") || pluginDirectory.Contains(".."))
+ return false;
+
+ // Ensure that the plugin directory is nested in the plugin base path:
+ if (!pluginDirectory.StartsWith(PLUGIN_BASE_PATH, StringComparison.OrdinalIgnoreCase))
+ return false;
+
+ var path = Path.Join(pluginDirectory, $"{moduleName}.lua");
+ return File.Exists(path);
+ }
+
+ ///
+ public async ValueTask LoadAsync(string moduleName, CancellationToken cancellationToken = default)
+ {
+ var path = Path.Join(pluginDirectory, $"{moduleName}.lua");
+ var code = await File.ReadAllTextAsync(path, Encoding.UTF8, cancellationToken);
+
+ return new(moduleName, code);
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroup.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroup.cs
new file mode 100644
index 00000000..102aa857
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroup.cs
@@ -0,0 +1,20 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public enum PluginTargetGroup
+{
+ NONE,
+
+ EVERYONE,
+ CHILDREN,
+ TEENAGERS,
+ STUDENTS,
+ ADULTS,
+
+ INDUSTRIAL_WORKERS,
+ OFFICE_WORKERS,
+ BUSINESS_PROFESSIONALS,
+ SOFTWARE_DEVELOPERS,
+ SCIENTISTS,
+ TEACHERS,
+ ARTISTS,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroupExtensions.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroupExtensions.cs
new file mode 100644
index 00000000..7a14123f
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginTargetGroupExtensions.cs
@@ -0,0 +1,25 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public static class PluginTargetGroupExtensions
+{
+ public static string Name(this PluginTargetGroup group) => group switch
+ {
+ PluginTargetGroup.NONE => "No target group",
+
+ PluginTargetGroup.EVERYONE => "Everyone",
+ PluginTargetGroup.CHILDREN => "Children",
+ PluginTargetGroup.TEENAGERS => "Teenagers",
+ PluginTargetGroup.STUDENTS => "Students",
+ PluginTargetGroup.ADULTS => "Adults",
+
+ PluginTargetGroup.INDUSTRIAL_WORKERS => "Industrial workers",
+ PluginTargetGroup.OFFICE_WORKERS => "Office workers",
+ PluginTargetGroup.BUSINESS_PROFESSIONALS => "Business professionals",
+ PluginTargetGroup.SOFTWARE_DEVELOPERS => "Software developers",
+ PluginTargetGroup.SCIENTISTS => "Scientists",
+ PluginTargetGroup.TEACHERS => "Teachers",
+ PluginTargetGroup.ARTISTS => "Artists",
+
+ _ => "Unknown target group",
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginType.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginType.cs
new file mode 100644
index 00000000..5730e62f
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginType.cs
@@ -0,0 +1,11 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public enum PluginType
+{
+ NONE,
+
+ LANGUAGE,
+ ASSISTANT,
+ CONFIGURATION,
+ THEME,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginTypeExtensions.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginTypeExtensions.cs
new file mode 100644
index 00000000..b65eb502
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginTypeExtensions.cs
@@ -0,0 +1,24 @@
+namespace AIStudio.Tools.PluginSystem;
+
+public static class PluginTypeExtensions
+{
+ public static string GetName(this PluginType type) => type switch
+ {
+ PluginType.LANGUAGE => "Language plugin",
+ PluginType.ASSISTANT => "Assistant plugin",
+ PluginType.CONFIGURATION => "Configuration plugin",
+ PluginType.THEME => "Theme plugin",
+
+ _ => "Unknown plugin type",
+ };
+
+ public static string GetDirectory(this PluginType type) => type switch
+ {
+ PluginType.LANGUAGE => "languages",
+ PluginType.ASSISTANT => "assistants",
+ PluginType.CONFIGURATION => "configurations",
+ PluginType.THEME => "themes",
+
+ _ => "unknown",
+ };
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginVersion.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginVersion.cs
new file mode 100644
index 00000000..d5507a56
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginVersion.cs
@@ -0,0 +1,90 @@
+// ReSharper disable MemberCanBePrivate.Global
+namespace AIStudio.Tools.PluginSystem;
+
+///
+/// Represents a version number for a plugin.
+///
+/// The major version number.
+/// The minor version number.
+/// The patch version number.
+public readonly record struct PluginVersion(int Major, int Minor, int Patch) : IComparable
+{
+ ///
+ /// Represents no version number.
+ ///
+ public static readonly PluginVersion NONE = new(0, 0, 0);
+
+ ///
+ /// Tries to parse the input string as a plugin version number.
+ ///
+ /// The input string to parse.
+ /// The parsed version number.
+ /// True when the input string was successfully parsed; otherwise, false.
+ public static bool TryParse(string input, out PluginVersion version)
+ {
+ try
+ {
+ version = Parse(input);
+ return true;
+ }
+ catch
+ {
+ version = NONE;
+ return false;
+ }
+ }
+
+ ///
+ /// Parses the input string as a plugin version number.
+ ///
+ /// The input string to parse.
+ /// The parsed version number.
+ /// The input string is not in the correct format.
+ public static PluginVersion Parse(string input)
+ {
+ var segments = input.Split('.');
+ if (segments.Length != 3)
+ throw new FormatException("The input string must be in the format 'major.minor.patch'.");
+
+ var major = int.Parse(segments[0]);
+ var minor = int.Parse(segments[1]);
+ var patch = int.Parse(segments[2]);
+
+ if(major < 0 || minor < 0 || patch < 0)
+ throw new FormatException("The major, minor, and patch numbers must be greater than or equal to 0.");
+
+ return new PluginVersion(major, minor, patch);
+ }
+
+ ///
+ /// Converts the plugin version number to a string in the format 'major.minor.patch'.
+ ///
+ /// The plugin version number as a string.
+ public override string ToString() => $"{this.Major}.{this.Minor}.{this.Patch}";
+
+ ///
+ /// Compares the plugin version number to another plugin version number.
+ ///
+ /// The other plugin version number to compare to.
+ /// A value indicating the relative order of the plugin version numbers.
+ public int CompareTo(PluginVersion other)
+ {
+ var majorCompare = this.Major.CompareTo(other.Major);
+ if (majorCompare != 0)
+ return majorCompare;
+
+ var minorCompare = this.Minor.CompareTo(other.Minor);
+ if (minorCompare != 0)
+ return minorCompare;
+
+ return this.Patch.CompareTo(other.Patch);
+ }
+
+ public static bool operator >(PluginVersion left, PluginVersion right) => left.CompareTo(right) > 0;
+
+ public static bool operator <(PluginVersion left, PluginVersion right) => left.CompareTo(right) < 0;
+
+ public static bool operator >=(PluginVersion left, PluginVersion right) => left.CompareTo(right) >= 0;
+
+ public static bool operator <=(PluginVersion left, PluginVersion right) => left.CompareTo(right) <= 0;
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/packages.lock.json b/app/MindWork AI Studio/packages.lock.json
index 37a21d8a..8020366c 100644
--- a/app/MindWork AI Studio/packages.lock.json
+++ b/app/MindWork AI Studio/packages.lock.json
@@ -22,6 +22,12 @@
"resolved": "1.12.0",
"contentHash": "VHtVZmfoYhQyA/POvZRLuTpCz1zhzIDrdYRJIRV73e9wKAzjW71biYNOHOWx8MxEX3TE4TWVfx1QDRoZcj2AWw=="
},
+ "LuaCSharp": {
+ "type": "Direct",
+ "requested": "[0.4.2, )",
+ "resolved": "0.4.2",
+ "contentHash": "wS0hp7EFx+llJ/U/7Ykz4FSmQf8DH4mNejwo5/h1KuFyguzGZbKhTO22X54pXnuqa5cIKfEfQ29dluHHnCX05Q=="
+ },
"Microsoft.Extensions.FileProviders.Embedded": {
"type": "Direct",
"requested": "[9.0.3, )",
diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.39.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.39.md
new file mode 100644
index 00000000..4fb08d00
--- /dev/null
+++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.39.md
@@ -0,0 +1,3 @@
+# 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.
\ No newline at end of file
diff --git a/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/EmptyStringAnalyzer.cs b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/EmptyStringAnalyzer.cs
index f6cc65b7..5092d436 100644
--- a/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/EmptyStringAnalyzer.cs
+++ b/app/SourceCodeRules/SourceCodeRules/UsageAnalyzers/EmptyStringAnalyzer.cs
@@ -46,6 +46,9 @@ public sealed class EmptyStringAnalyzer : DiagnosticAnalyzer
if (IsInConstContext(stringLiteral))
return;
+ if (IsInParameterDefaultValue(stringLiteral))
+ return;
+
var diagnostic = Diagnostic.Create(RULE, stringLiteral.GetLocation());
context.ReportDiagnostic(diagnostic);
}
@@ -65,4 +68,21 @@ public sealed class EmptyStringAnalyzer : DiagnosticAnalyzer
_ => false
};
}
+
+ private static bool IsInParameterDefaultValue(LiteralExpressionSyntax stringLiteral)
+ {
+ // Prüfen, ob das String-Literal Teil eines Parameter-Defaults ist
+ var parameter = stringLiteral.FirstAncestorOrSelf();
+ if (parameter is null)
+ return false;
+
+ // Überprüfen, ob das String-Literal im Default-Wert des Parameters verwendet wird
+ if (parameter.Default is not null &&
+ parameter.Default.Value == stringLiteral)
+ {
+ return true;
+ }
+
+ return false;
+ }
}
\ No newline at end of file