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..7bf99ef9b --- /dev/null +++ b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor @@ -0,0 +1,545 @@ +@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 + +@if (this.RootComponent is null) +{ + + @this.T("No assistant plugin are currently installed.") + +} +else +{ + @foreach (var component in this.RootComponent.Children) + { + @this.RenderComponent(component) + } +} + +@code { + private RenderFragment RenderSwitch(AssistantSwitch assistantSwitch) => @ + @(switchFields[assistantSwitch.Name] ? assistantSwitch.LabelOn : assistantSwitch.LabelOff) + ; +} + +@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) + { + if (assistantDropdown.IsMultiselect) + { + + } + else + { + + } + } + break; + case AssistantComponentType.BUTTON: + if (component is AssistantButton assistantButton) + { + var button = assistantButton; + var icon = AssistantComponentPropHelper.GetIconSvg(button.StartIcon); + var iconColor = AssistantComponentPropHelper.GetColor(button.IconColor, Color.Inherit); + var color = AssistantComponentPropHelper.GetColor(button.Color, Color.Default); + var size = AssistantComponentPropHelper.GetComponentSize(button.Size, Size.Medium); + var iconSize = AssistantComponentPropHelper.GetComponentSize(button.IconSize, Size.Medium); + var variant = button.GetButtonVariant(); + var disabled = this.IsButtonActionRunning(button.Name); + var buttonClass = MergeClass(button.Class, ""); + var style = this.GetOptionalStyle(button.Style); + + if (!button.IsIconButton) + { + + @button.Text + + } + else + { + + } + + } + 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.LAYOUT_ACCORDION: + if (component is AssistantAccordion assistantAccordion) + { + var accordion = assistantAccordion; + + @this.RenderChildren(accordion.Children) + + } + break; + case AssistantComponentType.LAYOUT_ACCORDION_SECTION: + if (component is AssistantAccordionSection assistantAccordionSection) + { + var accordionSection = assistantAccordionSection; + var textColor = accordionSection.IsDisabled ? Color.Info : AssistantComponentPropHelper.GetColor(accordionSection.HeaderColor, Color.Inherit); + + +
+ + + @accordionSection.HeaderText + +
+
+ + @this.RenderChildren(accordionSection.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; + + if (string.IsNullOrEmpty(assistantSwitch.Label)) + { + @this.RenderSwitch(assistantSwitch) + } + else + { + + @this.RenderSwitch(assistantSwitch) + + } + } + 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) + { + var iconColor = AssistantComponentPropHelper.GetColor(item.IconColor, Color.Default); + + @if (item.Type == "LINK") + { + @item.Text + } + else + { + var icon = !string.IsNullOrEmpty(item.Icon) ? AssistantComponentPropHelper.GetIconSvg(item.Icon) : string.Empty; + @item.Text + } + } + + } + break; + case AssistantComponentType.COLOR_PICKER: + if (component is AssistantColorPicker assistantColorPicker) + { + var colorPicker = assistantColorPicker; + var variant = colorPicker.GetPickerVariant(); + var rounded = variant == PickerVariant.Static; + + + + + } + break; + case AssistantComponentType.DATE_PICKER: + if (component is AssistantDatePicker assistantDatePicker) + { + var datePicker = assistantDatePicker; + var format = datePicker.GetDateFormat(); + + + + + } + break; + case AssistantComponentType.DATE_RANGE_PICKER: + if (component is AssistantDateRangePicker assistantDateRangePicker) + { + var dateRangePicker = assistantDateRangePicker; + var format = dateRangePicker.GetDateFormat(); + + + + + } + break; + case AssistantComponentType.TIME_PICKER: + if (component is AssistantTimePicker assistantTimePicker) + { + var timePicker = assistantTimePicker; + var format = timePicker.GetTimeFormat(); + + + + + } + break; + } +
; + + 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 = paper.Style; + 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..d576df94b --- /dev/null +++ b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs @@ -0,0 +1,883 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +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 title = string.Empty; + private string description = string.Empty; + private string systemPrompt = string.Empty; + private bool allowProfiles = true; + private string submitText = string.Empty; + private bool showFooterProfileSelection = true; + private PluginAssistants? assistantPlugin; + + private readonly Dictionary inputFields = new(); + private readonly Dictionary dropdownFields = new(); + private readonly Dictionary> multiselectDropdownFields = new(); + private readonly Dictionary switchFields = new(); + private readonly Dictionary webContentFields = new(); + private readonly Dictionary fileContentFields = new(); + private readonly Dictionary colorPickerFields = new(); + private readonly Dictionary datePickerFields = new(); + private readonly Dictionary dateRangePickerFields = new(); + private readonly Dictionary timePickerFields = new(); + private readonly Dictionary imageCache = new(); + private readonly HashSet executingButtonActions = []; + private readonly HashSet executingSwitchActions = []; + private string pluginPath = string.Empty; + private const string PLUGIN_SCHEME = "plugin://"; + private const string ASSISTANT_QUERY_KEY = "assistantId"; + private static readonly CultureInfo INVARIANT_CULTURE = CultureInfo.InvariantCulture; + private static readonly string[] FALLBACK_DATE_FORMATS = ["yyyy-MM-dd", "dd.MM.yyyy", "MM/dd/yyyy"]; + private static readonly string[] FALLBACK_TIME_FORMATS = ["HH:mm", "HH:mm:ss", "hh:mm tt", "h:mm tt"]; + + 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() + .Where(plugin => this.SettingsManager.IsPluginEnabled(plugin)) + .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; + } + foreach (var entry in this.datePickerFields) + { + this.datePickerFields[entry.Key] = string.Empty; + } + foreach (var entry in this.dateRangePickerFields) + { + this.dateRangePickerFields[entry.Key] = string.Empty; + } + foreach (var entry in this.timePickerFields) + { + this.timePickerFields[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.multiselectDropdownFields) + fields[entry.Key] = CreateLuaArray(entry.Value); + 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; + foreach (var entry in this.datePickerFields) + fields[entry.Key] = entry.Value ?? string.Empty; + foreach (var entry in this.dateRangePickerFields) + fields[entry.Key] = entry.Value ?? string.Empty; + foreach (var entry in this.timePickerFields) + 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) + { + if (dropdown.IsMultiselect) + { + if (!this.multiselectDropdownFields.ContainsKey(dropdown.Name)) + this.multiselectDropdownFields.Add(dropdown.Name, CreateInitialMultiselectValues(dropdown)); + } + else if (!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; + case AssistantComponentType.DATE_PICKER: + if (component is AssistantDatePicker datePicker && !this.datePickerFields.ContainsKey(datePicker.Name)) + this.datePickerFields.Add(datePicker.Name, datePicker.Value); + break; + case AssistantComponentType.DATE_RANGE_PICKER: + if (component is AssistantDateRangePicker dateRangePicker && !this.dateRangePickerFields.ContainsKey(dateRangePicker.Name)) + this.dateRangePickerFields.Add(dateRangePicker.Name, dateRangePicker.Value); + break; + case AssistantComponentType.TIME_PICKER: + if (component is AssistantTimePicker timePicker && !this.timePickerFields.ContainsKey(timePicker.Name)) + this.timePickerFields.Add(timePicker.Name, timePicker.Value); + 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 bool IsSwitchActionRunning(string switchName) => this.executingSwitchActions.Contains(switchName); + + 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.ApplyActionResult(result, AssistantComponentType.BUTTON); + } + finally + { + this.executingButtonActions.Remove(button.Name); + await this.InvokeAsync(this.StateHasChanged); + } + } + + private async Task ExecuteSwitchChangedAsync(AssistantSwitch switchComponent, bool value) + { + if (string.IsNullOrWhiteSpace(switchComponent.Name)) + return; + + this.switchFields[switchComponent.Name] = value; + + if (this.assistantPlugin is null || switchComponent.OnChanged is null) + { + await this.InvokeAsync(this.StateHasChanged); + return; + } + + if (!this.executingSwitchActions.Add(switchComponent.Name)) + return; + + try + { + var input = this.BuildPromptInput(); + var cancellationToken = this.cancellationTokenSource?.Token ?? CancellationToken.None; + var result = await this.assistantPlugin.TryInvokeSwitchChangedAsync(switchComponent, input, cancellationToken); + if (result is not null) + this.ApplyActionResult(result, AssistantComponentType.SWITCH); + } + finally + { + this.executingSwitchActions.Remove(switchComponent.Name); + await this.InvokeAsync(this.StateHasChanged); + } + } + + private void ApplyActionResult(LuaTable result, AssistantComponentType sourceType) + { + if (!result.TryGetValue("fields", out var fieldsValue)) + return; + + if (!fieldsValue.TryRead(out var fieldsTable)) + { + this.Logger.LogWarning("Assistant {ComponentType} callback returned a non-table 'fields' value. The result is ignored.", sourceType); + return; + } + + foreach (var pair in fieldsTable) + { + if (!pair.Key.TryRead(out var fieldName) || string.IsNullOrWhiteSpace(fieldName)) + continue; + + this.TryApplyFieldUpdate(fieldName, pair.Value, sourceType); + } + } + + private void TryApplyFieldUpdate(string fieldName, LuaValue value, AssistantComponentType sourceType) + { + if (this.inputFields.ContainsKey(fieldName)) + { + if (value.TryRead(out var textValue)) + this.inputFields[fieldName] = textValue ?? string.Empty; + else + this.LogFieldUpdateTypeMismatch(fieldName, "string", sourceType); + return; + } + + if (this.dropdownFields.ContainsKey(fieldName)) + { + if (value.TryRead(out var dropdownValue)) + this.dropdownFields[fieldName] = dropdownValue ?? string.Empty; + else + this.LogFieldUpdateTypeMismatch(fieldName, "string", sourceType); + return; + } + + if (this.multiselectDropdownFields.ContainsKey(fieldName)) + { + if (value.TryRead(out var multiselectDropdownValue)) + this.multiselectDropdownFields[fieldName] = ReadStringValues(multiselectDropdownValue); + else if (value.TryRead(out var singleDropdownValue)) + this.multiselectDropdownFields[fieldName] = string.IsNullOrWhiteSpace(singleDropdownValue) ? [] : [singleDropdownValue]; + else + this.LogFieldUpdateTypeMismatch(fieldName, "string[]", sourceType); + return; + } + + if (this.switchFields.ContainsKey(fieldName)) + { + if (value.TryRead(out var boolValue)) + this.switchFields[fieldName] = boolValue; + else + this.LogFieldUpdateTypeMismatch(fieldName, "boolean", sourceType); + return; + } + + if (this.colorPickerFields.ContainsKey(fieldName)) + { + if (value.TryRead(out var colorValue)) + this.colorPickerFields[fieldName] = colorValue ?? string.Empty; + else + this.LogFieldUpdateTypeMismatch(fieldName, "string", sourceType); + return; + } + + if (this.datePickerFields.ContainsKey(fieldName)) + { + if (value.TryRead(out var dateValue)) + this.datePickerFields[fieldName] = dateValue ?? string.Empty; + else + this.LogFieldUpdateTypeMismatch(fieldName, "string", sourceType); + return; + } + + if (this.dateRangePickerFields.ContainsKey(fieldName)) + { + if (value.TryRead(out var dateRangeValue)) + this.dateRangePickerFields[fieldName] = dateRangeValue ?? string.Empty; + else + this.LogFieldUpdateTypeMismatch(fieldName, "string", sourceType); + return; + } + + if (this.timePickerFields.ContainsKey(fieldName)) + { + if (value.TryRead(out var timeValue)) + this.timePickerFields[fieldName] = timeValue ?? string.Empty; + else + this.LogFieldUpdateTypeMismatch(fieldName, "string", sourceType); + 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", sourceType); + 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", sourceType); + return; + } + + this.Logger.LogWarning("Assistant {ComponentType} callback tried to update unknown field '{FieldName}'. The value is ignored.", sourceType, fieldName); + } + + private void LogFieldUpdateTypeMismatch(string fieldName, string expectedType, AssistantComponentType sourceType) + { + this.Logger.LogWarning("Assistant {ComponentType} callback tried to write an invalid value to '{FieldName}'. Expected {ExpectedType}.", sourceType, fieldName, expectedType); + } + + private EventCallback> CreateMultiselectDropdownChangedCallback(string fieldName) => + EventCallback.Factory.Create>(this, values => + { + this.multiselectDropdownFields[fieldName] = values; + }); + + 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; + case AssistantDatePicker datePicker: + this.AddMetaEntry(meta, datePicker.Name, component.Type, datePicker.Label, datePicker.UserPrompt); + break; + case AssistantDateRangePicker dateRangePicker: + this.AddMetaEntry(meta, dateRangePicker.Name, component.Type, dateRangePicker.Label, dateRangePicker.UserPrompt); + break; + case AssistantTimePicker timePicker: + this.AddMetaEntry(meta, timePicker.Name, component.Type, timePicker.Label, timePicker.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 (dropdown.IsMultiselect && this.multiselectDropdownFields.TryGetValue(dropdown.Name, out var selections)) + prompt += $"user prompt:{Environment.NewLine}{string.Join(Environment.NewLine, selections.OrderBy(static value => value, StringComparer.Ordinal))}"; + else 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; + case AssistantComponentType.DATE_PICKER: + if (component is AssistantDatePicker datePicker) + { + prompt += $"context:{Environment.NewLine}{datePicker.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"; + if (this.datePickerFields.TryGetValue(datePicker.Name, out userInput)) + prompt += $"user prompt:{Environment.NewLine}{userInput}"; + } + break; + case AssistantComponentType.DATE_RANGE_PICKER: + if (component is AssistantDateRangePicker dateRangePicker) + { + prompt += $"context:{Environment.NewLine}{dateRangePicker.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"; + if (this.dateRangePickerFields.TryGetValue(dateRangePicker.Name, out userInput)) + prompt += $"user prompt:{Environment.NewLine}{userInput}"; + } + break; + case AssistantComponentType.TIME_PICKER: + if (component is AssistantTimePicker timePicker) + { + prompt += $"context:{Environment.NewLine}{timePicker.UserPrompt}{Environment.NewLine}---{Environment.NewLine}"; + if (this.timePickerFields.TryGetValue(timePicker.Name, out userInput)) + prompt += $"user prompt:{Environment.NewLine}{userInput}"; + } + break; + } + + if (component.Children.Count > 0) + prompt += this.CollectUserPromptFallback(component.Children); + } + + return prompt; + } + + private static HashSet CreateInitialMultiselectValues(AssistantDropdown dropdown) + { + if (string.IsNullOrWhiteSpace(dropdown.Default.Value)) + return []; + + return [dropdown.Default.Value]; + } + + private static LuaTable CreateLuaArray(IEnumerable values) + { + var luaArray = new LuaTable(); + var index = 1; + + foreach (var value in values.OrderBy(static value => value, StringComparer.Ordinal)) + luaArray[index++] = value; + + return luaArray; + } + + private static HashSet ReadStringValues(LuaTable values) + { + var parsedValues = new HashSet(StringComparer.Ordinal); + + foreach (var entry in values) + { + if (entry.Value.TryRead(out var value) && !string.IsNullOrWhiteSpace(value)) + parsedValues.Add(value); + } + + return parsedValues; + } + + private DateTime? ParseDatePickerValue(string? value, string? format) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + if (TryParseDate(value, format, out var parsedDate)) + return parsedDate; + + return null; + } + + private void SetDatePickerValue(string fieldName, DateTime? value, string? format) + { + this.datePickerFields[fieldName] = value.HasValue ? FormatDate(value.Value, format) : string.Empty; + } + + private DateRange? ParseDateRangePickerValue(string? value, string? format) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + var parts = value.Split(" - ", 2, StringSplitOptions.TrimEntries); + if (parts.Length != 2) + return null; + + if (!TryParseDate(parts[0], format, out var start) || !TryParseDate(parts[1], format, out var end)) + return null; + + return new DateRange(start, end); + } + + private void SetDateRangePickerValue(string fieldName, DateRange? value, string? format) + { + if (value?.Start is null || value.End is null) + { + this.dateRangePickerFields[fieldName] = string.Empty; + return; + } + + this.dateRangePickerFields[fieldName] = $"{FormatDate(value.Start.Value, format)} - {FormatDate(value.End.Value, format)}"; + } + + private TimeSpan? ParseTimePickerValue(string? value, string? format) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + if (TryParseTime(value, format, out var parsedTime)) + return parsedTime; + + return null; + } + + private void SetTimePickerValue(string fieldName, TimeSpan? value, string? format) + { + this.timePickerFields[fieldName] = value.HasValue ? FormatTime(value.Value, format) : string.Empty; + } + + private static bool TryParseDate(string value, string? format, out DateTime parsedDate) + { + if (!string.IsNullOrWhiteSpace(format) && + DateTime.TryParseExact(value, format, INVARIANT_CULTURE, DateTimeStyles.AllowWhiteSpaces, out parsedDate)) + { + return true; + } + + if (DateTime.TryParseExact(value, FALLBACK_DATE_FORMATS, INVARIANT_CULTURE, DateTimeStyles.AllowWhiteSpaces, out parsedDate)) + return true; + + return DateTime.TryParse(value, INVARIANT_CULTURE, DateTimeStyles.AllowWhiteSpaces, out parsedDate); + } + + private static bool TryParseTime(string value, string? format, out TimeSpan parsedTime) + { + if (!string.IsNullOrWhiteSpace(format) && + DateTime.TryParseExact(value, format, INVARIANT_CULTURE, DateTimeStyles.AllowWhiteSpaces, out var dateTime)) + { + parsedTime = dateTime.TimeOfDay; + return true; + } + + if (DateTime.TryParseExact(value, FALLBACK_TIME_FORMATS, INVARIANT_CULTURE, DateTimeStyles.AllowWhiteSpaces, out dateTime)) + { + parsedTime = dateTime.TimeOfDay; + return true; + } + + if (TimeSpan.TryParse(value, INVARIANT_CULTURE, out parsedTime)) + return true; + + parsedTime = default; + return false; + } + + private static string FormatDate(DateTime value, string? format) + { + try + { + return value.ToString(string.IsNullOrWhiteSpace(format) ? "yyyy-MM-dd" : format, INVARIANT_CULTURE); + } + catch (FormatException) + { + return value.ToString("yyyy-MM-dd", INVARIANT_CULTURE); + } + } + + private static string FormatTime(TimeSpan value, string? format) + { + var dateTime = DateTime.Today.Add(value); + + try + { + return dateTime.ToString(string.IsNullOrWhiteSpace(format) ? "HH:mm" : format, INVARIANT_CULTURE); + } + catch (FormatException) + { + return dateTime.ToString("HH:mm", INVARIANT_CULTURE); + } + } +} 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..eb53ac382 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -535,6 +535,12 @@ 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" +-- No assistant plugin are currently installed. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DYNAMIC::ASSISTANTDYNAMIC::T1913566603"] = "No assistant plugin are currently installed." + +-- 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 +4933,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 +6076,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..35ad27cf1 --- /dev/null +++ b/app/MindWork AI Studio/Components/DynamicAssistantDropdown.razor @@ -0,0 +1,53 @@ +@using AIStudio.Tools.PluginSystem.Assistants.DataModel + + @if (this.IsMultiselect) + { + + @foreach (var item in this.GetRenderedItems()) + { + + @item.Display + + } + + } + else + { + + @foreach (var item in this.GetRenderedItems()) + { + + @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..86c485ae4 --- /dev/null +++ b/app/MindWork AI Studio/Components/DynamicAssistantDropdown.razor.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +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 HashSet SelectedValues { get; set; } = []; + + [Parameter] public EventCallback> SelectedValuesChanged { get; set; } + + [Parameter] public string Label { get; set; } = string.Empty; + + [Parameter] public string HelperText { get; set; } = string.Empty; + + [Parameter] public Func ValidateSelection { get; set; } = _ => null; + + [Parameter] public string OpenIcon { get; set; } = Icons.Material.Filled.ArrowDropDown; + + [Parameter] public string CloseIcon { get; set; } = Icons.Material.Filled.ArrowDropUp; + + [Parameter] public Color IconColor { get; set; } = Color.Default; + + [Parameter] public Adornment IconPosition { get; set; } = Adornment.End; + + [Parameter] public Variant Variant { get; set; } = Variant.Outlined; + + [Parameter] public bool IsMultiselect { get; set; } + + [Parameter] public bool HasSelectAll { get; set; } + + [Parameter] public string SelectAllText { get; set; } = string.Empty; + + [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); + } + } + + private async Task OnSelectedValuesChanged(IEnumerable? newValues) + { + var updatedValues = newValues? + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Select(value => value!) + .ToHashSet(StringComparer.Ordinal) ?? []; + + if (this.SelectedValues.SetEquals(updatedValues)) + return; + + this.SelectedValues = updatedValues; + await this.SelectedValuesChanged.InvokeAsync(updatedValues); + } + + private List GetRenderedItems() + { + var items = this.Items ?? []; + if (string.IsNullOrWhiteSpace(this.Default.Value)) + return items; + + if (items.Any(item => string.Equals(item.Value, this.Default.Value, StringComparison.Ordinal))) + return items; + + return [this.Default, .. items]; + } + + private string GetMultiSelectionText(List? selectedValues) + { + if (selectedValues is null || selectedValues.Count == 0) + return this.Default.Display; + + var labels = selectedValues + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Select(value => this.ResolveDisplayText(value!)) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .ToList(); + + return labels.Count == 0 ? this.Default.Display : string.Join(", ", labels); + } + + private string ResolveDisplayText(string value) + { + var item = this.GetRenderedItems().FirstOrDefault(item => string.Equals(item.Value, value, StringComparison.Ordinal)); + return item?.Display ?? value; + } + + private string MergeClasses(string custom, string fallback) + { + var trimmedCustom = custom?.Trim() ?? string.Empty; + var trimmedFallback = fallback?.Trim() ?? string.Empty; + if (string.IsNullOrEmpty(trimmedCustom)) + return trimmedFallback; + + return string.IsNullOrEmpty(trimmedFallback) ? trimmedCustom : $"{trimmedCustom} {trimmedFallback}"; + } + } +} 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..6074030b1 --- /dev/null +++ b/app/MindWork AI Studio/Plugins/assistants/README.md @@ -0,0 +1,1056 @@ +# 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. + +## Table of Contents +- [Assistant Plugin Reference](#assistant-plugin-reference) + - [How to Use This Documentation](#how-to-use-this-documentation) + - [Directory Structure](#directory-structure) + - [Structure](#structure) + - [Minimal Requirements Assistant Table](#example-minimal-requirements-assistant-table) + - [Supported types (matching the Blazor UI components):](#supported-types-matching-the-blazor-ui-components) + - [Component References](#component-references) + - [`TEXT_AREA` reference](#text_area-reference) + - [`DROPDOWN` reference](#dropdown-reference) + - [`BUTTON` reference](#button-reference) + - [`Action(input)` interface](#actioninput-interface) + - [`BUTTON_GROUP` reference](#button_group-reference) + - [`SWITCH` reference](#switch-reference) + - [`COLOR_PICKER` reference](#color_picker-reference) + - [`DATE_PICKER` reference](#date_picker-reference) + - [`DATE_RANGE_PICKER` reference](#date_range_picker-reference) + - [`TIME_PICKER` reference](#time_picker-reference) + - [Prompt Assembly - UserPrompt Property](#prompt-assembly---userprompt-property) + - [Advanced Prompt Assembly - BuildPrompt()](#advanced-prompt-assembly---buildprompt) + - [Interface](#interface) + - [`input` table shape](#input-table-shape) + - [Using `meta` inside BuildPrompt](#using-meta-inside-buildprompt) + - [Example: iterate all fields with labels and include their values](#example-iterate-all-fields-with-labels-and-include-their-values) + - [Example: handle types differently](#example-handle-types-differently) + - [Using `profile` inside BuildPrompt](#using-profile-inside-buildprompt) + - [Example: Add user profile context to the prompt](#example-add-user-profile-context-to-the-prompt) + - [Advanced Layout Options](#advanced-layout-options) + - [`LAYOUT_GRID` reference](#layout_grid-reference) + - [`LAYOUT_ITEM` reference](#layout_item-reference) + - [`LAYOUT_PAPER` reference](#layout_paper-reference) + - [`LAYOUT_STACK` reference](#layout_stack-reference) + - [`LAYOUT_ACCORDION` reference](#layout_accordion-reference) + - [`LAYOUT_ACCORDION_SECTION` reference](#layout_accordion_section-reference) + - [Useful Lua Functions](#useful-lua-functions) + - [Included lua libraries](#included-lua-libraries) + - [Logging helpers](#logging-helpers) + - [Example: Use Logging in lua functions](#example-use-logging-in-lua-functions) + - [Date/time helpers (assistant plugins only)](#datetime-helpers-assistant-plugins-only) + - [Example: Use Logging in lua functions](#example-use-logging-in-lua-functions) + - [General Tips](#general-tips) + - [Useful Resources](#useful-resources) + +## How to Use This Documentation +Use this README in layers. The early sections are a quick reference for the overall assistant manifest shape and the available component types, while the later `... reference` sections are the full detail for each component and advanced behavior. + +When you build a plugin, start with the directory layout and the `Structure` section, then jump to the component references you actually use. The resource links at the end are the primary sources for Lua and MudBlazor behavior, and the `General Tips` section collects the practical rules and gotchas that matter most while authoring `plugin.lua`. + +## Directory Structure +Each assistant plugin lives in its own directory under the assistants plugin root. In practice, you usually keep the manifest in `plugin.lua`, optional icon rendering in `icon.lua`, and any bundled media in `assets/`. + +``` +. +└── com.github.mindwork-ai.ai-studio/ + └── data/ + └── plugins/ + └── assistants/ + └── your-assistant-directory/ + ├── assets/ + │ └── your-media-files.jpg + ├── icon.lua + └── plugin.lua +``` + +## 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. + +### Example: Minimal Requirements Assistant Table +```lua +ASSISTANT = { + ["Title"] = "", + ["Description"] = "", + ["SystemPrompt"] = "", + ["SubmitText"] = "", + ["AllowProfiles"] = true, + ["UI"] = { + ["Type"] = "FORM", + ["Children"] = { + -- Components + } + }, +} +``` + + +#### 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 `IsIconButton`, `Variant`, `Color`, `IsFullWidth`, `Size`, `StartIcon`, `EndIcon`, `IconColor`, `IconSize`, `Class`, `Style`. Use this for stateless actions, including icon-only action buttons. +- `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`. +- `LAYOUT_ACCORDION`: renders a `MudExpansionPanels`; may include `AllowMultiSelection`, `IsDense`, `HasOutline`, `IsSquare`, `Elevation`, `HasSectionPaddings`, `Class`, `Style`. +- `LAYOUT_ACCORDION_SECTION`: renders a `MudExpansionPanel`; requires `Name`, `HeaderText`, and may include `IsDisabled`, `IsExpanded`, `IsDense`, `HasInnerPadding`, `HideIcon`, `HeaderIcon`, `HeaderColor`, `HeaderTypo`, `HeaderAlign`, `MaxHeight`, `ExpandIcon`, `Class`, `Style`. +- `SWITCH`: boolean option; requires `Name`, `Label`, `Value`, and may include `OnChanged`, `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`. +- `DATE_PICKER`: date input based on `MudDatePicker`; requires `Name`, `Label`, and may include `Value`, `Color`, `Placeholder`, `HelperText`, `DateFormat`, `PickerVariant`, `UserPrompt`, `Class`, `Style`. +- `DATE_RANGE_PICKER`: date range input based on `MudDateRangePicker`; requires `Name`, `Label`, and may include `Value`, `Color`, `PlaceholderStart`, `PlaceholderEnd`, `HelperText`, `DateFormat`, `PickerVariant`, `UserPrompt`, `Class`, `Style`. +- `TIME_PICKER`: time input based on `MudTimePicker`; requires `Name`, `Label`, and may include `Value`, `Color`, `Placeholder`, `HelperText`, `TimeFormat`, `AmPm`, `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. + +| Component | Required Props | Optional Props | Renders | +|----------------------------|-------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------| +| `TEXT_AREA` | `Name`, `Label` | `HelperText`, `HelperTextOnFocus`, `Adornment`, `AdornmentIcon`, `AdornmentText`, `AdornmentColor`, `Counter`, `MaxLength`, `IsImmediate`, `UserPrompt`, `PrefillText`, `IsSingleLine`, `ReadOnly`, `Class`, `Style` | [MudTextField](https://www.mudblazor.com/components/textfield) | +| `DROPDOWN` | `Name`, `Label`, `Default`, `Items` | `IsMultiselect`, `HasSelectAll`, `SelectAllText`, `HelperText`, `OpenIcon`, `CloseIcon`, `IconColor`, `IconPositon`, `Variant`, `ValueType`, `UserPrompt` | [MudSelect](https://www.mudblazor.com/components/select) | +| `BUTTON` | `Name`, `Text`, `Action` | `IsIconButton`, `Variant`, `Color`, `IsFullWidth`, `Size`, `StartIcon`, `EndIcon`, `IconColor`, `IconSize`, `Class`, `Style` | [MudButton](https://www.mudblazor.com/components/button) / [MudIconButton](https://www.mudblazor.com/components/button#icon-button) | +| `SWITCH` | `Name`, `Label`, `Value` | `OnChanged`, `Disabled`, `UserPrompt`, `LabelOn`, `LabelOff`, `LabelPlacement`, `Icon`, `IconColor`, `CheckedColor`, `UncheckedColor`, `Class`, `Style` | [MudSwitch](https://www.mudblazor.com/components/switch) | +| `PROVIDER_SELECTION` | `None` | `None` | [`internal`](https://github.com/MindWorkAI/AI-Studio/blob/main/app/MindWork%20AI%20Studio/Components/ProviderSelection.razor) | +| `PROFILE_SELECTION` | `None` | `None` | [`internal`](https://github.com/MindWorkAI/AI-Studio/blob/main/app/MindWork%20AI%20Studio/Components/ProfileSelection.razor) | +| `FILE_CONTENT_READER` | `Name` | `UserPrompt` | [`internal`](https://github.com/MindWorkAI/AI-Studio/blob/main/app/MindWork%20AI%20Studio/Components/ReadFileContent.razor) | +| `WEB_CONTENT_READER` | `Name` | `UserPrompt` | [`internal`](https://github.com/MindWorkAI/AI-Studio/blob/main/app/MindWork%20AI%20Studio/Components/ReadWebContent.razor) | +| `COLOR_PICKER` | `Name`, `Label` | `Placeholder`, `Color`, `ShowAlpha`, `ShowToolbar`, `ShowModeSwitch`, `PickerVariant`, `UserPrompt`, `Class`, `Style` | [MudColorPicker](https://www.mudblazor.com/components/colorpicker) | +| `DATE_PICKER` | `Name`, `Label` | `Value`, `Color`, `Placeholder`, `HelperText`, `DateFormat`, `PickerVariant`, `UserPrompt`, `Class`, `Style` | [MudDatePicker](https://www.mudblazor.com/components/datepicker) | +| `DATE_RANGE_PICKER` | `Name`, `Label` | `Value`, `Color`, `PlaceholderStart`, `PlaceholderEnd`, `HelperText`, `DateFormat`, `PickerVariant`, `UserPrompt`, `Class`, `Style` | [MudDateRangePicker](https://www.mudblazor.com/components/daterangepicker) | +| `TIME_PICKER` | `Name`, `Label` | `Value`, `Placeholder`, `HelperText`, `TimeFormat`, `AmPm`, `PickerVariant`, `UserPrompt`, `Class`, `Style` | [MudTimePicker](https://www.mudblazor.com/components/timepicker) | +| `HEADING` | `Text` | `Level` | [MudText Typo="Typo."](https://www.mudblazor.com/components/typography) | +| `TEXT` | `Content` | `None` | [MudText Typo="Typo.body1"](https://www.mudblazor.com/components/typography) | +| `LIST` | `None` | `Items (LIST_ITEM)`, `Class`, `Style` | [MudList](https://www.mudblazor.com/componentss/list) | +| `LIST_ITEM` | `Type`, `Text` | `Href`, `Icon`, `IconColor` | [MudList](https://www.mudblazor.com/componentss/list) | +| `IMAGE` | `Src` | `Alt`, `Caption`,`Src` | [MudImage](https://www.mudblazor.com/components/image) | +| `BUTTON_GROUP` | `None` | `Variant`, `Color`, `Size`, `OverrideStyles`, `Vertical`, `DropShadow`, `Class`, `Style` | [MudButtonGroup](https://www.mudblazor.com/components/buttongroup) | +| `LAYOUT_PAPER` | `None` | `Elevation`, `Height`, `MaxHeight`, `MinHeight`, `Width`, `MaxWidth`, `MinWidth`, `IsOutlined`, `IsSquare`, `Class`, `Style` | [MudPaper](https://www.mudblazor.com/components/paper) | +| `LAYOUT_ITEM` | `None` | `Xs`, `Sm`, `Md`, `Lg`, `Xl`, `Xxl`, `Class`, `Style` | [MudItem](https://www.mudblazor.com/api/MudItem) | +| `LAYOUT_STACK` | `None` | `IsRow`, `IsReverse`, `Breakpoint`, `Align`, `Justify`, `Stretch`, `Wrap`, `Spacing`, `Class`, `Style` | [MudStack](https://www.mudblazor.com/components/stack) | +| `LAYOUT_GRID` | `None` | `Justify`, `Spacing`, `Class`, `Style` | [MudGrid](https://www.mudblazor.com/components/grid) | +| `LAYOUT_ACCORDION` | `None` | `AllowMultiSelection`, `IsDense`, `HasOutline`, `IsSquare`, `Elevation`, `HasSectionPaddings`, `Class`, `Style` | [MudExpansionPanels](https://www.mudblazor.com/components/expansionpanels) | +| `LAYOUT_ACCORDION_SECTION` | `Name`, `HeaderText` | `IsDisabled`, `IsExpanded`, `IsDense`, `HasInnerPadding`, `HideIcon`, `HeaderIcon`, `HeaderColor`, `HeaderTypo`, `HeaderAlign`, `MaxHeight`, `ExpandIcon`, `Class`, `Style` | [MudExpansionPanel](https://www.mudblazor.com/components/expansionpanels) | +More information on rendered components can be found [here](https://www.mudblazor.com/docs/overview). + +## Component References + +### `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 Textarea component +```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 + } +} +``` +--- + +### `DROPDOWN` reference +- Use `Type = "DROPDOWN"` to render a MudBlazor select field. +- Required props: + - `Name`: unique state key used in prompt assembly, button actions, and `BuildPrompt(input.fields)`. + - `Label`: visible field label. + - `Default`: dropdown item table with the shape `{ ["Value"] = "", ["Display"] = "" }`. + - `Items`: array of dropdown item tables with the same shape as `Default`. +- Optional props: + - `UserPrompt`: prompt context text for this field. + - `ValueType`: one of `string`, `int`, `double`, `bool`; currently the dropdown values exposed to prompt building and button actions are handled as the configured item `Value`s, with typical usage being `string`. + - `IsMultiselect`: defaults to `false`; when `true`, the component allows selecting multiple items. + - `HasSelectAll`: defaults to `false`; enables MudBlazor's select-all behavior for multiselect dropdowns. + - `SelectAllText`: custom label for the select-all action in multiselect mode. + - `HelperText`: helper text rendered below the dropdown. + - `OpenIcon`: MudBlazor icon identifier used while the dropdown is closed. + - `CloseIcon`: MudBlazor icon identifier used while the dropdown is open. + - `IconColor`: one of the MudBlazor `Color` enum names such as `Primary`, `Secondary`, `Warning`; invalid or omitted values fall back to `Default`. + - `IconPositon`: one of `Start` or `End`; controls where the icon adornment is rendered. + - `Variant`: one of the MudBlazor `Variant` enum names such as `Text`, `Filled`, `Outlined`; invalid or omitted values fall back to `Outlined`. + - `Class`, `Style`: forwarded to the rendered component for layout/styling. +- Dropdown item shape: + - `Value`: the internal raw value stored in component state and passed to prompt building. + - `Display`: the visible label shown to the user in the menu and selection text. +- Behavior notes: + - For single-select dropdowns, `input.fields.` is a single raw value such as `germany`. + - For multiselect dropdowns, `input.fields.` is an array-like Lua table of raw values. + - The UI shows the `Display` text, while prompt assembly and `BuildPrompt(input)` receive the raw `Value`. + - `Default` should usually also exist in `Items`. If it is missing there, the runtime currently still renders it as an available option. + +#### Example Dropdown component +```lua +{ + ["Type"] = "DROPDOWN", + ["Props"] = { + ["Name"] = "targetCountries", + ["Label"] = "Target countries", + ["UserPrompt"] = "Use the selected countries in your answer.", + ["ValueType"] = "string", + ["IsMultiselect"] = true, + ["HasSelectAll"] = true, + ["SelectAllText"] = "Select all countries", + ["HelperText"] = "Pick one or more countries.", + ["OpenIcon"] = "Icons.Material.Filled.ArrowDropDown", + ["CloseIcon"] = "Icons.Material.Filled.ArrowDropUp", + ["IconColor"] = "Secondary", + ["IconPositon"] = "End", + ["Variant"] = "Filled", + ["Default"] = { ["Value"] = "germany", ["Display"] = "Germany" }, + ["Items"] = { + { ["Value"] = "germany", ["Display"] = "Germany" }, + { ["Value"] = "austria", ["Display"] = "Austria" }, + { ["Value"] = "france", ["Display"] = "France" } + }, + ["Class"] = "mb-3", + ["Style"] = "min-width: 16rem;" + } +} +``` +--- + +### `BUTTON` reference +- Use `Type = "BUTTON"` to render a clickable action button. +- `BUTTON` is the only action-button component in the assistant plugin API. Keep plugin authoring simple by treating it as one concept with two visual modes: + - default button mode: text button, optionally with start/end icons + - icon-button mode: set `IsIconButton = true` to render the action as an icon-only button +- Do not model persistent on/off state with `BUTTON`. For boolean toggles, use `SWITCH`. The plugin API intentionally does not expose a separate `TOGGLE_BUTTON` component. +- Required props: + - `Name`: unique identifier used to track execution state and logging. + - `Text`: button label used for standard buttons. Keep providing it for icon buttons too so the manifest stays self-describing. + - `Action`: Lua function called on button click. +- Optional props: + - `IsIconButton`: defaults to `false`; when `true`, renders the action as a `MudIconButton` using `StartIcon` as the icon glyph. + - `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, or used as the icon itself when `IsIconButton = true`. + - `EndIcon`: MudBlazor icon identifier string rendered after the button text. + - `IconColor`: one of the MudBlazor `Color` enum names for text-button icons; 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)` interface +- 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`, single-select `DROPDOWN`, `WEB_CONTENT_READER`, `FILE_CONTENT_READER`, `COLOR_PICKER`, `DATE_PICKER`, `DATE_RANGE_PICKER`, `TIME_PICKER`: string values + - multiselect `DROPDOWN`: array-like Lua table of strings + - `SWITCH`: boolean values +- Unknown field names and wrong value types are ignored and logged. + +#### Example Button component +```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 = { + outputTextField = output + } + } + end, + ["Class"] = "mb-3", + ["Style"] = "min-width: 12rem;" + } +} +``` + +#### Example Icon-Button action +```lua +{ + ["Type"] = "BUTTON", + ["Props"] = { + ["Name"] = "refreshPreview", + ["Text"] = "Refresh preview", + ["IsIconButton"] = true, + ["Variant"] = "Outlined", + ["Color"] = "Primary", + ["Size"] = "Medium", + ["StartIcon"] = "Icons.Material.Filled.Refresh", + ["Action"] = function(input) + return { + fields = { + outputTextField = "Preview refreshed at " .. Timestamp() + } + } + end + } +} +``` +--- + +### `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)`. That includes `IsIconButton = true` when you want an icon-only action inside the group. + +#### Example Button-Group component +```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" + } + } + } +} +``` +--- + +### `SWITCH` reference +- Use `Type = "SWITCH"` to render a boolean toggle. +- Required props: + - `Name`: unique state key used in prompt assembly and `BuildPrompt(input.fields)`. + - `Value`: initial boolean state (`true` or `false`). +- Optional props: + - `Label`: If set, renders the switch inside an outlines Box, otherwise renders it raw. Visible label for the switch field. + - `OnChanged`: Lua callback invoked after the switch value changes. It receives the same `input` table as `BUTTON.Action(input)` and may return `{ fields = { ... } }` to update component state. The new switch value is already reflected in `input.fields[Name]`. + - `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 Switch component +```lua +{ + ["Type"] = "SWITCH", + ["Props"] = { + ["Name"] = "IncludeSummary", + ["Label"] = "Include summary", + ["Value"] = true, + ["OnChanged"] = function(input) + local includeSummary = input.fields.IncludeSummary or false + return { + fields = { + SummaryMode = includeSummary and "short-summary" or "no-summary" + } + } + end, + ["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 Colorpicker component +```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." + } +} +``` + +--- + +### `DATE_PICKER` reference +- Use `Type = "DATE_PICKER"` to render a MudBlazor date picker. +- Required props: + - `Name`: unique state key used in prompt assembly and `BuildPrompt(input.fields)`. + - `Label`: visible field label. +- Optional props: + - `Value`: initial date string. Use the same format as `DateFormat`; default recommendation is `yyyy-MM-dd`. + - `Placeholder`: hint text shown before a date is selected. + - `Color`: one of the MudBlazor `Color` enum names such as `Primary`, `Secondary`, `Warning`; omitted values default to `Primary`. + - `HelperText`: helper text rendered below the picker. + - `DateFormat`: output and parsing format; defaults to `yyyy-MM-dd`. + - `PickerVariant`: one of `Dialog`, `Inline`, `Static`; invalid or omitted values fall back to `Dialog`. + - `UserPrompt`: prompt context text for the selected date. + - `Class`, `Style`: forwarded to the rendered component for layout/styling. + +#### Example DatePicker component +```lua +{ + ["Type"] = "DATE_PICKER", + ["Props"] = { + ["Name"] = "deadline", + ["Label"] = "Deadline", + ["Value"] = "2026-03-31", + ["Placeholder"] = "YYYY-MM-DD", + ["Color"] = "Warning", + ["HelperText"] = "Pick the target completion date.", + ["DateFormat"] = "yyyy-MM-dd", + ["PickerVariant"] = "Dialog", + ["UserPrompt"] = "Use this as the relevant deadline." + } +} +``` + +--- + +### `DATE_RANGE_PICKER` reference +- Use `Type = "DATE_RANGE_PICKER"` to render a MudBlazor date range picker. +- Required props: + - `Name`: unique state key used in prompt assembly and `BuildPrompt(input.fields)`. + - `Label`: visible field label. +- Optional props: + - `Value`: initial range string using ` - `, for example `2026-03-01 - 2026-03-31`. + - `Color`: one of the MudBlazor `Color` enum names such as `Primary`, `Secondary`, `Warning`; omitted values default to `Primary`. + - `PlaceholderStart`: hint text for the start date input. + - `PlaceholderEnd`: hint text for the end date input. + - `HelperText`: helper text rendered below the picker. + - `DateFormat`: output and parsing format for both dates; defaults to `yyyy-MM-dd`. + - `PickerVariant`: one of `Dialog`, `Inline`, `Static`; invalid or omitted values fall back to `Dialog`. + - `UserPrompt`: prompt context text for the selected date range. + - `Class`, `Style`: forwarded to the rendered component for layout/styling. + +#### Example DateRangePicker component +```lua +{ + ["Type"] = "DATE_RANGE_PICKER", + ["Props"] = { + ["Name"] = "travelWindow", + ["Label"] = "Travel window", + ["Value"] = "2026-06-01 - 2026-06-07", + ["Color"] = "Secondary", + ["PlaceholderStart"] = "Start date", + ["PlaceholderEnd"] = "End date", + ["HelperText"] = "Select the full period.", + ["DateFormat"] = "yyyy-MM-dd", + ["PickerVariant"] = "Dialog", + ["UserPrompt"] = "Use this as the allowed date range." + } +} +``` + +--- + +### `TIME_PICKER` reference +- Use `Type = "TIME_PICKER"` to render a MudBlazor time picker. +- Required props: + - `Name`: unique state key used in prompt assembly and `BuildPrompt(input.fields)`. + - `Label`: visible field label. +- Optional props: + - `Value`: initial time string. Use the same format as `TimeFormat`; default recommendations are `HH:mm` or `hh:mm tt`. + - `Placeholder`: hint text shown before a time is selected. + - `Color`: one of the MudBlazor `Color` enum names such as `Primary`, `Secondary`, `Warning`; omitted values default to `Primary`. + - `HelperText`: helper text rendered below the picker. + - `TimeFormat`: output and parsing format; defaults to `HH:mm`, or `hh:mm tt` when `AmPm = true`. + - `AmPm`: defaults to `false`; toggles 12-hour mode. + - `PickerVariant`: one of `Dialog`, `Inline`, `Static`; invalid or omitted values fall back to `Dialog`. + - `UserPrompt`: prompt context text for the selected time. + - `Class`, `Style`: forwarded to the rendered component for layout/styling. + +#### Example TimePicker component +```lua +{ + ["Type"] = "TIME_PICKER", + ["Props"] = { + ["Name"] = "meetingTime", + ["Label"] = "Meeting time", + ["Value"] = "14:30", + ["Placeholder"] = "HH:mm", + ["Color"] = "Error", + ["HelperText"] = "Pick the preferred meeting time.", + ["TimeFormat"] = "HH:mm", + ["AmPm"] = false, + ["PickerVariant"] = "Dialog", + ["UserPrompt"] = "Use this as the preferred time." + } +} +``` + +## Prompt Assembly - UserPrompt Property +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); for date and time pickers it is the formatted date, date range, or time string. Always provide a meaningful `UserPrompt` so the final concatenated prompt remains coherent from the LLM’s perspective. + +## Advanced Prompt Assembly - BuildPrompt() +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. + +--- +### Interface +- `ASSISTANT.BuildPrompt(LuaTable input) => string` must return a **string**, the complete User Prompt. +- 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` Lua table with: +- `input.fields`: values keyed by component `Name` + - Text area, single-select dropdown, and readers are strings + - Multiselect dropdown is an array-like Lua table of strings + - Switch is a boolean + - Color picker is the selected color as a string + - Date picker is the selected date as a string + - Date range picker is the selected range as a single string in ` - ` format + - Time picker is the selected time as a string +- `input.meta`: per-component metadata keyed by component `Name` + - `Type` (string, e.g. `TEXT_AREA`, `DROPDOWN`, `SWITCH`, `COLOR_PICKER`, `DATE_PICKER`, `DATE_RANGE_PICKER`, `TIME_PICKER`) + - `Label` (string, when provided) + - `UserPrompt` (string, when provided) +- `input.profile`: selected profile data + - `Name`, `NeedToKnow`, `Actions`, `Num` + - When no profile is selected, values match the built-in "Use no profile" entry +``` +input = { + fields = { + [""] = "", + ... + }, + meta = { + [""] = { + Type = "", + Label = "", + UserPrompt = "" + }, + ... + }, + profile = { + Name = "", + NeedToKnow = "", + Actions = "", + Num = + } +} + +-- is the value you set in the components name property +``` +--- + +### 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 meta.Type == "DATE_PICKER" and value and value ~= "" then + table.insert(parts, name .. ": " .. value) + elseif meta.Type == "DATE_RANGE_PICKER" and value and value ~= "" then + table.insert(parts, name .. ": " .. value) + elseif meta.Type == "TIME_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 +``` +--- +### 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: Add user profile context to the prompt +```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 +``` +## Advanced Layout Options + +### `LAYOUT_GRID` reference +A 12-column grid system for organizing content with responsive breakpoints for different screen sizes. +``` ++------------------------------------------------------------+ +| 12 | ++------------------------------------------------------------+ + ++----------------------------+ +----------------------------+ +| 6 | | 6 | ++----------------------------+ +----------------------------+ + ++------------+ +------------+ +-----------+ +-------------+ +| 3 | | 3 | | 3 | | 3 | ++------------+ +------------+ +-----------+ +-------------+ + +``` + +- 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: How to define a flexible grid +```lua +{ + ["Type"] = "LAYOUT_GRID", + ["Props"] = { + ["Name"] = "mainGrid", + ["Justify"] = "FlexStart", + ["Spacing"] = 2 + }, + ["Children"] = { + { + ["Type"] = "LAYOUT_ITEM", + ["Props"] = { + ["Name"] = "contentColumn", + ["Xs"] = 12, + ["Lg"] = 8 + }, + ["Children"] = { + ["Type"] = "", + ["Props"] = {...}, + }, + }, + { + ["Type"] = "LAYOUT_ITEM", + ["Props"] = { + ["Name"] = "contentColumn2", + ["Xs"] = 12, + ["Lg"] = 8 + }, + ["Children"] = { + ["Type"] = "", + ["Props"] = {...}, + }, + }, + ... + } +} +``` +For a visual example and a full explanation look [here](https://www.mudblazor.com/components/grid#spacing) + +--- + +### `LAYOUT_ITEM` reference +`LAYOUT_ITEM` is used to wrap children components to use them into a grid. +The Breakpoints define how many columns the wrapped components take up in a 12-column grid. +Read more about breakpoint [here](https://www.mudblazor.com/features/breakpoints#breakpoints). + +- 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: How to wrap a child component and define its breakpoints +```lua +{ + ["Type"] = "LAYOUT_ITEM", + ["Props"] = { + ["Name"] = "contentColumn", + ["Xs"] = 12, + ["Lg"] = 8 + }, + ["Children"] = { + { + ["Type"] = "", + ["Props"] = {...}, + } + } +} +``` +For a full explanation look [here](https://www.mudblazor.com/api/MudItem#pages) + +--- + +### `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: How to define a MudPaper wrapping child components +```lua +{ + ["Type"] = "LAYOUT_PAPER", + ["Props"] = { + ["Name"] = "contentPaper", + ["Elevation"] = 2, + ["Width"] = "100%", + ["IsOutlined"] = true + }, + ["Children"] = { + { + ["Type"] = "", + ["Props"] = {...}, + }, + ... + } +} +``` +For a visual example and a full explanation look [here](https://www.mudblazor.com/components/paper#material-design) + +--- + +### `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: Define a stack of children components +```lua +{ + ["Type"] = "LAYOUT_STACK", + ["Props"] = { + ["Name"] = "toolbarRow", + ["IsRow"] = true, + ["Align"] = "Center", + ["Justify"] = "SpaceBetween", + ["Spacing"] = 2 + }, + ["Children"] = { + { + ["Type"] = "", + ["Props"] = {...}, + }, + ... + } +} +``` +For a visual example and a full explanation look [here](https://www.mudblazor.com/components/stack#basic-usage) + +--- + +### `LAYOUT_ACCORDION` reference +- Use `Type = "LAYOUT_ACCORDION"` to render a MudBlazor accordion container (`MudExpansionPanels`). +- Required props: + - `Name`: unique identifier for the layout node. +- Required structure: + - `Children`: array of `LAYOUT_ACCORDION_SECTION` component tables. Other child component types are ignored by intent and should be avoided. +- Optional props: + - `AllowMultiSelection`: defaults to `false`; allows multiple sections to stay expanded at the same time. + - `IsDense`: defaults to `false`; reduces the visual density of the accordion. + - `HasOutline`: defaults to `false`; toggles outlined panel styling. + - `IsSquare`: defaults to `false`; removes rounded corners from the accordion container. + - `Elevation`: integer elevation; omitted values fall back to `0`. + - `HasSectionPaddings`: defaults to `false`; toggles the section gutter/padding behavior. + - `Class`, `Style`: forwarded to the rendered `MudExpansionPanels` for layout/styling. + +#### Example: Define an accordion container +```lua +{ + ["Type"] = "LAYOUT_ACCORDION", + ["Props"] = { + ["Name"] = "settingsAccordion", + ["AllowMultiSelection"] = true, + ["IsDense"] = false, + ["HasOutline"] = true, + ["IsSquare"] = false, + ["Elevation"] = 0, + ["HasSectionPaddings"] = true + }, + ["Children"] = { + { + ["Type"] = "LAYOUT_ACCORDION_SECTION", + ["Props"] = { + ["Name"] = "generalSection", + ["HeaderText"] = "General" + }, + ["Children"] = { + { + ["Type"] = "", + ["Props"] = {...}, + } + } + } + } +} +``` +Use `LAYOUT_ACCORDION` as the outer wrapper and put the actual content into one or more `LAYOUT_ACCORDION_SECTION` children. + +--- + +### `LAYOUT_ACCORDION_SECTION` reference +- Use `Type = "LAYOUT_ACCORDION_SECTION"` to render one expandable section inside `LAYOUT_ACCORDION`. +- Required props: + - `Name`: unique identifier for the layout node. + - `HeaderText`: visible header text shown in the section title row. +- Intended parent: + - Use this component inside `LAYOUT_ACCORDION`. +- Optional props: + - `IsDisabled`: defaults to `false`; disables user interaction for the section. + - `IsExpanded`: defaults to `false`; sets the initial expanded state. + - `IsDense`: defaults to `false`; reduces section density. + - `HasInnerPadding`: defaults to `true`; controls the inner content gutter/padding. + - `HideIcon`: defaults to `false`; hides the expand/collapse icon. + - `HeaderIcon`: MudBlazor icon identifier rendered before the header text. + - `HeaderColor`: one of the MudBlazor `Color` enum names such as `Primary`, `Secondary`, `Warning`; omitted values fall back to `Inherit`. + - `HeaderTypo`: one of the MudBlazor `Typo` enum names such as `body1`, `subtitle1`, `h6`; omitted values follow the renderer default. + - `HeaderAlign`: one of the MudBlazor `Align` enum names such as `Start`, `Center`, `End`; omitted values follow the renderer default. + - `MaxHeight`: nullable integer max height in pixels for the expanded content area. + - `ExpandIcon`: MudBlazor icon identifier used for the expand/collapse control. + - `Class`, `Style`: forwarded to the rendered `MudExpansionPanel` for layout/styling. +- `Children` may contain any other assistant components you want to reveal inside the section. + +#### Example: Define an accordion section +```lua +{ + ["Type"] = "LAYOUT_ACCORDION_SECTION", + ["Props"] = { + ["Name"] = "advancedOptions", + ["HeaderText"] = "Advanced options", + ["IsDisabled"] = false, + ["IsExpanded"] = true, + ["IsDense"] = false, + ["HasInnerPadding"] = true, + ["HideIcon"] = false, + ["HeaderIcon"] = "Icons.Material.Filled.Tune", + ["HeaderColor"] = "Primary", + ["HeaderTypo"] = "subtitle1", + ["HeaderAlign"] = "Start", + ["MaxHeight"] = 320, + ["ExpandIcon"] = "Icons.Material.Filled.ExpandMore" + }, + ["Children"] = { + { + ["Type"] = "", + ["Props"] = {...}, + } + } +} +``` +`MaxHeight` is an integer pixel value, unlike `LAYOUT_PAPER` sizing props which accept CSS length strings such as `24rem` or `50vh`. + +## Useful Lua Functions +### 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) +--- + +> **Warning:** some common lua functions might not be available in this lua environment. Examples are: +> 1. `tostring()` +> 2. `pairs()`\\`ipairs()` + +### Logging helpers +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: Use Logging in lua functions +```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: Use the datetime functions in lua +```lua +local dt = DateTime("yyyy-MM-dd HH:mm:ss") +LogInfo(dt.formatted) +LogInfo(Timestamp()) +LogInfo(dt.day .. "." .. dt.month .. "." .. dt.year) +``` + +## General Tips + +1. Give every component a _**unique**_ `Name`— it’s used to track state and treated like an Id. +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. + +## Useful Resources +- [plugin.lua - Lua Manifest](https://github.com/MindWorkAI/AI-Studio/tree/main/app/MindWork%20AI%20Studio/Plugins/assistants/plugin.lua) +- [AI Studio Repository](https://github.com/MindWorkAI/AI-Studio/) +- [Lua 5.2 Reference Manual](https://www.lua.org/manual/5.2/manual.html) +- [MudBlazor Documentation](https://www.mudblazor.com/docs/overview) 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..ea9ecc7fe --- /dev/null +++ b/app/MindWork AI Studio/Plugins/assistants/plugin.lua @@ -0,0 +1,403 @@ +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 SENSITIVE + ["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 component>", -- required + ["Label"] = "<heading of component>", -- required + ["UserPrompt"] = "<direct input of instructions, questions, or tasks by a user>", + ["IsMultiselect"] = false, + ["HasSelectAll"] = false, + ["SelectAllText"] = "<label for 'SelectAll'-Button", + ["HelperText"] = "<helping text rendered under the component>", + ["OpenIcon"] = "Icons.Material.Filled.ArrowDropDown", + ["OpenClose"] = "Icons.Material.Filled.ArrowDropUp", + ["IconColor"] = "<Dark|Error|Info|Inherit|Primary|Secondary|Success|Surface|Tertiary|Transparent|Warning>", + ["IconPositon"] = "<Start|End>", + ["Variant"] = "<Text|Filled|Outlined>", + ["ValueType"] = "<string|int|bool>", -- 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>", -- Switches render mode between boxed switch and normal switch + ["Value"] = true, -- initial switch state + ["OnChanged"] = function(input) -- optional; same input and return contract as BUTTON.Action(input) + return nil + end, + ["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", -- keep this even for icon-only buttons so the manifest stays readable + ["IsIconButton"] = false, -- when true, renders an icon-only action button using StartIcon + ["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, or the main icon for icon-only buttons. 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 on text buttons. 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_ACCORDION", + ["Props"] = { + ["Name"] = "exampleAccordion", + ["AllowMultiSelection"] = false, -- if true, multiple sections can stay open at the same time + ["IsDense"] = false, -- denser layout with less spacing + ["HasOutline"] = false, -- outlined accordion panels + ["IsSquare"] = false, -- removes rounded corners + ["Elevation"] = 0, -- shadow depth of the accordion container + ["HasSectionPaddings"] = true, -- controls section gutters / inner frame paddings + ["Class"] = "<optional MudBlazor or css classes>", + ["Style"] = "<optional css styles>", + }, + ["Children"] = { + -- LAYOUT_ACCORDION_SECTION elements + } + }, + { + ["Type"] = "LAYOUT_ACCORDION_SECTION", + ["Props"] = { + ["Name"] = "exampleAccordionSection", -- required + ["HeaderText"] = "<section title shown in the accordion header>", -- required + ["IsDisabled"] = false, -- disables expanding/collapsing and interaction + ["IsExpanded"] = false, -- initial expansion state + ["IsDense"] = false, -- denser panel layout + ["HasInnerPadding"] = true, -- controls padding around the section content + ["HideIcon"] = false, -- hides the expand/collapse icon + ["HeaderIcon"] = "Icons.Material.Filled.ExpandMore", -- icon shown before the header text + ["HeaderColor"] = "<Dark|Error|Info|Inherit|Primary|Secondary|Success|Surface|Tertiary|Transparent|Warning>", + ["HeaderTypo"] = "<body1|subtitle1|h6|...>", -- MudBlazor typo value used for the header + ["HeaderAlign"] = "<Start|Center|End|Justify>", -- header text alignment + ["MaxHeight"] = 320, -- nullable integer pixel height for the expanded content area + ["ExpandIcon"] = "Icons.Material.Filled.ExpandMore", -- override the expand/collapse icon + ["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 + ["IconColor"] = "<Dark|Error|Info|Inherit|Primary|Secondary|Success|Surface|Tertiary|Transparent|Warning>", + }, + { + ["Type"] = "TEXT", -- required + ["Text"] = "<user readable text>", + ["Icon"] = "Icons.Material.Filled.HorizontalRule", + ["IconColor"] = "<Dark|Error|Info|Inherit|Primary|Secondary|Success|Surface|Tertiary|Transparent|Warning>", + } + }, + ["Class"] = "<optional MudBlazor or css classes>", + ["Style"] = "<optional css styles>", + } + }, + { + ["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>", + } + }, + { + ["Type"] = "DATE_PICKER", + ["Props"] = { + ["Name"] = "<unique identifier of this component>", -- required + ["Label"] = "<heading of your component>", -- required + ["Value"] = "2026-03-16", -- optional initial value + ["Color"] = "<Dark|Error|Info|Inherit|Primary|Secondary|Success|Surface|Tertiary|Transparent|Warning>", + ["Placeholder"] = "YYYY-MM-DD", + ["HelperText"] = "<optional help text rendered under the picker>", + ["DateFormat"] = "yyyy-MM-dd", + ["PickerVariant"] = "<Dialog|Inline|Static>", + ["UserPrompt"] = "<prompt context for the selected date>", + ["Class"] = "<optional MudBlazor or css classes>", + ["Style"] = "<optional css styles>", + } + }, + { + ["Type"] = "DATE_RANGE_PICKER", + ["Props"] = { + ["Name"] = "<unique identifier of this component>", -- required + ["Label"] = "<heading of your component>", -- required + ["Value"] = "2026-03-16 - 2026-03-20", -- optional initial range + ["Color"] = "<Dark|Error|Info|Inherit|Primary|Secondary|Success|Surface|Tertiary|Transparent|Warning>", + ["PlaceholderStart"] = "Start date", + ["PlaceholderEnd"] = "End date", + ["HelperText"] = "<optional help text rendered under the picker>", + ["DateFormat"] = "yyyy-MM-dd", + ["PickerVariant"] = "<Dialog|Inline|Static>", + ["UserPrompt"] = "<prompt context for the selected date range>", + ["Class"] = "<optional MudBlazor or css classes>", + ["Style"] = "<optional css styles>", + } + }, + { + ["Type"] = "TIME_PICKER", + ["Props"] = { + ["Name"] = "<unique identifier of this component>", -- required + ["Label"] = "<heading of your component>", -- required + ["Value"] = "14:30", -- optional initial time + ["Color"] = "<Dark|Error|Info|Inherit|Primary|Secondary|Success|Surface|Tertiary|Transparent|Warning>", + ["Placeholder"] = "HH:mm", + ["HelperText"] = "<optional help text rendered under the picker>", + ["TimeFormat"] = "HH:mm", + ["AmPm"] = false, + ["PickerVariant"] = "<Dialog|Inline|Static>", + ["UserPrompt"] = "<prompt context for the selected time>", + ["Class"] = "<optional MudBlazor or css classes>", + ["Style"] = "<optional css styles>", + } + }, + } + }, +} 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..3afbf26f5 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,12 @@ 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" +-- No assistant plugin are currently installed. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DYNAMIC::ASSISTANTDYNAMIC::T1913566603"] = "Derzeit sind keine Assistant-Plugins installiert." + +-- 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 +4935,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 +6078,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..aa7031853 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,12 @@ 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" +-- No assistant plugin are currently installed. +UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DYNAMIC::ASSISTANTDYNAMIC::T1913566603"] = "No assistant plugin are currently installed." + +-- 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 +4935,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 +6078,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..73366af2a --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/AssistantComponentFactory.cs @@ -0,0 +1,70 @@ +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.DATE_PICKER: + return new AssistantDatePicker { Props = props, Children = children }; + case AssistantComponentType.DATE_RANGE_PICKER: + return new AssistantDateRangePicker { Props = props, Children = children }; + case AssistantComponentType.TIME_PICKER: + return new AssistantTimePicker { 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 }; + case AssistantComponentType.LAYOUT_ACCORDION: + return new AssistantAccordion { Props = props, Children = children }; + case AssistantComponentType.LAYOUT_ACCORDION_SECTION: + return new AssistantAccordionSection { 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..58864a6d5 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantButton.cs @@ -0,0 +1,97 @@ +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 bool IsIconButton + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.IsIconButton), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.IsIconButton), 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..e77a45ebd --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantColorPicker.cs @@ -0,0 +1,75 @@ +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 int Elevation + { + get => AssistantComponentPropHelper.ReadInt(this.Props, nameof(this.Elevation), 6); + set => AssistantComponentPropHelper.WriteInt(this.Props, nameof(this.Elevation), 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..758dedd77 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantComponentPropHelper.cs @@ -0,0 +1,76 @@ +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 MudBlazor.Variant GetVariant(string value, Variant fallback) => Enum.TryParse<MudBlazor.Variant>(value, out var variant) ? variant : fallback; + public static MudBlazor.Adornment GetAdornment(string value, Adornment fallback) => Enum.TryParse<MudBlazor.Adornment>(value, out var adornment) ? adornment : 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? GetItemsAlignment(string value) => Enum.TryParse<AlignItems>(value, out var alignment) ? alignment : null; + public static Align GetAlignment(string value, Align fallback = Align.Inherit) => Enum.TryParse<Align>(value, out var alignment) ? alignment : fallback; + public static Typo GetTypography(string value, Typo fallback = Typo.body1) => Enum.TryParse<Typo>(value, out var typo) ? typo : fallback; + 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; + public static PickerVariant GetPickerVariant(string pickerValue, PickerVariant fallback) => Enum.TryParse<PickerVariant>(pickerValue, out var variant) ? variant : 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..f65a2a92c --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantComponentType.cs @@ -0,0 +1,29 @@ +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, + DATE_PICKER, + DATE_RANGE_PICKER, + TIME_PICKER, + LAYOUT_ITEM, + LAYOUT_GRID, + LAYOUT_PAPER, + LAYOUT_STACK, + LAYOUT_ACCORDION, + LAYOUT_ACCORDION_SECTION, +} diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDatePicker.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDatePicker.cs new file mode 100644 index 000000000..4be09a792 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDatePicker.cs @@ -0,0 +1,82 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +internal sealed class AssistantDatePicker : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.DATE_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 Value + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Value)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Value), value); + } + + public string Color + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Color)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Color), value); + } + + public string Placeholder + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Placeholder)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Placeholder), value); + } + + public string HelperText + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.HelperText)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.HelperText), value); + } + + public string DateFormat + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.DateFormat)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.DateFormat), 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 int Elevation + { + get => AssistantComponentPropHelper.ReadInt(this.Props, nameof(this.Elevation), 6); + set => AssistantComponentPropHelper.WriteInt(this.Props, nameof(this.Elevation), 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 string GetDateFormat() => string.IsNullOrWhiteSpace(this.DateFormat) ? "yyyy-MM-dd" : this.DateFormat; +} diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDateRangePicker.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDateRangePicker.cs new file mode 100644 index 000000000..f80db2dc5 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDateRangePicker.cs @@ -0,0 +1,88 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +internal sealed class AssistantDateRangePicker : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.DATE_RANGE_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 Value + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Value)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Value), value); + } + + public string Color + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Color)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Color), value); + } + + public string PlaceholderStart + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.PlaceholderStart)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.PlaceholderStart), value); + } + + public string PlaceholderEnd + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.PlaceholderEnd)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.PlaceholderEnd), value); + } + + public string HelperText + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.HelperText)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.HelperText), value); + } + + public string DateFormat + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.DateFormat)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.DateFormat), 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 int Elevation + { + get => AssistantComponentPropHelper.ReadInt(this.Props, nameof(this.Elevation), 6); + set => AssistantComponentPropHelper.WriteInt(this.Props, nameof(this.Elevation), 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 string GetDateFormat() => string.IsNullOrWhiteSpace(this.DateFormat) ? "yyyy-MM-dd" : this.DateFormat; +} 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..926cb1458 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDropdown.cs @@ -0,0 +1,142 @@ +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 bool IsMultiselect + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.IsMultiselect), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.IsMultiselect), value); + } + + public bool HasSelectAll + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.HasSelectAll), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.HasSelectAll), value); + } + + public string SelectAllText + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.SelectAllText)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.SelectAllText), value); + } + + public string HelperText + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.HelperText)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.HelperText), value); + } + + public string OpenIcon + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.OpenIcon)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.OpenIcon), value); + } + + public string CloseIcon + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.CloseIcon)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.CloseIcon), value); + } + + public string IconColor + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.IconColor)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.IconColor), value); + } + + public string IconPositon + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.IconPositon)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.IconPositon), value); + } + + public string Variant + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Variant)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Variant), 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..6c00cfab0 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantDropdownItem.cs @@ -0,0 +1,9 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public sealed 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..49b2864f6 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantListItem.cs @@ -0,0 +1,10 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public class AssistantListItem +{ + public string Type { get; set; } = "TEXT"; + public string Text { get; set; } = string.Empty; + public string Icon { get; set; } = string.Empty; + public string IconColor { 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..91d5146d0 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantSwitch.cs @@ -0,0 +1,105 @@ +using AIStudio.Tools.PluginSystem.Assistants.Icons; +using Lua; + +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +public 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 LuaFunction? OnChanged + { + get => this.Props.TryGetValue(nameof(this.OnChanged), out var value) && value is LuaFunction onChanged ? onChanged : null; + set => AssistantComponentPropHelper.WriteObject(this.Props, nameof(this.OnChanged), 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/AssistantTimePicker.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantTimePicker.cs new file mode 100644 index 000000000..17b610c65 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantTimePicker.cs @@ -0,0 +1,94 @@ +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; + +internal sealed class AssistantTimePicker : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.TIME_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 Value + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Value)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Value), value); + } + + public string Placeholder + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Placeholder)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Placeholder), value); + } + + public string HelperText + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.HelperText)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.HelperText), value); + } + + public string Color + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Color)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Color), value); + } + + public string TimeFormat + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.TimeFormat)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.TimeFormat), value); + } + + public bool AmPm + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.AmPm), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.AmPm), 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 int Elevation + { + get => AssistantComponentPropHelper.ReadInt(this.Props, nameof(this.Elevation), 6); + set => AssistantComponentPropHelper.WriteInt(this.Props, nameof(this.Elevation), 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 string GetTimeFormat() + { + if (!string.IsNullOrWhiteSpace(this.TimeFormat)) + return this.TimeFormat; + + return this.AmPm ? "hh:mm tt" : "HH:mm"; + } +} 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..88f4ea17d --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/ComponentPropSpecs.cs @@ -0,0 +1,142 @@ +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", "Action"], + optional: [ + "Text", "IsIconButton", "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", "IsMultiselect", "HasSelectAll", "SelectAllText", "HelperText", "ValueType", + "OpenIcon", "CloseIcon", "IconColor", "IconPositon", "Variant", "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", "Value"], + optional: [ + "Label", "OnChanged", "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.DATE_PICKER] = new( + required: ["Name", "Label"], + optional: [ + "Value", "Placeholder", "HelperText", "DateFormat", "Color", "Elevation", + "PickerVariant", "UserPrompt", "Class", "Style" + ] + ), + [AssistantComponentType.DATE_RANGE_PICKER] = new( + required: ["Name", "Label"], + optional: [ + "Value", "PlaceholderStart", "PlaceholderEnd", "HelperText", "DateFormat", + "Elevation", "Color", "PickerVariant", "UserPrompt", "Class", "Style" + ] + ), + [AssistantComponentType.TIME_PICKER] = new( + required: ["Name", "Label"], + optional: [ + "Value", "Placeholder", "HelperText", "TimeFormat", "AmPm", "Color", + "Elevation", "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", + ] + ), + [AssistantComponentType.LAYOUT_ACCORDION] = new( + required: ["Name"], + optional: [ + "AllowMultiSelection", "IsDense", "HasOutline", "IsSquare", "Elevation", + "HasSectionPaddings", "Class", "Style", + ] + ), + [AssistantComponentType.LAYOUT_ACCORDION_SECTION] = new( + required: ["Name", "HeaderText"], + optional: [ + "IsDisabled", "IsExpanded", "IsDense", "HasInnerPadding", "HideIcon", "HeaderIcon", "HeaderColor", + "HeaderTypo", "HeaderAlign", "MaxHeight","ExpandIcon", "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/AssistantAccordion.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/Layout/AssistantAccordion.cs new file mode 100644 index 000000000..617224da4 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/Layout/AssistantAccordion.cs @@ -0,0 +1,64 @@ +using Lua; + +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel.Layout; + +internal sealed class AssistantAccordion : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.LAYOUT_ACCORDION; + 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 AllowMultiSelection + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.AllowMultiSelection), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.AllowMultiSelection), value); + } + + public bool IsDense + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.IsDense), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.IsDense), value); + } + + public bool HasOutline + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.HasOutline), true); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.HasOutline), value); + } + + public bool IsSquare + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.IsSquare), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.IsSquare), value); + } + + public int Elevation + { + get => AssistantComponentPropHelper.ReadInt(this.Props, nameof(this.Elevation), 0); + set => AssistantComponentPropHelper.WriteInt(this.Props, nameof(this.Elevation), value); + } + + public bool HasSectionPaddings + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.HasSectionPaddings), true); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.HasSectionPaddings), 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/AssistantAccordionSection.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/Layout/AssistantAccordionSection.cs new file mode 100644 index 000000000..ab943b477 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/Layout/AssistantAccordionSection.cs @@ -0,0 +1,102 @@ +using Lua; + +namespace AIStudio.Tools.PluginSystem.Assistants.DataModel.Layout; + +internal sealed class AssistantAccordionSection : AssistantComponentBase +{ + public override AssistantComponentType Type => AssistantComponentType.LAYOUT_ACCORDION_SECTION; + public override Dictionary<string, object> Props { get; set; } = new(); + public override List<IAssistantComponent> Children { get; set; } = new(); + + public bool KeepContentAlive = true; + + public string Name + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.Name)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.Name), value); + } + + public string HeaderText + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.HeaderText)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.HeaderText), value); + } + + public string HeaderColor + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.HeaderColor)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.HeaderColor), value); + } + + public string HeaderIcon + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.HeaderIcon)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.HeaderIcon), value); + } + + public string HeaderTypo + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.HeaderTypo)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.HeaderTypo), value); + } + + public string HeaderAlign + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.HeaderAlign)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.HeaderAlign), value); + } + + public bool IsDisabled + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.IsDisabled), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.IsDisabled), value); + } + + public bool IsExpanded + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.IsExpanded), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.IsExpanded), value); + } + + public bool IsDense + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.IsDense), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.IsDense), value); + } + + public bool HasInnerPadding + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.HasInnerPadding), true); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.HasInnerPadding), value); + } + + public bool HideIcon + { + get => AssistantComponentPropHelper.ReadBool(this.Props, nameof(this.HideIcon), false); + set => AssistantComponentPropHelper.WriteBool(this.Props, nameof(this.HideIcon), value); + } + + public int? MaxHeight + { + get => AssistantComponentPropHelper.ReadNullableInt(this.Props, nameof(this.MaxHeight)); + set => AssistantComponentPropHelper.WriteNullableInt(this.Props, nameof(this.MaxHeight), value); + } + + public string ExpandIcon + { + get => AssistantComponentPropHelper.ReadString(this.Props, nameof(this.ExpandIcon)); + set => AssistantComponentPropHelper.WriteString(this.Props, nameof(this.ExpandIcon), 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/AssistantGrid.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/Layout/AssistantGrid.cs new file mode 100644 index 000000000..8d78cefc1 --- /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; + +internal 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..d037394e2 --- /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; + +internal 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..549c16930 --- /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; + +internal 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..26b78685a --- /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; + +internal 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 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/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..d7a456584 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs @@ -0,0 +1,583 @@ +using AIStudio.Tools.PluginSystem.Assistants.DataModel; +using AIStudio.Tools.PluginSystem.Assistants.DataModel.Layout; +using Lua; +using System.Text; + +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) + { + return await this.TryInvokeComponentCallbackAsync(button.Action, AssistantComponentType.BUTTON, button.Name, input, cancellationToken); + } + + public async Task<LuaTable?> TryInvokeSwitchChangedAsync(AssistantSwitch switchComponent, LuaTable input, CancellationToken cancellationToken = default) + { + return await this.TryInvokeComponentCallbackAsync(switchComponent.OnChanged, AssistantComponentType.SWITCH, switchComponent.Name, input, cancellationToken); + } + + private async Task<LuaTable?> TryInvokeComponentCallbackAsync(LuaFunction? callback, AssistantComponentType componentType, string componentName, LuaTable input, CancellationToken cancellationToken = default) + { + if (callback is null) + return null; + + try + { + cancellationToken.ThrowIfCancellationRequested(); + var results = await this.state.CallAsync(callback, [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}' {ComponentType} '{ComponentName}' callback returned a non-table value. The result is ignored.", this.Name, componentType, componentName); + return null; + } + catch (Exception e) + { + LOGGER.LogError(e, "Assistant plugin '{PluginName}' {ComponentType} '{ComponentName}' callback failed to execute.", this.Name, componentType, componentName); + 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; + } + + if (type == AssistantComponentType.SWITCH && key == "OnChanged" && val.TryRead<LuaFunction>(out var onChanged)) + { + result = onChanged; + 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; + + table.TryGetValue("Icon", out var iconVal); + iconVal.TryRead<string>(out var icon); + icon ??= string.Empty; + + table.TryGetValue("IconColor", out var iconColorVal); + iconColorVal.TryRead<string>(out var iconColor); + iconColor ??= string.Empty; + + + item.Text = text; + item.Type = type; + item.Icon = icon; + item.IconColor = iconColor; + + 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