diff --git a/app/MindWork AI Studio.sln b/app/MindWork AI Studio.sln index 0bb1ab52f..ab62feb14 100644 --- a/app/MindWork AI Studio.sln +++ b/app/MindWork AI Studio.sln @@ -8,6 +8,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build Script", "Build\Build EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedTools", "SharedTools\SharedTools.csproj", "{969C74DF-7678-4CD5-B269-D03E1ECA3D2A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGeneratedMappings", "SourceGeneratedMappings\SourceGeneratedMappings.csproj", "{4D7141D5-9C22-4D85-B748-290D15FF484C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -30,6 +32,10 @@ Global {969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Debug|Any CPU.Build.0 = Debug|Any CPU {969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Release|Any CPU.ActiveCfg = Release|Any CPU {969C74DF-7678-4CD5-B269-D03E1ECA3D2A}.Release|Any CPU.Build.0 = Release|Any CPU + {4D7141D5-9C22-4D85-B748-290D15FF484C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4D7141D5-9C22-4D85-B748-290D15FF484C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4D7141D5-9C22-4D85-B748-290D15FF484C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4D7141D5-9C22-4D85-B748-290D15FF484C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution EndGlobalSection 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..5ac6359a7 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor @@ -0,0 +1,363 @@ +@attribute [Route(Routes.ASSISTANT_DYNAMIC)] +@using AIStudio.Components +@using AIStudio.Settings +@using AIStudio.Tools.PluginSystem.Assistants.DataModel +@using AIStudio.Tools.PluginSystem.Assistants.DataModel.Layout +@inherits AssistantBaseCore + +@foreach (var component in this.RootComponent!.Children) +{ + @this.RenderComponent(component) +} + +@code { + private RenderFragment RenderChildren(IEnumerable children) => @ + @foreach (var child in children) + { + @this.RenderComponent(child) + } + ; + + private RenderFragment RenderComponent(IAssistantComponent component) => @ + @switch (component.Type) + { + case AssistantComponentType.TEXT_AREA: + if (component is AssistantTextArea textArea) + { + var lines = textArea.IsSingleLine ? 1 : 6; + + + } + break; + case AssistantComponentType.IMAGE: + if (component is AssistantImage assistantImage) + { + var resolvedSource = this.ResolveImageSource(assistantImage); + if (!string.IsNullOrWhiteSpace(resolvedSource)) + { + var image = assistantImage; +
+ + @if (!string.IsNullOrWhiteSpace(image.Caption)) + { + @image.Caption + } +
+ } + } + break; + case AssistantComponentType.WEB_CONTENT_READER: + if (component is AssistantWebContentReader webContent && this.webContentFields.TryGetValue(webContent.Name, out var webState)) + { +
+ +
+ } + break; + case AssistantComponentType.FILE_CONTENT_READER: + if (component is AssistantFileContentReader fileContent && this.fileContentFields.TryGetValue(fileContent.Name, out var fileState)) + { +
+ +
+ } + break; + case AssistantComponentType.DROPDOWN: + if (component is AssistantDropdown assistantDropdown) + { + + } + break; + case AssistantComponentType.BUTTON: + if (component is AssistantButton assistantButton) + { + var button = assistantButton; +
+ + @button.Text + +
+ } + break; + case AssistantComponentType.BUTTON_GROUP: + if (component is AssistantButtonGroup assistantButtonGroup) + { + var buttonGroup = assistantButtonGroup; + + @this.RenderChildren(buttonGroup.Children) + + } + break; + case AssistantComponentType.LAYOUT_GRID: + if (component is AssistantGrid assistantGrid) + { + var grid = assistantGrid; + + @this.RenderChildren(grid.Children) + + } + break; + case AssistantComponentType.LAYOUT_ITEM: + if (component is AssistantItem assistantItem) + { + @this.RenderLayoutItem(assistantItem) + } + break; + case AssistantComponentType.LAYOUT_PAPER: + if (component is AssistantPaper assistantPaper) + { + var paper = assistantPaper; + + @this.RenderChildren(paper.Children) + + } + break; + case AssistantComponentType.LAYOUT_STACK: + if (component is AssistantStack assistantStack) + { + var stack = assistantStack; + + @this.RenderChildren(stack.Children) + + } + break; + case AssistantComponentType.PROVIDER_SELECTION: + if (component is AssistantProviderSelection providerSelection) + { +
+ +
+ } + break; + case AssistantComponentType.PROFILE_SELECTION: + if (component is AssistantProfileSelection profileSelection) + { + var selection = profileSelection; +
+ +
+ } + break; + case AssistantComponentType.SWITCH: + if (component is AssistantSwitch switchComponent) + { + var assistantSwitch = switchComponent; + var currentValue = this.switchFields[assistantSwitch.Name]; + + + @(currentValue ? assistantSwitch.LabelOn : assistantSwitch.LabelOff) + + + } + break; + case AssistantComponentType.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 AssistantComponentType.TEXT: + if (component is AssistantText assistantText) + { + var text = assistantText; + @text.Content + } + break; + case AssistantComponentType.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; + case AssistantComponentType.COLOR_PICKER: + if (component is AssistantColorPicker assistantColorPicker) + { + var colorPicker = assistantColorPicker; + var variant = colorPicker.GetPickerVariant(); + var elevation = variant == PickerVariant.Static ? 6 : 0; + var rounded = variant == PickerVariant.Static; + + + + + } + break; + } +
; + + private string? GetRawStyle(IAssistantComponent component) + { + if (!component.Props.TryGetValue("Style", out var rawStyle) || rawStyle is null) + return null; + + return rawStyle as string ?? rawStyle.ToString(); + } + + private string? BuildPaperStyle(AssistantPaper paper) + { + List styles = []; + + this.AddStyle(styles, "height", paper.Height); + this.AddStyle(styles, "max-height", paper.MaxHeight); + this.AddStyle(styles, "min-height", paper.MinHeight); + this.AddStyle(styles, "width", paper.Width); + this.AddStyle(styles, "max-width", paper.MaxWidth); + this.AddStyle(styles, "min-width", paper.MinWidth); + + var customStyle = this.GetRawStyle(paper); + if (!string.IsNullOrWhiteSpace(customStyle)) + styles.Add(customStyle.Trim().TrimEnd(';')); + + return styles.Count == 0 ? null : string.Join("; ", styles); + } + + private RenderFragment RenderLayoutItem(AssistantItem item) => builder => + { + builder.OpenComponent(0); + + if (item.Xs.HasValue) + builder.AddAttribute(1, "xs", item.Xs.Value); + + if (item.Sm.HasValue) + builder.AddAttribute(2, "sm", item.Sm.Value); + + if (item.Md.HasValue) + builder.AddAttribute(3, "md", item.Md.Value); + + if (item.Lg.HasValue) + builder.AddAttribute(4, "lg", item.Lg.Value); + + if (item.Xl.HasValue) + builder.AddAttribute(5, "xl", item.Xl.Value); + + if (item.Xxl.HasValue) + builder.AddAttribute(6, "xxl", item.Xxl.Value); + + var itemClass = item.Class; + if (!string.IsNullOrWhiteSpace(itemClass)) + builder.AddAttribute(7, nameof(MudItem.Class), itemClass); + + var itemStyle = this.GetOptionalStyle(item.Style); + if (!string.IsNullOrWhiteSpace(itemStyle)) + builder.AddAttribute(8, nameof(MudItem.Style), itemStyle); + + builder.AddAttribute(9, nameof(MudItem.ChildContent), this.RenderChildren(item.Children)); + builder.CloseComponent(); + }; + + private void AddStyle(List styles, string key, string value) + { + if (!string.IsNullOrWhiteSpace(value)) + styles.Add($"{key}: {value.Trim().TrimEnd(';')}"); + } +} 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..f3abcd7f5 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs @@ -0,0 +1,570 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using AIStudio.Dialogs.Settings; +using AIStudio.Tools.PluginSystem; +using AIStudio.Settings; +using AIStudio.Tools.PluginSystem.Assistants; +using AIStudio.Tools.PluginSystem.Assistants.DataModel; +using Lua; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.WebUtilities; + +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 bool ShowProfileSelection => this.showFooterProfileSelection; + protected override string SubmitText => this.submitText; + protected override Func SubmitAction => this.Submit; + // Dynamic assistants do not have dedicated settings yet. + // Reuse chat-level provider filtering/preselection instead of NONE. + protected override Tools.Components Component => Tools.Components.CHAT; + + 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 bool showFooterProfileSelection = true; + private PluginAssistants? assistantPlugin; + + private readonly Dictionary inputFields = new(); + private readonly Dictionary dropdownFields = new(); + private readonly Dictionary switchFields = new(); + private readonly Dictionary webContentFields = new(); + private readonly Dictionary fileContentFields = new(); + private readonly Dictionary colorPickerFields = new(); + private readonly Dictionary imageCache = new(); + private readonly HashSet executingButtonActions = []; + private string pluginPath = string.Empty; + private const string PLUGIN_SCHEME = "plugin://"; + private const string ASSISTANT_QUERY_KEY = "assistantId"; + + protected override void OnInitialized() + { + var assistantPlugin = this.ResolveAssistantPlugin(); + if (assistantPlugin is null) + { + this.Logger.LogWarning("AssistantDynamic could not resolve a registered assistant plugin."); + base.OnInitialized(); + return; + } + + this.assistantPlugin = 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; + this.showFooterProfileSelection = !assistantPlugin.HasEmbeddedProfileSelection; + this.pluginPath = assistantPlugin.PluginPath; + + var rootComponent = this.RootComponent; + if (rootComponent is not null) + { + this.InitializeComponentState(rootComponent.Children); + } + + base.OnInitialized(); + } + + private PluginAssistants? ResolveAssistantPlugin() + { + var assistantPlugins = PluginFactory.RunningPlugins.OfType().ToList(); + if (assistantPlugins.Count == 0) + return null; + + var requestedPluginId = this.TryGetAssistantIdFromQuery(); + if (requestedPluginId is not { } id) return assistantPlugins.First(); + + var requestedPlugin = assistantPlugins.FirstOrDefault(p => p.Id == id); + return requestedPlugin ?? assistantPlugins.First(); + } + + private Guid? TryGetAssistantIdFromQuery() + { + var uri = this.NavigationManager.ToAbsoluteUri(this.NavigationManager.Uri); + if (string.IsNullOrWhiteSpace(uri.Query)) + return null; + + var query = QueryHelpers.ParseQuery(uri.Query); + if (!query.TryGetValue(ASSISTANT_QUERY_KEY, out var values)) + return null; + + var value = values.FirstOrDefault(); + if (string.IsNullOrWhiteSpace(value)) + return null; + + if (Guid.TryParse(value, out var assistantId)) + return assistantId; + + this.Logger.LogWarning("AssistantDynamic query parameter '{Parameter}' is not a valid GUID.", value); + return null; + } + + protected override void ResetForm() + { + foreach (var entry in this.inputFields) + { + this.inputFields[entry.Key] = string.Empty; + } + foreach (var entry in this.webContentFields) + { + entry.Value.Content = string.Empty; + entry.Value.AgentIsRunning = false; + } + foreach (var entry in this.fileContentFields) + { + entry.Value.Content = string.Empty; + } + foreach (var entry in this.colorPickerFields) + { + this.colorPickerFields[entry.Key] = string.Empty; + } + } + + protected override bool MightPreselectValues() + { + // Dynamic assistants have arbitrary fields supplied via plugins, so there + // isn't a built-in settings section to prefill values. Always return + // false to keep the plugin-specified defaults. + return false; + } + + private string ResolveImageSource(AssistantImage image) + { + if (string.IsNullOrWhiteSpace(image.Src)) + return string.Empty; + + if (this.imageCache.TryGetValue(image.Src, out var cached) && !string.IsNullOrWhiteSpace(cached)) + return cached; + + var resolved = image.Src; + + if (resolved.StartsWith(PLUGIN_SCHEME, StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(this.pluginPath)) + { + var relative = resolved[PLUGIN_SCHEME.Length..].TrimStart('/', '\\').Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar); + var filePath = Path.Join(this.pluginPath, relative); + if (File.Exists(filePath)) + { + var mime = GetImageMimeType(filePath); + var data = Convert.ToBase64String(File.ReadAllBytes(filePath)); + resolved = $"data:{mime};base64,{data}"; + } + else + { + resolved = string.Empty; + } + } + else if (Uri.TryCreate(resolved, UriKind.Absolute, out var uri)) + { + if (uri.Scheme is not ("http" or "https" or "data")) + resolved = string.Empty; + } + else + { + resolved = string.Empty; + } + + this.imageCache[image.Src] = resolved; + return resolved; + } + + private static string GetImageMimeType(string path) + { + var extension = Path.GetExtension(path).TrimStart('.').ToLowerInvariant(); + return extension switch + { + "svg" => "image/svg+xml", + "png" => "image/png", + "jpg" => "image/jpeg", + "jpeg" => "image/jpeg", + "gif" => "image/gif", + "webp" => "image/webp", + "bmp" => "image/bmp", + _ => "image/png", + }; + } + + private async Task CollectUserPromptAsync() + { + if (this.assistantPlugin?.HasCustomPromptBuilder != true) return this.CollectUserPromptFallback(); + + var input = this.BuildPromptInput(); + var prompt = await this.assistantPlugin.TryBuildPromptAsync(input, this.cancellationTokenSource?.Token ?? CancellationToken.None); + return !string.IsNullOrWhiteSpace(prompt) ? prompt : this.CollectUserPromptFallback(); + } + + private LuaTable BuildPromptInput() + { + var input = new LuaTable(); + + var fields = new LuaTable(); + foreach (var entry in this.inputFields) + fields[entry.Key] = entry.Value ?? string.Empty; + foreach (var entry in this.dropdownFields) + fields[entry.Key] = entry.Value ?? string.Empty; + foreach (var entry in this.switchFields) + fields[entry.Key] = entry.Value; + foreach (var entry in this.webContentFields) + fields[entry.Key] = entry.Value.Content ?? string.Empty; + foreach (var entry in this.fileContentFields) + fields[entry.Key] = entry.Value.Content ?? string.Empty; + foreach (var entry in this.colorPickerFields) + fields[entry.Key] = entry.Value ?? string.Empty; + + input["fields"] = fields; + + var meta = new LuaTable(); + var rootComponent = this.RootComponent; + if (rootComponent is not null) + this.AddMetaEntries(meta, rootComponent.Children); + + input["meta"] = meta; + + var profile = new LuaTable + { + ["Name"] = this.currentProfile.Name, + ["NeedToKnow"] = this.currentProfile.NeedToKnow, + ["Actions"] = this.currentProfile.Actions, + ["Num"] = this.currentProfile.Num, + }; + input["profile"] = profile; + + return input; + } + + private void AddMetaEntry(LuaTable meta, string name, AssistantComponentType type, string? label, string? userPrompt) + { + if (string.IsNullOrWhiteSpace(name)) + return; + + var entry = new LuaTable + { + ["Type"] = type.ToString(), + }; + + if (!string.IsNullOrWhiteSpace(label)) + entry["Label"] = label!; + if (!string.IsNullOrWhiteSpace(userPrompt)) + entry["UserPrompt"] = userPrompt!; + + meta[name] = entry; + } + + private string CollectUserPromptFallback() + { + var prompt = string.Empty; + var rootComponent = this.RootComponent; + if (rootComponent is null) + return prompt; + + return this.CollectUserPromptFallback(rootComponent.Children); + } + + private void InitializeComponentState(IEnumerable components) + { + foreach (var component in components) + { + switch (component.Type) + { + case AssistantComponentType.TEXT_AREA: + if (component is AssistantTextArea textArea && !this.inputFields.ContainsKey(textArea.Name)) + this.inputFields.Add(textArea.Name, textArea.PrefillText); + break; + case AssistantComponentType.DROPDOWN: + if (component is AssistantDropdown dropdown && !this.dropdownFields.ContainsKey(dropdown.Name)) + this.dropdownFields.Add(dropdown.Name, dropdown.Default.Value); + break; + case AssistantComponentType.SWITCH: + if (component is AssistantSwitch switchComponent && !this.switchFields.ContainsKey(switchComponent.Name)) + this.switchFields.Add(switchComponent.Name, switchComponent.Value); + break; + case AssistantComponentType.WEB_CONTENT_READER: + if (component is AssistantWebContentReader webContent && !this.webContentFields.ContainsKey(webContent.Name)) + { + this.webContentFields.Add(webContent.Name, new WebContentState + { + Preselect = webContent.Preselect, + PreselectContentCleanerAgent = webContent.PreselectContentCleanerAgent, + }); + } + break; + case AssistantComponentType.FILE_CONTENT_READER: + if (component is AssistantFileContentReader fileContent && !this.fileContentFields.ContainsKey(fileContent.Name)) + this.fileContentFields.Add(fileContent.Name, new FileContentState()); + break; + case AssistantComponentType.COLOR_PICKER: + if (component is AssistantColorPicker assistantColorPicker && !this.colorPickerFields.ContainsKey(assistantColorPicker.Name)) + this.colorPickerFields.Add(assistantColorPicker.Name, assistantColorPicker.Placeholder); + break; + } + + if (component.Children.Count > 0) + this.InitializeComponentState(component.Children); + } + } + + private static string MergeClass(string customClass, string fallback) + { + var trimmedCustom = customClass?.Trim() ?? string.Empty; + var trimmedFallback = fallback?.Trim() ?? string.Empty; + if (string.IsNullOrEmpty(trimmedCustom)) + return trimmedFallback; + + if (string.IsNullOrEmpty(trimmedFallback)) + return trimmedCustom; + + return $"{trimmedCustom} {trimmedFallback}"; + } + + private string? GetOptionalStyle(string? style) => string.IsNullOrWhiteSpace(style) ? null : style; + + private bool IsButtonActionRunning(string buttonName) => this.executingButtonActions.Contains(buttonName); + + private async Task ExecuteButtonActionAsync(AssistantButton button) + { + if (this.assistantPlugin is null || button.Action is null || string.IsNullOrWhiteSpace(button.Name)) + return; + + if (!this.executingButtonActions.Add(button.Name)) + return; + + try + { + var input = this.BuildPromptInput(); + var cancellationToken = this.cancellationTokenSource?.Token ?? CancellationToken.None; + var result = await this.assistantPlugin.TryInvokeButtonActionAsync(button, input, cancellationToken); + if (result is not null) + this.ApplyButtonActionResult(result); + } + finally + { + this.executingButtonActions.Remove(button.Name); + await this.InvokeAsync(this.StateHasChanged); + } + } + + private void ApplyButtonActionResult(LuaTable result) + { + if (!result.TryGetValue("fields", out var fieldsValue)) + return; + + if (!fieldsValue.TryRead(out var fieldsTable)) + { + this.Logger.LogWarning("Assistant BUTTON action returned a non-table 'fields' value. The result is ignored."); + return; + } + + foreach (var pair in fieldsTable) + { + if (!pair.Key.TryRead(out var fieldName) || string.IsNullOrWhiteSpace(fieldName)) + continue; + + this.TryApplyFieldUpdate(fieldName, pair.Value); + } + } + + private void TryApplyFieldUpdate(string fieldName, LuaValue value) + { + if (this.inputFields.ContainsKey(fieldName)) + { + if (value.TryRead(out var textValue)) + this.inputFields[fieldName] = textValue ?? string.Empty; + else + this.LogFieldUpdateTypeMismatch(fieldName, "string"); + return; + } + + if (this.dropdownFields.ContainsKey(fieldName)) + { + if (value.TryRead(out var dropdownValue)) + this.dropdownFields[fieldName] = dropdownValue ?? string.Empty; + else + this.LogFieldUpdateTypeMismatch(fieldName, "string"); + return; + } + + if (this.switchFields.ContainsKey(fieldName)) + { + if (value.TryRead(out var boolValue)) + this.switchFields[fieldName] = boolValue; + else + this.LogFieldUpdateTypeMismatch(fieldName, "boolean"); + return; + } + + if (this.colorPickerFields.ContainsKey(fieldName)) + { + if (value.TryRead(out var colorValue)) + this.colorPickerFields[fieldName] = colorValue ?? string.Empty; + else + this.LogFieldUpdateTypeMismatch(fieldName, "string"); + return; + } + + if (this.webContentFields.TryGetValue(fieldName, out var webContentState)) + { + if (value.TryRead(out var webContentValue)) + webContentState.Content = webContentValue ?? string.Empty; + else + this.LogFieldUpdateTypeMismatch(fieldName, "string"); + return; + } + + if (this.fileContentFields.TryGetValue(fieldName, out var fileContentState)) + { + if (value.TryRead(out var fileContentValue)) + fileContentState.Content = fileContentValue ?? string.Empty; + else + this.LogFieldUpdateTypeMismatch(fieldName, "string"); + return; + } + + this.Logger.LogWarning("Assistant BUTTON action tried to update unknown field '{FieldName}'. The value is ignored.", fieldName); + } + + private void LogFieldUpdateTypeMismatch(string fieldName, string expectedType) + { + this.Logger.LogWarning("Assistant BUTTON action tried to write an invalid value to '{FieldName}'. Expected {ExpectedType}.", fieldName, expectedType); + } + + private string? ValidateProfileSelection(AssistantProfileSelection profileSelection, Profile? profile) + { + if (profile == default || profile == Profile.NO_PROFILE) + { + if (!string.IsNullOrWhiteSpace(profileSelection.ValidationMessage)) + return profileSelection.ValidationMessage; + + return this.T("Please select one of your profiles."); + } + + return null; + } + + private async Task Submit() + { + this.CreateChatThread(); + var time = this.AddUserRequest(await this.CollectUserPromptAsync()); + await this.AddAIResponseAsync(time); + } + + private void AddMetaEntries(LuaTable meta, IEnumerable components) + { + foreach (var component in components) + { + switch (component) + { + case AssistantTextArea textArea: + this.AddMetaEntry(meta, textArea.Name, component.Type, textArea.Label, textArea.UserPrompt); + break; + case AssistantDropdown dropdown: + this.AddMetaEntry(meta, dropdown.Name, component.Type, dropdown.Label, dropdown.UserPrompt); + break; + case AssistantSwitch switchComponent: + this.AddMetaEntry(meta, switchComponent.Name, component.Type, switchComponent.Label, switchComponent.UserPrompt); + break; + case AssistantWebContentReader webContent: + this.AddMetaEntry(meta, webContent.Name, component.Type, null, webContent.UserPrompt); + break; + case AssistantFileContentReader fileContent: + this.AddMetaEntry(meta, fileContent.Name, component.Type, null, fileContent.UserPrompt); + break; + case AssistantColorPicker colorPicker: + this.AddMetaEntry(meta, colorPicker.Name, component.Type, colorPicker.Label, colorPicker.UserPrompt); + break; + } + + if (component.Children.Count > 0) + this.AddMetaEntries(meta, component.Children); + } + } + + private string CollectUserPromptFallback(IEnumerable components) + { + var prompt = string.Empty; + + foreach (var component in components) + { + var userInput = string.Empty; + var userDecision = false; + + switch (component.Type) + { + case AssistantComponentType.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 AssistantComponentType.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 AssistantComponentType.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; + case AssistantComponentType.WEB_CONTENT_READER: + if (component is AssistantWebContentReader webContent && + this.webContentFields.TryGetValue(webContent.Name, out var webState)) + { + if (!string.IsNullOrWhiteSpace(webContent.UserPrompt)) + prompt += $"{Environment.NewLine}context:{Environment.NewLine}{webContent.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"; + + if (!string.IsNullOrWhiteSpace(webState.Content)) + prompt += $"user prompt:{Environment.NewLine}{webState.Content}"; + } + break; + case AssistantComponentType.FILE_CONTENT_READER: + if (component is AssistantFileContentReader fileContent && + this.fileContentFields.TryGetValue(fileContent.Name, out var fileState)) + { + if (!string.IsNullOrWhiteSpace(fileContent.UserPrompt)) + prompt += $"{Environment.NewLine}context:{Environment.NewLine}{fileContent.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"; + + if (!string.IsNullOrWhiteSpace(fileState.Content)) + prompt += $"user prompt:{Environment.NewLine}{fileState.Content}"; + } + break; + case AssistantComponentType.COLOR_PICKER: + if (component is AssistantColorPicker colorPicker) + { + prompt += $"context:{Environment.NewLine}{colorPicker.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"; + if (this.colorPickerFields.TryGetValue(colorPicker.Name, out userInput)) + prompt += $"user prompt:{Environment.NewLine}{userInput}"; + } + break; + } + + if (component.Children.Count > 0) + prompt += this.CollectUserPromptFallback(component.Children); + } + + return prompt; + } +} diff --git a/app/MindWork AI Studio/Assistants/Dynamic/FileContentState.cs b/app/MindWork AI Studio/Assistants/Dynamic/FileContentState.cs new file mode 100644 index 000000000..f7e0da602 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/Dynamic/FileContentState.cs @@ -0,0 +1,6 @@ +namespace AIStudio.Assistants.Dynamic; + +internal sealed class FileContentState +{ + public string Content { get; set; } = string.Empty; +} diff --git a/app/MindWork AI Studio/Assistants/Dynamic/WebContentState.cs b/app/MindWork AI Studio/Assistants/Dynamic/WebContentState.cs new file mode 100644 index 000000000..d9398f05e --- /dev/null +++ b/app/MindWork AI Studio/Assistants/Dynamic/WebContentState.cs @@ -0,0 +1,9 @@ +namespace AIStudio.Assistants.Dynamic; + +internal sealed class WebContentState +{ + public string Content { get; set; } = string.Empty; + public bool Preselect { get; set; } + public bool PreselectContentCleanerAgent { get; set; } + public bool AgentIsRunning { get; set; } +} diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 022339505..b4fbb646d 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -535,6 +535,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTA -- Yes, hide the policy definition UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T940701960"] = "Yes, hide the policy definition" +-- Please select one of your profiles. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DYNAMIC::ASSISTANTDYNAMIC::T465395981"] = "Please select one of your profiles." + -- Provide a list of bullet points and some basic information for an e-mail. The assistant will generate an e-mail based on that input. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::EMAIL::ASSISTANTEMAIL::T1143222914"] = "Provide a list of bullet points and some basic information for an e-mail. The assistant will generate an e-mail based on that input." @@ -4927,6 +4930,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T2830810750"] = "AI Studio Develop -- Generate a job posting for a given job description. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T2831103254"] = "Generate a job posting for a given job description." +-- Installed Assistants +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T295232966"] = "Installed Assistants" + -- My Tasks UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3011450657"] = "My Tasks" @@ -6067,6 +6073,33 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3290596792"] = "Error during Mi -- Microsoft Word export successful UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T4256043333"] = "Microsoft Word export successful" +-- Failed to parse the UI render tree from the ASSISTANT lua table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T1318499252"] = "Failed to parse the UI render tree from the ASSISTANT lua table." + +-- The provided ASSISTANT lua table does not contain a valid UI table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T1841068402"] = "The provided ASSISTANT lua table does not contain a valid UI table." + +-- The provided ASSISTANT lua table does not contain a valid description. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T2514141654"] = "The provided ASSISTANT lua table does not contain a valid description." + +-- The provided ASSISTANT lua table does not contain a valid title. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T2814605990"] = "The provided ASSISTANT lua table does not contain a valid title." + +-- The ASSISTANT lua table does not exist or is not a valid table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T3017816936"] = "The ASSISTANT lua table does not exist or is not a valid table." + +-- The provided ASSISTANT lua table does not contain a valid system prompt. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T3402798667"] = "The provided ASSISTANT lua table does not contain a valid system prompt." + +-- 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." + +-- ASSISTANT.BuildPrompt exists but is not a Lua function or has invalid syntax. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T683382975"] = "ASSISTANT.BuildPrompt exists but is not a Lua function or has invalid syntax." + +-- The provided ASSISTANT lua table does not contain the boolean flag to control the allowance of profiles. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T781921072"] = "The provided ASSISTANT lua table does not contain the boolean flag to control the allowance of profiles." + -- 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..0c209c11a --- /dev/null +++ b/app/MindWork AI Studio/Components/DynamicAssistantDropdown.razor @@ -0,0 +1,21 @@ + + + @foreach (var item in Items) + { + + @item.Display + + } + + 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..cde6cdfd8 --- /dev/null +++ b/app/MindWork AI Studio/Components/DynamicAssistantDropdown.razor.cs @@ -0,0 +1,52 @@ +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; + + [Parameter] public string Class { get; set; } = string.Empty; + + [Parameter] public string Style { get; set; } = string.Empty; + + private async Task OnValueChanged(string newValue) + { + if (this.Value != newValue) + { + this.Value = newValue; + await this.ValueChanged.InvokeAsync(newValue); + } + } + + internal string MergeClasses(string custom, string fallback) + { + var trimmedCustom = custom?.Trim() ?? string.Empty; + var trimmedFallback = fallback?.Trim() ?? string.Empty; + if (string.IsNullOrEmpty(trimmedCustom)) + return trimmedFallback; + + if (string.IsNullOrEmpty(trimmedFallback)) + return trimmedCustom; + + return $"{trimmedCustom} {trimmedFallback}"; + } + } +} \ 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/MindWork AI Studio.csproj b/app/MindWork AI Studio/MindWork AI Studio.csproj index d48580d0a..8c9725e20 100644 --- a/app/MindWork AI Studio/MindWork AI Studio.csproj +++ b/app/MindWork AI Studio/MindWork AI Studio.csproj @@ -62,6 +62,23 @@ + + ..\SourceGeneratedMappings\SourceGeneratedMappings.csproj + ..\SourceGeneratedMappings\bin\$(Configuration)\net9.0\SourceGeneratedMappings.dll + + + + + + + + + + + + + + diff --git a/app/MindWork AI Studio/Pages/Assistants.razor b/app/MindWork AI Studio/Pages/Assistants.razor index 610a68cdc..19f88cee1 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 @@ -30,6 +33,23 @@ } + @if (this.AssistantPlugins.Count > 0) + { + + @T("Installed Assistants") + + + @foreach (var assistantPlugin in this.AssistantPlugins) + { + + } + + } + @if (this.SettingsManager.IsAnyCategoryAssistantVisible("Business", (Components.EMAIL_ASSISTANT, PreviewFeatures.NONE), (Components.DOCUMENT_ANALYSIS_ASSISTANT, PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025), diff --git a/app/MindWork AI Studio/Pages/Assistants.razor.cs b/app/MindWork AI Studio/Pages/Assistants.razor.cs index e2c2de49f..6bf932267 100644 --- a/app/MindWork AI Studio/Pages/Assistants.razor.cs +++ b/app/MindWork AI Studio/Pages/Assistants.razor.cs @@ -1,5 +1,15 @@ using AIStudio.Components; +using AIStudio.Tools.PluginSystem; +using AIStudio.Tools.PluginSystem.Assistants; +using System.Collections.Generic; +using System.Linq; namespace AIStudio.Pages; -public partial class Assistants : MSGComponentBase; \ No newline at end of file +public partial class Assistants : MSGComponentBase +{ + private IReadOnlyCollection AssistantPlugins => + PluginFactory.RunningPlugins.OfType() + .Where(plugin => this.SettingsManager.IsPluginEnabled(plugin)) + .ToList(); +} diff --git a/app/MindWork AI Studio/Plugins/assistants/README.md b/app/MindWork AI Studio/Plugins/assistants/README.md new file mode 100644 index 000000000..15e0d1d7d --- /dev/null +++ b/app/MindWork AI Studio/Plugins/assistants/README.md @@ -0,0 +1,577 @@ +# Assistant Plugin Reference + +This folder keeps the Lua manifest (`plugin.lua`) that defines a custom assistant. Treat it as the single source of truth for how AI Studio renders your assistant UI and builds the submitted prompt. + +## Structure +- `ASSISTANT` is the root table. It must contain `Title`, `Description`, `SystemPrompt`, `SubmitText`, `AllowProfiles`, and the nested `UI` definition. +- `UI.Type` is always `"FORM"` and `UI.Children` is a list of component tables. +- Each component table declares `Type`, an optional `Children` array, and a `Props` table that feeds the component’s parameters. + +Supported types (matching the Blazor UI components): + +- `TEXT_AREA`: user input field based on `MudTextField`; requires `Name`, `Label`, and may include `HelperText`, `HelperTextOnFocus`, `Adornment`, `AdornmentIcon`, `AdornmentText`, `AdornmentColor`, `Counter`, `MaxLength`, `IsImmediate`, `UserPrompt`, `PrefillText`, `IsSingleLine`, `ReadOnly`, `Class`, `Style`. +- `DROPDOWN`: selects between variants; `Props` must include `Name`, `Label`, `Default`, `Items`, and optionally `ValueType` plus `UserPrompt`. +- `BUTTON`: invokes a Lua callback; `Props` must include `Name`, `Text`, `Action`, and may include `Variant`, `Color`, `IsFullWidth`, `Size`, `StartIcon`, `EndIcon`, `IconColor`, `IconSize`, `Class`, `Style`. +- `BUTTON_GROUP`: groups multiple `BUTTON` children in a `MudButtonGroup`; `Children` must contain only `BUTTON` components and `Props` may include `Variant`, `Color`, `Size`, `OverrideStyles`, `Vertical`, `DropShadow`, `Class`, `Style`. +- `LAYOUT_GRID`: renders a `MudGrid`; `Children` must contain only `LAYOUT_ITEM` components and `Props` may include `Justify`, `Spacing`, `Class`, `Style`. +- `LAYOUT_ITEM`: renders a `MudItem`; use it inside `LAYOUT_GRID` and configure breakpoints with `Xs`, `Sm`, `Md`, `Lg`, `Xl`, `Xxl`, plus optional `Class`, `Style`. +- `LAYOUT_PAPER`: renders a `MudPaper`; may include `Elevation`, `Height`, `MaxHeight`, `MinHeight`, `Width`, `MaxWidth`, `MinWidth`, `IsOutlined`, `IsSquare`, `Class`, `Style`. +- `LAYOUT_STACK`: renders a `MudStack`; may include `IsRow`, `IsReverse`, `Breakpoint`, `Align`, `Justify`, `Stretch`, `Wrap`, `Spacing`, `Class`, `Style`. +- `SWITCH`: boolean option; requires `Name`, `Label`, `Value`, and may include `Disabled`, `UserPrompt`, `LabelOn`, `LabelOff`, `LabelPlacement`, `Icon`, `IconColor`, `CheckedColor`, `UncheckedColor`, `Class`, `Style`. +- `COLOR_PICKER`: color input based on `MudColorPicker`; requires `Name`, `Label`, and may include `Placeholder`, `ShowAlpha`, `ShowToolbar`, `ShowModeSwitch`, `PickerVariant`, `UserPrompt`, `Class`, `Style`. +- `PROVIDER_SELECTION` / `PROFILE_SELECTION`: hooks into the shared provider/profile selectors. +- `WEB_CONTENT_READER`: renders `ReadWebContent`; include `Name`, `UserPrompt`, `Preselect`, `PreselectContentCleanerAgent`. +- `FILE_CONTENT_READER`: renders `ReadFileContent`; include `Name`, `UserPrompt`. +- `IMAGE`: embeds a static illustration; `Props` must include `Src` plus optionally `Alt` and `Caption`. `Src` can be an HTTP/HTTPS URL, a `data:` URI, or a plugin-relative path (`plugin://assets/your-image.png`). The runtime will convert plugin-relative paths into `data:` URLs (base64). +- `HEADING`, `TEXT`, `LIST`: descriptive helpers. + +Images referenced via the `plugin://` scheme must exist in the plugin directory (e.g., `assets/example.png`). Drop the file there and point `Src` at it. The component will read the file at runtime, encode it as Base64, and render it inside the assistant UI. + +## Prompt Assembly +Each component exposes a `UserPrompt` string. When the assistant runs, `AssistantDynamic` recursively iterates over the component tree and, for each component that has a prompt, emits: + +``` +context: + +--- +user prompt: + +``` + +For switches the “value” is the boolean `true/false`; for readers it is the fetched/selected content; for color pickers it is the selected color text (for example `#FFAA00` or `rgba(...)`, depending on the picker mode). Always provide a meaningful `UserPrompt` so the final concatenated prompt remains coherent from the LLM’s perspective. + +### Advanced: BuildPrompt (optional) +If you want full control over prompt composition, define `ASSISTANT.BuildPrompt` as a Lua function. When present, AI Studio calls it and uses its return value as the final user prompt. The default prompt assembly is skipped. + +#### Contract +- `ASSISTANT.BuildPrompt(input)` must return a **string**. +- If the function is missing, returns `nil`, or returns a non-string, AI Studio falls back to the default prompt assembly. +- Errors in the function are caught and logged, then fall back to the default prompt assembly. + +#### Input table shape +The function receives a single `input` table with: +- `input.fields`: values keyed by component `Name` + - Text area, dropdown, and readers are strings + - Switch is a boolean + - Color picker is the selected color as a string +- `input.meta`: per-component metadata keyed by component `Name` + - `Type` (string, e.g. `TEXT_AREA`, `DROPDOWN`, `SWITCH`, `COLOR_PICKER`) + - `Label` (string, when provided) + - `UserPrompt` (string, when provided) +- `input.profile`: selected profile data + - `Id`, `Name`, `NeedToKnow`, `Actions`, `Num` + - When no profile is selected, values match the built-in "Use no profile" entry + +#### Table shapes (quick reference) +``` +input = { + fields = { + [""] = "", + ... + }, + meta = { + [""] = { + Type = "", + Label = "", + UserPrompt = "" + }, + ... + }, + profile = { + Name = "", + NeedToKnow = "", + Actions = "", + Num = + } +} +``` + +#### Using `meta` inside BuildPrompt +`input.meta` is useful when you want to dynamically build the prompt based on component type or reuse existing UI text (labels/user prompts). + +Example: iterate all fields with labels and include their values +```lua +ASSISTANT.BuildPrompt = function(input) + local parts = {} + for name, value in pairs(input.fields) do + local meta = input.meta[name] + if meta and meta.Label and value ~= "" then + table.insert(parts, meta.Label .. ": " .. tostring(value)) + end + end + return table.concat(parts, "\n") +end +``` + +Example: handle types differently +```lua +ASSISTANT.BuildPrompt = function(input) + local parts = {} + for name, meta in pairs(input.meta) do + local value = input.fields[name] + if meta.Type == "SWITCH" then + table.insert(parts, name .. ": " .. tostring(value)) + elseif meta.Type == "COLOR_PICKER" and value and value ~= "" then + table.insert(parts, name .. ": " .. value) + elseif value and value ~= "" then + table.insert(parts, name .. ": " .. value) + end + end + return table.concat(parts, "\n") +end +``` + +### `TEXT_AREA` reference +- Use `Type = "TEXT_AREA"` to render a MudBlazor text input or textarea. +- Required props: + - `Name`: unique state key used in prompt assembly and `BuildPrompt(input.fields)`. + - `Label`: visible field label. +- Optional props: + - `HelperText`: helper text rendered below the input. + - `HelperTextOnFocus`: defaults to `false`; show helper text only while the field is focused. + - `Adornment`: one of `Start`, `End`, `None`; invalid or omitted values fall back to `Start`. + - `AdornmentIcon`: MudBlazor icon identifier string for the adornment. + - `AdornmentText`: plain adornment text. Do not set this together with `AdornmentIcon`. + - `AdornmentColor`: one of the MudBlazor `Color` enum names such as `Primary`, `Secondary`, `Warning`; invalid or omitted values fall back to `Default`. + - `Counter`: nullable integer. Omit it to hide the counter entirely. Set `0` to show only the current character count. Set `1` or higher to show `current/max`. + - `MaxLength`: maximum number of characters allowed; defaults to `524288`. + - `IsImmediate`: defaults to `false`; updates the bound value on each input event instead of on blur/change. + - `UserPrompt`: prompt context text for this field. + - `PrefillText`: initial input value. + - `IsSingleLine`: defaults to `false`; render as a one-line input instead of a textarea. + - `ReadOnly`: defaults to `false`; disables editing. + - `Class`, `Style`: forwarded to the rendered component for layout/styling. + +Example: +```lua +{ + ["Type"] = "TEXT_AREA", + ["Props"] = { + ["Name"] = "Budget", + ["Label"] = "Budget", + ["HelperText"] = "Enter the expected amount.", + ["Adornment"] = "Start", + ["AdornmentIcon"] = "Icons.Material.Filled.AttachMoney", + ["AdornmentColor"] = "Success", + ["Counter"] = 0, + ["MaxLength"] = 100, + ["IsImmediate"] = true, + ["UserPrompt"] = "Use this budget information in your answer.", + ["PrefillText"] = "", + ["IsSingleLine"] = true + } +} +``` + +### `BUTTON` reference +- Use `Type = "BUTTON"` to render a clickable action button. +- Required props: + - `Name`: unique identifier used to track execution state and logging. + - `Text`: visible button label. + - `Action`: Lua function called on button click. +- Optional props: + - `Variant`: one of the MudBlazor `Variant` enum names such as `Filled`, `Outlined`, `Text`; omitted values fall back to `Filled`. + - `Color`: one of the MudBlazor `Color` enum names such as `Default`, `Primary`, `Secondary`, `Info`; omitted values fall back to `Default`. + - `IsFullWidth`: defaults to `false`; when `true`, the button expands to the available width. + - `Size`: one of the MudBlazor `Size` enum names such as `Small`, `Medium`, `Large`; omitted values fall back to `Medium`. + - `StartIcon`: MudBlazor icon identifier string rendered before the button text. + - `EndIcon`: MudBlazor icon identifier string rendered after the button text. + - `IconColor`: one of the MudBlazor `Color` enum names; omitted values fall back to `Inherit`. + - `IconSize`: one of the MudBlazor `Size` enum names; omitted values fall back to `Medium`. + - `Class`, `Style`: forwarded to the rendered component for layout/styling. + +#### `Action(input)` contract +- The function receives the same `input` structure as `ASSISTANT.BuildPrompt(input)`. +- Return `nil` for no state update. +- To update component state, return a table with a `fields` table. +- `fields` keys must reference existing component `Name` values. +- Supported write targets: + - `TEXT_AREA`, `DROPDOWN`, `WEB_CONTENT_READER`, `FILE_CONTENT_READER`, `COLOR_PICKER`: string values + - `SWITCH`: boolean values +- Unknown field names and wrong value types are ignored and logged. + +Example: +```lua +{ + ["Type"] = "BUTTON", + ["Props"] = { + ["Name"] = "buildEmailOutput", + ["Text"] = "Build output", + ["Variant"] = "Filled", + ["Color"] = "Primary", + ["IsFullWidth"] = false, + ["Size"] = "Medium", + ["StartIcon"] = "Icons.Material.Filled.AutoFixHigh", + ["EndIcon"] = "Icons.Material.Filled.ArrowForward", + ["IconColor"] = "Inherit", + ["IconSize"] = "Medium", + ["Action"] = function(input) + local email = input.fields.emailContent or "" + local translate = input.fields.translateEmail or false + local output = email + + if translate then + output = output .. "\n\nTranslate this email." + end + + return { + fields = { + outputBuffer = output + } + } + end, + ["Class"] = "mb-3", + ["Style"] = "min-width: 12rem;" + } +} +``` + +### `BUTTON_GROUP` reference +- Use `Type = "BUTTON_GROUP"` to render multiple `BUTTON` children as a single MudBlazor button group. +- Required structure: + - `Children`: array of `BUTTON` component tables. Other child component types are ignored. +- Optional props: + - `Variant`: one of the MudBlazor `Variant` enum names such as `Filled`, `Outlined`, `Text`; omitted values fall back to `Filled`. + - `Color`: one of the MudBlazor `Color` enum names such as `Default`, `Primary`, `Secondary`, `Info`; omitted values fall back to `Default`. + - `Size`: one of the MudBlazor `Size` enum names such as `Small`, `Medium`, `Large`; omitted values fall back to `Medium`. + - `OverrideStyles`: defaults to `false`; enables MudBlazor button-group style overrides. + - `Vertical`: defaults to `false`; when `true`, buttons are rendered vertically instead of horizontally. + - `DropShadow`: defaults to `true`; controls the group shadow. + - `Class`, `Style`: forwarded to the rendered `MudButtonGroup` for layout/styling. +- Child buttons use the existing `BUTTON` props and behavior, including Lua `Action(input)`. + +Example: +```lua +{ + ["Type"] = "BUTTON_GROUP", + ["Props"] = { + ["Variant"] = "Filled", + ["Color"] = "Primary", + ["Size"] = "Medium", + ["OverrideStyles"] = false, + ["Vertical"] = false, + ["DropShadow"] = true + }, + ["Children"] = { + { + ["Type"] = "BUTTON", + ["Props"] = { + ["Name"] = "buildEmailOutput", + ["Text"] = "Build output", + ["Action"] = function(input) + return { + fields = { + outputBuffer = input.fields.emailContent or "" + } + } + end, + ["StartIcon"] = "Icons.Material.Filled.Build" + } + }, + { + ["Type"] = "BUTTON", + ["Props"] = { + ["Name"] = "logColor", + ["Text"] = "Log color", + ["Action"] = function(input) + LogError("ColorPicker value: " .. tostring(input.fields.colorPicker or "")) + return nil + end, + ["EndIcon"] = "Icons.Material.Filled.BugReport" + } + } + } +} +``` + +### `LAYOUT_GRID` reference +- Use `Type = "LAYOUT_GRID"` to render a MudBlazor grid container. +- Required props: + - `Name`: unique identifier for the layout node. +- Required structure: + - `Children`: array of `LAYOUT_ITEM` component tables. Other child component types are ignored. +- Optional props: + - `Justify`: one of the MudBlazor `Justify` enum names such as `FlexStart`, `Center`, `SpaceBetween`; omitted values fall back to `FlexStart`. + - `Spacing`: integer spacing between grid items; omitted values fall back to `6`. + - `Class`, `Style`: forwarded to the rendered `MudGrid` for layout/styling. + +Example: +```lua +{ + ["Type"] = "LAYOUT_GRID", + ["Props"] = { + ["Name"] = "mainGrid", + ["Justify"] = "FlexStart", + ["Spacing"] = 2 + }, + ["Children"] = { + { + ["Type"] = "LAYOUT_ITEM", + ["Props"] = { + ["Name"] = "leftItem", + ["Xs"] = 12, + ["Md"] = 6 + } + }, + { + ["Type"] = "LAYOUT_ITEM", + ["Props"] = { + ["Name"] = "rightItem", + ["Xs"] = 12, + ["Md"] = 6 + } + } + } +} +``` + +### `LAYOUT_ITEM` reference +- Use `Type = "LAYOUT_ITEM"` to render a MudBlazor grid item. +- Required props: + - `Name`: unique identifier for the layout node. +- Intended parent: + - Use this component inside `LAYOUT_GRID`. +- Optional props: + - `Xs`, `Sm`, `Md`, `Lg`, `Xl`, `Xxl`: integer breakpoint widths. Omit a breakpoint to leave it unset. + - `Class`, `Style`: forwarded to the rendered `MudItem` for layout/styling. +- `Children` may contain any other assistant components you want to place inside the item. + +Example: +```lua +{ + ["Type"] = "LAYOUT_ITEM", + ["Props"] = { + ["Name"] = "contentColumn", + ["Xs"] = 12, + ["Lg"] = 8 + } +} +``` + +### `LAYOUT_PAPER` reference +- Use `Type = "LAYOUT_PAPER"` to render a MudBlazor paper container. +- Required props: + - `Name`: unique identifier for the layout node. +- Optional props: + - `Elevation`: integer elevation; omitted values fall back to `1`. + - `Height`, `MaxHeight`, `MinHeight`, `Width`, `MaxWidth`, `MinWidth`: CSS size values such as `100%`, `24rem`, `50vh`. + - `IsOutlined`: defaults to `false`; toggles outlined mode. + - `IsSquare`: defaults to `false`; removes rounded corners. + - `Class`, `Style`: forwarded to the rendered `MudPaper` for layout/styling. +- `Children` may contain any other assistant components you want to wrap. + +Example: +```lua +{ + ["Type"] = "LAYOUT_PAPER", + ["Props"] = { + ["Name"] = "contentPaper", + ["Elevation"] = 2, + ["Width"] = "100%", + ["IsOutlined"] = true + } +} +``` + +### `LAYOUT_STACK` reference +- Use `Type = "LAYOUT_STACK"` to render a MudBlazor stack layout. +- Required props: + - `Name`: unique identifier for the layout node. +- Optional props: + - `IsRow`: defaults to `false`; renders items horizontally. + - `IsReverse`: defaults to `false`; reverses the visual order. + - `Breakpoint`: one of the MudBlazor `Breakpoint` enum names such as `Sm`, `Md`, `Lg`; omitted values fall back to `None`. + - `Align`: one of the MudBlazor `AlignItems` enum names such as `Start`, `Center`, `Stretch`; omitted values fall back to `Stretch`. + - `Justify`: one of the MudBlazor `Justify` enum names such as `FlexStart`, `Center`, `SpaceBetween`; omitted values fall back to `FlexStart`. + - `Stretch`: one of the MudBlazor `StretchItems` enum names such as `None`, `Start`, `End`, `Stretch`; omitted values fall back to `None`. + - `Wrap`: one of the MudBlazor `Wrap` enum names such as `Wrap`, `NoWrap`, `WrapReverse`; omitted values fall back to `Wrap`. + - `Spacing`: integer spacing between child components; omitted values fall back to `3`. + - `Class`, `Style`: forwarded to the rendered `MudStack` for layout/styling. +- `Children` may contain any other assistant components you want to arrange. + +Example: +```lua +{ + ["Type"] = "LAYOUT_STACK", + ["Props"] = { + ["Name"] = "toolbarRow", + ["IsRow"] = true, + ["Align"] = "Center", + ["Justify"] = "SpaceBetween", + ["Spacing"] = 2 + } +} +``` + +### `SWITCH` reference +- Use `Type = "SWITCH"` to render a boolean toggle. +- Required props: + - `Name`: unique state key used in prompt assembly and `BuildPrompt(input.fields)`. + - `Label`: visible label for the switch field. + - `Value`: initial boolean state (`true` or `false`). +- Optional props: + - `Disabled`: defaults to `false`; disables user interaction while still allowing the value to be included in prompt assembly. + - `UserPrompt`: prompt context text for this field. + - `LabelOn`: text shown when the switch value is `true`. + - `LabelOff`: text shown when the switch value is `false`. + - `LabelPlacement`: one of `Bottom`, `End`, `Left`, `Right`, `Start`, `Top`; omitted values follow the renderer default. + - `Icon`: MudBlazor icon identifier string displayed inside the switch thumb. + - `IconColor`: one of the MudBlazor `Color` enum names such as `Primary`, `Secondary`, `Warning`; omitted values default to `Inherit`. + - `CheckedColor`: color used when the switch state is `true`; omitted values default to `Inherit`. + - `UncheckedColor`: color used when the switch state is `false`; omitted values default to `Inherit`. + - `Class`, `Style`: forwarded to the rendered component for layout/styling. + - +Example: +```lua +{ + ["Type"] = "SWITCH", + ["Props"] = { + ["Name"] = "IncludeSummary", + ["Label"] = "Include summary", + ["Value"] = true, + ["Disabled"] = false, + ["UserPrompt"] = "Decide whether the final answer should include a short summary.", + ["LabelOn"] = "Summary enabled", + ["LabelOff"] = "Summary disabled", + ["LabelPlacement"] = "End", + ["Icon"] = "Icons.Material.Filled.Summarize", + ["IconColor"] = "Primary", + ["CheckedColor"] = "Success", + ["UncheckedColor"] = "Default", + ["Class"] = "mb-6", + } +} +``` + +### `COLOR_PICKER` reference +- Use `Type = "COLOR_PICKER"` to render a MudBlazor color picker. +- Required props: + - `Name`: unique state key used in prompt assembly and `BuildPrompt(input.fields)`. + - `Label`: visible field label. +- Optional props: + - `Placeholder`: default color hex string (e.g. `#FF10FF`) or initial hint text. + - `ShowAlpha`: defaults to `true`; enables alpha channel editing. + - `ShowToolbar`: defaults to `true`; shows picker/grid/palette toolbar. + - `ShowModeSwitch`: defaults to `true`; allows switching between HEX/RGB(A)/HSL modes. + - `PickerVariant`: one of `DIALOG`, `INLINE`, `STATIC`; invalid or omitted values fall back to `STATIC`. + - `UserPrompt`: prompt context text for the selected color. + - `Class`, `Style`: forwarded to the rendered component for layout/styling. + +Example: +```lua +{ + ["Type"] = "COLOR_PICKER", + ["Props"] = { + ["Name"] = "accentColor", + ["Label"] = "Accent color", + ["Placeholder"] = "#FFAA00", + ["ShowAlpha"] = false, + ["ShowToolbar"] = true, + ["ShowModeSwitch"] = true, + ["PickerVariant"] = "STATIC", + ["UserPrompt"] = "Use this as the accent color for the generated design." + } +} +``` + +#### Using `profile` inside BuildPrompt +Profiles are optional user context (e.g., "NeedToKnow" and "Actions"). You can inject this directly into the user prompt if you want the LLM to always see it. + +Example: +```lua +ASSISTANT.BuildPrompt = function(input) + local parts = {} + if input.profile and input.profile.NeedToKnow ~= "" then + table.insert(parts, "User context:") + table.insert(parts, input.profile.NeedToKnow) + table.insert(parts, "") + end + table.insert(parts, input.fields.Main or "") + return table.concat(parts, "\n") +end +``` + +### Included lua libraries +- [Basic Functions Library](https://www.lua.org/manual/5.2/manual.html#6.1) +- [Coroutine Manipulation Library](https://www.lua.org/manual/5.2/manual.html#6.2) +- [String Manipulation Library](https://www.lua.org/manual/5.2/manual.html#6.4) +- [Table Manipulation Library](https://www.lua.org/manual/5.2/manual.html#6.5) +- [Mathematical Functions Library](https://www.lua.org/manual/5.2/manual.html#6.6) +- [Bitwise Operations Library](https://www.lua.org/manual/5.2/manual.html#6.7) + +### Logging helpers (assistant plugins only) +The assistant runtime exposes basic logging helpers to Lua. Use them to debug custom prompt building. + +- `LogDebug(message)` +- `LogInfo(message)` +- `LogWarn(message)` +- `LogError(message)` + +Example: +```lua +ASSISTANT.BuildPrompt = function(input) + LogInfo("BuildPrompt called") + return input.fields.Text or "" +end +``` + +### Date/time helpers (assistant plugins only) +Use these when you need timestamps inside Lua. + +- `DateTime(format)` returns a table with date/time parts plus a formatted string. + - `format` is optional; default is `yyyy-MM-dd HH:mm:ss` (ISO 8601-like). + - `formatted` contains the date in your desired format (e.g. `dd.MM.yyyy HH:mm`) or the default. + - Members: `year`, `month`, `day`, `hour`, `minute`, `second`, `millisecond`, `formatted`. +- `Timestamp()` returns a UTC timestamp in ISO-8601 format (`O` / round-trip), e.g. `2026-03-02T21:15:30.1234567Z`. + +Example: +```lua +local dt = DateTime("yyyy-MM-dd HH:mm:ss") +LogInfo(dt.formatted) +LogInfo(Timestamp()) +LogInfo(dt.day .. "." .. dt.month .. "." .. dt.year) +``` + +#### Example: simple custom prompt +```lua +ASSISTANT.BuildPrompt = function(input) + local f = input.fields + return "Topic: " .. (f.Topic or "") .. "\nDetails:\n" .. (f.Details or "") +end +``` + +#### Example: structured prompt (similar to Coding assistant) +```lua +ASSISTANT.BuildPrompt = function(input) + local f = input.fields + local parts = {} + + if (f.Code or "") ~= "" then + table.insert(parts, "I have the following code:") + table.insert(parts, "```") + table.insert(parts, f.Code) + table.insert(parts, "```") + table.insert(parts, "") + end + + if (f.CompilerMessages or "") ~= "" then + table.insert(parts, "I have the following compiler messages:") + table.insert(parts, "```") + table.insert(parts, f.CompilerMessages) + table.insert(parts, "```") + table.insert(parts, "") + end + + table.insert(parts, "My questions are:") + table.insert(parts, f.Questions or "") + return table.concat(parts, "\n") +end +``` + +# Tips + +1. Give every component a unique `Name`— it’s used to track state. +2. Keep in mind that components and their properties are case-sensitive (e.g. if you write `["Type"] = "heading"` instead of `["Type"] = "HEADING"` the component will not be registered). Always copy-paste the component from the `plugin.lua` manifest to avoid this. +3. When you expect default content (e.g., a textarea with instructions), keep `UserPrompt` but also set `PrefillText` so the user starts with a hint. +4. If you need extra explanatory text (before or after the interactive controls), use `TEXT` or `HEADING` components. +5. Keep `Preselect`/`PreselectContentCleanerAgent` flags in `WEB_CONTENT_READER` to simplify the initial UI for the user. 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..49028340f --- /dev/null +++ b/app/MindWork AI Studio/Plugins/assistants/plugin.lua @@ -0,0 +1,295 @@ +require("icon") + +--[[ + This sample assistant shows how plugin authors map Lua tables into UI components. + Each component declares a `UserPrompt` which is prepended as a `context` block, followed + by the actual component value in `user prompt`. See + `app/MindWork AI Studio/Plugins/assistants/README.md` for the full data-model reference. +]] + +-- 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"] = "<assistant description>", -- required + ["SystemPrompt"] = "<prompt that fundamentally 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 + ["Adornment"] = "<Start|End|None>", -- location of the `AdornmentIcon` OR `AdornmentText`; CASE SENSITIV + ["AdornmentIcon"] = "Icons.Material.Filled.AppSettingsAlt", -- The Mudblazor icon displayed for the adornment + ["AdornmentText"] = "", -- The text displayed for the adornment + ["AdornmentColor"] = "<Dark|Error|Info|Inherit|Primary|Secondary|Success|Surface|Tertiary|Transparent|Warning>", -- the color of AdornmentText or AdornmentIcon; CASE SENSITIVE + ["Counter"] = 0, -- shows a character counter. When 0, the current character count is displayed. When 1 or greater, the character count and this count are displayed. Defaults to `null` + ["MaxLength"] = 100, -- max number of characters allowed, prevents more input characters; use together with the character counter. Defaults to 524,288 + ["HelperText"] = "<a helping text rendered under the text area to give hints to users>", + ["IsImmediate"] = false, -- changes the value as soon as input is received. Defaults to false but will be true if counter or maxlength is set to reflect changes + ["HelperTextOnFocus"] = true, -- if true, shows the helping text only when the user focuses on the text area + ["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) + ["Class"] = "<optional MudBlazor or css classes>", + ["Style"] = "<optional css styles>", + } + }, + { + ["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, -- initial switch state + ["Disabled"] = false, -- if true, disables user interaction but the value can still be used in the user prompt (use for presentation purposes) + ["UserPrompt"] = "<direct input of instructions, questions, or tasks by a user>", + ["LabelOn"] = "<text if state is true>", + ["LabelOff"] = "<text if state is false>", + ["LabelPlacement"] = "<Bottom|End|Left|Right|Start|Top>", -- Defaults to End (right of the switch) + ["Icon"] = "Icons.Material.Filled.Bolt", -- places a thumb icon inside the switch + ["IconColor"] = "<Dark|Error|Info|Inherit|Primary|Secondary|Success|Surface|Tertiary|Transparent|Warning>", -- color of the thumb icon. Defaults to `Inherit` + ["CheckedColor"] = "<Dark|Error|Info|Inherit|Primary|Secondary|Success|Surface|Tertiary|Transparent|Warning>", -- color of the switch if state is true. Defaults to `Inherit` + ["UncheckedColor"] = "<Dark|Error|Info|Inherit|Primary|Secondary|Success|Surface|Tertiary|Transparent|Warning>", -- color of the switch if state is false. Defaults to `Inherit` + ["Class"] = "<optional MudBlazor or css classes>", + ["Style"] = "<optional css styles>", + } + }, + { + ["Type"] = "BUTTON", + ["Props"] = { + ["Name"] = "buildEmailOutput", + ["Text"] = "Build email output", + ["Size"] = "<Small|Medium|Large>", -- size of the button. Defaults to Medium + ["Variant"] = "<Filled|Outlined|Text>", -- display variation to use. Defaults to Text + ["Color"] = "<Dark|Error|Info|Inherit|Primary|Secondary|Success|Surface|Tertiary|Transparent|Warning>", -- color of the button. Defaults to Default + ["IsFullWidth"] = false, -- ignores sizing and renders a long full width button. Defaults to false + ["StartIcon"] = "Icons.Material.Filled.ArrowRight", -- icon displayed before the text. Defaults to null + ["EndIcon"] = "Icons.Material.Filled.ArrowLeft", -- icon displayed after the text. Defaults to null + ["IconColor"] = "<Dark|Error|Info|Inherit|Primary|Secondary|Success|Surface|Tertiary|Transparent|Warning>", -- color of start and end icons. Defaults to Inherit + ["IconSize"] = "<Small|Medium|Large>", -- size of icons. Defaults to null. When null, the value of ["Size"] is used + ["Action"] = function(input) + local email = input.fields.emailContent or "" + local translate = input.fields.translateEmail or false + local output = email + + if translate then + output = output .. "\n\nTranslate this email." + end + + return { + fields = { + outputBuffer = output + } + } + end, + ["Class"] = "<optional MudBlazor or css classes>", + ["Style"] = "<optional css styles>", + } + }, + { + ["Type"] = "BUTTON_GROUP", + ["Props"] = { + ["Variant"] = "<Filled|Outlined|Text>", -- display variation of the group. Defaults to Filled + ["Color"] = "<Dark|Error|Info|Inherit|Primary|Secondary|Success|Surface|Tertiary|Transparent|Warning>", -- color of the group. Defaults to Default + ["Size"] = "<Small|Medium|Large>", -- size of the group. Defaults to Medium + ["OverrideStyles"] = false, -- allows MudBlazor group style overrides. Defaults to false + ["Vertical"] = false, -- renders buttons vertically instead of horizontally. Defaults to false + ["DropShadow"] = true, -- applies a group shadow. Defaults to true + ["Class"] = "<optional MudBlazor or css classes>", + ["Style"] = "<optional css styles>", + }, + ["Children"] = { + -- BUTTON_ELEMENTS + } + }, + { + ["Type"] = "LAYOUT_STACK", + ["Props"] = { + ["Name"] = "exampleStack", + ["IsRow"] = true, + ["Align"] = "Center", + ["Justify"] = "SpaceBetween", + ["Wrap"] = "Wrap", + ["Spacing"] = 2, + ["Class"] = "<optional MudBlazor or css classes>", + ["Style"] = "<optional css styles>", + }, + ["Children"] = { + -- CHILDREN + } + }, + { + ["Type"] = "LAYOUT_PAPER", + ["Props"] = { + ["Name"] = "examplePaper", + ["Elevation"] = 2, + ["Width"] = "100%", + ["Class"] = "pa-4 mb-3", + ["Style"] = "<optional css styles>", + }, + ["Children"] = { + -- CHILDREN + } + }, + { + ["Type"] = "LAYOUT_GRID", + ["Props"] = { + ["Name"] = "exampleGrid", + ["Justify"] = "FlexStart", + ["Spacing"] = 2, + ["Class"] = "<optional MudBlazor or css classes>", + ["Style"] = "<optional css styles>", + }, + ["Children"] = { + -- CHILDREN + } + }, + { + ["Type"] = "PROVIDER_SELECTION", -- required + ["Props"] = { + ["Name"] = "Provider", + ["Label"] = "Choose LLM" + } + }, + -- If you add a PROFILE_SELECTION component, AI Studio will hide the footer selection and use this block instead: + { + ["Type"] = "PROFILE_SELECTION", + ["Props"] = { + ["ValidationMessage"] = "<warning message that is shown when the user has not picked a profile>" + } + }, + { + ["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>" + } + } + } + }, + { + ["Type"] = "IMAGE", + ["Props"] = { + ["Src"] = "plugin://assets/example.png", + ["Alt"] = "SVG-inspired placeholder", + ["Caption"] = "Static illustration via the IMAGE component." + } + }, + { + ["Type"] = "WEB_CONTENT_READER", -- allows the user to fetch a URL and clean it + ["Props"] = { + ["Name"] = "<unique identifier of this component>", -- required + ["UserPrompt"] = "<help text that explains the purpose of this reader>", + ["Preselect"] = false, -- automatically show the reader when the assistant opens + ["PreselectContentCleanerAgent"] = true -- run the content cleaner by default + } + }, + { + ["Type"] = "FILE_CONTENT_READER", -- allows the user to load local files + ["Props"] = { + ["Name"] = "<unique identifier of this component>", -- required + ["UserPrompt"] = "<help text reminding the user what kind of file they should load>" + } + }, + { + ["Type"] = "COLOR_PICKER", + ["Props"] = { + ["Name"] = "<unique identifier of this component>", -- required + ["Label"] = "<heading of your component>", -- required + ["Placeholder"] = "<use this as a default color property with HEX code (e.g '#FFFF12') or just show hints to the user>", + ["ShowAlpha"] = true, -- weather alpha channels are shown + ["ShowToolbar"] = true, -- weather the toolbar to toggle between picker, grid or palette is shown + ["ShowModeSwitch"] = true, -- weather switch to toggle between RGB(A), HEX or HSL color mode is shown + ["PickerVariant"] = "<Dialog|Inline|Static>", -- different rendering modes: `Dialog` opens the picker in a modal type screen, `Inline` shows the picker next to the input field and `Static` renders the picker widget directly (default); Case sensitiv + ["UserPrompt"] = "<help text reminding the user what kind of file they should load>", + } + }, + } + }, +} diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index c518d4393..bcf13dbc5 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -537,6 +537,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTA -- Yes, hide the policy definition UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T940701960"] = "Ja, die Definition des Regelwerks ausblenden" +-- Please select one of your profiles. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DYNAMIC::ASSISTANTDYNAMIC::T465395981"] = "Bitte wählen Sie eines Ihrer Profile aus." + -- Provide a list of bullet points and some basic information for an e-mail. The assistant will generate an e-mail based on that input. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::EMAIL::ASSISTANTEMAIL::T1143222914"] = "Geben Sie eine Liste von Stichpunkten sowie einige Basisinformationen für eine E-Mail ein. Der Assistent erstellt anschließend eine E-Mail auf Grundlage ihrer Angaben." @@ -4929,6 +4932,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T2830810750"] = "AI Studio Entwick -- Generate a job posting for a given job description. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T2831103254"] = "Erstellen Sie eine Stellenanzeige anhand einer vorgegebenen Stellenbeschreibung." +-- Installed Assistants +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T295232966"] = "Installierte Assistenten" + -- My Tasks UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3011450657"] = "Meine Aufgaben" @@ -6069,6 +6075,33 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3290596792"] = "Fehler beim Exp -- Microsoft Word export successful UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T4256043333"] = "Export nach Microsoft Word erfolgreich" +-- Failed to parse the UI render tree from the ASSISTANT lua table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T1318499252"] = "Der UI-Render-Baum konnte nicht aus der ASSISTANT-Lua-Tabelle geparst werden." + +-- The provided ASSISTANT lua table does not contain a valid UI table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T1841068402"] = "Die bereitgestellte ASSISTANT-Lua-Tabelle enthält keine gültige UI-Tabelle." + +-- The provided ASSISTANT lua table does not contain a valid description. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T2514141654"] = "Die bereitgestellte ASSISTANT-Lua-Tabelle enthält keine gültige Beschreibung." + +-- The provided ASSISTANT lua table does not contain a valid title. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T2814605990"] = "Die bereitgestellte ASSISTANT-Lua-Tabelle enthält keinen gültigen Titel." + +-- The ASSISTANT lua table does not exist or is not a valid table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T3017816936"] = "Die Lua-Tabelle **ASSISTANT** existiert nicht oder ist keine gültige Tabelle." + +-- The provided ASSISTANT lua table does not contain a valid system prompt. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T3402798667"] = "Die bereitgestellte ASSISTANT-Lua-Tabelle enthält keine gültige Systemaufforderung." + +-- The ASSISTANT table does not contain a valid system prompt. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T3723171842"] = "Die Tabelle **ASSISTANT** enthält keine gültige Systemanweisung." + +-- ASSISTANT.BuildPrompt exists but is not a Lua function or has invalid syntax. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T683382975"] = "`ASSISTANT.BuildPrompt` ist vorhanden, aber keine Lua-Funktion oder hat eine ungültige Syntax." + +-- The provided ASSISTANT lua table does not contain the boolean flag to control the allowance of profiles. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T781921072"] = "Die bereitgestellte ASSISTANT-Lua-Tabelle enthält kein boolesches Flag, mit dem sich die Zulassung von Profilen steuern lässt." + -- The table AUTHORS does not exist or is using an invalid syntax. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T1068328139"] = "Die Tabelle AUTHORS existiert nicht oder verwendet eine ungültige Syntax." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index a4fdfd5c1..7fc084757 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -537,6 +537,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTA -- Yes, hide the policy definition UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T940701960"] = "Yes, hide the policy definition" +-- Please select one of your profiles. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DYNAMIC::ASSISTANTDYNAMIC::T465395981"] = "Please select one of your profiles." + -- Provide a list of bullet points and some basic information for an e-mail. The assistant will generate an e-mail based on that input. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::EMAIL::ASSISTANTEMAIL::T1143222914"] = "Provide a list of bullet points and some basic information for an e-mail. The assistant will generate an e-mail based on that input." @@ -4929,6 +4932,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T2830810750"] = "AI Studio Develop -- Generate a job posting for a given job description. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T2831103254"] = "Generate a job posting for a given job description." +-- Installed Assistants +UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T295232966"] = "Installed Assistants" + -- My Tasks UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T3011450657"] = "My Tasks" @@ -6069,6 +6075,33 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3290596792"] = "Error during Mi -- Microsoft Word export successful UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T4256043333"] = "Microsoft Word export successful" +-- Failed to parse the UI render tree from the ASSISTANT lua table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T1318499252"] = "Failed to parse the UI render tree from the ASSISTANT lua table." + +-- The provided ASSISTANT lua table does not contain a valid UI table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T1841068402"] = "The provided ASSISTANT lua table does not contain a valid UI table." + +-- The provided ASSISTANT lua table does not contain a valid description. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T2514141654"] = "The provided ASSISTANT lua table does not contain a valid description." + +-- The provided ASSISTANT lua table does not contain a valid title. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T2814605990"] = "The provided ASSISTANT lua table does not contain a valid title." + +-- The ASSISTANT lua table does not exist or is not a valid table. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T3017816936"] = "The ASSISTANT lua table does not exist or is not a valid table." + +-- The provided ASSISTANT lua table does not contain a valid system prompt. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T3402798667"] = "The provided ASSISTANT lua table does not contain a valid system prompt." + +-- 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." + +-- ASSISTANT.BuildPrompt exists but is not a Lua function or has invalid syntax. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T683382975"] = "ASSISTANT.BuildPrompt exists but is not a Lua function or has invalid syntax." + +-- The provided ASSISTANT lua table does not contain the boolean flag to control the allowance of profiles. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T781921072"] = "The provided ASSISTANT lua table does not contain the boolean flag to control the allowance of profiles." + -- 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/Routes.razor.cs b/app/MindWork AI Studio/Routes.razor.cs index 836cab0ee..932a7190c 100644 --- a/app/MindWork AI Studio/Routes.razor.cs +++ b/app/MindWork AI Studio/Routes.razor.cs @@ -28,5 +28,6 @@ public sealed partial class Routes public const string ASSISTANT_ERI = "/assistant/eri"; public const string ASSISTANT_AI_STUDIO_I18N = "/assistant/ai-studio/i18n"; public const string ASSISTANT_DOCUMENT_ANALYSIS = "/assistant/document-analysis"; + 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..78c79c8e2 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/AssistantComponentFactory.cs @@ -0,0 +1,60 @@ +using AIStudio.Tools.PluginSystem.Assistants.DataModel; +using AIStudio.Tools.PluginSystem.Assistants.DataModel.Layout; + +namespace AIStudio.Tools.PluginSystem.Assistants; + +public class AssistantComponentFactory +{ + private static readonly ILogger<AssistantComponentFactory> LOGGER = Program.LOGGER_FACTORY.CreateLogger<AssistantComponentFactory>(); + + public static IAssistantComponent CreateComponent( + AssistantComponentType type, + Dictionary<string, object> props, + List<IAssistantComponent> children) + { + switch (type) + { + case AssistantComponentType.FORM: + return new AssistantForm { Props = props, Children = children }; + case AssistantComponentType.TEXT_AREA: + return new AssistantTextArea { Props = props, Children = children }; + case AssistantComponentType.BUTTON: + return new AssistantButton { Props = props, Children = children}; + case AssistantComponentType.BUTTON_GROUP: + return new AssistantButtonGroup { Props = props, Children = children }; + case AssistantComponentType.DROPDOWN: + return new AssistantDropdown { Props = props, Children = children }; + case AssistantComponentType.PROVIDER_SELECTION: + return new AssistantProviderSelection { Props = props, Children = children }; + case AssistantComponentType.PROFILE_SELECTION: + return new AssistantProfileSelection { Props = props, Children = children }; + case AssistantComponentType.SWITCH: + return new AssistantSwitch { Props = props, Children = children }; + case AssistantComponentType.HEADING: + return new AssistantHeading { Props = props, Children = children }; + case AssistantComponentType.TEXT: + return new AssistantText { Props = props, Children = children }; + case AssistantComponentType.LIST: + return new AssistantList { Props = props, Children = children }; + case AssistantComponentType.WEB_CONTENT_READER: + return new AssistantWebContentReader { Props = props, Children = children }; + case AssistantComponentType.FILE_CONTENT_READER: + return new AssistantFileContentReader { Props = props, Children = children }; + case AssistantComponentType.IMAGE: + return new AssistantImage { Props = props, Children = children }; + case AssistantComponentType.COLOR_PICKER: + return new AssistantColorPicker { Props = props, Children = children }; + case AssistantComponentType.LAYOUT_ITEM: + return new AssistantItem { Props = props, Children = children }; + case AssistantComponentType.LAYOUT_GRID: + return new AssistantGrid { Props = props, Children = children }; + case AssistantComponentType.LAYOUT_PAPER: + return new AssistantPaper { Props = props, Children = children }; + case AssistantComponentType.LAYOUT_STACK: + return new AssistantStack { 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}"); + } + } +} 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..cc079354e --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantButton.cs @@ -0,0 +1,91 @@ +using Lua; + +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public sealed class AssistantButton : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.BUTTON; + public override Dictionary<string, object> Props { get; set; } = new(); + public override List<IAssistantComponent> Children { get; set; } = new(); + + public string Name + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Name)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Name), value); + } + + public string Text + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Text)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Text), value); + } + + public LuaFunction? Action + { + get => this.Props.TryGetValue(nameof(this.Action), out var value) && value is LuaFunction action ? action : null; + set => AssistantComponentPropHelper.WriteObject(this.Props, nameof(this.Action), value); + } + + public string Variant + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Variant)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Variant), value); + } + + public string Color + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Color)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Color), value); + } + + public bool IsFullWidth + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.IsFullWidth), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.IsFullWidth), value); + } + + public string StartIcon + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.StartIcon)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.StartIcon), value); + } + + public string EndIcon + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.EndIcon)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.EndIcon), value); + } + + public string IconColor + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.IconColor)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.IconColor), value); + } + + public string IconSize + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.IconSize)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.IconSize), value); + } + + public string Size + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Size)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Size), value); + } + + public string Class + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value); + } + + public string Style + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value); + } + + public Variant GetButtonVariant() => Enum.TryParse<Variant>(this.Variant, out var variant) ? variant : MudBlazor.Variant.Filled; + +} diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantButtonGroup.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantButtonGroup.cs new file mode 100644 index 000000000..668cd8418 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantButtonGroup.cs @@ -0,0 +1,58 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public sealed class AssistantButtonGroup : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.BUTTON_GROUP; + public override Dictionary<string, object> Props { get; set; } = new(); + public override List<IAssistantComponent> Children { get; set; } = new(); + + public string Variant + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Variant)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Variant), value); + } + + public string Color + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Color)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Color), value); + } + + public string Size + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Size)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Size), value); + } + + public bool OverrideStyles + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.OverrideStyles), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.OverrideStyles), value); + } + + public bool Vertical + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.Vertical), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.Vertical), value); + } + + public bool DropShadow + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.DropShadow), true); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.DropShadow), value); + } + + public string Class + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value); + } + + public string Style + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value); + } + + public Variant GetVariant() => Enum.TryParse<Variant>(this.Variant, out var variant) ? variant : MudBlazor.Variant.Filled; +} diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantColorPicker.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantColorPicker.cs new file mode 100644 index 000000000..6a8680f76 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantColorPicker.cs @@ -0,0 +1,69 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +internal sealed class AssistantColorPicker : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.COLOR_PICKER; + public override Dictionary<string, object> Props { get; set; } = new(); + public override List<IAssistantComponent> Children { get; set; } = new(); + + public string Name + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Name)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Name), value); + } + public string Label + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Label)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Label), value); + } + + public string Placeholder + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Placeholder)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Placeholder), value); + } + + public bool ShowAlpha + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.ShowAlpha), true); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.ShowAlpha), value); + } + + public bool ShowToolbar + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.ShowToolbar), true); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.ShowToolbar), value); + } + + public bool ShowModeSwitch + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.ShowModeSwitch), true); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.ShowModeSwitch), value); + } + + public string PickerVariant + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.PickerVariant)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.PickerVariant), value); + } + + public string UserPrompt + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.UserPrompt)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.UserPrompt), value); + } + + public string Class + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value); + } + + public string Style + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value); + } + + public PickerVariant GetPickerVariant() => Enum.TryParse<PickerVariant>(this.PickerVariant, out var variant) ? variant : MudBlazor.PickerVariant.Static; +} 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..c92f4eee4 --- /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 AssistantComponentType Type { get; } + public abstract Dictionary<string, object> Props { get; set; } + public abstract List<IAssistantComponent> Children { get; set; } +} diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantComponentPropHelper.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantComponentPropHelper.cs new file mode 100644 index 000000000..11ddc4468 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantComponentPropHelper.cs @@ -0,0 +1,71 @@ +using AIStudio.Tools.PluginSystem.Assistants.Icons; + +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +internal static class AssistantComponentPropHelper +{ + public static string ReadString(Dictionary<string, object> props, string key) + { + if (props.TryGetValue(key, out var value)) + { + return value?.ToString() ?? string.Empty; + } + + return string.Empty; + } + + public static void WriteString(Dictionary<string, object> props, string key, string value) + { + props[key] = value ?? string.Empty; + } + + public static int ReadInt(Dictionary<string, object> props, string key, int fallback = 0) + { + return props.TryGetValue(key, out var value) && int.TryParse(value?.ToString(), out var i) ? i : fallback; + } + + public static void WriteInt(Dictionary<string, object> props, string key, int value) + { + props[key] = value; + } + + public static int? ReadNullableInt(Dictionary<string, object> props, string key) + { + return props.TryGetValue(key, out var value) && int.TryParse(value?.ToString(), out var i) ? i : null; + } + + public static void WriteNullableInt(Dictionary<string, object> props, string key, int? value) + { + if (value.HasValue) + props[key] = value.Value; + else + props.Remove(key); + } + + public static bool ReadBool(Dictionary<string, object> props, string key, bool fallback = false) + { + return props.TryGetValue(key, out var value) && bool.TryParse(value.ToString(), out var b) ? b : fallback; + } + + public static void WriteBool(Dictionary<string, object> props, string key, bool value) + { + props[key] = value; + } + + public static void WriteObject(Dictionary<string, object> props, string key, object? value) + { + if (value is null) + props.Remove(key); + else + props[key] = value; + } + + public static MudBlazor.Color GetColor(string value, Color fallback) => Enum.TryParse<MudBlazor.Color>(value, out var color) ? color : fallback; + public static string GetIconSvg(string value) => MudBlazorIconRegistry.TryGetSvg(value, out var svg) ? svg : string.Empty; + public static Size GetComponentSize(string value, Size fallback) => Enum.TryParse<Size>(value, out var size) ? size : fallback; + public static Justify? GetJustify(string value) => Enum.TryParse<Justify>(value, out var justify) ? justify : null; + public static AlignItems? GetAlignment(string value) => Enum.TryParse<AlignItems>(value, out var alignment) ? alignment : null; + public static Wrap? GetWrap(string value) => Enum.TryParse<Wrap>(value, out var wrap) ? wrap : null; + public static StretchItems? GetStretching(string value) => Enum.TryParse<StretchItems>(value, out var stretch) ? stretch : null; + public static Breakpoint GetBreakpoint(string value, Breakpoint fallback) => Enum.TryParse<Breakpoint>(value, out var breakpoint) ? breakpoint : fallback; +} diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantComponentType.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantComponentType.cs new file mode 100644 index 000000000..10acbb8ce --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantComponentType.cs @@ -0,0 +1,24 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public enum AssistantComponentType +{ + FORM, + TEXT_AREA, + BUTTON, + BUTTON_GROUP, + DROPDOWN, + PROVIDER_SELECTION, + PROFILE_SELECTION, + SWITCH, + HEADING, + TEXT, + LIST, + WEB_CONTENT_READER, + FILE_CONTENT_READER, + IMAGE, + COLOR_PICKER, + LAYOUT_ITEM, + LAYOUT_GRID, + LAYOUT_PAPER, + LAYOUT_STACK, +} 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..0b3653307 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDropdown.cs @@ -0,0 +1,88 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +internal sealed class AssistantDropdown : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.DROPDOWN; + public override Dictionary<string, object> Props { get; set; } = new(); + public override List<IAssistantComponent> Children { get; set; } = new(); + + public string Name + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Name)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Name), value); + } + + public string Label + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Label)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Label), value); + } + + public string UserPrompt + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.UserPrompt)); + set => AssistantComponentPropHelper.WriteString(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; + } + } + } + + public string Class + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value); + } + + public string Style + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value); + } +} 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/AssistantFileContentReader.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantFileContentReader.cs new file mode 100644 index 000000000..b59110843 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantFileContentReader.cs @@ -0,0 +1,32 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +internal sealed class AssistantFileContentReader : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.FILE_CONTENT_READER; + public override Dictionary<string, object> Props { get; set; } = new(); + public override List<IAssistantComponent> Children { get; set; } = new(); + + public string Name + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Name)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Name), value); + } + + public string UserPrompt + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.UserPrompt)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.UserPrompt), value); + } + + public string Class + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value); + } + + public string Style + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value); + } +} 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..5b8b611fd --- /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 AssistantComponentType Type => AssistantComponentType.FORM; + public override Dictionary<string, object> Props { get; set; } = new(); + public override 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..ce2bc2de2 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantHeading.cs @@ -0,0 +1,32 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +internal sealed class AssistantHeading : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.HEADING; + public override Dictionary<string, object> Props { get; set; } = new(); + public override List<IAssistantComponent> Children { get; set; } = new(); + + public string Text + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Text)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Text), value); + } + + public int Level + { + get => AssistantComponentPropHelper.ReadInt(this.Props, nameof(this.Level), 2); + set => AssistantComponentPropHelper.WriteInt(this.Props, nameof(this.Level), value); + } + + public string Class + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value); + } + + public string Style + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value); + } +} diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantImage.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantImage.cs new file mode 100644 index 000000000..885f4ea05 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantImage.cs @@ -0,0 +1,38 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +internal sealed class AssistantImage : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.IMAGE; + public override Dictionary<string, object> Props { get; set; } = new(); + public override List<IAssistantComponent> Children { get; set; } = new(); + + public string Src + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Src)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Src), value); + } + + public string Alt + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Alt)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Alt), value); + } + + public string Caption + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Caption)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Caption), value); + } + + public string Class + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value); + } + + public string Style + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value); + } +} 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..6c2b04106 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantList.cs @@ -0,0 +1,28 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +internal sealed class AssistantList : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.LIST; + public override Dictionary<string, object> Props { get; set; } = new(); + public override 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; + } + + public string Class + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value); + } + + public string Style + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value); + } +} 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/AssistantProfileSelection.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantProfileSelection.cs new file mode 100644 index 000000000..3116b2608 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantProfileSelection.cs @@ -0,0 +1,26 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +internal sealed class AssistantProfileSelection : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.PROFILE_SELECTION; + public override Dictionary<string, object> Props { get; set; } = new(); + public override List<IAssistantComponent> Children { get; set; } = new(); + + public string ValidationMessage + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.ValidationMessage)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.ValidationMessage), value); + } + + public string Class + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value); + } + + public string Style + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value); + } +} 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..43711ef2a --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantProviderSelection.cs @@ -0,0 +1,32 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +internal sealed class AssistantProviderSelection : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.PROVIDER_SELECTION; + public override Dictionary<string, object> Props { get; set; } = new(); + public override List<IAssistantComponent> Children { get; set; } = new(); + + public string Name + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Name)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Name), value); + } + + public string Label + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Label)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Label), value); + } + + public string Class + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value); + } + + public string Style + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value); + } +} 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..798a1dd0f --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantSwitch.cs @@ -0,0 +1,98 @@ +using AIStudio.Tools.PluginSystem.Assistants.Icons; + +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +internal sealed class AssistantSwitch : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.SWITCH; + public override Dictionary<string, object> Props { get; set; } = new(); + public override List<IAssistantComponent> Children { get; set; } = new(); + + public string Name + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Name)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Name), value); + } + + public string Label + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Label)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Label), value); + } + + public bool Value + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.Value), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.Value), value); + } + + public bool Disabled + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.Disabled), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.Disabled), value); + } + + public string UserPrompt + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.UserPrompt)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.UserPrompt), value); + } + + public string LabelOn + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.LabelOn)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.LabelOn), value); + } + + public string LabelOff + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.LabelOff)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.LabelOff), value); + } + + public string LabelPlacement + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.LabelPlacement)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.LabelPlacement), value); + } + + public string CheckedColor + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.CheckedColor)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.CheckedColor), value); + } + + public string UncheckedColor + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.UncheckedColor)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.UncheckedColor), value); + } + + public string Icon + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Icon)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Icon), value); + } + + public string IconColor + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.IconColor)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.IconColor), value); + } + + public string Class + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value); + } + + public string Style + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value); + } + + public MudBlazor.Color GetColor(string colorString) => Enum.TryParse<Color>(colorString, out var color) ? color : MudBlazor.Color.Inherit; + public Placement GetLabelPlacement() => Enum.TryParse<Placement>(this.LabelPlacement, out var placement) ? placement : Placement.Right; + public string GetIconSvg() => MudBlazorIconRegistry.TryGetSvg(this.Icon, out var svg) ? svg : string.Empty; +} 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..68f8537e1 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantText.cs @@ -0,0 +1,28 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +internal sealed class AssistantText : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.TEXT; + + public override Dictionary<string, object> Props { get; set; } = new(); + + public override List<IAssistantComponent> Children { get; set; } = new(); + + public string Content + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Content)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Content), value); + } + + public string Class + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value); + } + + public string Style + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value); + } +} 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..dd4336c69 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantTextArea.cs @@ -0,0 +1,118 @@ +using AIStudio.Tools.PluginSystem.Assistants.Icons; + +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +internal sealed class AssistantTextArea : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.TEXT_AREA; + public override Dictionary<string, object> Props { get; set; } = new(); + public override List<IAssistantComponent> Children { get; set; } = new(); + + public string Name + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Name)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Name), value); + } + + public string Label + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Label)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Label), value); + } + + public string HelperText + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.HelperText)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.HelperText), value); + } + + public bool HelperTextOnFocus + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.HelperTextOnFocus), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.HelperTextOnFocus), value); + } + + public string Adornment + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Adornment)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Adornment), value); + } + + public string AdornmentIcon + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.AdornmentIcon)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.AdornmentIcon), value); + } + + public string AdornmentText + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.AdornmentText)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.AdornmentText), value); + } + + public string AdornmentColor + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.AdornmentColor)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.AdornmentColor), value); + } + + public string UserPrompt + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.UserPrompt)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.UserPrompt), value); + } + + public string PrefillText + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.PrefillText)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.PrefillText), value); + } + + public int? Counter + { + get => AssistantComponentPropHelper.ReadNullableInt(this.Props, nameof(this.Counter)); + set => AssistantComponentPropHelper.WriteNullableInt(this.Props, nameof(this.Counter), value); + } + + public int MaxLength + { + get => AssistantComponentPropHelper.ReadInt(this.Props, nameof(this.MaxLength), PluginAssistants.TEXT_AREA_MAX_VALUE); + set => AssistantComponentPropHelper.WriteInt(this.Props, nameof(this.MaxLength), value); + } + + public bool IsImmediate + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.IsImmediate)); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.IsImmediate), value); + } + + public bool IsSingleLine + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.IsSingleLine), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.IsSingleLine), value); + } + + public bool ReadOnly + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.ReadOnly), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.ReadOnly), value); + } + + public string Class + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value); + } + + public string Style + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value); + } + + public Adornment GetAdornmentPos() => Enum.TryParse<MudBlazor.Adornment>(this.Adornment, out var position) ? position : MudBlazor.Adornment.Start; + + public Color GetAdornmentColor() => Enum.TryParse<Color>(this.AdornmentColor, out var color) ? color : Color.Default; + + public string GetIconSvg() => MudBlazorIconRegistry.TryGetSvg(this.AdornmentIcon, out var svg) ? svg : string.Empty; +} diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantWebContentReader.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantWebContentReader.cs new file mode 100644 index 000000000..75855de4d --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantWebContentReader.cs @@ -0,0 +1,44 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +internal sealed class AssistantWebContentReader : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.WEB_CONTENT_READER; + public override Dictionary<string, object> Props { get; set; } = new(); + public override List<IAssistantComponent> Children { get; set; } = new(); + + public string Name + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Name)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Name), value); + } + + public string UserPrompt + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.UserPrompt)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.UserPrompt), value); + } + + public bool Preselect + { + get => this.Props.TryGetValue(nameof(this.Preselect), out var v) && v is true; + set => this.Props[nameof(this.Preselect)] = value; + } + + public bool PreselectContentCleanerAgent + { + get => this.Props.TryGetValue(nameof(this.PreselectContentCleanerAgent), out var v) && v is true; + set => this.Props[nameof(this.PreselectContentCleanerAgent)] = value; + } + + public string Class + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value); + } + + public string Style + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value); + } +} 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..5e86b9a68 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/ComponentPropSpecs.cs @@ -0,0 +1,104 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public static class ComponentPropSpecs +{ + public static readonly IReadOnlyDictionary<AssistantComponentType, PropSpec> SPECS = + new Dictionary<AssistantComponentType, PropSpec> + { + [AssistantComponentType.FORM] = new( + required: ["Children"], + optional: ["Class", "Style"] + ), + [AssistantComponentType.TEXT_AREA] = new( + required: ["Name", "Label"], + optional: [ + "HelperText", "HelperTextOnFocus", "UserPrompt", "PrefillText", + "ReadOnly", "IsSingleLine", "Counter", "MaxLength", "IsImmediate", + "Adornment", "AdornmentIcon", "AdornmentText", "AdornmentColor", "Class", "Style", + ] + ), + [AssistantComponentType.BUTTON] = new( + required: ["Name", "Text", "Action"], + optional: [ + "Variant", "Color", "IsFullWidth", "Size", + "StartIcon", "EndIcon", "IconColor", "IconSize", "Class", "Style" + ] + ), + [AssistantComponentType.BUTTON_GROUP] = new( + required: [], + optional: ["Variant", "Color", "Size", "OverrideStyles", "Vertical", "DropShadow", "Class", "Style"] + ), + [AssistantComponentType.DROPDOWN] = new( + required: ["Name", "Label", "Default", "Items"], + optional: ["UserPrompt", "Class", "Style"] + ), + [AssistantComponentType.PROVIDER_SELECTION] = new( + required: ["Name", "Label"], + optional: ["Class", "Style"] + ), + [AssistantComponentType.PROFILE_SELECTION] = new( + required: [], + optional: ["ValidationMessage", "Class", "Style"] + ), + [AssistantComponentType.SWITCH] = new( + required: ["Name", "Label", "Value"], + optional: [ + "LabelOn", "LabelOff", "LabelPlacement", "Icon", "IconColor", "UserPrompt", + "CheckedColor", "UncheckedColor", "Disabled", "Class", "Style", + ] + ), + [AssistantComponentType.HEADING] = new( + required: ["Text", "Level"], + optional: ["Class", "Style"] + ), + [AssistantComponentType.TEXT] = new( + required: ["Content"], + optional: ["Class", "Style"] + ), + [AssistantComponentType.LIST] = new( + required: ["Items"], + optional: ["Class", "Style"] + ), + [AssistantComponentType.WEB_CONTENT_READER] = new( + required: ["Name"], + optional: ["UserPrompt", "Preselect", "PreselectContentCleanerAgent", "Class", "Style"] + ), + [AssistantComponentType.FILE_CONTENT_READER] = new( + required: ["Name"], + optional: ["UserPrompt", "Class", "Style"] + ), + [AssistantComponentType.IMAGE] = new( + required: ["Src"], + optional: ["Alt", "Caption", "Class", "Style"] + ), + [AssistantComponentType.COLOR_PICKER] = new( + required: ["Name", "Label"], + optional: [ + "Placeholder", "ShowAlpha", "ShowToolbar", "ShowModeSwitch", + "PickerVariant", "UserPrompt", "Class", "Style" + ] + ), + [AssistantComponentType.LAYOUT_ITEM] = new( + required: ["Name"], + optional: ["Xs", "Sm", "Md", "Lg", "Xl", "Xxl", "Class", "Style"] + ), + [AssistantComponentType.LAYOUT_GRID] = new( + required: ["Name"], + optional: ["Justify", "Spacing", "Class", "Style"] + ), + [AssistantComponentType.LAYOUT_PAPER] = new( + required: ["Name"], + optional: [ + "Elevation", "Height", "MaxHeight", "MinHeight", "Width", "MaxWidth", "MinWidth", + "IsOutlined", "IsSquare", "Class", "Style" + ] + ), + [AssistantComponentType.LAYOUT_STACK] = new( + required: ["Name"], + optional: [ + "IsRow", "IsReverse", "Breakpoint", "Align", "Justify", "Stretch", + "Wrap", "Spacing", "Class", "Style", + ] + ), + }; +} 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..1835c50d5 --- /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 +{ + AssistantComponentType 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/Layout/AssistantGrid.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/Layout/AssistantGrid.cs new file mode 100644 index 000000000..6a6bad68c --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/Layout/AssistantGrid.cs @@ -0,0 +1,40 @@ +using Lua; + +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel.Layout; + +public sealed class AssistantGrid : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.LAYOUT_GRID; + public override Dictionary<string, object> Props { get; set; } = new(); + public override List<IAssistantComponent> Children { get; set; } = new(); + + public string Name + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Name)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Name), value); + } + + public string Justify + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Justify)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Justify), value); + } + + public int Spacing + { + get => AssistantComponentPropHelper.ReadInt(this.Props, nameof(this.Spacing), 6); + set => AssistantComponentPropHelper.WriteInt(this.Props, nameof(this.Spacing), value); + } + + public string Class + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value); + } + + public string Style + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value); + } +} diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/Layout/AssistantItem.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/Layout/AssistantItem.cs new file mode 100644 index 000000000..2309748a2 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/Layout/AssistantItem.cs @@ -0,0 +1,64 @@ +using Lua; + +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel.Layout; + +public sealed class AssistantItem : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.LAYOUT_ITEM; + public override Dictionary<string, object> Props { get; set; } = new(); + public override List<IAssistantComponent> Children { get; set; } = new(); + + public string Name + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Name)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Name), value); + } + + public int? Xs + { + get => AssistantComponentPropHelper.ReadNullableInt(this.Props, nameof(this.Xs)); + set => AssistantComponentPropHelper.WriteNullableInt(this.Props, nameof(this.Xs), value); + } + + public int? Sm + { + get => AssistantComponentPropHelper.ReadNullableInt(this.Props, nameof(this.Sm)); + set => AssistantComponentPropHelper.WriteNullableInt(this.Props, nameof(this.Sm), value); + } + + public int? Md + { + get => AssistantComponentPropHelper.ReadNullableInt(this.Props, nameof(this.Md)); + set => AssistantComponentPropHelper.WriteNullableInt(this.Props, nameof(this.Md), value); + } + + public int? Lg + { + get => AssistantComponentPropHelper.ReadNullableInt(this.Props, nameof(this.Lg)); + set => AssistantComponentPropHelper.WriteNullableInt(this.Props, nameof(this.Lg), value); + } + + public int? Xl + { + get => AssistantComponentPropHelper.ReadNullableInt(this.Props, nameof(this.Xl)); + set => AssistantComponentPropHelper.WriteNullableInt(this.Props, nameof(this.Xl), value); + } + + public int? Xxl + { + get => AssistantComponentPropHelper.ReadNullableInt(this.Props, nameof(this.Xxl)); + set => AssistantComponentPropHelper.WriteNullableInt(this.Props, nameof(this.Xxl), value); + } + + public string Class + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value); + } + + public string Style + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value); + } +} diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/Layout/AssistantPaper.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/Layout/AssistantPaper.cs new file mode 100644 index 000000000..7c0ed09b8 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/Layout/AssistantPaper.cs @@ -0,0 +1,82 @@ +using Lua; + +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel.Layout; + +public sealed class AssistantPaper : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.LAYOUT_PAPER; + public override Dictionary<string, object> Props { get; set; } = new(); + public override List<IAssistantComponent> Children { get; set; } = new(); + + public string Name + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Name)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Name), value); + } + + public int Elevation + { + get => AssistantComponentPropHelper.ReadInt(this.Props, nameof(this.Elevation), 1); + set => AssistantComponentPropHelper.WriteInt(this.Props, nameof(this.Elevation), value); + } + + public string Height + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Height)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Height), value); + } + + public string MaxHeight + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.MaxHeight)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.MaxHeight), value); + } + + public string MinHeight + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.MinHeight)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.MinHeight), value); + } + + public string Width + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Width)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Width), value); + } + + public string MaxWidth + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.MaxWidth)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.MaxWidth), value); + } + + public string MinWidth + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.MinWidth)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.MinWidth), value); + } + + public bool IsOutlined + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.IsOutlined), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.IsOutlined), value); + } + + public bool IsSquare + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.IsSquare), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.IsSquare), value); + } + + public string Class + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value); + } + + public string Style + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Style)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Style), value); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/Layout/AssistantStack.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/Layout/AssistantStack.cs new file mode 100644 index 000000000..92a058a05 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/Layout/AssistantStack.cs @@ -0,0 +1,76 @@ +using Lua; + +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel.Layout; + +public sealed class AssistantStack : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.LAYOUT_STACK; + public override Dictionary<string, object> Props { get; set; } = new(); + public override List<IAssistantComponent> Children { get; set; } = new(); + + public string Name + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Name)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Name), value); + } + + public bool IsRow + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.IsRow), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.IsRow), value); + } + + public bool IsReverse + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.IsReverse), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.IsReverse), value); + } + + public string Breakpoint + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Breakpoint)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Breakpoint), value); + } + + public string Align + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Align)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Align), value); + } + + public string Justify + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Justify)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Justify), value); + } + + public string Stretch + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Stretch)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Stretch), value); + } + + public string Wrap + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Wrap)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Wrap), value); + } + + public int Spacing + { + get => AssistantComponentPropHelper.ReadInt(this.Props, nameof(this.Spacing), 3); + set => AssistantComponentPropHelper.WriteInt(this.Props, nameof(this.Spacing), value); + } + + public string Class + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Class)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Class), value); + } + + public int Style + { + get => AssistantComponentPropHelper.ReadInt(this.Props, nameof(this.Style), 3); + set => AssistantComponentPropHelper.WriteInt(this.Props, nameof(this.Style), value); + } +} 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..68f6525b7 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs @@ -0,0 +1,555 @@ +using AIStudio.Tools.PluginSystem.Assistants.DataModel; +using AIStudio.Tools.PluginSystem.Assistants.DataModel.Layout; +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; private set; } + public string AssistantTitle { get; private set; } = string.Empty; + public string AssistantDescription { get; private set; } = string.Empty; + public string SystemPrompt { get; private set; } = string.Empty; + public string SubmitText { get; private set; } = string.Empty; + public bool AllowProfiles { get; private set; } = true; + public bool HasEmbeddedProfileSelection { get; private set; } + public bool HasCustomPromptBuilder => this.buildPromptFunction is not null; + public const int TEXT_AREA_MAX_VALUE = 524288; + + private LuaFunction? buildPromptFunction; + + 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; + this.HasEmbeddedProfileSelection = false; + this.buildPromptFunction = null; + + this.RegisterLuaHelpers(); + + // 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 lua 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 provided ASSISTANT lua 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 provided ASSISTANT lua 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 provided ASSISTANT lua 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 provided ASSISTANT lua table does not contain the boolean flag to control the allowance of profiles."); + return false; + } + + if (assistantTable.TryGetValue("BuildPrompt", out var buildPromptValue)) + { + if (buildPromptValue.TryRead<LuaFunction>(out var buildPrompt)) + this.buildPromptFunction = buildPrompt; + else + message = TB("ASSISTANT.BuildPrompt exists but is not a Lua function or has invalid syntax."); + } + + 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 provided ASSISTANT lua table does not contain a valid UI table."); + return false; + } + + if (!this.TryReadRenderTree(uiTable, out var rootComponent)) + { + message = TB("Failed to parse the UI render tree from the ASSISTANT lua table."); + return false; + } + + this.RootComponent = (AssistantForm)rootComponent; + return true; + } + + public async Task<string?> TryBuildPromptAsync(LuaTable input, CancellationToken cancellationToken = default) + { + if (this.buildPromptFunction is null) + return null; + + try + { + cancellationToken.ThrowIfCancellationRequested(); + var results = await this.state.CallAsync(this.buildPromptFunction, [input]); + if (results.Length == 0) + return string.Empty; + + if (results[0].TryRead<string>(out var prompt)) + return prompt; + + LOGGER.LogWarning("ASSISTANT.BuildPrompt returned a non-string value."); + return string.Empty; + } + catch (Exception e) + { + LOGGER.LogError(e, "ASSISTANT.BuildPrompt failed to execute."); + return string.Empty; + } + } + + public async Task<LuaTable?> TryInvokeButtonActionAsync(AssistantButton button, LuaTable input, CancellationToken cancellationToken = default) + { + if (button.Action is null) + return null; + + try + { + cancellationToken.ThrowIfCancellationRequested(); + var results = await this.state.CallAsync(button.Action, [input]); + if (results.Length == 0) + return null; + + if (results[0].Type is LuaValueType.Nil) + return null; + + if (results[0].TryRead<LuaTable>(out var updateTable)) + return updateTable; + + LOGGER.LogWarning("Assistant plugin '{PluginName}' BUTTON '{ButtonName}' returned a non-table value. The result is ignored.", this.Name, button.Name); + return null; + } + catch (Exception e) + { + LOGGER.LogError(e, "Assistant plugin '{PluginName}' BUTTON '{ButtonName}' action failed to execute.", this.Name, button.Name); + return null; + } + } + + /// <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<AssistantComponentType>(typeText, true, out var type) + || type != AssistantComponentType.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(AssistantComponentType.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<AssistantComponentType>(typeText, true, out var type)) + { + LOGGER.LogWarning($"Component #{idx} missing valid Type."); + return false; + } + + if (type == AssistantComponentType.PROFILE_SELECTION) + this.HasEmbeddedProfileSelection = true; + + 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); + + if (component is AssistantTextArea textArea) + { + if (!string.IsNullOrWhiteSpace(textArea.AdornmentIcon) && !string.IsNullOrWhiteSpace(textArea.AdornmentText)) + LOGGER.LogWarning($"Assistant plugin '{this.Name}' TEXT_AREA '{textArea.Name}' defines both '[\"AdornmentIcon\"]' and '[\"AdornmentText\"]', thus both will be ignored by the renderer. You`re only allowed to use either one of them."); + + if (textArea.MaxLength == 0) + { + LOGGER.LogWarning($"Assistant plugin '{this.Name}' TEXT_AREA '{textArea.Name}' defines a MaxLength of `0`. This is not applicable, if you want a readonly Textfield, set the [\"ReadOnly\"] field to `true`. MAXLENGTH IS SET TO DEFAULT {TEXT_AREA_MAX_VALUE}."); + textArea.MaxLength = TEXT_AREA_MAX_VALUE; + } + + if (textArea.MaxLength != 0 && textArea.MaxLength != TEXT_AREA_MAX_VALUE) + textArea.Counter = textArea.MaxLength; + + if (textArea.Counter != null) + textArea.IsImmediate = true; + } + + if (component is AssistantButtonGroup buttonGroup) + { + var invalidChildren = buttonGroup.Children.Where(child => child.Type != AssistantComponentType.BUTTON).ToList(); + if (invalidChildren.Count > 0) + { + LOGGER.LogWarning("Assistant plugin '{PluginName}' BUTTON_GROUP contains non-BUTTON children. Only BUTTON children are supported and invalid children are ignored.", this.Name); + buttonGroup.Children = buttonGroup.Children.Where(child => child.Type == AssistantComponentType.BUTTON).ToList(); + } + } + + if (component is AssistantGrid grid) + { + var invalidChildren = grid.Children.Where(child => child.Type != AssistantComponentType.LAYOUT_ITEM).ToList(); + if (invalidChildren.Count > 0) + { + LOGGER.LogWarning("Assistant plugin '{PluginName}' LAYOUT_GRID contains non-LAYOUT_ITEM children. Only LAYOUT_ITEM children are supported and invalid children are ignored.", this.Name); + grid.Children = grid.Children.Where(child => child.Type == AssistantComponentType.LAYOUT_ITEM).ToList(); + } + } + + return true; + } + + private bool TryReadComponentProps(AssistantComponentType 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.TryConvertComponentPropValue(type, key, 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.TryConvertComponentPropValue(type, key, luaVal, out var dotNetVal)) + { + LOGGER.LogWarning($"Component {type}: optional prop '{key}' has wrong type, skipping."); + continue; + } + props[key] = dotNetVal; + } + + return true; + } + + private bool TryConvertComponentPropValue(AssistantComponentType type, string key, LuaValue val, out object result) + { + if (type == AssistantComponentType.BUTTON && key == "Action" && val.TryRead<LuaFunction>(out var action)) + { + result = action; + return true; + } + + return this.TryConvertLuaValue(val, out result); + } + + 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; + } + + private void RegisterLuaHelpers() + { + + this.state.Environment["LogInfo"] = new LuaFunction((context, _) => + { + if (context.ArgumentCount == 0) return new(0); + + var message = context.GetArgument<string>(0); + LOGGER.LogInformation($"[Lua] [Assistants] [{this.Name}]: {message}"); + return new(0); + }); + + this.state.Environment["LogDebug"] = new LuaFunction((context, _) => + { + if (context.ArgumentCount == 0) return new(0); + + var message = context.GetArgument<string>(0); + LOGGER.LogDebug($"[Lua] [Assistants] [{this.Name}]: {message}"); + return new(0); + }); + + this.state.Environment["LogWarning"] = new LuaFunction((context, _) => + { + if (context.ArgumentCount == 0) return new(0); + + var message = context.GetArgument<string>(0); + LOGGER.LogWarning($"[Lua] [Assistants] [{this.Name}]: {message}"); + return new(0); + }); + + this.state.Environment["LogError"] = new LuaFunction((context, _) => + { + if (context.ArgumentCount == 0) return new(0); + + var message = context.GetArgument<string>(0); + LOGGER.LogError($"[Lua] [Assistants] [{this.Name}]: {message}"); + return new(0); + }); + + this.state.Environment["DateTime"] = new LuaFunction((context, _) => + { + var format = context.ArgumentCount > 0 ? context.GetArgument<string>(0) : "yyyy-MM-dd HH:mm:ss"; + var now = DateTime.Now; + var formattedDate = now.ToString(format); + + var table = new LuaTable + { + ["year"] = now.Year, + ["month"] = now.Month, + ["day"] = now.Day, + ["hour"] = now.Hour, + ["minute"] = now.Minute, + ["second"] = now.Second, + ["millisecond"] = now.Millisecond, + ["formatted"] = formattedDate, + }; + return new(context.Return(table)); + }); + + this.state.Environment["Timestamp"] = new LuaFunction((context, _) => + { + var timestamp = DateTime.UtcNow.ToString("o"); + return new(context.Return(timestamp)); + }); + } +} diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs index afff3d35e..b6377a990 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs @@ -56,6 +56,11 @@ public abstract partial class PluginBase : IPluginMetadata /// <inheritdoc /> public bool IsInternal { get; } + + /// <summary> + /// The absolute path to the plugin directory (where `plugin.lua` lives). + /// </summary> + public string PluginPath { get; internal set; } = string.Empty; /// <summary> /// The issues that occurred during the initialization of this plugin. @@ -533,4 +538,4 @@ protected void ReadTextTable(string parent, LuaTable table, out Dictionary<strin } #endregion -} \ 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 be6de5783..63c6af4de 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -1,7 +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; @@ -294,6 +296,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 861dfce6e..04bf73e38 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) @@ -95,6 +95,7 @@ private static async Task<PluginBase> Start(IAvailablePlugin meta, CancellationT var code = await File.ReadAllTextAsync(pluginMainFile, Encoding.UTF8, cancellationToken); var plugin = await Load(meta.LocalPath, code, cancellationToken); + plugin.PluginPath = meta.LocalPath; if (plugin is NoPlugin noPlugin) { LOG.LogError($"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reason: {noPlugin.Issues.First()}"); @@ -119,4 +120,4 @@ private static async Task<PluginBase> Start(IAvailablePlugin meta, CancellationT LOG.LogError($"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reasons: {string.Join("; ", plugin.Issues)}"); return new NoPlugin($"Was not able to start plugin: Id='{meta.Id}', Type='{meta.Type}', Name='{meta.Name}', Version='{meta.Version}'. Reasons: {string.Join("; ", plugin.Issues)}"); } -} \ 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 5770dcec4..308f87589 100644 --- a/app/MindWork AI Studio/packages.lock.json +++ b/app/MindWork AI Studio/packages.lock.json @@ -37,9 +37,9 @@ }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[9.0.13, )", - "resolved": "9.0.13", - "contentHash": "f7t15I9ZXV7fNk3FIzPAlkJNG1A1tkSeDpRh+TFWEToGGqA+uj6uqU15I8YOkkYICNY2tqOVm2CMe6ScPFPwEg==" + "requested": "[9.0.12, )", + "resolved": "9.0.12", + "contentHash": "StA3kyImQHqDo8A8ZHaSxgASbEuT5UIqgeCvK5SzUPj//xE1QSys421J9pEs4cYuIVwq7CJvWSKxtyH7aPr1LA==" }, "MudBlazor": { "type": "Direct", @@ -235,6 +235,6 @@ "type": "Project" } }, - "net9.0/osx-arm64": {} + "net9.0/win-x64": {} } } \ No newline at end of file diff --git a/app/SourceGeneratedMappings/MappingRegistryGenerator.cs b/app/SourceGeneratedMappings/MappingRegistryGenerator.cs new file mode 100644 index 000000000..668f0d72e --- /dev/null +++ b/app/SourceGeneratedMappings/MappingRegistryGenerator.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace SourceGeneratedMappings; + +[Generator] +public sealed class MappingRegistryGenerator : IIncrementalGenerator +{ + private const string GENERATED_NAMESPACE = "AIStudio.Tools.PluginSystem.Assistants.Icons"; + private const string ROOT_TYPE_NAME = "MudBlazor.Icons"; + private static readonly string[] ALLOWED_GROUP_PATHS = ["Material.Filled", "Material.Outlined"]; + + private static readonly DiagnosticDescriptor ROOT_TYPE_MISSING = new( + id: "MBI001", + title: "MudBlazor icon root type was not found", + messageFormat: "The generator could not find '{0}' in the current compilation references. No icon registry was generated.", + category: "SourceGeneration", + DiagnosticSeverity.Info, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor NO_ICONS_FOUND = new( + id: "MBI002", + title: "No MudBlazor icons were discovered", + messageFormat: "The generator found '{0}', but no nested icon constants were discovered below it.", + category: "SourceGeneration", + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterSourceOutput(context.CompilationProvider, static (spc, compilation) => + { + Generate(spc, compilation); + }); + } + + private static void Generate(SourceProductionContext context, Compilation compilation) + { + var rootType = compilation.GetTypeByMetadataName(ROOT_TYPE_NAME); + if (rootType is null) + { + context.ReportDiagnostic(Diagnostic.Create(ROOT_TYPE_MISSING, Location.None, ROOT_TYPE_NAME)); + return; + } + + var icons = new List<IconDefinition>(); + CollectIcons(rootType, new List<string>(), icons); + + if (icons.Count == 0) + { + context.ReportDiagnostic(Diagnostic.Create(NO_ICONS_FOUND, Location.None, ROOT_TYPE_NAME)); + return; + } + + var source = RenderSource(icons); + context.AddSource("MudBlazorIconRegistry.g.cs", SourceText.From(source, Encoding.UTF8)); + } + + private static void CollectIcons(INamedTypeSymbol currentType, List<string> path, List<IconDefinition> icons) + { + foreach (var nestedType in currentType.GetTypeMembers().OrderBy(static t => t.Name, StringComparer.Ordinal)) + { + path.Add(nestedType.Name); + CollectIcons(nestedType, path, icons); + path.RemoveAt(path.Count - 1); + } + + foreach (var field in currentType.GetMembers().OfType<IFieldSymbol>().OrderBy(static f => f.Name, StringComparer.Ordinal)) + { + if (!field.IsConst || field.Type.SpecialType != SpecialType.System_String || field.ConstantValue is not string svg) + continue; + + if (path.Count == 0) + continue; + + var groupPath = string.Join(".", path); + if (!ALLOWED_GROUP_PATHS.Contains(groupPath, StringComparer.Ordinal)) + continue; + + icons.Add(new IconDefinition( + QualifiedName: $"Icons.{groupPath}.{field.Name}", + Svg: svg)); + } + } + + private static string RenderSource(IReadOnlyList<IconDefinition> icons) + { + var builder = new StringBuilder(); + + builder.AppendLine("// <auto-generated />"); + builder.AppendLine("#nullable enable"); + builder.AppendLine("using System;"); + builder.AppendLine("using System.Collections.Generic;"); + builder.AppendLine(); + builder.Append("namespace ").Append(GENERATED_NAMESPACE).AppendLine(";"); + builder.AppendLine(); + builder.AppendLine("public static class MudBlazorIconRegistry"); + builder.AppendLine("{"); + builder.AppendLine(" public static readonly IReadOnlyDictionary<string, string> SvgByIdentifier = new Dictionary<string, string>(StringComparer.Ordinal)"); + builder.AppendLine(" {"); + + foreach (var icon in icons) + { + builder.Append(" [") + .Append(ToLiteral(icon.QualifiedName)) + .Append("] = ") + .Append(ToLiteral(icon.Svg)) + .AppendLine(","); + } + + builder.AppendLine(" };"); + builder.AppendLine(); + builder.AppendLine(" public static bool TryGetSvg(string identifier, out string svg)"); + builder.AppendLine(" {"); + builder.AppendLine(" return SvgByIdentifier.TryGetValue(identifier, out svg!);"); + builder.AppendLine(" }"); + builder.AppendLine("}"); + + return builder.ToString(); + } + + private static string ToLiteral(string value) + { + return Microsoft.CodeAnalysis.CSharp.SymbolDisplay.FormatLiteral(value, quote: true); + } + + private sealed record IconDefinition(string QualifiedName, string Svg); +} diff --git a/app/SourceGeneratedMappings/SourceGeneratedMappings.csproj b/app/SourceGeneratedMappings/SourceGeneratedMappings.csproj new file mode 100644 index 000000000..aa671143b --- /dev/null +++ b/app/SourceGeneratedMappings/SourceGeneratedMappings.csproj @@ -0,0 +1,29 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net9.0</TargetFramework> + <IsPackable>false</IsPackable> + <Nullable>enable</Nullable> + <LangVersion>latest</LangVersion> + + <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules> + <IsRoslynComponent>true</IsRoslynComponent> + + <RootNamespace>SourceGeneratedMappings</RootNamespace> + <AssemblyName>SourceGeneratedMappings</AssemblyName> + <Version>1.0.0</Version> + <PackageId>SourceGeneratedMappings</PackageId> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.CodeAnalysis"> + <HintPath>$(MSBuildSDKsPath)\..\Roslyn\bincore\Microsoft.CodeAnalysis.dll</HintPath> + <Private>false</Private> + </Reference> + <Reference Include="Microsoft.CodeAnalysis.CSharp"> + <HintPath>$(MSBuildSDKsPath)\..\Roslyn\bincore\Microsoft.CodeAnalysis.CSharp.dll</HintPath> + <Private>false</Private> + </Reference> + </ItemGroup> + +</Project> diff --git a/metadata.txt b/metadata.txt index 77e953d56..db22a53e2 100644 --- a/metadata.txt +++ b/metadata.txt @@ -7,6 +7,6 @@ 8.15.0 1.8.1 3eb367d4c9e, release -osx-arm64 +win-x64 144.0.7543.0 1.17.0 \ No newline at end of file