From 0d57f1883cf0642dac9a30225e4d290177380fe8 Mon Sep 17 00:00:00 2001 From: krut_ni Date: Mon, 16 Mar 2026 14:15:29 +0100 Subject: [PATCH] Button now supports IconButton; Switch supports OnChange hook now --- .../Assistants/Dynamic/AssistantDynamic.razor | 64 +++++++++---- .../Dynamic/AssistantDynamic.razor.cs | 63 ++++++++++--- .../Plugins/assistants/README.md | 93 +++++++++++++------ .../Plugins/assistants/plugin.lua | 10 +- .../Assistants/DataModel/AssistantButton.cs | 6 ++ .../Assistants/DataModel/AssistantSwitch.cs | 9 +- .../DataModel/ComponentPropSpecs.cs | 4 +- .../Assistants/PluginAssistants.cs | 18 +++- 8 files changed, 195 insertions(+), 72 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor index 2d63798c..11f1d165 100644 --- a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor +++ b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor @@ -129,22 +129,47 @@ if (component is AssistantButton assistantButton) { var button = assistantButton; -
- - @button.Text - -
+ 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, "mb-3"); + var style = this.GetOptionalStyle(button.Style); + + if (!button.IsIconButton) + { +
+ + @button.Text + +
+ } + else + { + + } + } break; case AssistantComponentType.BUTTON_GROUP: @@ -281,16 +306,17 @@ { var assistantSwitch = switchComponent; var currentValue = this.switchFields[assistantSwitch.Name]; - + var disabled = assistantSwitch.Disabled || this.IsSwitchActionRunning(assistantSwitch.Name); + @(currentValue ? assistantSwitch.LabelOn : assistantSwitch.LabelOff) diff --git a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs index fa0d31be..8d333f8c 100644 --- a/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs +++ b/app/MindWork AI Studio/Assistants/Dynamic/AssistantDynamic.razor.cs @@ -50,6 +50,7 @@ public partial class AssistantDynamic : AssistantBaseCore private readonly Dictionary colorPickerFields = 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"; @@ -348,6 +349,7 @@ public partial class AssistantDynamic : AssistantBaseCore 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) { @@ -363,7 +365,7 @@ public partial class AssistantDynamic : AssistantBaseCore var cancellationToken = this.cancellationTokenSource?.Token ?? CancellationToken.None; var result = await this.assistantPlugin.TryInvokeButtonActionAsync(button, input, cancellationToken); if (result is not null) - this.ApplyButtonActionResult(result); + this.ApplyActionResult(result, AssistantComponentType.BUTTON); } finally { @@ -372,14 +374,45 @@ public partial class AssistantDynamic : AssistantBaseCore } } - private void ApplyButtonActionResult(LuaTable result) + 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 BUTTON action returned a non-table 'fields' value. The result is ignored."); + this.Logger.LogWarning("Assistant {ComponentType} callback returned a non-table 'fields' value. The result is ignored.", sourceType); return; } @@ -388,18 +421,18 @@ public partial class AssistantDynamic : AssistantBaseCore if (!pair.Key.TryRead(out var fieldName) || string.IsNullOrWhiteSpace(fieldName)) continue; - this.TryApplyFieldUpdate(fieldName, pair.Value); + this.TryApplyFieldUpdate(fieldName, pair.Value, sourceType); } } - private void TryApplyFieldUpdate(string fieldName, LuaValue value) + 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"); + this.LogFieldUpdateTypeMismatch(fieldName, "string", sourceType); return; } @@ -408,7 +441,7 @@ public partial class AssistantDynamic : AssistantBaseCore if (value.TryRead(out var dropdownValue)) this.dropdownFields[fieldName] = dropdownValue ?? string.Empty; else - this.LogFieldUpdateTypeMismatch(fieldName, "string"); + this.LogFieldUpdateTypeMismatch(fieldName, "string", sourceType); return; } @@ -419,7 +452,7 @@ public partial class AssistantDynamic : AssistantBaseCore else if (value.TryRead(out var singleDropdownValue)) this.multiselectDropdownFields[fieldName] = string.IsNullOrWhiteSpace(singleDropdownValue) ? [] : [singleDropdownValue]; else - this.LogFieldUpdateTypeMismatch(fieldName, "string[]"); + this.LogFieldUpdateTypeMismatch(fieldName, "string[]", sourceType); return; } @@ -428,7 +461,7 @@ public partial class AssistantDynamic : AssistantBaseCore if (value.TryRead(out var boolValue)) this.switchFields[fieldName] = boolValue; else - this.LogFieldUpdateTypeMismatch(fieldName, "boolean"); + this.LogFieldUpdateTypeMismatch(fieldName, "boolean", sourceType); return; } @@ -437,7 +470,7 @@ public partial class AssistantDynamic : AssistantBaseCore if (value.TryRead(out var colorValue)) this.colorPickerFields[fieldName] = colorValue ?? string.Empty; else - this.LogFieldUpdateTypeMismatch(fieldName, "string"); + this.LogFieldUpdateTypeMismatch(fieldName, "string", sourceType); return; } @@ -446,7 +479,7 @@ public partial class AssistantDynamic : AssistantBaseCore if (value.TryRead(out var webContentValue)) webContentState.Content = webContentValue ?? string.Empty; else - this.LogFieldUpdateTypeMismatch(fieldName, "string"); + this.LogFieldUpdateTypeMismatch(fieldName, "string", sourceType); return; } @@ -455,16 +488,16 @@ public partial class AssistantDynamic : AssistantBaseCore if (value.TryRead(out var fileContentValue)) fileContentState.Content = fileContentValue ?? string.Empty; else - this.LogFieldUpdateTypeMismatch(fieldName, "string"); + this.LogFieldUpdateTypeMismatch(fieldName, "string", sourceType); return; } - this.Logger.LogWarning("Assistant BUTTON action tried to update unknown field '{FieldName}'. The value is ignored.", fieldName); + 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) + private void LogFieldUpdateTypeMismatch(string fieldName, string expectedType, AssistantComponentType sourceType) { - this.Logger.LogWarning("Assistant BUTTON action tried to write an invalid value to '{FieldName}'. Expected {ExpectedType}.", fieldName, expectedType); + this.Logger.LogWarning("Assistant {ComponentType} callback tried to write an invalid value to '{FieldName}'. Expected {ExpectedType}.", sourceType, fieldName, expectedType); } private EventCallback> CreateMultiselectDropdownChangedCallback(string fieldName) => diff --git a/app/MindWork AI Studio/Plugins/assistants/README.md b/app/MindWork AI Studio/Plugins/assistants/README.md index b1ccb3f9..af0cb8b3 100644 --- a/app/MindWork AI Studio/Plugins/assistants/README.md +++ b/app/MindWork AI Studio/Plugins/assistants/README.md @@ -90,7 +90,7 @@ ASSISTANT = { - `TEXT_AREA`: user input field based on `MudTextField`; requires `Name`, `Label`, and may include `HelperText`, `HelperTextOnFocus`, `Adornment`, `AdornmentIcon`, `AdornmentText`, `AdornmentColor`, `Counter`, `MaxLength`, `IsImmediate`, `UserPrompt`, `PrefillText`, `IsSingleLine`, `ReadOnly`, `Class`, `Style`. - `DROPDOWN`: selects between variants; `Props` must include `Name`, `Label`, `Default`, `Items`, and optionally `ValueType` plus `UserPrompt`. -- `BUTTON`: invokes a Lua callback; `Props` must include `Name`, `Text`, `Action`, and may include `Variant`, `Color`, `IsFullWidth`, `Size`, `StartIcon`, `EndIcon`, `IconColor`, `IconSize`, `Class`, `Style`. +- `BUTTON`: 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`. @@ -98,7 +98,7 @@ ASSISTANT = { - `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 `Disabled`, `UserPrompt`, `LabelOn`, `LabelOff`, `LabelPlacement`, `Icon`, `IconColor`, `CheckedColor`, `UncheckedColor`, `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`. - `PROVIDER_SELECTION` / `PROFILE_SELECTION`: hooks into the shared provider/profile selectors. - `WEB_CONTENT_READER`: renders `ReadWebContent`; include `Name`, `UserPrompt`, `Preselect`, `PreselectContentCleanerAgent`. @@ -108,28 +108,28 @@ ASSISTANT = { 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` | `Variant`, `Color`, `IsFullWidth`, `Size`, `StartIcon`, `EndIcon`, `IconColor`, `IconSize`, `Class`, `Style` | [MudButton](https://www.mudblazor.com/components/button) | -| `SWITCH` | `Name`, `Label`, `Value` | `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`, `ShowAlpha`, `ShowToolbar`, `ShowModeSwitch`, `PickerVariant`, `UserPrompt`, `Class`, `Style` | [MudColorPicker](https://www.mudblazor.com/components/colorpicker) | -| `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` | `Type`, `Text` | `Href` | [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) | +| 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`, `ShowAlpha`, `ShowToolbar`, `ShowModeSwitch`, `PickerVariant`, `UserPrompt`, `Class`, `Style` | [MudColorPicker](https://www.mudblazor.com/components/colorpicker) | +| `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` | `Type`, `Text` | `Href` | [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 @@ -239,18 +239,23 @@ More information on rendered components can be found [here](https://www.mudblazo ### `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`: visible button label. + - `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. + - `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; omitted values fall back to `Inherit`. + - `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. @@ -300,6 +305,29 @@ More information on rendered components can be found [here](https://www.mudblazo } } ``` + +#### 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 @@ -314,7 +342,7 @@ More information on rendered components can be found [here](https://www.mudblazo - `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)`. +- 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 @@ -368,6 +396,7 @@ More information on rendered components can be found [here](https://www.mudblazo - `Label`: visible label for the switch field. - `Value`: initial boolean state (`true` or `false`). - Optional props: + - `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`. @@ -387,6 +416,14 @@ More information on rendered components can be found [here](https://www.mudblazo ["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", diff --git a/app/MindWork AI Studio/Plugins/assistants/plugin.lua b/app/MindWork AI Studio/Plugins/assistants/plugin.lua index cf59e8df..94681438 100644 --- a/app/MindWork AI Studio/Plugins/assistants/plugin.lua +++ b/app/MindWork AI Studio/Plugins/assistants/plugin.lua @@ -116,6 +116,9 @@ ASSISTANT = { ["Name"] = "", -- required ["Label"] = "", -- required ["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"] = "", ["LabelOn"] = "", @@ -133,14 +136,15 @@ ASSISTANT = { ["Type"] = "BUTTON", ["Props"] = { ["Name"] = "buildEmailOutput", - ["Text"] = "Build email output", + ["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"] = "", -- size of the button. Defaults to Medium ["Variant"] = "", -- display variation to use. Defaults to Text ["Color"] = "", -- color of the button. Defaults to Default ["IsFullWidth"] = false, -- ignores sizing and renders a long full width button. Defaults to false - ["StartIcon"] = "Icons.Material.Filled.ArrowRight", -- icon displayed before the text. Defaults to null + ["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"] = "", -- color of start and end icons. Defaults to Inherit + ["IconColor"] = "", -- color of start and end icons on text buttons. Defaults to Inherit ["IconSize"] = "", -- size of icons. Defaults to null. When null, the value of ["Size"] is used ["Action"] = function(input) local email = input.fields.emailContent or "" diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantButton.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantButton.cs index cc079354..58864a6d 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantButton.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantButton.cs @@ -19,6 +19,12 @@ public sealed class AssistantButton : AssistantComponentBase 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 { diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantSwitch.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantSwitch.cs index 798a1dd0..91d5146d 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantSwitch.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/AssistantSwitch.cs @@ -1,8 +1,9 @@ using AIStudio.Tools.PluginSystem.Assistants.Icons; +using Lua; namespace AIStudio.Tools.PluginSystem.Assistants.DataModel; -internal sealed class AssistantSwitch : AssistantComponentBase +public sealed class AssistantSwitch : AssistantComponentBase { public override AssistantComponentType Type => AssistantComponentType.SWITCH; public override Dictionary Props { get; set; } = new(); @@ -37,6 +38,12 @@ internal sealed class AssistantSwitch : AssistantComponentBase 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 { diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/ComponentPropSpecs.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/ComponentPropSpecs.cs index e2c9d7ea..00247bff 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/ComponentPropSpecs.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/DataModel/ComponentPropSpecs.cs @@ -20,7 +20,7 @@ public static class ComponentPropSpecs [AssistantComponentType.BUTTON] = new( required: ["Name", "Text", "Action"], optional: [ - "Variant", "Color", "IsFullWidth", "Size", + "IsIconButton", "Variant", "Color", "IsFullWidth", "Size", "StartIcon", "EndIcon", "IconColor", "IconSize", "Class", "Style" ] ), @@ -46,7 +46,7 @@ public static class ComponentPropSpecs [AssistantComponentType.SWITCH] = new( required: ["Name", "Label", "Value"], optional: [ - "LabelOn", "LabelOff", "LabelPlacement", "Icon", "IconColor", "UserPrompt", + "OnChanged", "LabelOn", "LabelOff", "LabelPlacement", "Icon", "IconColor", "UserPrompt", "CheckedColor", "UncheckedColor", "Disabled", "Class", "Style", ] ), diff --git a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs index 68f6525b..9b0c10db 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/Assistants/PluginAssistants.cs @@ -146,13 +146,23 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType public async Task TryInvokeButtonActionAsync(AssistantButton button, LuaTable input, CancellationToken cancellationToken = default) { - if (button.Action is null) + return await this.TryInvokeComponentCallbackAsync(button.Action, AssistantComponentType.BUTTON, button.Name, input, cancellationToken); + } + + public async Task TryInvokeSwitchChangedAsync(AssistantSwitch switchComponent, LuaTable input, CancellationToken cancellationToken = default) + { + return await this.TryInvokeComponentCallbackAsync(switchComponent.OnChanged, AssistantComponentType.SWITCH, switchComponent.Name, input, cancellationToken); + } + + private async Task 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(button.Action, [input]); + var results = await this.state.CallAsync(callback, [input]); if (results.Length == 0) return null; @@ -162,12 +172,12 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType if (results[0].TryRead(out var updateTable)) return updateTable; - LOGGER.LogWarning("Assistant plugin '{PluginName}' BUTTON '{ButtonName}' returned a non-table value. The result is ignored.", this.Name, button.Name); + 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}' BUTTON '{ButtonName}' action failed to execute.", this.Name, button.Name); + LOGGER.LogError(e, "Assistant plugin '{PluginName}' {ComponentType} '{ComponentName}' callback failed to execute.", this.Name, componentType, componentName); return null; } }