diff --git a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor new file mode 100644 index 000000000..6f7e91b2f --- /dev/null +++ b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor @@ -0,0 +1,87 @@ +@attribute [Route(Routes.ASSISTANT_DYNAMIC)] +@using AIStudio.Tools.PluginSystem.Assistants.DataModel +@inherits AssistantBaseCore + +@foreach (var component in this.RootComponent!.Children) +{ + @switch (component.Type) + { + case AssistantUiCompontentType.TEXT_AREA: + if (component is AssistantTextArea textArea) + { + var lines = textArea.IsSingleLine ? 1 : 6; + + } + break; + + case AssistantUiCompontentType.DROPDOWN: + if (component is AssistantDropdown assistantDropdown) + { + + } + break; + case AssistantUiCompontentType.PROVIDER_SELECTION: + if (component is AssistantProviderSelection providerSelection) + { + + } + break; + case AssistantUiCompontentType.SWITCH: + if (component is AssistantSwitch assistantSwitch) + { + + } + break; + case AssistantUiCompontentType.HEADING: + if (component is AssistantHeading assistantHeading) + { + var heading = assistantHeading; + @switch (assistantHeading.Level) + { + case 1: + @heading.Text + break; + case 2: + @heading.Text + break; + case 3: + @heading.Text + break; + default: + @heading.Text + break; + } + } + break; + case AssistantUiCompontentType.TEXT: + if (component is AssistantText assistantText) + { + var text = assistantText; + @text.Content + } + break; + case AssistantUiCompontentType.LIST: + if (component is AssistantList assistantList) + { + var list = assistantList; + + @foreach (var item in list.Items) + { + @if (item.Type == "LINK") + { + @item.Text + } + else + { + @item.Text + } + } + + } + break; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs new file mode 100644 index 000000000..433afeb4a --- /dev/null +++ b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs @@ -0,0 +1,147 @@ +using AIStudio.Dialogs.Settings; +using AIStudio.Tools.PluginSystem; +using AIStudio.Tools.PluginSystem.Assistants; +using AIStudio.Tools.PluginSystem.Assistants.DataModel; +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Assistants.Dynamic; + +public partial class AssistantDynamic : AssistantBaseCore +{ + [Parameter] + public AssistantForm? RootComponent { get; set; } = null!; + + protected override string Title => this.title; + protected override string Description => this.description; + protected override string SystemPrompt => this.systemPrompt; + protected override bool AllowProfiles => this.allowProfiles; + protected override string SubmitText => this.submitText; + protected override Func SubmitAction => this.Submit; + public override Tools.Components Component { get; } + + private string? inputText; + private string title = string.Empty; + private string description = string.Empty; + private string systemPrompt = string.Empty; + private bool allowProfiles = true; + private string submitText = string.Empty; + private string selectedTargetLanguage = string.Empty; + private string customTargetLanguage = string.Empty; + + private Dictionary inputFields = new(); + private Dictionary dropdownFields = new(); + private Dictionary switchFields = new(); + + protected override void OnInitialized() + { + var guid = Guid.Parse("958312de-a9e7-4666-901f-4d5b61647efb"); + var plugin = PluginFactory.RunningPlugins.FirstOrDefault(e => e.Id == guid); + if (plugin is PluginAssistants assistantPlugin) + { + this.RootComponent = assistantPlugin.RootComponent; + this.title = assistantPlugin.AssistantTitle; + this.description = assistantPlugin.AssistantDescription; + this.systemPrompt = assistantPlugin.SystemPrompt; + this.submitText = assistantPlugin.SubmitText; + this.allowProfiles = assistantPlugin.AllowProfiles; + } + + foreach (var component in this.RootComponent!.Children) + { + switch (component.Type) + { + case AssistantUiCompontentType.TEXT_AREA: + if (component is AssistantTextArea textArea) + { + this.inputFields.Add(textArea.Name, textArea.PrefillText); + } + break; + case AssistantUiCompontentType.DROPDOWN: + if (component is AssistantDropdown dropdown) + { + this.dropdownFields.Add(dropdown.Name, dropdown.Default.Value); + } + break; + case AssistantUiCompontentType.SWITCH: + if (component is AssistantSwitch switchComponent) + { + this.switchFields.Add(switchComponent.Name, switchComponent.Value); + } + break; + } + } + base.OnInitialized(); + } + + protected override void ResetForm() + { + foreach (var entry in this.inputFields) + { + this.inputFields[entry.Key] = string.Empty; + } + } + + protected override bool MightPreselectValues() + { + Console.WriteLine("throw new NotImplementedException();"); + return false; + } + + private string? ValidateCustomLanguage(string value) => string.Empty; + + private string CollectUserPrompt() + { + var prompt = string.Empty; + + foreach (var component in this.RootComponent!.Children) + { + var userInput = string.Empty; + var userDecision = false; + switch (component.Type) + { + case AssistantUiCompontentType.TEXT_AREA: + if (component is AssistantTextArea textArea) + { + prompt += $"context:{Environment.NewLine}{textArea.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"; + if (this.inputFields.TryGetValue(textArea.Name, out userInput)) + { + prompt += $"user prompt:{Environment.NewLine}{userInput}"; + } + } + break; + case AssistantUiCompontentType.DROPDOWN: + if (component is AssistantDropdown dropdown) + { + prompt += $"{Environment.NewLine}context:{Environment.NewLine}{dropdown.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"; + if (this.dropdownFields.TryGetValue(dropdown.Name, out userInput)) + { + prompt += $"user prompt:{Environment.NewLine}{userInput}"; + } + } + break; + case AssistantUiCompontentType.SWITCH: + if (component is AssistantSwitch switchComponent) + { + prompt += $"{Environment.NewLine}context:{Environment.NewLine}{switchComponent.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"; + if (this.switchFields.TryGetValue(switchComponent.Name, out userDecision)) + { + prompt += $"user decision:{Environment.NewLine}{userDecision}"; + } + } + break; + default: + prompt += $"{userInput}{Environment.NewLine}"; + break; + } + } + + return prompt; + } + + private async Task Submit() + { + this.CreateChatThread(); + var time = this.AddUserRequest(this.CollectUserPrompt()); + await this.AddAIResponseAsync(time); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index c1f40eb5f..544e20cad 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -5353,6 +5353,27 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "It seems that Pandoc i -- The latest Pandoc version was not found, installing version {0} instead. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "The latest Pandoc version was not found, installing version {0} instead." +-- The ASSISTANT table does not contain a valid description. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T2080819991"] = "The ASSISTANT table does not contain a valid description." + +-- The ASSISTANT table does not contain a the boolean flag to control the allowance of profiles. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T221472268"] = "The ASSISTANT table does not contain a the boolean flag to control the allowance of profiles." + +-- Failed to parse the UI render tree. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T2583341941"] = "Failed to parse the UI render tree." + +-- The ASSISTANT table does not contain a valid UI section. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T3126717084"] = "The ASSISTANT table does not contain a valid UI section." + +-- The ASSISTANT table does not contain a valid system prompt. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T3723171842"] = "The ASSISTANT table does not contain a valid system prompt." + +-- The ASSISTANT table does not exist or is not a valid table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T6004146"] = "The ASSISTANT table does not exist or is not a valid table." + +-- The ASSISTANT table does not contain a valid title. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T998753547"] = "The ASSISTANT table does not contain a valid title." + -- The table AUTHORS does not exist or is using an invalid syntax. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T1068328139"] = "The table AUTHORS does not exist or is using an invalid syntax." diff --git a/app/MindWork AI Studio/Components/DynamicAssistantDropdown.razor b/app/MindWork AI Studio/Components/DynamicAssistantDropdown.razor new file mode 100644 index 000000000..647bc2caf --- /dev/null +++ b/app/MindWork AI Studio/Components/DynamicAssistantDropdown.razor @@ -0,0 +1,21 @@ + + + @foreach (var item in Items) + { + + @item.Display + + } + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/DynamicAssistantDropdown.razor.cs b/app/MindWork AI Studio/Components/DynamicAssistantDropdown.razor.cs new file mode 100644 index 000000000..5ecc5a1ab --- /dev/null +++ b/app/MindWork AI Studio/Components/DynamicAssistantDropdown.razor.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AIStudio.Tools.PluginSystem.Assistants.DataModel; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace AIStudio.Components +{ + public partial class DynamicAssistantDropdown : ComponentBase + { + [Parameter] + public List Items { get; set; } = new(); + + [Parameter] + public AssistantDropdownItem Default { get; set; } = new(); + + [Parameter] + public string Value { get; set; } = string.Empty; + + [Parameter] + public EventCallback ValueChanged { get; set; } + + [Parameter] + public string Label { get; set; } = string.Empty; + + [Parameter] + public Func ValidateSelection { get; set; } = _ => null; + + [Parameter] + public string Icon { get; set; } = Icons.Material.Filled.ArrowDropDown; + + private async Task OnValueChanged(string newValue) + { + if (this.Value != newValue) + { + this.Value = newValue; + await this.ValueChanged.InvokeAsync(newValue); + } + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDynamic.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDynamic.razor new file mode 100644 index 000000000..3dcb1e784 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDynamic.razor @@ -0,0 +1,5 @@ +@using AIStudio.Settings +@inherits SettingsDialogBase + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDynamic.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDynamic.razor.cs new file mode 100644 index 000000000..5ee86a7f3 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDynamic.razor.cs @@ -0,0 +1,5 @@ +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Dialogs.Settings; + +public partial class SettingsDialogDynamic : SettingsDialogBase; \ No newline at end of file diff --git a/app/MindWork AI Studio/Pages/Assistants.razor b/app/MindWork AI Studio/Pages/Assistants.razor index d7fe0bed6..8ebce7f6d 100644 --- a/app/MindWork AI Studio/Pages/Assistants.razor +++ b/app/MindWork AI Studio/Pages/Assistants.razor @@ -1,5 +1,8 @@ @using AIStudio.Dialogs.Settings @using AIStudio.Settings.DataModel +@using AIStudio.Tools.PluginSystem +@using AIStudio.Tools.PluginSystem.Assistants +@using ReverseMarkdown.Converters @attribute [Route(Routes.ASSISTANTS)] @inherits MSGComponentBase @@ -31,6 +34,14 @@ + + @foreach (var assistant in PluginFactory.RunningPlugins.Where(e => e.Type == PluginType.ASSISTANT)) + { + if (assistant is PluginAssistants assistantPlugin) + { + + } + } diff --git a/app/MindWork AI Studio/Plugins/assistants/icon.lua b/app/MindWork AI Studio/Plugins/assistants/icon.lua new file mode 100644 index 000000000..045bd983f --- /dev/null +++ b/app/MindWork AI Studio/Plugins/assistants/icon.lua @@ -0,0 +1 @@ +SVG = [[]] \ No newline at end of file diff --git a/app/MindWork AI Studio/Plugins/assistants/plugin.lua b/app/MindWork AI Studio/Plugins/assistants/plugin.lua new file mode 100644 index 000000000..38c46b4aa --- /dev/null +++ b/app/MindWork AI Studio/Plugins/assistants/plugin.lua @@ -0,0 +1,140 @@ +require("icon") + +-- ------ +-- This is an example of an assistant plugin that will build an assistant for you. +-- Please replace the placeholders and assign a valid ID. +-- ------ + +-- The ID for this plugin: +ID = "00000000-0000-0000-0000-000000000000" + +-- The icon for the plugin: +ICON_SVG = SVG + +-- The name of the plugin: +NAME = " - Configuration for " + +-- The description of the plugin: +DESCRIPTION = "This is a pre-defined configuration of " + +-- The version of the plugin: +VERSION = "1.0.0" + +-- The type of the plugin: +TYPE = "ASSISTANT" + +-- The authors of the plugin: +AUTHORS = {""} + +-- The support contact for the plugin: +SUPPORT_CONTACT = "" + +-- The source URL for the plugin: +SOURCE_URL = "" + +-- 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 = "" + +ASSISTANT = { + ["Title"] = "", + ["Description"] = "<Description presented to the users, explaining your assistant>", + ["UI"] = { + ["Type"] = "FORM", + ["Children"] = {} + }, +} + +-- usage example with the full feature set: +ASSISTANT = { + ["Title"] = "<main title of assistant>", -- required + ["Description"] = "<assitant description>", -- required + ["SystemPrompt"] = "<prompt that fudamentally changes behaviour, personality and task focus of your assistant. Invisible to the user>", -- required + ["SubmitText"] = "<label for submit button>", -- required + ["AllowProfiles"] = true, -- if true, allows AiStudios profiles; required + ["UI"] = { + ["Type"] = "FORM", + ["Children"] = { + { + ["Type"] = "TEXT_AREA", -- required + ["Props"] = { + ["Name"] = "<unique identifier of this component>", -- required + ["Label"] = "<heading of your component>", -- required + ["UserPrompt"] = "<direct input of instructions, questions, or tasks by a user>", + ["PrefillText"] = "<text to show in the field initially>", + ["IsSingleLine"] = false, -- if true, shows a text field instead of an area + ["ReadOnly"] = false -- if true, deactivates user input (make sure to provide a PrefillText) + } + }, + { + ["Type"] = "DROPDOWN", -- required + ["Props"] = { + ["Name"] = "<unique identifier of this component>", -- required + ["Label"] = "<heading of your component>", -- required + ["UserPrompt"] = "<direct input of instructions, questions, or tasks by a user>", + ["ValueType"] = "<data type of item values>", -- required + ["Default"] = { ["Value"] = "<internal data>", ["Display"] = "<user readable representation>" }, -- required + ["Items"] = { + { ["Value"] = "<internal data>", ["Display"] = "<user readable representation>" }, + { ["Value"] = "<internal data>", ["Display"] = "<user readable representation>" }, + } -- required + } + }, + { + ["Type"] = "SWITCH", + ["Props"] = { + ["Name"] = "<unique identifier of this component>", -- required + ["Label"] = "<heading of your component>", -- required + ["Value"] = true, -- intial switch state + ["UserPrompt"] = "<direct input of instructions, questions, or tasks by a user>", + ["LabelOn"] = "<text if state is true>", -- required + ["LabelOff"] = "<text if state is false>" -- required + } + }, + { + ["Type"] = "PROVIDER_SELECTION", -- required + ["Props"] = { + ["Name"] = "Anbieter", + ["Label"] = "LLM auswählen" + } + }, + { + ["Type"] = "HEADING", -- descriptive component for headings + ["Props"] = { + ["Text"] = "<heading content>", -- required + ["Level"] = 2 -- Heading level, 1 - 3 + } + }, + { + ["Type"] = "TEXT", -- descriptive component for normal text + ["Props"] = { + ["Content"] = "<text content>" + } + }, + { + ["Type"] = "LIST", -- descriptive list component + ["Props"] = { + ["Items"] = { + { + ["Type"] = "LINK", -- required + ["Text"] = "<user readable link text>", + ["Href"] = "<link>" -- required + }, + { + ["Type"] = "TEXT", -- required + ["Text"] = "<user readable text>" + } + } + } + }, + } + }, +} \ 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 d59bffac9..e58a77194 100644 --- a/app/MindWork AI Studio/Routes.razor.cs +++ b/app/MindWork AI Studio/Routes.razor.cs @@ -27,5 +27,6 @@ public sealed partial class Routes public const string ASSISTANT_BIAS = "/assistant/bias-of-the-day"; public const string ASSISTANT_ERI = "/assistant/eri"; public const string ASSISTANT_AI_STUDIO_I18N = "/assistant/ai-studio/i18n"; + public const string ASSISTANT_DYNAMIC = "/assistant/dynamic"; // ReSharper restore InconsistentNaming } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/AssistantComponentFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/AssistantComponentFactory.cs new file mode 100644 index 000000000..5dc67e7da --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/AssistantComponentFactory.cs @@ -0,0 +1,39 @@ +using AIStudio.Tools.PluginSystem.Assistants.DataModel; + +namespace AIStudio.Tools.PluginSystem.Assistants; + +public class AssistantComponentFactory +{ + private static readonly ILogger<AssistantComponentFactory> LOGGER = Program.LOGGER_FACTORY.CreateLogger<AssistantComponentFactory>(); + + public static IAssistantComponent CreateComponent( + AssistantUiCompontentType type, + Dictionary<string, object> props, + List<IAssistantComponent> children) + { + switch (type) + { + case AssistantUiCompontentType.FORM: + return new AssistantForm { Props = props, Children = children }; + case AssistantUiCompontentType.TEXT_AREA: + return new AssistantTextArea { Props = props, Children = children }; + case AssistantUiCompontentType.BUTTON: + return new AssistantButton { Props = props, Children = children}; + case AssistantUiCompontentType.DROPDOWN: + return new AssistantDropdown { Props = props, Children = children }; + case AssistantUiCompontentType.PROVIDER_SELECTION: + return new AssistantProviderSelection { Props = props, Children = children }; + case AssistantUiCompontentType.SWITCH: + return new AssistantSwitch { Props = props, Children = children }; + case AssistantUiCompontentType.HEADING: + return new AssistantHeading { Props = props, Children = children }; + case AssistantUiCompontentType.TEXT: + return new AssistantText { Props = props, Children = children }; + case AssistantUiCompontentType.LIST: + return new AssistantList { Props = props, Children = children }; + default: + LOGGER.LogError($"Unknown assistant component type!\n{type} is not a supported assistant component type"); + throw new Exception($"Unknown assistant component type: {type}"); + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantButton.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantButton.cs new file mode 100644 index 000000000..b66fac794 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantButton.cs @@ -0,0 +1,30 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public class AssistantButton : AssistantComponentBase +{ + public override AssistantUiCompontentType Type => AssistantUiCompontentType.BUTTON; + public Dictionary<string, object> Props { get; set; } = new(); + public List<IAssistantComponent> Children { get; set; } = new(); + + public string Name + { + get => this.Props.TryGetValue(nameof(this.Name), out var v) + ? v.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.Name)] = value; + } + public string Text + { + get => this.Props.TryGetValue(nameof(this.Text), out var v) + ? v.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.Text)] = value; + } + public string Action + { + get => this.Props.TryGetValue(nameof(this.Action), out var v) + ? v.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.Action)] = value; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantComponentBase.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantComponentBase.cs new file mode 100644 index 000000000..8e41b4f5f --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantComponentBase.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public abstract class AssistantComponentBase : IAssistantComponent +{ + public abstract AssistantUiCompontentType Type { get; } + public Dictionary<string, object> Props { get; } + public List<IAssistantComponent> Children { get; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDropdown.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDropdown.cs new file mode 100644 index 000000000..080be355e --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDropdown.cs @@ -0,0 +1,81 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public class AssistantDropdown : AssistantComponentBase +{ + public override AssistantUiCompontentType Type => AssistantUiCompontentType.DROPDOWN; + public Dictionary<string, object> Props { get; set; } = new(); + public List<IAssistantComponent> Children { get; set; } = new(); + + public string Name + { + get => this.Props.TryGetValue(nameof(this.Name), out var v) + ? v.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.Name)] = value; + } + public string Label + { + get => this.Props.TryGetValue(nameof(this.Label), out var v) + ? v.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.Label)] = value; + } + + public string UserPrompt + { + get => this.Props.TryGetValue(nameof(this.UserPrompt), out var v) + ? v.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.UserPrompt)] = value; + } + + public AssistantDropdownItem Default + { + get + { + if (this.Props.TryGetValue(nameof(this.Default), out var v) && v is AssistantDropdownItem adi) + return adi; + + return this.Items.Count > 0 ? this.Items[0] : AssistantDropdownItem.Default(); + } + set => this.Props[nameof(this.Default)] = value; + } + + public List<AssistantDropdownItem> Items + { + get => this.Props.TryGetValue(nameof(this.Items), out var v) && v is List<AssistantDropdownItem> list + ? list + : []; + set => this.Props[nameof(this.Items)] = value; + } + + public string ValueType + { + get => this.Props.TryGetValue(nameof(this.ValueType), out var v) + ? v.ToString() ?? "string" + : "string"; + set => this.Props[nameof(this.ValueType)] = value; + } + + public IEnumerable<object> GetParsedDropdownValues() + { + foreach (var item in this.Items) + { + switch (this.ValueType.ToLowerInvariant()) + { + case "int": + if (int.TryParse(item.Value, out var i)) yield return i; + break; + case "double": + if (double.TryParse(item.Value, out var d)) yield return d; + break; + case "bool": + if (bool.TryParse(item.Value, out var b)) yield return b; + break; + default: + yield return item.Value; + break; + } + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDropdownItem.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDropdownItem.cs new file mode 100644 index 000000000..91e2831f9 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDropdownItem.cs @@ -0,0 +1,9 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public class AssistantDropdownItem +{ + public string Value { get; set; } = string.Empty; + public string Display { get; set; } = string.Empty; + + public static AssistantDropdownItem Default() => new() { Value = string.Empty, Display = string.Empty }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantForm.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantForm.cs new file mode 100644 index 000000000..a805fc8ae --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantForm.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public class AssistantForm : AssistantComponentBase +{ + public override AssistantUiCompontentType Type => AssistantUiCompontentType.FORM; + public Dictionary<string, object> Props { get; set; } = new(); + public List<IAssistantComponent> Children { get; set; } = new(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantHeading.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantHeading.cs new file mode 100644 index 000000000..68f6f4502 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantHeading.cs @@ -0,0 +1,27 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public class AssistantHeading : AssistantComponentBase +{ + public override AssistantUiCompontentType Type => AssistantUiCompontentType.HEADING; + + public Dictionary<string, object> Props { get; set; } = new(); + + public List<IAssistantComponent> Children { get; set; } = new(); + + public string Text + { + get => this.Props.TryGetValue(nameof(this.Text), out var v) + ? v.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.Text)] = value; + } + + public int Level + { + get => this.Props.TryGetValue(nameof(this.Level), out var v) + && int.TryParse(v.ToString(), out var i) + ? i + : 2; + set => this.Props[nameof(this.Level)] = value; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantList.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantList.cs new file mode 100644 index 000000000..f44cbf3dc --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantList.cs @@ -0,0 +1,18 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public class AssistantList : AssistantComponentBase +{ + public override AssistantUiCompontentType Type => AssistantUiCompontentType.LIST; + + public Dictionary<string, object> Props { get; set; } = new(); + + public List<IAssistantComponent> Children { get; set; } = new(); + + public List<AssistantListItem> Items + { + get => this.Props.TryGetValue(nameof(this.Items), out var v) && v is List<AssistantListItem> list + ? list + : []; + set => this.Props[nameof(this.Items)] = value; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantListItem.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantListItem.cs new file mode 100644 index 000000000..43bd60e13 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantListItem.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public class AssistantListItem +{ + public string Type { get; set; } = "TEXT"; + public string Text { get; set; } = string.Empty; + public string? Href { get; set; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantProviderSelection.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantProviderSelection.cs new file mode 100644 index 000000000..9767cd1b0 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantProviderSelection.cs @@ -0,0 +1,23 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public class AssistantProviderSelection : AssistantComponentBase +{ + public override AssistantUiCompontentType Type => AssistantUiCompontentType.PROVIDER_SELECTION; + public Dictionary<string, object> Props { get; set; } = new(); + public List<IAssistantComponent> Children { get; set; } = new(); + + public string Name + { + get => this.Props.TryGetValue(nameof(this.Name), out var v) + ? v.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.Name)] = value; + } + public string Label + { + get => this.Props.TryGetValue(nameof(this.Label), out var v) + ? v.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.Label)] = value; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantSwitch.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantSwitch.cs new file mode 100644 index 000000000..33082508e --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantSwitch.cs @@ -0,0 +1,54 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public class AssistantSwitch : AssistantComponentBase +{ + public override AssistantUiCompontentType Type => AssistantUiCompontentType.SWITCH; + public Dictionary<string, object> Props { get; set; } = new(); + public List<IAssistantComponent> Children { get; set; } = new(); + + public string Name + { + get => this.Props.TryGetValue(nameof(this.Name), out var v) + ? v.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.Name)] = value; + } + + public string Label + { + get => this.Props.TryGetValue(nameof(this.Label), out var v) + ? v.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.Label)] = value; + } + + public bool Value + { + get => this.Props.TryGetValue(nameof(this.Value), out var val) && val is true; + set => this.Props[nameof(this.Value)] = value; + } + + public string UserPrompt + { + get => this.Props.TryGetValue(nameof(this.UserPrompt), out var val) + ? val.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.UserPrompt)] = value; + } + + public string LabelOn + { + get => this.Props.TryGetValue(nameof(this.LabelOn), out var v) + ? v.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.LabelOn)] = value; + } + + public string LabelOff + { + get => this.Props.TryGetValue(nameof(this.LabelOff), out var v) + ? v.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.LabelOff)] = value; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantText.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantText.cs new file mode 100644 index 000000000..01bec2683 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantText.cs @@ -0,0 +1,18 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public class AssistantText : AssistantComponentBase +{ + public override AssistantUiCompontentType Type => AssistantUiCompontentType.TEXT; + + public Dictionary<string, object> Props { get; set; } = new(); + + public List<IAssistantComponent> Children { get; set; } = new(); + + public string Content + { + get => this.Props.TryGetValue(nameof(this.Content), out var v) + ? v.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.Content)] = value; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantTextArea.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantTextArea.cs new file mode 100644 index 000000000..94194301a --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantTextArea.cs @@ -0,0 +1,52 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public class AssistantTextArea : AssistantComponentBase +{ + public override AssistantUiCompontentType Type => AssistantUiCompontentType.TEXT_AREA; + public Dictionary<string, object> Props { get; set; } = new(); + public List<IAssistantComponent> Children { get; set; } = new(); + + public string Name + { + get => this.Props.TryGetValue(nameof(this.Name), out var val) + ? val.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.Name)] = value; + } + + public string Label + { + get => this.Props.TryGetValue(nameof(this.Label), out var val) + ? val.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.Label)] = value; + } + + public string UserPrompt + { + get => this.Props.TryGetValue(nameof(this.UserPrompt), out var val) + ? val.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.UserPrompt)] = value; + } + + public string PrefillText + { + get => this.Props.TryGetValue(nameof(this.PrefillText), out var val) + ? val.ToString() ?? string.Empty + : string.Empty; + set => this.Props[nameof(this.PrefillText)] = value; + } + + public bool IsSingleLine + { + get => this.Props.TryGetValue(nameof(this.IsSingleLine), out var val) && val is true; + set => this.Props[nameof(this.IsSingleLine)] = value; + } + + public bool ReadOnly + { + get => this.Props.TryGetValue(nameof(this.ReadOnly), out var val) && val is true; + set => this.Props[nameof(this.ReadOnly)] = value; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantUiCompontentType.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantUiCompontentType.cs new file mode 100644 index 000000000..9ec6e948e --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantUiCompontentType.cs @@ -0,0 +1,14 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public enum AssistantUiCompontentType +{ + FORM, + TEXT_AREA, + BUTTON, + DROPDOWN, + PROVIDER_SELECTION, + SWITCH, + HEADING, + TEXT, + LIST, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/ComponentPropSpecs.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/ComponentPropSpecs.cs new file mode 100644 index 000000000..5846dda2a --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/ComponentPropSpecs.cs @@ -0,0 +1,45 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public static class ComponentPropSpecs +{ + public static readonly IReadOnlyDictionary<AssistantUiCompontentType, PropSpec> SPECS = + new Dictionary<AssistantUiCompontentType, PropSpec> + { + [AssistantUiCompontentType.FORM] = new( + required: ["Children"], + optional: [] + ), + [AssistantUiCompontentType.TEXT_AREA] = new( + required: ["Name", "Label"], + optional: ["UserPrompt", "PrefillText", "ReadOnly", "IsSingleLine"] + ), + [AssistantUiCompontentType.BUTTON] = new( + required: ["Name", "Text", "Action"], + optional: [] + ), + [AssistantUiCompontentType.DROPDOWN] = new( + required: ["Name", "Label", "Default", "Items"], + optional: ["UserPrompt"] + ), + [AssistantUiCompontentType.PROVIDER_SELECTION] = new( + required: ["Name", "Label"], + optional: [] + ), + [AssistantUiCompontentType.SWITCH] = new( + required: ["Name", "Label", "LabelOn", "LabelOff", "Value"], + optional: ["UserPrompt"] + ), + [AssistantUiCompontentType.HEADING] = new( + required: ["Text", "Level"], + optional: [] + ), + [AssistantUiCompontentType.TEXT] = new( + required: ["Content"], + optional: [] + ), + [AssistantUiCompontentType.LIST] = new( + required: ["Items"], + optional: [] + ), + }; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/IAssistantComponent.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/IAssistantComponent.cs new file mode 100644 index 000000000..0c00b9667 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/IAssistantComponent.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public interface IAssistantComponent +{ + AssistantUiCompontentType Type { get; } + Dictionary<string, object> Props { get; } + List<IAssistantComponent> Children { get; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/PropSpec.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/PropSpec.cs new file mode 100644 index 000000000..7aa5e7b1c --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/PropSpec.cs @@ -0,0 +1,7 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public class PropSpec(IEnumerable<string> required, IEnumerable<string> optional) +{ + public IReadOnlyList<string> Required { get; } = required.ToArray(); + public IReadOnlyList<string> Optional { get; } = optional.ToArray(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs new file mode 100644 index 000000000..ba8bae8dd --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs @@ -0,0 +1,369 @@ +using System.Xml.XPath; +using AIStudio.Tools.PluginSystem.Assistants.DataModel; +using Lua; + +namespace AIStudio.Tools.PluginSystem.Assistants; + +public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType type) : PluginBase(isInternal, state, type) +{ + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginAssistants).Namespace, nameof(PluginAssistants)); + + private static readonly ILogger<PluginAssistants> LOGGER = Program.LOGGER_FACTORY.CreateLogger<PluginAssistants>(); + + public AssistantForm? RootComponent { get; set; } + public string AssistantTitle { get; set; } = string.Empty; + public string AssistantDescription { get; set; } = string.Empty; + public string SystemPrompt { get; set; } = string.Empty; + public string SubmitText { get; set; } = string.Empty; + public bool AllowProfiles { get; set; } = true; + + public void TryLoad() + { + if(!this.TryProcessAssistant(out var issue)) + this.pluginIssues.Add(issue); + } + + /// <summary> + /// Tries to parse the assistant table into our internal assistant render tree data model. It follows this process: + /// <list type="number"> + /// <item><description>ASSISTANT → Title/Description → UI</description></item> + /// <item><description>UI: Root element → required Children → Components</description></item> + /// <item><description>Components: Type → Props → Children (recursively)</description></item> + /// </list> + /// </summary> + /// <param name="message">The error message, when parameters from the table could not be read.</param> + /// <returns>True, when the assistant could be read successfully indicating the data model is populated.</returns> + private bool TryProcessAssistant(out string message) + { + message = string.Empty; + + // Ensure that the main ASSISTANT table exists and is a valid Lua table: + if (!this.state.Environment["ASSISTANT"].TryRead<LuaTable>(out var assistantTable)) + { + message = TB("The ASSISTANT table does not exist or is not a valid table."); + return false; + } + + if (!assistantTable.TryGetValue("Title", out var assistantTitleValue) || + !assistantTitleValue.TryRead<string>(out var assistantTitle)) + { + message = TB("The ASSISTANT table does not contain a valid title."); + return false; + } + + if (!assistantTable.TryGetValue("Description", out var assistantDescriptionValue) || + !assistantDescriptionValue.TryRead<string>(out var assistantDescription)) + { + message = TB("The ASSISTANT table does not contain a valid description."); + return false; + } + + if (!assistantTable.TryGetValue("SystemPrompt", out var assistantSystemPromptValue) || + !assistantSystemPromptValue.TryRead<string>(out var assistantSystemPrompt)) + { + message = TB("The ASSISTANT table does not contain a valid system prompt."); + return false; + } + + if (!assistantTable.TryGetValue("SubmitText", out var assistantSubmitTextValue) || + !assistantSubmitTextValue.TryRead<string>(out var assistantSubmitText)) + { + message = TB("The ASSISTANT table does not contain a valid system prompt."); + return false; + } + + if (!assistantTable.TryGetValue("AllowProfiles", out var assistantAllowProfilesValue) || + !assistantAllowProfilesValue.TryRead<bool>(out var assistantAllowProfiles)) + { + message = TB("The ASSISTANT table does not contain a the boolean flag to control the allowance of profiles."); + return false; + } + + this.AssistantTitle = assistantTitle; + this.AssistantDescription = assistantDescription; + this.SystemPrompt = assistantSystemPrompt; + this.SubmitText = assistantSubmitText; + this.AllowProfiles = assistantAllowProfiles; + + // Ensure that the UI table exists nested in the ASSISTANT table and is a valid Lua table: + if (!assistantTable.TryGetValue("UI", out var uiVal) || !uiVal.TryRead<LuaTable>(out var uiTable)) + { + message = TB("The ASSISTANT table does not contain a valid UI section."); + return false; + } + + if (!this.TryReadRenderTree(uiTable, out var rootComponent)) + { + message = TB("Failed to parse the UI render tree."); + return false; + } + + this.RootComponent = (AssistantForm)rootComponent; + return true; + } + + /// <summary> + /// Parses the root <c>FORM</c> component and start to parse its required children (main ui components) + /// </summary> + /// <param name="uiTable">The <c>LuaTable</c> containing all UI components</param> + /// <param name="root">Outputs the root <c>FORM</c> component, if the parsing is successful. </param> + /// <returns>True, when the UI table could be read successfully.</returns> + private bool TryReadRenderTree(LuaTable uiTable, out IAssistantComponent root) + { + root = null!; + + if (!uiTable.TryGetValue("Type", out var typeVal) + || !typeVal.TryRead<string>(out var typeText) + || !Enum.TryParse<AssistantUiCompontentType>(typeText, true, out var type) + || type != AssistantUiCompontentType.FORM) + { + LOGGER.LogWarning("UI table of the ASSISTANT table has no valid Form type."); + return false; + } + + if (!uiTable.TryGetValue("Children", out var childrenVal) || + !childrenVal.TryRead<LuaTable>(out var childrenTable)) + { + LOGGER.LogWarning("Form has no valid Children table."); + return false; + } + + var children = new List<IAssistantComponent>(); + var count = childrenTable.ArrayLength; + for (var idx = 1; idx <= count; idx++) + { + var childVal = childrenTable[idx]; + if (!childVal.TryRead<LuaTable>(out var childTable)) + { + LOGGER.LogWarning($"Child #{idx} is not a table."); + continue; + } + + if (!this.TryReadComponentTable(idx, childTable, out var comp)) + { + LOGGER.LogWarning($"Child #{idx} could not be parsed."); + continue; + } + + children.Add(comp); + } + + root = AssistantComponentFactory.CreateComponent(AssistantUiCompontentType.FORM, new Dictionary<string, object>(), children); + return true; + } + + /// <summary> + /// Parses the components' table containing all members and properties. + /// Recursively calls itself, if the component has a children table + /// </summary> + /// <param name="idx">Current index inside the <c>FORM</c> children</param> + /// <param name="componentTable">The <c>LuaTable</c> containing all component properties</param> + /// <param name="component">Outputs the component if the parsing is successful</param> + /// <returns>True, when the component table could be read successfully.</returns> + private bool TryReadComponentTable(int idx, LuaTable componentTable, out IAssistantComponent component) + { + component = null!; + + if (!componentTable.TryGetValue("Type", out var typeVal) + || !typeVal.TryRead<string>(out var typeText) + || !Enum.TryParse<AssistantUiCompontentType>(typeText, true, out var type)) + { + LOGGER.LogWarning($"Component #{idx} missing valid Type."); + return false; + } + + Dictionary<string, object> props = new(); + if (componentTable.TryGetValue("Props", out var propsVal) + && propsVal.TryRead<LuaTable>(out var propsTable)) + { + if (!this.TryReadComponentProps(type, propsTable, out props)) + LOGGER.LogWarning($"Component #{idx} Props could not be fully read."); + } + + var children = new List<IAssistantComponent>(); + if (componentTable.TryGetValue("Children", out var childVal) + && childVal.TryRead<LuaTable>(out var childTable)) + { + var cnt = childTable.ArrayLength; + for (var i = 1; i <= cnt; i++) + { + var cv = childTable[i]; + if (cv.TryRead<LuaTable>(out var ct) + && this.TryReadComponentTable(i, ct, out var childComp)) + { + children.Add(childComp); + } + } + } + + component = AssistantComponentFactory.CreateComponent(type, props, children); + return true; + } + + private bool TryReadComponentProps( + AssistantUiCompontentType type, + LuaTable propsTable, + out Dictionary<string, object> props) + { + props = new Dictionary<string, object>(); + + if (!ComponentPropSpecs.SPECS.TryGetValue(type, out var spec)) + { + LOGGER.LogWarning($"No PropSpec defined for component type {type}"); + return false; + } + + foreach (var key in spec.Required) + { + if (!propsTable.TryGetValue(key, out var luaVal)) + { + LOGGER.LogWarning($"Component {type} missing required prop '{key}'."); + return false; + } + if (!this.TryConvertLuaValue(luaVal, out var dotNetVal)) + { + LOGGER.LogWarning($"Component {type}: prop '{key}' has wrong type."); + return false; + } + props[key] = dotNetVal; + } + + foreach (var key in spec.Optional) + { + if (!propsTable.TryGetValue(key, out var luaVal)) + continue; + + if (!this.TryConvertLuaValue(luaVal, out var dotNetVal)) + { + LOGGER.LogWarning($"Component {type}: optional prop '{key}' has wrong type, skipping."); + continue; + } + props[key] = dotNetVal; + } + + return true; + } + + private bool TryConvertLuaValue(LuaValue val, out object result) + { + if (val.TryRead<string>(out var s)) + { + result = s; + return true; + } + if (val.TryRead<bool>(out var b)) + { + result = b; + return true; + } + if (val.TryRead<double>(out var d)) + { + result = d; + return true; + } + + if (val.TryRead<LuaTable>(out var table) && this.TryParseDropdownItem(table, out var item)) + { + result = item; + return true; + } + + if (val.TryRead<LuaTable>(out var listTable) && this.TryParseDropdownItemList(listTable, out var itemList)) + { + result = itemList; + return true; + } + + if (val.TryRead<LuaTable>(out var listItemListTable) && this.TryParseListItemList(listItemListTable, out var listItemList)) + { + result = listItemList; + return true; + } + + result = null!; + return false; + } + + private bool TryParseDropdownItem(LuaTable table, out AssistantDropdownItem item) + { + item = new AssistantDropdownItem(); + + if (!table.TryGetValue("Value", out var valueVal) || !valueVal.TryRead<string>(out var value)) + return false; + + if (!table.TryGetValue("Display", out var displayVal) || !displayVal.TryRead<string>(out var display)) + return false; + + item.Value = value; + item.Display = display; + return true; + } + + + private bool TryParseDropdownItemList(LuaTable table, out List<AssistantDropdownItem> items) + { + items = new List<AssistantDropdownItem>(); + + var length = table.ArrayLength; + for (var i = 1; i <= length; i++) + { + var value = table[i]; + + if (value.TryRead<LuaTable>(out var subTable) && this.TryParseDropdownItem(subTable, out var item)) + { + items.Add(item); + } + else + { + items = null!; + return false; + } + } + + return true; + } + + private bool TryParseListItem(LuaTable table, out AssistantListItem item) + { + item = new AssistantListItem(); + + if (!table.TryGetValue("Text", out var textVal) || !textVal.TryRead<string>(out var text)) + return false; + + if (!table.TryGetValue("Type", out var typeVal) || !typeVal.TryRead<string>(out var type)) + return false; + + item.Text = text; + item.Type = type; + + if (table.TryGetValue("Href", out var hrefVal) && hrefVal.TryRead<string>(out var href)) + { + item.Href = href; + } + + return true; + } + + private bool TryParseListItemList(LuaTable table, out List<AssistantListItem> items) + { + items = new List<AssistantListItem>(); + + var length = table.ArrayLength; + for (var i = 1; i <= length; i++) + { + var value = table[i]; + + if (value.TryRead<LuaTable>(out var subTable) && this.TryParseListItem(subTable, out var item)) + { + items.Add(item); + } + else + { + items = null!; + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index 92f77344b..d937df071 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -1,8 +1,9 @@ +using System.Runtime.CompilerServices; using System.Text; using AIStudio.Settings; using AIStudio.Settings.DataModel; - +using AIStudio.Tools.PluginSystem.Assistants; using Lua; using Lua.Standard; @@ -210,6 +211,11 @@ public static async Task<PluginBase> Load(string? pluginPath, string code, Cance await configPlug.InitializeAsync(true); return configPlug; + case PluginType.ASSISTANT: + var assistantPlugin = new PluginAssistants(isInternal, state, type); + assistantPlugin.TryLoad(); + return assistantPlugin; + default: return 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/PluginFactory.Starting.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs index 5d734b06f..b2228bbf1 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs @@ -64,7 +64,7 @@ private static async Task<List<PluginConfigurationObject>> RestartAllPlugins(Can try { - if (availablePlugin.IsInternal || SETTINGS_MANAGER.IsPluginEnabled(availablePlugin) || availablePlugin.Type == PluginType.CONFIGURATION) + if (availablePlugin.IsInternal || SETTINGS_MANAGER.IsPluginEnabled(availablePlugin) || availablePlugin.Type == PluginType.CONFIGURATION || availablePlugin.Type == PluginType.ASSISTANT) if(await Start(availablePlugin, cancellationToken) is { IsValid: true } plugin) { if (plugin is PluginConfiguration configPlugin)