From 035412f7ff5f86d2410cf96700ab8128fa1daaab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peer=20Sch=C3=BCtt?= Date: Sat, 9 Aug 2025 19:29:43 +0200 Subject: [PATCH 1/4] Adding providers can now be disabled using config plugins (#522) Co-authored-by: Thorsten Sommer --- .../Assistants/AssistantBase.razor | 20 +- .../Assistants/I18N/allTexts.lua | 3 + .../Components/ConfigurationBase.razor | 24 ++- .../Components/ConfigurationBase.razor.cs | 43 +++- .../Components/ConfigurationBaseCore.cs | 15 ++ .../ConfigurationMinConfidenceSelection.razor | 2 +- ...nfigurationMinConfidenceSelection.razor.cs | 12 +- .../Components/ConfigurationMultiSelect.razor | 10 +- .../ConfigurationMultiSelect.razor.cs | 15 +- .../Components/ConfigurationOption.razor | 10 +- .../Components/ConfigurationOption.razor.cs | 15 +- .../ConfigurationProviderSelection.razor | 2 +- .../ConfigurationProviderSelection.razor.cs | 22 +- .../Components/ConfigurationSelect.razor | 6 +- .../Components/ConfigurationSelect.razor.cs | 26 ++- .../Components/ConfigurationSlider.razor | 10 +- .../Components/ConfigurationSlider.razor.cs | 14 +- .../Components/ConfigurationText.razor | 8 +- .../Components/ConfigurationText.razor.cs | 19 +- .../Components/LockableButton.razor | 5 + .../Components/LockableButton.razor.cs | 39 ++++ .../Settings/SettingsPanelApp.razor | 2 +- .../Components/Settings/SettingsPanelBase.cs | 3 - .../Settings/SettingsPanelProviders.razor | 6 +- .../Layout/MainLayout.razor.cs | 4 + .../Plugins/configuration/plugin.lua | 6 +- .../plugin.lua | 3 + .../plugin.lua | 3 + app/MindWork AI Studio/Program.cs | 1 - app/MindWork AI Studio/Settings/ConfigMeta.cs | 88 ++++++++ .../Settings/ConfigMetaBase.cs | 6 + .../Settings/DataModel/Data.cs | 2 +- .../Settings/DataModel/DataApp.cs | 23 +- .../DataModel/PreviousModels/DataV4.cs | 2 +- .../Settings/ExpressionExtensions.cs | 26 +++ app/MindWork AI Studio/Settings/IConfig.cs | 3 + .../Settings/ManagedConfiguration.cs | 198 ++++++++++++++++++ app/MindWork AI Studio/Settings/NoConfig.cs | 12 ++ .../Settings/SettingsLocker.cs | 78 ------- .../Settings/SettingsManager.cs | 4 +- .../Settings/SettingsMigrations.cs | 2 +- .../Tools/PluginSystem/PluginConfiguration.cs | 34 +-- .../PluginSystem/PluginFactory.HotReload.cs | 19 +- .../PluginSystem/PluginFactory.Loading.cs | 30 ++- .../PluginSystem/PluginFactory.Starting.cs | 2 +- .../Tools/PluginSystem/PluginFactory.cs | 1 - .../wwwroot/changelog/v0.9.50.md | 4 +- 47 files changed, 663 insertions(+), 219 deletions(-) create mode 100644 app/MindWork AI Studio/Components/ConfigurationBaseCore.cs create mode 100644 app/MindWork AI Studio/Components/LockableButton.razor create mode 100644 app/MindWork AI Studio/Components/LockableButton.razor.cs create mode 100644 app/MindWork AI Studio/Settings/ConfigMeta.cs create mode 100644 app/MindWork AI Studio/Settings/ConfigMetaBase.cs create mode 100644 app/MindWork AI Studio/Settings/ExpressionExtensions.cs create mode 100644 app/MindWork AI Studio/Settings/IConfig.cs create mode 100644 app/MindWork AI Studio/Settings/ManagedConfiguration.cs create mode 100644 app/MindWork AI Studio/Settings/NoConfig.cs delete mode 100644 app/MindWork AI Studio/Settings/SettingsLocker.cs diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor b/app/MindWork AI Studio/Assistants/AssistantBase.razor index bc4d01df..92ae6bff 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor @@ -6,10 +6,10 @@ - @(this.Title) + @this.Title - + @@ -26,13 +26,13 @@ - + @this.SubmitText @if (this.isProcessing && this.cancellationTokenSource is not null) { - + } @@ -80,7 +80,7 @@ @foreach (var assistant in Enum.GetValues().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length)) { - + @assistant.Name() } @@ -94,14 +94,14 @@ { case ButtonData buttonData when !string.IsNullOrWhiteSpace(buttonData.Tooltip): - + @buttonData.Text break; case ButtonData buttonData: - + @buttonData.Text break; @@ -110,7 +110,7 @@ @foreach (var assistant in Enum.GetValues().Where(n => n.AllowSendTo()).OrderBy(n => n.Name().Length)) { - + @assistant.Name() } @@ -121,14 +121,14 @@ @if (this.ShowCopyResult) { - + @TB("Copy result") } @if (this.ShowReset) { - + @TB("Reset") } diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 83ed2620..942a744f 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -1450,6 +1450,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIDENCEINFO::T3243388657"] = "Confiden -- Shows and hides the confidence card with information about the selected LLM provider. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIDENCEINFO::T847071819"] = "Shows and hides the confidence card with information about the selected LLM provider." +-- This feature is managed by your organization and has therefore been disabled. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONBASE::T1416426626"] = "This feature is managed by your organization and has therefore been disabled." + -- Choose the minimum confidence level that all LLM providers must meet. This way, you can ensure that only trustworthy providers are used. You cannot use any provider that falls below this level. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMINCONFIDENCESELECTION::T2526727283"] = "Choose the minimum confidence level that all LLM providers must meet. This way, you can ensure that only trustworthy providers are used. You cannot use any provider that falls below this level." diff --git a/app/MindWork AI Studio/Components/ConfigurationBase.razor b/app/MindWork AI Studio/Components/ConfigurationBase.razor index 2233093a..875a08c7 100644 --- a/app/MindWork AI Studio/Components/ConfigurationBase.razor +++ b/app/MindWork AI Studio/Components/ConfigurationBase.razor @@ -1 +1,23 @@ -@inherits MSGComponentBase \ No newline at end of file +@inherits MSGComponentBase + +@if (this.Body is not null) +{ + @if (!this.Disabled() && this.IsLocked()) + { + + + @* MudTooltip.RootStyle is set as a workaround for issue -> https://github.com/MudBlazor/MudBlazor/issues/10882 *@ + + + + @this.Body + + + } + else + { + + @this.Body + + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ConfigurationBase.razor.cs b/app/MindWork AI Studio/Components/ConfigurationBase.razor.cs index 10f4ae3f..59ef82b2 100644 --- a/app/MindWork AI Studio/Components/ConfigurationBase.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationBase.razor.cs @@ -5,7 +5,7 @@ namespace AIStudio.Components; /// /// A base class for configuration options. /// -public partial class ConfigurationBase : MSGComponentBase +public abstract partial class ConfigurationBase : MSGComponentBase { /// /// The description of the option, i.e., the name. Should be @@ -26,7 +26,42 @@ public partial class ConfigurationBase : MSGComponentBase [Parameter] public Func Disabled { get; set; } = () => false; - protected const string MARGIN_CLASS = "mb-6"; + /// + /// Is the option locked by a configuration plugin? + /// + [Parameter] + public Func IsLocked { get; set; } = () => false; + + /// + /// Should the option be stretched to fill the available space? + /// + protected abstract bool Stretch { get; } + + /// + /// The CSS class to apply to the component. + /// + protected virtual string GetClassForBase => string.Empty; + + /// + /// The visual variant of the option. + /// + protected virtual Variant Variant => Variant.Text; + + /// + /// The label to display for the option. + /// + protected virtual string Label => string.Empty; + + private StretchItems StretchItems => this.Stretch ? StretchItems.End : StretchItems.None; + + protected bool IsDisabled => this.Disabled() || this.IsLocked(); + + private string Classes => $"{this.GetClassForBase} {MARGIN_CLASS}"; + + private protected virtual RenderFragment? Body => null; + + private const string MARGIN_CLASS = "mb-6"; + protected static readonly Dictionary SPELLCHECK_ATTRIBUTES = new(); #region Overrides of ComponentBase @@ -39,7 +74,9 @@ public partial class ConfigurationBase : MSGComponentBase } #endregion - + + private string TB(string fallbackEN) => this.T(fallbackEN, typeof(ConfigurationBase).Namespace, nameof(ConfigurationBase)); + protected async Task InformAboutChange() => await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); #region Overrides of MSGComponentBase diff --git a/app/MindWork AI Studio/Components/ConfigurationBaseCore.cs b/app/MindWork AI Studio/Components/ConfigurationBaseCore.cs new file mode 100644 index 00000000..8fe80f6c --- /dev/null +++ b/app/MindWork AI Studio/Components/ConfigurationBaseCore.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; + +namespace AIStudio.Components; + +public abstract class ConfigurationBaseCore : ConfigurationBase +{ + private protected sealed override RenderFragment Body => this.BuildRenderTree; + + // Allow content to be provided by a .razor file but without + // overriding the content of the base class + protected new virtual void BuildRenderTree(RenderTreeBuilder builder) + { + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ConfigurationMinConfidenceSelection.razor b/app/MindWork AI Studio/Components/ConfigurationMinConfidenceSelection.razor index c3f21593..93a47e8b 100644 --- a/app/MindWork AI Studio/Components/ConfigurationMinConfidenceSelection.razor +++ b/app/MindWork AI Studio/Components/ConfigurationMinConfidenceSelection.razor @@ -1,3 +1,3 @@ @using AIStudio.Settings @inherits MSGComponentBase - \ No newline at end of file + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ConfigurationMinConfidenceSelection.razor.cs b/app/MindWork AI Studio/Components/ConfigurationMinConfidenceSelection.razor.cs index 858bbc01..b5d130c8 100644 --- a/app/MindWork AI Studio/Components/ConfigurationMinConfidenceSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationMinConfidenceSelection.razor.cs @@ -18,17 +18,17 @@ public partial class ConfigurationMinConfidenceSelection : MSGComponentBase [Parameter] public Action SelectionUpdate { get; set; } = _ => { }; - /// - /// Is the selection component disabled? - /// - [Parameter] - public Func Disabled { get; set; } = () => false; - /// /// Boolean value indicating whether the selection is restricted to a global minimum confidence level. /// [Parameter] public bool RestrictToGlobalMinimumConfidence { get; set; } + + [Parameter] + public Func Disabled { get; set; } = () => false; + + [Parameter] + public Func IsLocked { get; set; } = () => false; private ConfidenceLevel FilteredSelectedValue() { diff --git a/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor b/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor index 9c974e02..6d9d7b89 100644 --- a/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor +++ b/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor @@ -1,4 +1,4 @@ -@inherits ConfigurationBase +@inherits ConfigurationBaseCore @typeparam TData @foreach (var data in this.Data) { diff --git a/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor.cs b/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor.cs index bdbe1063..f2e5ed51 100644 --- a/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationMultiSelect.razor.cs @@ -8,7 +8,7 @@ namespace AIStudio.Components; /// Configuration component for selecting many values from a list. /// /// The type of the value to select. -public partial class ConfigurationMultiSelect : ConfigurationBase +public partial class ConfigurationMultiSelect : ConfigurationBaseCore { /// /// The data to select from. @@ -28,6 +28,17 @@ public partial class ConfigurationMultiSelect : ConfigurationBase [Parameter] public Action> SelectionUpdate { get; set; } = _ => { }; + #region Overrides of ConfigurationBase + + /// + protected override bool Stretch => true; + + protected override Variant Variant => Variant.Outlined; + + protected override string Label => this.OptionDescription; + + #endregion + private async Task OptionChanged(IEnumerable? updatedValues) { if(updatedValues is null) @@ -39,8 +50,6 @@ public partial class ConfigurationMultiSelect : ConfigurationBase await this.InformAboutChange(); } - private static string GetClass => $"{MARGIN_CLASS} rounded-lg"; - private string GetMultiSelectionText(List? selectedValues) { if(selectedValues is null || selectedValues.Count == 0) diff --git a/app/MindWork AI Studio/Components/ConfigurationOption.razor b/app/MindWork AI Studio/Components/ConfigurationOption.razor index 73ffe235..a2ad673e 100644 --- a/app/MindWork AI Studio/Components/ConfigurationOption.razor +++ b/app/MindWork AI Studio/Components/ConfigurationOption.razor @@ -1,7 +1,5 @@ -@inherits ConfigurationBase +@inherits ConfigurationBaseCore - - - @(this.State() ? this.LabelOn : this.LabelOff) - - \ No newline at end of file + + @(this.State() ? this.LabelOn : this.LabelOff) + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ConfigurationOption.razor.cs b/app/MindWork AI Studio/Components/ConfigurationOption.razor.cs index b3bed551..bbd0da1a 100644 --- a/app/MindWork AI Studio/Components/ConfigurationOption.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationOption.razor.cs @@ -5,7 +5,7 @@ namespace AIStudio.Components; /// /// Configuration component for any boolean option. /// -public partial class ConfigurationOption : ConfigurationBase +public partial class ConfigurationOption : ConfigurationBaseCore { /// /// Text to display when the option is true. @@ -31,6 +31,19 @@ public partial class ConfigurationOption : ConfigurationBase [Parameter] public Action StateUpdate { get; set; } = _ => { }; + #region Overrides of ConfigurationBase + + /// + protected override bool Stretch => true; + + /// + protected override Variant Variant => Variant.Outlined; + + /// + protected override string Label => this.OptionDescription; + + #endregion + private async Task OptionChanged(bool updatedState) { this.StateUpdate(updatedState); diff --git a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor index 4653822f..be6a93cd 100644 --- a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor +++ b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor @@ -1,2 +1,2 @@ @inherits MSGComponentBase - \ No newline at end of file + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs index 28298f75..8eced0f2 100644 --- a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs @@ -20,27 +20,17 @@ public partial class ConfigurationProviderSelection : MSGComponentBase [Parameter] public IEnumerable> Data { get; set; } = new List>(); - /// - /// Is the selection component disabled? - /// - [Parameter] - public Func Disabled { get; set; } = () => false; - [Parameter] public Func HelpText { get; set; } = () => TB("Select a provider that is preselected."); [Parameter] public Tools.Components Component { get; set; } = Tools.Components.NONE; - - #region Overrides of ComponentBase - - protected override async Task OnParametersSetAsync() - { - this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]); - await base.OnParametersSetAsync(); - } - - #endregion + + [Parameter] + public Func Disabled { get; set; } = () => false; + + [Parameter] + public Func IsLocked { get; set; } = () => false; [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed")] private IEnumerable> FilteredData() diff --git a/app/MindWork AI Studio/Components/ConfigurationSelect.razor b/app/MindWork AI Studio/Components/ConfigurationSelect.razor index 99120aa9..c3459101 100644 --- a/app/MindWork AI Studio/Components/ConfigurationSelect.razor +++ b/app/MindWork AI Studio/Components/ConfigurationSelect.razor @@ -1,7 +1,7 @@ -@inherits ConfigurationBase -@typeparam T +@inherits ConfigurationBaseCore +@typeparam TConfig - + @foreach (var data in this.Data) { diff --git a/app/MindWork AI Studio/Components/ConfigurationSelect.razor.cs b/app/MindWork AI Studio/Components/ConfigurationSelect.razor.cs index 8fb876c4..14c73eea 100644 --- a/app/MindWork AI Studio/Components/ConfigurationSelect.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationSelect.razor.cs @@ -7,33 +7,43 @@ namespace AIStudio.Components; /// /// Configuration component for selecting a value from a list. /// -/// The type of the value to select. -public partial class ConfigurationSelect : ConfigurationBase +/// The type of the value to select. +public partial class ConfigurationSelect : ConfigurationBaseCore { /// /// The data to select from. /// [Parameter] - public IEnumerable> Data { get; set; } = []; + public IEnumerable> Data { get; set; } = []; /// /// The selected value. /// [Parameter] - public Func SelectedValue { get; set; } = () => default!; + public Func SelectedValue { get; set; } = () => default!; /// /// An action that is called when the selection changes. /// [Parameter] - public Action SelectionUpdate { get; set; } = _ => { }; + public Action SelectionUpdate { get; set; } = _ => { }; - private async Task OptionChanged(T updatedValue) + #region Overrides of ConfigurationBase + + /// + protected override bool Stretch => true; + + /// + protected override string Label => this.OptionDescription; + + protected override Variant Variant => Variant.Outlined; + + #endregion + + private async Task OptionChanged(TConfig updatedValue) { this.SelectionUpdate(updatedValue); await this.SettingsManager.StoreSettings(); await this.InformAboutChange(); } - - private static string GetClass => $"{MARGIN_CLASS} rounded-lg"; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ConfigurationSlider.razor b/app/MindWork AI Studio/Components/ConfigurationSlider.razor index b42f4a4d..b04b511d 100644 --- a/app/MindWork AI Studio/Components/ConfigurationSlider.razor +++ b/app/MindWork AI Studio/Components/ConfigurationSlider.razor @@ -1,8 +1,6 @@ @typeparam T -@inherits ConfigurationBase +@inherits ConfigurationBaseCore - - - @this.Value() @this.Unit - - \ No newline at end of file + + @this.Value() @this.Unit + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ConfigurationSlider.razor.cs b/app/MindWork AI Studio/Components/ConfigurationSlider.razor.cs index 7d91cb8b..429119e1 100644 --- a/app/MindWork AI Studio/Components/ConfigurationSlider.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationSlider.razor.cs @@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Components; namespace AIStudio.Components; -public partial class ConfigurationSlider : ConfigurationBase where T : struct, INumber +public partial class ConfigurationSlider : ConfigurationBaseCore where T : struct, INumber { /// /// The minimum value for the slider. @@ -42,6 +42,18 @@ public partial class ConfigurationSlider : ConfigurationBase where T : struct [Parameter] public Action ValueUpdate { get; set; } = _ => { }; + #region Overrides of ConfigurationBase + + /// + protected override bool Stretch => true; + + /// + protected override Variant Variant => Variant.Outlined; + + protected override string Label => this.OptionDescription; + + #endregion + #region Overrides of ComponentBase protected override async Task OnInitializedAsync() diff --git a/app/MindWork AI Studio/Components/ConfigurationText.razor b/app/MindWork AI Studio/Components/ConfigurationText.razor index a3cc3233..80ec63ae 100644 --- a/app/MindWork AI Studio/Components/ConfigurationText.razor +++ b/app/MindWork AI Studio/Components/ConfigurationText.razor @@ -1,12 +1,10 @@ -@inherits ConfigurationBase +@inherits ConfigurationBaseCore \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ConfigurationText.razor.cs b/app/MindWork AI Studio/Components/ConfigurationText.razor.cs index 4e6bb7f9..7bcce41e 100644 --- a/app/MindWork AI Studio/Components/ConfigurationText.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationText.razor.cs @@ -4,7 +4,7 @@ using Timer = System.Timers.Timer; namespace AIStudio.Components; -public partial class ConfigurationText : ConfigurationBase +public partial class ConfigurationText : ConfigurationBaseCore { /// /// The text used for the textfield. @@ -43,10 +43,21 @@ public partial class ConfigurationText : ConfigurationBase public int MaxLines { get; set; } = 12; private string internalText = string.Empty; - private Timer timer = new(TimeSpan.FromMilliseconds(500)) + private readonly Timer timer = new(TimeSpan.FromMilliseconds(500)) { AutoReset = false }; + + #region Overrides of ConfigurationBase + + /// + protected override bool Stretch => true; + + protected override Variant Variant => Variant.Outlined; + + protected override string Label => this.OptionDescription; + + #endregion #region Overrides of ConfigurationBase @@ -56,8 +67,6 @@ public partial class ConfigurationText : ConfigurationBase await base.OnInitializedAsync(); } - #region Overrides of ComponentBase - protected override async Task OnParametersSetAsync() { this.internalText = this.Text(); @@ -66,8 +75,6 @@ public partial class ConfigurationText : ConfigurationBase #endregion - #endregion - private bool AutoGrow => this.NumLines > 1; private int GetMaxLines => this.AutoGrow ? this.MaxLines : 1; diff --git a/app/MindWork AI Studio/Components/LockableButton.razor b/app/MindWork AI Studio/Components/LockableButton.razor new file mode 100644 index 00000000..825c5a62 --- /dev/null +++ b/app/MindWork AI Studio/Components/LockableButton.razor @@ -0,0 +1,5 @@ +@inherits ConfigurationBaseCore + + + @this.Text + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/LockableButton.razor.cs b/app/MindWork AI Studio/Components/LockableButton.razor.cs new file mode 100644 index 00000000..cbfbd910 --- /dev/null +++ b/app/MindWork AI Studio/Components/LockableButton.razor.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +public partial class LockableButton : ConfigurationBaseCore +{ + [Parameter] + public string Icon { get; set; } = Icons.Material.Filled.Info; + + [Parameter] + public Func OnClickAsync { get; set; } = () => Task.CompletedTask; + + [Parameter] + public Action OnClick { get; set; } = () => { }; + + [Parameter] + public string Text { get; set; } = string.Empty; + + [Parameter] + public string Class { get; set; } = string.Empty; + + #region Overrides of ConfigurationBase + + /// + protected override bool Stretch => false; + + protected override string GetClassForBase => this.Class; + + #endregion + + private async Task ClickAsync() + { + if (this.IsLocked() || this.Disabled()) + return; + + await this.OnClickAsync(); + this.OnClick(); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor index 1ad60ba2..fc466e7a 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor @@ -13,7 +13,7 @@ - + diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelBase.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelBase.cs index dded906c..bad3fca3 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelBase.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelBase.cs @@ -15,7 +15,4 @@ public abstract class SettingsPanelBase : MSGComponentBase [Inject] protected RustService RustService { get; init; } = null!; - - [Inject] - protected SettingsLocker SettingsLocker { get; init; } = null!; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor index 1389425a..616149d0 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor @@ -74,10 +74,8 @@ @T("No providers configured yet.") } - - - @T("Add Provider") - + + @T("LLM Provider Confidence") diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index 2367aff4..cc115a2c 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -215,7 +215,11 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan await PluginFactory.TryDownloadingConfigPluginAsync(enterpriseEnvironment.ConfigurationId, enterpriseEnvironment.ConfigurationServerUrl); // Load (but not start) all plugins without waiting for them: + #if DEBUG + var pluginLoadingTimeout = new CancellationTokenSource(); + #else var pluginLoadingTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + #endif await PluginFactory.LoadAll(pluginLoadingTimeout.Token); // Set up hot reloading for plugins: diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index d80fc0d9..b81e8686 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -64,4 +64,8 @@ CONFIG["SETTINGS"] = {} -- Configure the update behavior: -- Allowed values are: NO_CHECK, ONCE_STARTUP, HOURLY, DAILY, WEEKLY --- CONFIG["SETTINGS"]["DataApp.UpdateBehavior"] = "NO_CHECK" \ No newline at end of file +-- CONFIG["SETTINGS"]["DataApp.UpdateBehavior"] = "NO_CHECK" + +-- Configure the user permission to add providers: +-- Allowed values are: true, false +-- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false \ No newline at end of file diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 13dc2f12..042978b0 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 @@ -1452,6 +1452,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIDENCEINFO::T3243388657"] = "Vertraue -- Shows and hides the confidence card with information about the selected LLM provider. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIDENCEINFO::T847071819"] = "Zeigt oder verbirgt die Vertrauenskarte mit Informationen über den ausgewählten LLM-Anbieter." +-- This feature is managed by your organization and has therefore been disabled. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONBASE::T1416426626"] = "Diese Funktion wird von Ihrer Organisation verwaltet und wurde daher deaktiviert." + -- Choose the minimum confidence level that all LLM providers must meet. This way, you can ensure that only trustworthy providers are used. You cannot use any provider that falls below this level. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMINCONFIDENCESELECTION::T2526727283"] = "Wählen Sie das minimale Vertrauensniveau, das alle LLM-Anbieter erfüllen müssen. So stellen Sie sicher, dass nur vertrauenswürdige Anbieter verwendet werden. Anbieter, die dieses Niveau unterschreiten, können nicht verwendet werden." 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 99371d03..45aa0caa 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 @@ -1452,6 +1452,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIDENCEINFO::T3243388657"] = "Confiden -- Shows and hides the confidence card with information about the selected LLM provider. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIDENCEINFO::T847071819"] = "Shows and hides the confidence card with information about the selected LLM provider." +-- This feature is managed by your organization and has therefore been disabled. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONBASE::T1416426626"] = "This feature is managed by your organization and has therefore been disabled." + -- Choose the minimum confidence level that all LLM providers must meet. This way, you can ensure that only trustworthy providers are used. You cannot use any provider that falls below this level. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::CONFIGURATIONMINCONFIDENCESELECTION::T2526727283"] = "Choose the minimum confidence level that all LLM providers must meet. This way, you can ensure that only trustworthy providers are used. You cannot use any provider that falls below this level." diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index 072dd3ad..a4d9c2b4 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -126,7 +126,6 @@ internal sealed class Program builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); diff --git a/app/MindWork AI Studio/Settings/ConfigMeta.cs b/app/MindWork AI Studio/Settings/ConfigMeta.cs new file mode 100644 index 00000000..f8d50ecc --- /dev/null +++ b/app/MindWork AI Studio/Settings/ConfigMeta.cs @@ -0,0 +1,88 @@ +using System.Linq.Expressions; + +using AIStudio.Settings.DataModel; + +namespace AIStudio.Settings; + +/// +/// Represents configuration metadata for a specific class and property. +/// +/// The class type that contains the configuration property. +/// The type of the configuration property value. +public record ConfigMeta : ConfigMetaBase +{ + public ConfigMeta(Expression> configSelection, Expression> propertyExpression) + { + this.ConfigSelection = configSelection; + this.PropertyExpression = propertyExpression; + } + + /// + /// The expression to select the configuration class from the settings data. + /// + private Expression> ConfigSelection { get; } + + /// + /// The expression to select the property within the configuration class. + /// + private Expression> PropertyExpression { get; } + + /// + /// Indicates whether the configuration is managed by a plugin and is therefore locked. + /// + public bool IsLocked { get; private set; } + + /// + /// The ID of the plugin that manages this configuration. This is set when the configuration is locked. + /// + public Guid MangedByConfigPluginId { get; private set; } + + /// + /// The default value for the configuration property. This is used when resetting the property to its default state. + /// + public required TValue Default { get; init; } + + /// + /// Locks the configuration state, indicating that it is managed by a specific plugin. + /// + /// The ID of the plugin that is managing this configuration. + public void LockManagedState(Guid pluginId) + { + this.IsLocked = true; + this.MangedByConfigPluginId = pluginId; + } + + /// + /// Resets the managed state of the configuration, allowing it to be modified again. + /// This will also reset the property to its default value. + /// + public void ResetManagedState() + { + this.IsLocked = false; + this.MangedByConfigPluginId = Guid.Empty; + this.Reset(); + } + + /// + /// Resets the configuration property to its default value. + /// + public void Reset() + { + var configInstance = this.ConfigSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData); + var memberExpression = this.PropertyExpression.GetMemberExpression(); + if (memberExpression.Member is System.Reflection.PropertyInfo propertyInfo) + propertyInfo.SetValue(configInstance, this.Default); + } + + /// + /// Sets the value of the configuration property to the specified value. + /// + /// The value to set for the configuration property. + public void SetValue(TValue value) + { + var configInstance = this.ConfigSelection.Compile().Invoke(SETTINGS_MANAGER.ConfigurationData); + var memberExpression = this.PropertyExpression.GetMemberExpression(); + if (memberExpression.Member is System.Reflection.PropertyInfo propertyInfo) + propertyInfo.SetValue(configInstance, value); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ConfigMetaBase.cs b/app/MindWork AI Studio/Settings/ConfigMetaBase.cs new file mode 100644 index 00000000..4ef74e88 --- /dev/null +++ b/app/MindWork AI Studio/Settings/ConfigMetaBase.cs @@ -0,0 +1,6 @@ +namespace AIStudio.Settings; + +public abstract record ConfigMetaBase : IConfig +{ + protected static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/Data.cs b/app/MindWork AI Studio/Settings/DataModel/Data.cs index 695d2ad8..0e825aa0 100644 --- a/app/MindWork AI Studio/Settings/DataModel/Data.cs +++ b/app/MindWork AI Studio/Settings/DataModel/Data.cs @@ -71,7 +71,7 @@ public sealed class Data /// public uint NextChatTemplateNum { get; set; } = 1; - public DataApp App { get; init; } = new(); + public DataApp App { get; init; } = new(x => x.App); public DataChat Chat { get; init; } = new(); diff --git a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs index e189cbbd..477afa30 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs @@ -1,7 +1,16 @@ +using System.Linq.Expressions; + namespace AIStudio.Settings.DataModel; -public sealed class DataApp +public sealed class DataApp(Expression>? configSelection = null) { + /// + /// The default constructor for the JSON deserializer. + /// + public DataApp() : this(null) + { + } + /// /// The language behavior. /// @@ -21,7 +30,7 @@ public sealed class DataApp /// Should we save energy? When true, we will update content streamed /// from the server, i.e., AI, less frequently. /// - public bool IsSavingEnergy { get; set; } + public bool IsSavingEnergy { get; set; } = ManagedConfiguration.Register(configSelection, n => n.IsSavingEnergy, false); /// /// Should we enable spellchecking for all input fields? @@ -31,7 +40,7 @@ public sealed class DataApp /// /// If and when we should look for updates. /// - public UpdateBehavior UpdateBehavior { get; set; } = UpdateBehavior.HOURLY; + public UpdateBehavior UpdateBehavior { get; set; } = ManagedConfiguration.Register(configSelection, n => n.UpdateBehavior, UpdateBehavior.HOURLY); /// /// The navigation behavior. @@ -41,7 +50,7 @@ public sealed class DataApp /// /// The visibility setting for previews features. /// - public PreviewVisibility PreviewVisibility { get; set; } = PreviewVisibility.NONE; + public PreviewVisibility PreviewVisibility { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreviewVisibility, PreviewVisibility.NONE); /// /// The enabled preview features. @@ -58,9 +67,13 @@ public sealed class DataApp /// public string PreselectedProfile { get; set; } = string.Empty; - /// /// Should we preselect a chat template for the entire app? /// public string PreselectedChatTemplate { get; set; } = string.Empty; + + /// + /// Should the user be allowed to add providers? + /// + public bool AllowUserToAddProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToAddProvider, true); } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV4.cs b/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV4.cs index ecaebe8a..61555a3c 100644 --- a/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV4.cs +++ b/app/MindWork AI Studio/Settings/DataModel/PreviousModels/DataV4.cs @@ -33,7 +33,7 @@ public sealed class DataV4 /// public uint NextProfileNum { get; set; } = 1; - public DataApp App { get; init; } = new(); + public DataApp App { get; init; } = new(x => x.App); public DataChat Chat { get; init; } = new(); diff --git a/app/MindWork AI Studio/Settings/ExpressionExtensions.cs b/app/MindWork AI Studio/Settings/ExpressionExtensions.cs new file mode 100644 index 00000000..5d567c03 --- /dev/null +++ b/app/MindWork AI Studio/Settings/ExpressionExtensions.cs @@ -0,0 +1,26 @@ +using System.Linq.Expressions; + +namespace AIStudio.Settings; + +public static class ExpressionExtensions +{ + private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(typeof(ExpressionExtensions)); + + public static MemberExpression GetMemberExpression(this Expression> expression) + { + switch (expression.Body) + { + // Case for value types, which are wrapped in UnaryExpression: + case UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression: + return (MemberExpression)unaryExpression.Operand; + + // Case for reference types, which are directly MemberExpressions: + case MemberExpression memberExpression: + return memberExpression; + + default: + LOGGER.LogError($"Expression '{expression}' is not a valid property expression."); + throw new ArgumentException($"Expression '{expression}' is not a valid property expression.", nameof(expression)); + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/IConfig.cs b/app/MindWork AI Studio/Settings/IConfig.cs new file mode 100644 index 00000000..c9db8958 --- /dev/null +++ b/app/MindWork AI Studio/Settings/IConfig.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Settings; + +public interface IConfig; \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/ManagedConfiguration.cs b/app/MindWork AI Studio/Settings/ManagedConfiguration.cs new file mode 100644 index 00000000..3767cd66 --- /dev/null +++ b/app/MindWork AI Studio/Settings/ManagedConfiguration.cs @@ -0,0 +1,198 @@ +using System.Collections.Concurrent; +using System.Linq.Expressions; + +using AIStudio.Settings.DataModel; +using AIStudio.Tools.PluginSystem; + +using Lua; + +namespace AIStudio.Settings; + +public static class ManagedConfiguration +{ + private static readonly ConcurrentDictionary METADATA = new(); + + /// + /// Registers a configuration setting with a default value. + /// + /// + /// When called from the JSON deserializer, the configSelection parameter will be null. + /// In this case, the method will return the default value without registering the setting. + /// + /// The expression to select the configuration class. + /// The expression to select the property within the configuration class. + /// The default value to use when the setting is not configured. + /// The type of the configuration class. + /// The type of the property within the configuration class. + /// The default value. + public static TValue Register(Expression>? configSelection, Expression> propertyExpression, TValue defaultValue) + { + // When called from the JSON deserializer by using the standard constructor, + // we ignore the register call and return the default value: + if(configSelection is null) + return defaultValue; + + var configPath = Path(configSelection, propertyExpression); + + // If the metadata already exists for this configuration path, we return the default value: + if (METADATA.ContainsKey(configPath)) + return defaultValue; + + METADATA[configPath] = new ConfigMeta(configSelection, propertyExpression) + { + Default = defaultValue, + }; + + return defaultValue; + } + + /// + /// Attempts to retrieve the configuration metadata for a given configuration selection and property expression. + /// + /// + /// When no configuration metadata is found, it returns a NoConfig instance with the default value set to default(TValue). + /// This allows the caller to handle the absence of configuration gracefully. In such cases, the return value of the method will be false. + /// + /// The expression to select the configuration class. + /// The expression to select the property within the configuration class. + /// The output parameter that will hold the configuration metadata if found. + /// The type of the configuration class. + /// The type of the property within the configuration class. + /// True if the configuration metadata was found, otherwise false. + public static bool TryGet(Expression> configSelection, Expression> propertyExpression, out ConfigMeta configMeta) + { + var configPath = Path(configSelection, propertyExpression); + if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta meta) + { + configMeta = meta; + return true; + } + + configMeta = new NoConfig(configSelection, propertyExpression) + { + Default = default!, + }; + + return false; + } + + /// + /// Attempts to process the configuration settings from a Lua table. + /// + /// + /// When the configuration is successfully processed, it updates the configuration metadata with the configured value. + /// Furthermore, it locks the managed state of the configuration metadata to the provided configuration plugin ID. + /// The setting's value is set to the configured value. + /// + /// The ID of the related configuration plugin. + /// The Lua table containing the settings to process. + /// The expression to select the configuration class. + /// The expression to select the property within the configuration class. + /// When true, the method will not apply any changes, but only check if the configuration can be read. + /// The type of the configuration class. + /// The type of the property within the configuration class. + /// True when the configuration was successfully processed, otherwise false. + public static bool TryProcessConfiguration(Expression> configSelection, Expression> propertyExpression, Guid configPluginId, LuaTable settings, bool dryRun) + { + if(!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + var (configuredValue, successful) = configMeta.Default switch + { + Enum => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredEnumValue) && configuredEnumValue.TryRead(out var configuredEnumText) && Enum.TryParse(typeof(TValue), configuredEnumText, true, out var configuredEnum) ? ((TValue)configuredEnum, true) : (configMeta.Default, false), + Guid => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredGuidValue) && configuredGuidValue.TryRead(out var configuredGuidText) && Guid.TryParse(configuredGuidText, out var configuredGuid) ? ((TValue)(object)configuredGuid, true) : (configMeta.Default, false), + + string => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredTextValue) && configuredTextValue.TryRead(out var configuredText) ? ((TValue)(object)configuredText, true) : (configMeta.Default, false), + bool => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredBoolValue) && configuredBoolValue.TryRead(out var configuredState) ? ((TValue)(object)configuredState, true) : (configMeta.Default, false), + + int => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredIntValue) && configuredIntValue.TryRead(out var configuredInt) ? ((TValue)(object)configuredInt, true) : (configMeta.Default, false), + double => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredDoubleValue) && configuredDoubleValue.TryRead(out var configuredDouble) ? ((TValue)(object)configuredDouble, true) : (configMeta.Default, false), + float => settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredFloatValue) && configuredFloatValue.TryRead(out var configuredFloat) ? ((TValue)(object)configuredFloat, true) : (configMeta.Default, false), + + _ => (configMeta.Default, false), + }; + + if(dryRun) + return successful; + + switch (successful) + { + case true: + // + // Case: the setting was configured, and we could read the value successfully. + // + configMeta.SetValue(configuredValue); + configMeta.LockManagedState(configPluginId); + break; + + case false when configMeta.IsLocked && configMeta.MangedByConfigPluginId == configPluginId: + // + // Case: the setting was configured previously, but we could not read the value successfully. + // This happens when the setting was removed from the configuration plugin. We handle that + // case only when the setting was locked and managed by the same configuration plugin. + // + // The other case, when the setting was locked and managed by a different configuration plugin, + // is handled by the IsConfigurationLeftOver method, which checks if the configuration plugin + // is still available. If it is not available, it resets the managed state of the + // configuration setting, allowing it to be reconfigured by a different plugin or left unchanged. + // + configMeta.ResetManagedState(); + break; + + case false: + // + // Case: the setting was not configured, or we could not read the value successfully. + // We do not change the setting, and it remains at whatever value it had before. + // + break; + } + + return successful; + } + + /// + /// Checks if a configuration setting is left over from a configuration plugin that is no longer available. + /// If the configuration setting is locked and managed by a configuration plugin that is not available, + /// it resets the managed state of the configuration setting and returns true. + /// Otherwise, it returns false. + /// + /// The expression to select the configuration class. + /// The expression to select the property within the configuration class. + /// The collection of available plugins to check against. + /// The type of the configuration class. + /// The type of the property within the configuration class. + /// True if the configuration setting is left over and was reset, otherwise false. + public static bool IsConfigurationLeftOver(Expression> configSelection, Expression> propertyExpression, IEnumerable availablePlugins) + { + if (!TryGet(configSelection, propertyExpression, out var configMeta)) + return false; + + if(configMeta.MangedByConfigPluginId == Guid.Empty || !configMeta.IsLocked) + return false; + + // Check if the configuration plugin ID is valid against the available plugin IDs: + var plugin = availablePlugins.FirstOrDefault(x => x.Id == configMeta.MangedByConfigPluginId); + if (plugin is null) + { + // Remove the locked state: + configMeta.ResetManagedState(); + return true; + } + + return false; + } + + private static string Path(Expression> configSelection, Expression> propertyExpression) + { + var className = typeof(TClass).Name; + + var memberExpressionConfig = configSelection.GetMemberExpression(); + var configName = memberExpressionConfig.Member.Name; + + var memberExpressionProperty = propertyExpression.GetMemberExpression(); + var propertyName = memberExpressionProperty.Member.Name; + + var configPath = $"{configName}.{className}.{propertyName}"; + return configPath; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/NoConfig.cs b/app/MindWork AI Studio/Settings/NoConfig.cs new file mode 100644 index 00000000..79a1f9dd --- /dev/null +++ b/app/MindWork AI Studio/Settings/NoConfig.cs @@ -0,0 +1,12 @@ +using System.Linq.Expressions; + +using AIStudio.Settings.DataModel; + +namespace AIStudio.Settings; + +public sealed record NoConfig : ConfigMeta +{ + public NoConfig(Expression> configSelection, Expression> propertyExpression) : base(configSelection, propertyExpression) + { + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/SettingsLocker.cs b/app/MindWork AI Studio/Settings/SettingsLocker.cs deleted file mode 100644 index b23e1085..00000000 --- a/app/MindWork AI Studio/Settings/SettingsLocker.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Linq.Expressions; - -namespace AIStudio.Settings; - -public sealed class SettingsLocker -{ - private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); - private readonly Dictionary> lockedProperties = new(); - - public void Register(Expression> propertyExpression, Guid configurationPluginId) - { - var memberExpression = GetMemberExpression(propertyExpression); - var className = typeof(T).Name; - var propertyName = memberExpression.Member.Name; - - if (!this.lockedProperties.ContainsKey(className)) - this.lockedProperties[className] = []; - - this.lockedProperties[className].TryAdd(propertyName, configurationPluginId); - } - - public void Remove(Expression> propertyExpression) - { - var memberExpression = GetMemberExpression(propertyExpression); - var className = typeof(T).Name; - var propertyName = memberExpression.Member.Name; - - if (this.lockedProperties.TryGetValue(className, out var props)) - { - if (props.Remove(propertyName)) - { - // If the property was removed, check if the class has no more locked properties: - if (props.Count == 0) - this.lockedProperties.Remove(className); - } - } - } - - public Guid GetConfigurationPluginId(Expression> propertyExpression) - { - var memberExpression = GetMemberExpression(propertyExpression); - var className = typeof(T).Name; - var propertyName = memberExpression.Member.Name; - - if (this.lockedProperties.TryGetValue(className, out var props) && props.TryGetValue(propertyName, out var configurationPluginId)) - return configurationPluginId; - - // No configuration plugin ID found for this property: - return Guid.Empty; - } - - public bool IsLocked(Expression> propertyExpression) - { - var memberExpression = GetMemberExpression(propertyExpression); - var className = typeof(T).Name; - var propertyName = memberExpression.Member.Name; - - return this.lockedProperties.TryGetValue(className, out var props) && props.ContainsKey(propertyName); - } - - private static MemberExpression GetMemberExpression(Expression> expression) - { - switch (expression.Body) - { - // Case for value types, which are wrapped in UnaryExpression: - case UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression: - return (MemberExpression)unaryExpression.Operand; - - // Case for reference types, which are directly MemberExpressions: - case MemberExpression memberExpression: - return memberExpression; - - default: - LOGGER.LogError($"Expression '{expression}' is not a valid property expression."); - throw new ArgumentException($"Expression '{expression}' is not a valid property expression.", nameof(expression)); - } - } -} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index cb376479..059d0f12 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -349,7 +349,7 @@ public sealed class SettingsManager } } - public static string ToSettingName(Expression> propertyExpression) + public static string ToSettingName(Expression> propertyExpression) { MemberExpression? memberExpr; @@ -363,6 +363,6 @@ public sealed class SettingsManager throw new ArgumentException("Expression must be a property access", nameof(propertyExpression)); // Return the full name of the property, including the class name: - return $"{typeof(T).Name}.{memberExpr.Member.Name}"; + return $"{typeof(TIn).Name}.{memberExpr.Member.Name}"; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/SettingsMigrations.cs b/app/MindWork AI Studio/Settings/SettingsMigrations.cs index 98482ceb..7c5a4293 100644 --- a/app/MindWork AI Studio/Settings/SettingsMigrations.cs +++ b/app/MindWork AI Studio/Settings/SettingsMigrations.cs @@ -137,7 +137,7 @@ public static class SettingsMigrations Providers = previousConfig.Providers, NextProviderNum = previousConfig.NextProviderNum, - App = new() + App = new(x => x.App) { EnableSpellchecking = previousConfig.EnableSpellchecking, IsSavingEnergy = previousConfig.IsSavingEnergy, diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs index 6355fad1..7de7137f 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -1,6 +1,5 @@ using AIStudio.Provider; using AIStudio.Settings; -using AIStudio.Settings.DataModel; using Lua; @@ -13,24 +12,27 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PluginConfiguration).Namespace, nameof(PluginConfiguration)); private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); - private static readonly SettingsLocker SETTINGS_LOCKER = Program.SERVICE_PROVIDER.GetRequiredService(); private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService(); - public async Task InitializeAsync() + public async Task InitializeAsync(bool dryRun) { - if(!this.TryProcessConfiguration(out var issue)) + if(!this.TryProcessConfiguration(dryRun, out var issue)) this.pluginIssues.Add(issue); - - await SETTINGS_MANAGER.StoreSettings(); - await MessageBus.INSTANCE.SendMessage(null, Event.CONFIGURATION_CHANGED); + + if (!dryRun) + { + await SETTINGS_MANAGER.StoreSettings(); + await MessageBus.INSTANCE.SendMessage(null, Event.CONFIGURATION_CHANGED); + } } - + /// /// Tries to initialize the UI text content of the plugin. /// + /// When true, the method will not apply any changes, but only check if the configuration can be read. /// The error message, when the UI text content could not be read. /// True, when the UI text content could be read successfully. - private bool TryProcessConfiguration(out string message) + private bool TryProcessConfiguration(bool dryRun, out string message) { // Ensure that the main CONFIG table exists and is a valid Lua table: if (!this.state.Environment["CONFIG"].TryRead(out var mainTable)) @@ -40,19 +42,21 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT } // + // =========================================== // Configured settings + // =========================================== // if (!mainTable.TryGetValue("SETTINGS", out var settingsValue) || !settingsValue.TryRead(out var settingsTable)) { message = TB("The SETTINGS table does not exist or is not a valid table."); return false; } - - if (settingsTable.TryGetValue(SettingsManager.ToSettingName(x => x.UpdateBehavior), out var updateBehaviorValue) && updateBehaviorValue.TryRead(out var updateBehaviorText) && Enum.TryParse(updateBehaviorText, true, out var updateBehavior)) - { - SETTINGS_LOCKER.Register(x => x.UpdateBehavior, this.Id); - SETTINGS_MANAGER.ConfigurationData.App.UpdateBehavior = updateBehavior; - } + + // Check for updates, and if so, how often? + ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.UpdateBehavior, this.Id, settingsTable, dryRun); + + // Allow the user to add providers? + ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddProvider, this.Id, settingsTable, dryRun); // // Configured providers diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs index b7cb0c18..b98fa3c7 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.HotReload.cs @@ -16,10 +16,21 @@ public static partial class PluginFactory try { HOT_RELOAD_WATCHER.IncludeSubdirectories = true; - HOT_RELOAD_WATCHER.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName; - HOT_RELOAD_WATCHER.Filter = "*.lua"; + HOT_RELOAD_WATCHER.NotifyFilter = NotifyFilters.CreationTime + | NotifyFilters.DirectoryName + | NotifyFilters.FileName + | NotifyFilters.LastAccess + | NotifyFilters.LastWrite + | NotifyFilters.Size; + HOT_RELOAD_WATCHER.Changed += HotReloadEventHandler; HOT_RELOAD_WATCHER.Deleted += HotReloadEventHandler; + HOT_RELOAD_WATCHER.Created += HotReloadEventHandler; + HOT_RELOAD_WATCHER.Renamed += HotReloadEventHandler; + HOT_RELOAD_WATCHER.Error += (_, args) => + { + LOG.LogError(args.GetException(), "Error in hot reload watcher."); + }; HOT_RELOAD_WATCHER.EnableRaisingEvents = true; } catch (Exception e) @@ -39,13 +50,13 @@ public static partial class PluginFactory var changeType = args.ChangeType.ToString().ToLowerInvariant(); if (!await HOT_RELOAD_SEMAPHORE.WaitAsync(0)) { - LOG.LogInformation($"File changed ({changeType}): {args.FullPath}. Already processing another change."); + LOG.LogInformation($"File changed '{args.FullPath}' (event={changeType}). Already processing another change."); return; } try { - LOG.LogInformation($"File changed ({changeType}): {args.FullPath}. Reloading plugins..."); + LOG.LogInformation($"File changed '{args.FullPath}' (event={changeType}). Reloading plugins..."); if (File.Exists(HOT_RELOAD_LOCK_FILE)) { LOG.LogInformation("Hot reload lock file exists. Waiting for it to be released before proceeding with the reload."); diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index 244e3984..667ed867 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -1,5 +1,6 @@ using System.Text; +using AIStudio.Settings; using AIStudio.Settings.DataModel; using Lua; @@ -120,8 +121,10 @@ public static partial class PluginFactory } // + // ========================================================= // Next, we have to clean up our settings. It is possible that a configuration plugin was removed. // We have to remove the related settings as well: + // ========================================================= // var wasConfigurationChanged = false; @@ -150,23 +153,18 @@ public static partial class PluginFactory #pragma warning restore MWAIS0001 // + // ========================================================== // Check all possible settings: + // ========================================================== // - if (SETTINGS_LOCKER.GetConfigurationPluginId(x => x.UpdateBehavior) is var updateBehaviorPluginId && updateBehaviorPluginId != Guid.Empty) - { - var sourcePlugin = AVAILABLE_PLUGINS.FirstOrDefault(plugin => plugin.Id == updateBehaviorPluginId); - if (sourcePlugin is null) - { - // Remove the locked state: - SETTINGS_LOCKER.Remove(x => x.UpdateBehavior); - - // Reset the setting to the default value: - SETTINGS_MANAGER.ConfigurationData.App.UpdateBehavior = UpdateBehavior.HOURLY; - - LOG.LogWarning($"The configured update behavior is based on a plugin that is not available anymore. Resetting the setting to the default value: {SETTINGS_MANAGER.ConfigurationData.App.UpdateBehavior}."); - wasConfigurationChanged = true; - } - } + + // Check for updates, and if so, how often? + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.UpdateBehavior, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + + // Allow the user to add providers? + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.AllowUserToAddProvider, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; if (wasConfigurationChanged) { @@ -225,7 +223,7 @@ public static partial class PluginFactory case PluginType.CONFIGURATION: var configPlug = new PluginConfiguration(isInternal, state, type); - await configPlug.InitializeAsync(); + await configPlug.InitializeAsync(true); return configPlug; default: diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs index 983b84da..0943a48e 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Starting.cs @@ -103,7 +103,7 @@ public static partial class PluginFactory languagePlugin.SetBaseLanguage(BASE_LANGUAGE_PLUGIN); if(plugin is PluginConfiguration configPlugin) - await configPlugin.InitializeAsync(); + await configPlugin.InitializeAsync(false); LOG.LogInformation($"Successfully started plugin: Id='{plugin.Id}', Type='{plugin.Type}', Name='{plugin.Name}', Version='{plugin.Version}'"); return plugin; diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs index 2d7b38b0..1fd30fb2 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs @@ -6,7 +6,6 @@ public static partial class PluginFactory { private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginFactory)); private static readonly SettingsManager SETTINGS_MANAGER = Program.SERVICE_PROVIDER.GetRequiredService(); - private static readonly SettingsLocker SETTINGS_LOCKER = Program.SERVICE_PROVIDER.GetRequiredService(); private static bool IS_INITIALIZED; private static string DATA_DIR = string.Empty; diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.50.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.50.md index cf1aee39..4973dca4 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v0.9.50.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.50.md @@ -1,3 +1,5 @@ # v0.9.50, build 225 (2025-07-xx xx:xx UTC) - Added an option for chat templates to predefine a user input. -- Added the ability to create chat templates from existing chats. \ No newline at end of file +- Added the ability to create chat templates from existing chats. +- Added an enterprise IT configuration option to prevent manual addition of LLM providers in managed environments. +- Improved hot reloading on Unix-like systems when entire plugins were added or removed. \ No newline at end of file From 96c1c0653d753adafc9e26d91628d5356fd2e343 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 9 Aug 2025 20:04:50 +0200 Subject: [PATCH 2/4] Fixed Windows pipeline (#529) --- .github/workflows/build-and-release.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index d1cc72fe..8d1d8de4 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -362,7 +362,10 @@ jobs: $PDFIUM_URL = "https://github.com/bblanchon/pdfium-binaries/releases/download/chromium%2F$($env:PDFIUM_VERSION)/pdfium-$PDFIUM_FILE" Write-Host "Download $PDFIUM_URL ..." - $TMP = New-TemporaryFile | Split-Path + + # Create a unique temporary directory (not just a file) + $TMP = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName()) + New-Item -ItemType Directory -Path $TMP -Force | Out-Null $ARCHIVE = Join-Path $TMP "pdfium.tgz" Invoke-WebRequest -Uri $PDFIUM_URL -OutFile $ARCHIVE @@ -380,8 +383,15 @@ jobs: Copy-Item -Path $SRC -Destination $DEST -Force Write-Host "Cleaning up ..." - Remove-Item $ARCHIVE -Force - Remove-Item $TMP -Recurse -Force + Remove-Item $ARCHIVE -Force -ErrorAction SilentlyContinue + + # Try to remove the temporary directory, but ignore errors if files are still in use + try { + Remove-Item $TMP -Recurse -Force -ErrorAction Stop + Write-Host "Successfully cleaned up temporary directory: $TMP" + } catch { + Write-Warning "Could not fully clean up temporary directory: $TMP. This is usually harmless as Windows will clean it up later. Error: $($_.Exception.Message)" + } - name: Build .NET project run: | From 7d22b77d47a97b56ad791948a8b71c7b5e033498 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 9 Aug 2025 20:28:40 +0200 Subject: [PATCH 3/4] Updated Git ignore (#530) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 8ac42484..81a01256 100644 --- a/.gitignore +++ b/.gitignore @@ -156,3 +156,6 @@ orleans.codegen.cs **/.idea/**/uiDesigner.xml **/.idea/**/dbnavigator.xml **/.vs + +# Ignore AI plugin config files: +/app/.idea/.idea.MindWork AI Studio/.idea/AugmentWebviewStateStore.xml From fe2baa8c00b7d47f15ecc3cb1b0efac4beeff7aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peer=20Sch=C3=BCtt?= Date: Sun, 10 Aug 2025 15:59:44 +0200 Subject: [PATCH 4/4] Hide the enterprise configuration details by default (#523) Co-authored-by: Thorsten Sommer --- .../Assistants/I18N/allTexts.lua | 58 +++++++--- .../Chat/ContentBlockComponent.razor | 5 +- .../Chat/ContentBlockComponent.razor.cs | 30 ----- .../Components/MudCopyClipboardButton.razor | 3 + .../MudCopyClipboardButton.razor.cs | 90 +++++++++++++++ app/MindWork AI Studio/Pages/About.razor | 103 ++++++++++++++++-- app/MindWork AI Studio/Pages/About.razor.cs | 68 ++++++++---- .../plugin.lua | 58 +++++++--- .../plugin.lua | 58 +++++++--- .../wwwroot/changelog/v0.9.50.md | 1 + 10 files changed, 364 insertions(+), 110 deletions(-) create mode 100644 app/MindWork AI Studio/Components/MudCopyClipboardButton.razor create mode 100644 app/MindWork AI Studio/Components/MudCopyClipboardButton.razor.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 942a744f..f40ea97d 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -1315,9 +1315,6 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T601166687"] = "AI" -- Edit Message UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1183581066"] = "Edit Message" --- Copies the content to the clipboard -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T12948066"] = "Copies the content to the clipboard" - -- Do you really want to remove this message? UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Do you really want to remove this message?" @@ -1351,9 +1348,6 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3587744975"] = "Regener -- Do you really want to regenerate this message? UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3878878761"] = "Do you really want to regenerate this message?" --- Cannot copy this content type to clipboard! -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4021525742"] = "Cannot copy this content type to clipboard!" - -- Remove Message UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Remove Message" @@ -1591,6 +1585,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T372007989"] = "Relying on we -- Cross-Platform and Modern Development UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T843057510"] = "Cross-Platform and Modern Development" +-- Copies the content to the clipboard +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T12948066"] = "Copies the content to the clipboard" + +-- Cannot copy this content type to clipboard. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T3937637647"] = "Cannot copy this content type to clipboard." + -- Alpha phase means that we are working on the last details before the beta phase. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PREVIEWALPHA::T166807685"] = "Alpha phase means that we are working on the last details before the beta phase." @@ -4186,8 +4186,11 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1020427799"] = "About MindWork AI Stud -- Browse AI Studio's source code on GitHub — we welcome your contributions. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1107156991"] = "Browse AI Studio's source code on GitHub — we welcome your contributions." --- AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is not yet available. -UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1297057566"] = "AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is not yet available." +-- This is a private AI Studio installation. It runs without an enterprise configuration. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1209549230"] = "This is a private AI Studio installation. It runs without an enterprise configuration." + +-- AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is not yet available. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1282228996"] = "AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is not yet available." -- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat." @@ -4195,12 +4198,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1388816916"] = "This library is used t -- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1421513382"] = "This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library." --- AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is active. -UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1454889560"] = "AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is active." - --- AI Studio runs with an enterprise configuration using the configuration plugin '{0}', without central configuration management. -UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1530477579"] = "AI Studio runs with an enterprise configuration using the configuration plugin '{0}', without central configuration management." - -- We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T162898512"] = "We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library." @@ -4219,6 +4216,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1806897624"] = "By clicking on the res -- Pandoc Installation UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T185447014"] = "Pandoc Installation" +-- Copies the configuration plugin ID to the clipboard +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1859295819"] = "Copies the configuration plugin ID to the clipboard" + -- Check for updates UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1890416390"] = "Check for updates" @@ -4234,21 +4234,30 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1924365263"] = "This library is used t -- We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1943216839"] = "We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust." +-- Copies the server URL to the clipboard +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2037899437"] = "Copies the server URL to the clipboard" + -- This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2173617769"] = "This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file." -- For the secure communication between the user interface and the runtime, we need to create certificates. This Rust library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2174764529"] = "For the secure communication between the user interface and the runtime, we need to create certificates. This Rust library is great for this purpose." --- AI Studio runs without an enterprise configuration. -UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2244723851"] = "AI Studio runs without an enterprise configuration." - -- OK UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2246359087"] = "OK" +-- Configuration server: +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2272122662"] = "Configuration server:" + -- We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2273492381"] = "We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose." +-- AI Studio runs with an enterprise configuration using a configuration plugin, without central configuration management. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2280402765"] = "AI Studio runs with an enterprise configuration using a configuration plugin, without central configuration management." + +-- Configuration plugin ID: +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2301484629"] = "Configuration plugin ID:" + -- The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2329884315"] = "The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK." @@ -4288,6 +4297,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2765814390"] = "Determine Pandoc versi -- Code in the Rust language can be specified as synchronous or asynchronous. Unlike .NET and the C# language, Rust cannot execute asynchronous code by itself. Rust requires support in the form of an executor for this. Tokio is one such executor. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2777988282"] = "Code in the Rust language can be specified as synchronous or asynchronous. Unlike .NET and the C# language, Rust cannot execute asynchronous code by itself. Rust requires support in the form of an executor for this. Tokio is one such executor." +-- Show Details +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T27924674"] = "Show Details" + -- View our project roadmap and help shape AI Studio's future development. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2829971158"] = "View our project roadmap and help shape AI Studio's future development." @@ -4303,12 +4315,18 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2868174483"] = "The .NET backend canno -- Changelog UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3017574265"] = "Changelog" +-- Enterprise configuration ID: +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3092349641"] = "Enterprise configuration ID:" + -- Connect AI Studio to your organization's data with our External Retrieval Interface (ERI). UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T313276297"] = "Connect AI Studio to your organization's data with our External Retrieval Interface (ERI)." -- Have feature ideas? Submit suggestions for future AI Studio enhancements. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3178730036"] = "Have feature ideas? Submit suggestions for future AI Studio enhancements." +-- Hide Details +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3183837919"] = "Hide Details" + -- Update Pandoc UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3249965383"] = "Update Pandoc" @@ -4333,6 +4351,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3563271893"] = "Motivation" -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat." +-- AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is active. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3741877842"] = "AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is active." + -- this version does not met the requirements UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3813932670"] = "this version does not met the requirements" @@ -4375,6 +4396,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T639371534"] = "Did you find a bug or a -- This Rust library is used to output the app's messages to the terminal. This is helpful during development and troubleshooting. This feature is initially invisible; when the app is started via the terminal, the messages become visible. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T64689067"] = "This Rust library is used to output the app's messages to the terminal. This is helpful during development and troubleshooting. This feature is initially invisible; when the app is started via the terminal, the messages become visible." +-- Copies the config ID to the clipboard +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T788846912"] = "Copies the config ID to the clipboard" + -- installed by AI Studio UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T833849470"] = "installed by AI Studio" diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index 9f3da228..25bf3d4c 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -1,5 +1,6 @@ @using AIStudio.Tools @using MudBlazor +@using AIStudio.Components @inherits AIStudio.Components.MSGComponentBase @@ -38,9 +39,7 @@ } - - - + diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 47adb668..1f5c86b7 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -1,5 +1,4 @@ using AIStudio.Components; -using AIStudio.Tools.Services; using Microsoft.AspNetCore.Components; @@ -61,12 +60,6 @@ public partial class ContentBlockComponent : MSGComponentBase [Parameter] public Func RegenerateEnabled { get; set; } = () => false; - [Inject] - private RustService RustService { get; init; } = null!; - - [Inject] - private ISnackbar Snackbar { get; init; } = null!; - [Inject] private IDialogService DialogService { get; init; } = null!; @@ -115,29 +108,6 @@ public partial class ContentBlockComponent : MSGComponentBase } #endregion - - /// - /// Copy this block's content to the clipboard. - /// - private async Task CopyToClipboard() - { - switch (this.Type) - { - case ContentType.TEXT: - var textContent = (ContentText) this.Content; - await this.RustService.CopyText2Clipboard(this.Snackbar, textContent.Text); - break; - - default: - this.Snackbar.Add(T("Cannot copy this content type to clipboard!"), Severity.Error, config => - { - config.Icon = Icons.Material.Filled.ContentCopy; - config.IconSize = Size.Large; - config.IconColor = Color.Error; - }); - break; - } - } private string CardClasses => $"my-2 rounded-lg {this.Class}"; diff --git a/app/MindWork AI Studio/Components/MudCopyClipboardButton.razor b/app/MindWork AI Studio/Components/MudCopyClipboardButton.razor new file mode 100644 index 00000000..ed7c34ea --- /dev/null +++ b/app/MindWork AI Studio/Components/MudCopyClipboardButton.razor @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/MudCopyClipboardButton.razor.cs b/app/MindWork AI Studio/Components/MudCopyClipboardButton.razor.cs new file mode 100644 index 00000000..644034f3 --- /dev/null +++ b/app/MindWork AI Studio/Components/MudCopyClipboardButton.razor.cs @@ -0,0 +1,90 @@ +using AIStudio.Chat; +using AIStudio.Tools.PluginSystem; +using AIStudio.Tools.Services; +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +public partial class MudCopyClipboardButton : ComponentBase +{ + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MudCopyClipboardButton).Namespace, nameof(MudCopyClipboardButton)); + + /// + /// The string, if you want to copy a string. + /// + [Parameter] + public string StringContent { get; set; } = string.Empty; + + /// + /// The content, if you want to copy content. + /// + [Parameter] + public IContent? Content { get; init; } + + /// + /// The content type, if you want to copy Content. + /// + [Parameter] + public ContentType Type { get; init; } = ContentType.NONE; + + /// + /// The tooltip that should be shown to the user. + /// + [Parameter] + public string TooltipMessage { get; set; } = TB("Copies the content to the clipboard"); + + /// + /// The size of the button. The default size is small. + /// + [Parameter] + public Size Size { get; set; } = Size.Small; + + [Inject] + private ISnackbar Snackbar { get; init; } = null!; + + [Inject] + private RustService RustService { get; init; } = null!; + + private async Task HandleCopyClick() + { + if (this.Type is ContentType.NONE) + await this.CopyToClipboard(this.StringContent); + else + await this.CopyToClipboard(this.Content); + } + + /// + /// Copy this the string to the clipboard. + /// + private async Task CopyToClipboard(string textContent) + { + await this.RustService.CopyText2Clipboard(this.Snackbar, textContent); + } + + /// + /// Copy this block's content to the clipboard. + /// + private async Task CopyToClipboard(IContent? contentToCopy) + { + if (contentToCopy is null) + return; + + switch (this.Type) + { + case ContentType.TEXT: + var textContent = (ContentText) contentToCopy; + await this.RustService.CopyText2Clipboard(this.Snackbar, textContent.Text); + break; + + default: + this.Snackbar.Add(TB("Cannot copy this content type to clipboard."), Severity.Error, config => + { + config.Icon = Icons.Material.Filled.ContentCopy; + config.IconSize = Size.Large; + config.IconColor = Color.Error; + }); + break; + } + } + +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Pages/About.razor b/app/MindWork AI Studio/Pages/About.razor index 04b43128..b7ecc945 100644 --- a/app/MindWork AI Studio/Pages/About.razor +++ b/app/MindWork AI Studio/Pages/About.razor @@ -1,4 +1,5 @@ @attribute [Route(Routes.ABOUT)] +@using AIStudio.Tools.Services @inherits MSGComponentBase
@@ -14,16 +15,104 @@ - - - - - + + + + + - + + @switch (EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.IsActive) + { + case false when this.configPlug is null: + + @T("This is a private AI Studio installation. It runs without an enterprise configuration.") + + break; + + case false: + + @T("AI Studio runs with an enterprise configuration using a configuration plugin, without central configuration management.") + + + +
+ + @T("Configuration plugin ID:") @this.configPlug!.Id + +
+
+
+ break; + + case true when this.configPlug is null: + + @T("AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is not yet available.") + + + +
+ + @T("Enterprise configuration ID:") @EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationId + +
+
+ + +
+ + @T("Configuration server:") @EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationServerUrl + +
+
+
+ break; + + case true: + + @T("AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is active.") + + + +
+ + @T("Enterprise configuration ID:") @EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationId + +
+
+ + +
+ + @T("Configuration server:") @EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.ConfigurationServerUrl + +
+
+ + +
+ + @T("Configuration plugin ID:") @this.configPlug!.Id + +
+
+
+ break; + } + + @if (this.HasEnterpriseConfigurationDetails) + { + + @(this.showEnterpriseConfigDetails ? T("Hide Details") : T("Show Details")) + + } +
@@ -48,7 +137,7 @@ @T("View our project roadmap and help shape AI Studio's future development.") - + @T("Did you find a bug or are you experiencing issues? Report your concern here.") diff --git a/app/MindWork AI Studio/Pages/About.razor.cs b/app/MindWork AI Studio/Pages/About.razor.cs index 8c4fe923..ecdf1d17 100644 --- a/app/MindWork AI Studio/Pages/About.razor.cs +++ b/app/MindWork AI Studio/Pages/About.razor.cs @@ -58,6 +58,35 @@ public partial class About : MSGComponentBase private GetLogPathsResponse logPaths; + private bool showEnterpriseConfigDetails; + + private IPluginMetadata? configPlug = PluginFactory.AvailablePlugins.FirstOrDefault(x => x.Type is PluginType.CONFIGURATION); + + /// + /// Determines whether the enterprise configuration has details that can be shown/hidden. + /// Returns true if there are details available, false otherwise. + /// + private bool HasEnterpriseConfigurationDetails + { + get + { + return EnterpriseEnvironmentService.CURRENT_ENVIRONMENT.IsActive switch + { + // Case 1: No enterprise config and no plugin - no details available + false when this.configPlug is null => false, + + // Case 2: Enterprise config with plugin but no central management - has details + false => true, + + // Case 3: Enterprise config active but no plugin - has details + true when this.configPlug is null => true, + + // Case 4: Enterprise config active with plugin - has details + true => true + }; + } + } + #region Overrides of ComponentBase protected override async Task OnInitializedAsync() @@ -74,6 +103,23 @@ public partial class About : MSGComponentBase #endregion + #region Overrides of MSGComponentBase + + protected override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default + { + switch (triggeredEvent) + { + case Event.PLUGINS_RELOADED: + this.configPlug = PluginFactory.AvailablePlugins.FirstOrDefault(x => x.Type is PluginType.CONFIGURATION); + await this.InvokeAsync(this.StateHasChanged); + break; + } + + await base.ProcessIncomingMessage(sendingComponent, triggeredEvent, data); + } + + #endregion + private async Task DeterminePandocVersion() { this.pandocInstallation = await Pandoc.CheckAvailabilityAsync(this.RustService, false); @@ -119,26 +165,10 @@ public partial class About : MSGComponentBase await dialogReference.Result; await this.DeterminePandocVersion(); } - - private string GetEnterpriseEnvironment() + + private void ToggleEnterpriseConfigDetails() { - var configPlug = PluginFactory.AvailablePlugins.FirstOrDefault(x => x.Type is PluginType.CONFIGURATION); - var currentEnvironment = EnterpriseEnvironmentService.CURRENT_ENVIRONMENT; - - switch (currentEnvironment) - { - case { IsActive: false } when configPlug is null: - return T("AI Studio runs without an enterprise configuration."); - - case { IsActive: false }: - return string.Format(T("AI Studio runs with an enterprise configuration using the configuration plugin '{0}', without central configuration management."), configPlug.Id); - - case { IsActive: true } when configPlug is null: - return string.Format(T("AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is not yet available."), currentEnvironment.ConfigurationId, currentEnvironment.ConfigurationServerUrl); - - case { IsActive: true }: - return string.Format(T("AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is active."), currentEnvironment.ConfigurationId, currentEnvironment.ConfigurationServerUrl); - } + this.showEnterpriseConfigDetails = !this.showEnterpriseConfigDetails; } private async Task CopyStartupLogPath() 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 042978b0..94f95f3e 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 @@ -1317,9 +1317,6 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T601166687"] = "KI" -- Edit Message UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1183581066"] = "Nachricht bearbeiten" --- Copies the content to the clipboard -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T12948066"] = "Kopiert den Inhalt in die Zwischenablage" - -- Do you really want to remove this message? UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Möchten Sie diese Nachricht wirklich löschen?" @@ -1353,9 +1350,6 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3587744975"] = "Neu gen -- Do you really want to regenerate this message? UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3878878761"] = "Möchten Sie diese Nachricht wirklich neu generieren?" --- Cannot copy this content type to clipboard! -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4021525742"] = "Dieser Inhaltstyp kann nicht in die Zwischenablage kopiert werden!" - -- Remove Message UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Nachricht entfernen" @@ -1593,6 +1587,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T372007989"] = "Sich auf Webd -- Cross-Platform and Modern Development UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T843057510"] = "Plattformübergreifende und moderne Entwicklung" +-- Copies the content to the clipboard +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T12948066"] = "Kopiert den Inhalt in die Zwischenablage" + +-- Cannot copy this content type to clipboard. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T3937637647"] = "Dieser Inhaltstyp kann nicht in die Zwischenablage kopiert werden." + -- Alpha phase means that we are working on the last details before the beta phase. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PREVIEWALPHA::T166807685"] = "Alpha-Phase bedeutet, dass wir an den letzten Details arbeiten, bevor die Beta-Phase beginnt." @@ -4188,8 +4188,11 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1020427799"] = "Über MindWork AI Stud -- Browse AI Studio's source code on GitHub — we welcome your contributions. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1107156991"] = "Sehen Sie sich den Quellcode von AI Studio auf GitHub an – wir freuen uns über ihre Beiträge." --- AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is not yet available. -UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1297057566"] = "AI Studio läuft mit der Konfigurations-ID '{0}' ihrer Organisation und dem Konfigurationsserver '{1}'. Das Konfigurations-Plugin ist noch nicht verfügbar." +-- This is a private AI Studio installation. It runs without an enterprise configuration. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1209549230"] = "Dies ist eine private AI Studio-Installation. Sie läuft ohne Unternehmenskonfiguration." + +-- AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is not yet available. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1282228996"] = "AI Studio läuft mit einer Unternehmenskonfiguration und einem Konfigurationsserver. Das Konfigurations-Plugin ist noch nicht verfügbar." -- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1388816916"] = "Diese Bibliothek wird verwendet, um PDF-Dateien zu lesen. Das ist zum Beispiel notwendig, um PDFs als Datenquelle für einen Chat zu nutzen." @@ -4197,12 +4200,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1388816916"] = "Diese Bibliothek wird -- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1421513382"] = "Diese Bibliothek wird verwendet, um die MudBlazor-Bibliothek zu erweitern. Sie stellt zusätzliche Komponenten bereit, die nicht Teil der MudBlazor-Bibliothek sind." --- AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is active. -UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1454889560"] = "AI Studio läuft mit der Konfigurations-ID '{0}' ihrer Organisation und dem Konfigurationsserver '{1}'. Das Konfigurations-Plugin ist aktiv." - --- AI Studio runs with an enterprise configuration using the configuration plugin '{0}', without central configuration management. -UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1530477579"] = "AI Studio läuft mit einer Unternehmenseinstellung und verwendet das Konfigurations-Plugin '{0}', jedoch ohne zentrale Konfigurationsverwaltung." - -- We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T162898512"] = "Wir verwenden Lua als Sprache für Plugins. Lua-CSharp ermöglicht die Kommunikation zwischen Lua-Skripten und AI Studio in beide Richtungen. Vielen Dank an Yusuke Nakada für diese großartige Bibliothek." @@ -4221,6 +4218,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1806897624"] = "Wenn Sie auf den jewei -- Pandoc Installation UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T185447014"] = "Pandoc-Installation" +-- Copies the configuration plugin ID to the clipboard +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1859295819"] = "Kopiert die Konfigurations-Plugin-ID in die Zwischenablage" + -- Check for updates UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1890416390"] = "Nach Updates suchen" @@ -4236,21 +4236,30 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1924365263"] = "Diese Bibliothek wird -- We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1943216839"] = "Wir verwenden Rocket zur Implementierung der Runtime-API. Dies ist notwendig, da die Runtime mit der Benutzeroberfläche (IPC) kommunizieren muss. Rocket ist ein ausgezeichnetes Framework zur Umsetzung von Web-APIs in Rust." +-- Copies the server URL to the clipboard +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2037899437"] = "Kopiert die Server-URL in die Zwischenablage" + -- This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2173617769"] = "Diese Bibliothek wird verwendet, um den Dateityp einer Datei zu bestimmen. Das ist zum Beispiel notwendig, wenn wir eine Datei streamen möchten." -- For the secure communication between the user interface and the runtime, we need to create certificates. This Rust library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2174764529"] = "Für die sichere Kommunikation zwischen der Benutzeroberfläche und der Laufzeit müssen wir Zertifikate erstellen. Diese Rust-Bibliothek eignet sich hervorragend dafür." --- This is a private AI Studio installation. It runs without an enterprise configuration. -UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2244723851"] = "Dies ist eine private AI Studio-Installation. Es wird keine Konfiguration einer Organisation verwendet." - -- OK UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2246359087"] = "OK" +-- Configuration server: +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2272122662"] = "Konfigurationsserver:" + -- We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2273492381"] = "Wir müssen Zufallszahlen erzeugen, z. B. um die Kommunikation zwischen der Benutzeroberfläche und der Laufzeitumgebung abzusichern. Die rand-Bibliothek eignet sich dafür hervorragend." +-- AI Studio runs with an enterprise configuration using a configuration plugin, without central configuration management. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2280402765"] = "AI Studio läuft mit einer Unternehmenskonfiguration über ein Konfigurations-Plugin, ohne zentrale Konfigurationsverwaltung." + +-- Configuration plugin ID: +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2301484629"] = "Konfigurations-Plugin-ID:" + -- The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2329884315"] = "Die Programmiersprache C# wird für die Umsetzung der Benutzeroberfläche und des Backends verwendet. Für die Entwicklung der Benutzeroberfläche mit C# kommt die Blazor-Technologie aus ASP.NET Core zum Einsatz. Alle diese Technologien sind im .NET SDK integriert." @@ -4290,6 +4299,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2765814390"] = "Pandoc-Version wird er -- Code in the Rust language can be specified as synchronous or asynchronous. Unlike .NET and the C# language, Rust cannot execute asynchronous code by itself. Rust requires support in the form of an executor for this. Tokio is one such executor. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2777988282"] = "Code in der Programmiersprache Rust kann als synchron oder asynchron spezifiziert werden. Im Gegensatz zu .NET und der Sprache C# kann Rust asynchronen Code jedoch nicht von selbst ausführen. Dafür benötigt Rust Unterstützung in Form eines Executors. Tokio ist ein solcher Executor." +-- Show Details +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T27924674"] = "Details anzeigen" + -- View our project roadmap and help shape AI Studio's future development. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2829971158"] = "Sehen Sie sich unsere Roadmap an und helfen Sie mit, die zukünftige Entwicklung von AI Studio mitzugestalten." @@ -4305,12 +4317,18 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2868174483"] = "Das .NET-Backend kann -- Changelog UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3017574265"] = "Änderungsprotokoll" +-- Enterprise configuration ID: +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3092349641"] = "Unternehmenskonfigurations-ID:" + -- Connect AI Studio to your organization's data with our External Retrieval Interface (ERI). UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T313276297"] = "Verbinden Sie AI Studio mit den Daten ihrer Organisation über unsere Schnittstelle für externe Datenabfrage (ERI)." -- Have feature ideas? Submit suggestions for future AI Studio enhancements. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3178730036"] = "Haben Sie Ideen für neue Funktionen? Senden Sie uns Vorschläge für zukünftige Verbesserungen von AI Studio." +-- Hide Details +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3183837919"] = "Details ausblenden" + -- Update Pandoc UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3249965383"] = "Pandoc aktualisieren" @@ -4335,6 +4353,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3563271893"] = "Motivation" -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3722989559"] = "Diese Bibliothek wird verwendet, um Excel- und OpenDocument-Tabellendateien zu lesen. Dies ist zum Beispiel notwendig, wenn Tabellen als Datenquelle für einen Chat verwendet werden sollen." +-- AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is active. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3741877842"] = "AI Studio läuft mit einer Unternehmenskonfiguration und einem Konfigurationsserver. Das Konfigurations-Plugin ist aktiv." + -- this version does not met the requirements UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3813932670"] = "diese Version erfüllt die Anforderungen nicht" @@ -4377,6 +4398,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T639371534"] = "Haben Sie einen Fehler -- This Rust library is used to output the app's messages to the terminal. This is helpful during development and troubleshooting. This feature is initially invisible; when the app is started via the terminal, the messages become visible. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T64689067"] = "Diese Rust-Bibliothek wird verwendet, um die Nachrichten der App im Terminal auszugeben. Das ist während der Entwicklung und Fehlersuche hilfreich. Diese Funktion ist zunächst unsichtbar; werden App über das Terminal gestartet, werden die Nachrichten sichtbar." +-- Copies the config ID to the clipboard +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T788846912"] = "Kopiert die Konfigurations-ID in die Zwischenablage" + -- installed by AI Studio UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T833849470"] = "installiert von AI Studio" 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 45aa0caa..c0b8aebe 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 @@ -1317,9 +1317,6 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CHATROLEEXTENSIONS::T601166687"] = "AI" -- Edit Message UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1183581066"] = "Edit Message" --- Copies the content to the clipboard -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T12948066"] = "Copies the content to the clipboard" - -- Do you really want to remove this message? UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T1347427447"] = "Do you really want to remove this message?" @@ -1353,9 +1350,6 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3587744975"] = "Regener -- Do you really want to regenerate this message? UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T3878878761"] = "Do you really want to regenerate this message?" --- Cannot copy this content type to clipboard! -UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4021525742"] = "Cannot copy this content type to clipboard!" - -- Remove Message UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Remove Message" @@ -1593,6 +1587,12 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T372007989"] = "Relying on we -- Cross-Platform and Modern Development UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T843057510"] = "Cross-Platform and Modern Development" +-- Copies the content to the clipboard +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T12948066"] = "Copies the content to the clipboard" + +-- Cannot copy this content type to clipboard. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MUDCOPYCLIPBOARDBUTTON::T3937637647"] = "Cannot copy this content type to clipboard." + -- Alpha phase means that we are working on the last details before the beta phase. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PREVIEWALPHA::T166807685"] = "Alpha phase means that we are working on the last details before the beta phase." @@ -4188,8 +4188,11 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1020427799"] = "About MindWork AI Stud -- Browse AI Studio's source code on GitHub — we welcome your contributions. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1107156991"] = "Browse AI Studio's source code on GitHub — we welcome your contributions." --- AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is not yet available. -UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1297057566"] = "AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is not yet available." +-- This is a private AI Studio installation. It runs without an enterprise configuration. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1209549230"] = "This is a private AI Studio installation. It runs without an enterprise configuration." + +-- AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is not yet available. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1282228996"] = "AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is not yet available." -- This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat." @@ -4197,12 +4200,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1388816916"] = "This library is used t -- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1421513382"] = "This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library." --- AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is active. -UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1454889560"] = "AI Studio runs with an enterprise configuration id '{0}' and configuration server URL '{1}'. The configuration plugin is active." - --- AI Studio runs with an enterprise configuration using the configuration plugin '{0}', without central configuration management. -UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1530477579"] = "AI Studio runs with an enterprise configuration using the configuration plugin '{0}', without central configuration management." - -- We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T162898512"] = "We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library." @@ -4221,6 +4218,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1806897624"] = "By clicking on the res -- Pandoc Installation UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T185447014"] = "Pandoc Installation" +-- Copies the configuration plugin ID to the clipboard +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1859295819"] = "Copies the configuration plugin ID to the clipboard" + -- Check for updates UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1890416390"] = "Check for updates" @@ -4236,21 +4236,30 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1924365263"] = "This library is used t -- We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T1943216839"] = "We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust." +-- Copies the server URL to the clipboard +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2037899437"] = "Copies the server URL to the clipboard" + -- This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2173617769"] = "This library is used to determine the file type of a file. This is necessary, e.g., when we want to stream a file." -- For the secure communication between the user interface and the runtime, we need to create certificates. This Rust library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2174764529"] = "For the secure communication between the user interface and the runtime, we need to create certificates. This Rust library is great for this purpose." --- This is a private AI Studio installation. It runs without an enterprise configuration. -UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2244723851"] = "This is a private AI Studio installation. It runs without an enterprise configuration." - -- OK UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2246359087"] = "OK" +-- Configuration server: +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2272122662"] = "Configuration server:" + -- We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2273492381"] = "We must generate random numbers, e.g., for securing the interprocess communication between the user interface and the runtime. The rand library is great for this purpose." +-- AI Studio runs with an enterprise configuration using a configuration plugin, without central configuration management. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2280402765"] = "AI Studio runs with an enterprise configuration using a configuration plugin, without central configuration management." + +-- Configuration plugin ID: +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2301484629"] = "Configuration plugin ID:" + -- The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2329884315"] = "The C# language is used for the implementation of the user interface and the backend. To implement the user interface with C#, the Blazor technology from ASP.NET Core is used. All these technologies are integrated into the .NET SDK." @@ -4290,6 +4299,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2765814390"] = "Determine Pandoc versi -- Code in the Rust language can be specified as synchronous or asynchronous. Unlike .NET and the C# language, Rust cannot execute asynchronous code by itself. Rust requires support in the form of an executor for this. Tokio is one such executor. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2777988282"] = "Code in the Rust language can be specified as synchronous or asynchronous. Unlike .NET and the C# language, Rust cannot execute asynchronous code by itself. Rust requires support in the form of an executor for this. Tokio is one such executor." +-- Show Details +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T27924674"] = "Show Details" + -- View our project roadmap and help shape AI Studio's future development. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2829971158"] = "View our project roadmap and help shape AI Studio's future development." @@ -4305,12 +4317,18 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T2868174483"] = "The .NET backend canno -- Changelog UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3017574265"] = "Changelog" +-- Enterprise configuration ID: +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3092349641"] = "Enterprise configuration ID:" + -- Connect AI Studio to your organization's data with our External Retrieval Interface (ERI). UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T313276297"] = "Connect AI Studio to your organization's data with our External Retrieval Interface (ERI)." -- Have feature ideas? Submit suggestions for future AI Studio enhancements. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3178730036"] = "Have feature ideas? Submit suggestions for future AI Studio enhancements." +-- Hide Details +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3183837919"] = "Hide Details" + -- Update Pandoc UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3249965383"] = "Update Pandoc" @@ -4335,6 +4353,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3563271893"] = "Motivation" -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat." +-- AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is active. +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3741877842"] = "AI Studio runs with an enterprise configuration and a configuration server. The configuration plugin is active." + -- this version does not met the requirements UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T3813932670"] = "this version does not met the requirements" @@ -4377,6 +4398,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T639371534"] = "Did you find a bug or a -- This Rust library is used to output the app's messages to the terminal. This is helpful during development and troubleshooting. This feature is initially invisible; when the app is started via the terminal, the messages become visible. UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T64689067"] = "This Rust library is used to output the app's messages to the terminal. This is helpful during development and troubleshooting. This feature is initially invisible; when the app is started via the terminal, the messages become visible." +-- Copies the config ID to the clipboard +UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T788846912"] = "Copies the config ID to the clipboard" + -- installed by AI Studio UI_TEXT_CONTENT["AISTUDIO::PAGES::ABOUT::T833849470"] = "installed by AI Studio" diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.50.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.50.md index 4973dca4..aa143b57 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v0.9.50.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.50.md @@ -2,4 +2,5 @@ - Added an option for chat templates to predefine a user input. - Added the ability to create chat templates from existing chats. - Added an enterprise IT configuration option to prevent manual addition of LLM providers in managed environments. +- Improved the display of enterprise configurations on the about page; configuration details are only shown when needed. - Improved hot reloading on Unix-like systems when entire plugins were added or removed. \ No newline at end of file