From 4d09f5ad433373295b6afaf05f807c5bb1c7f926 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 9 Apr 2026 14:01:41 +0200 Subject: [PATCH 01/15] Initial version --- .../Assistants/I18N/allTexts.lua | 44 +++++++-- .../Components/MandatoryInfoDisplay.razor | 42 ++++++++ .../Components/MandatoryInfoDisplay.razor.cs | 20 ++++ .../Dialogs/DialogOptions.cs | 6 ++ .../Dialogs/MandatoryInfoDialog.razor | 25 +++++ .../Dialogs/MandatoryInfoDialog.razor.cs | 19 ++++ .../Layout/MainLayout.razor.cs | 90 ++++++++++++++++- .../Pages/Information.razor | 13 +++ .../Pages/Information.razor.cs | 18 +++- .../Plugins/configuration/plugin.lua | 24 +++++ .../Settings/DataModel/Data.cs | 2 + .../Settings/DataModel/DataMandatoryInfo.cs | 96 +++++++++++++++++++ .../DataModel/DataMandatoryInfoAcceptance.cs | 24 +++++ .../DataModel/DataMandatoryInformation.cs | 24 +++++ .../Tools/PluginSystem/PluginConfiguration.cs | 32 +++++++ .../PluginSystem/PluginFactory.Loading.cs | 4 + .../Tools/PluginSystem/PluginFactory.cs | 9 ++ .../Tools/Rust/AppExitResponse.cs | 3 + .../Tools/Services/RustService.App.cs | 33 +++++++ runtime/src/app_window.rs | 35 +++++++ runtime/src/runtime_api.rs | 1 + 21 files changed, 551 insertions(+), 13 deletions(-) create mode 100644 app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor create mode 100644 app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor.cs create mode 100644 app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor create mode 100644 app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor.cs create mode 100644 app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs create mode 100644 app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfoAcceptance.cs create mode 100644 app/MindWork AI Studio/Settings/DataModel/DataMandatoryInformation.cs create mode 100644 app/MindWork AI Studio/Tools/Rust/AppExitResponse.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index c94b4b7ae..f0e307e81 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -2002,6 +2002,24 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANAGEPANDOCDEPENDENCY::T527187983"] = "C -- Install Pandoc UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANAGEPANDOCDEPENDENCY::T986578435"] = "Install Pandoc" +-- Version +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T1573770551"] = "Version" + +-- This mandatory info has not been accepted yet. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T1870532312"] = "This mandatory info has not been accepted yet." + +-- Accepted version +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T203086476"] = "Accepted version" + +-- The current version has not been accepted yet. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T2158667957"] = "The current version has not been accepted yet." + +-- Last accepted version +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T3407978086"] = "Last accepted version" + +-- Accepted at (UTC) +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T3511160492"] = "Accepted at (UTC)" + -- Given that my employer's workplace uses both Windows and Linux, I wanted a cross-platform solution that would work seamlessly across all major operating systems, including macOS. Additionally, I wanted to demonstrate that it is possible to create modern, efficient, cross-platform applications without resorting to Electron bloatware. The combination of .NET and Rust with Tauri proved to be an excellent technology stack for building such robust applications. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1057189794"] = "Given that my employer's workplace uses both Windows and Linux, I wanted a cross-platform solution that would work seamlessly across all major operating systems, including macOS. Additionally, I wanted to demonstrate that it is possible to create modern, efficient, cross-platform applications without resorting to Electron bloatware. The combination of .NET and Rust with Tauri proved to be an excellent technology stack for building such robust applications." @@ -4696,6 +4714,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T13933 -- Preselect aspects for the LLM to focus on when generating slides, such as bullet points or specific topics to emphasize. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1528169602"] = "Preselect aspects for the LLM to focus on when generating slides, such as bullet points or specific topics to emphasize." +-- Slide Planner Assistant options are preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1549358578"] = "Slide Planner Assistant options are preselected" + +-- No Slide Planner Assistant options are preselected +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1694374279"] = "No Slide Planner Assistant options are preselected" + -- Choose whether the assistant should use the app default profile, no profile, or a specific profile. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile." @@ -4705,9 +4729,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T20146 -- Which audience organizational level should be preselected? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T216511105"] = "Which audience organizational level should be preselected?" --- Preselect Slide Planner Assistant options? -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T227645894"] = "Preselect Slide Planner Assistant options?" - -- Preselect a profile UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2322771068"] = "Preselect a profile" @@ -4724,26 +4745,23 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T25714 UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2645589441"] = "Preselect the audience age group" -- Assistant: Slide Planner Assistant Options -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3215549988"] = "Assistant: Slide Planner Assistant Options" +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3226042276"] = "Assistant: Slide Planner Assistant Options" -- Which audience expertise should be preselected? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3228597992"] = "Which audience expertise should be preselected?" +-- Preselect Slide Planner Assistant options? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T339924858"] = "Preselect Slide Planner Assistant options?" + -- Close UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3448155331"] = "Close" -- Preselect important aspects UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3705987833"] = "Preselect important aspects" --- No Slide Planner Assistant options are preselected -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T4214398691"] = "No Slide Planner Assistant options are preselected" - -- Preselect the audience profile UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T861397972"] = "Preselect the audience profile" --- Slide Planner Assistant options are preselected -UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T93124146"] = "Slide Planner Assistant options are preselected" - -- Which audience age group should be preselected? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T956845877"] = "Which audience age group should be preselected?" @@ -5404,6 +5422,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID mismatch: the -- This is a private AI Studio installation. It runs without an enterprise configuration. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "This is a private AI Studio installation. It runs without an enterprise configuration." +-- Unknown configuration plugin +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unknown configuration plugin" + -- 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::INFORMATION::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat." @@ -5653,6 +5674,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T788846912"] = "Copies the config -- installed by AI Studio UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T833849470"] = "installed by AI Studio" +-- Provided by configuration plugin: {0} +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T836298648"] = "Provided by configuration plugin: {0}" + -- We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate." diff --git a/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor b/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor new file mode 100644 index 000000000..ac6713478 --- /dev/null +++ b/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor @@ -0,0 +1,42 @@ +@inherits MSGComponentBase + + + @if (this.ShowTitle) + { + @this.Info.Title + } + + + @T("Version"): @this.Info.VersionText + + + @if (this.ShowAcceptanceMetadata) + { + @if (this.Acceptance is null) + { + + @T("This mandatory info has not been accepted yet.") + + } + else if (!string.Equals(this.Acceptance.AcceptedVersion, this.Info.VersionText, StringComparison.Ordinal)) + { + + @T("The current version has not been accepted yet.") +
+ @T("Last accepted version"): @this.Acceptance.AcceptedVersion +
+ @T("Accepted at (UTC)"): @this.Acceptance.AcceptedAtUtc.UtcDateTime.ToString("u") +
+ } + else + { + + @T("Accepted version"): @this.Acceptance.AcceptedVersion +
+ @T("Accepted at (UTC)"): @this.Acceptance.AcceptedAtUtc.UtcDateTime.ToString("u") +
+ } + } + + +
\ No newline at end of file diff --git a/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor.cs b/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor.cs new file mode 100644 index 000000000..f2ac014ca --- /dev/null +++ b/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor.cs @@ -0,0 +1,20 @@ +using AIStudio.Settings.DataModel; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +public partial class MandatoryInfoDisplay +{ + [Parameter] + public DataMandatoryInfo Info { get; set; } = new(); + + [Parameter] + public DataMandatoryInfoAcceptance? Acceptance { get; set; } + + [Parameter] + public bool ShowTitle { get; set; } = true; + + [Parameter] + public bool ShowAcceptanceMetadata { get; set; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DialogOptions.cs b/app/MindWork AI Studio/Dialogs/DialogOptions.cs index 0f8e97f4b..eda952450 100644 --- a/app/MindWork AI Studio/Dialogs/DialogOptions.cs +++ b/app/MindWork AI Studio/Dialogs/DialogOptions.cs @@ -14,4 +14,10 @@ public static class DialogOptions CloseOnEscapeKey = true, FullWidth = true, MaxWidth = MaxWidth.Medium, }; + + public static readonly MudBlazor.DialogOptions BLOCKING_FULLSCREEN = new() + { + BackdropClick = false, + CloseOnEscapeKey = false, + }; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor b/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor new file mode 100644 index 000000000..1319e9078 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor @@ -0,0 +1,25 @@ +@inherits MSGComponentBase + + + +
+ +
+
+ + + + @this.Info.RejectButtonText + + + @this.Info.AcceptButtonText + + + +
\ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor.cs b/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor.cs new file mode 100644 index 000000000..4ed3451ee --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor.cs @@ -0,0 +1,19 @@ +using AIStudio.Components; +using AIStudio.Settings.DataModel; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Dialogs; + +public partial class MandatoryInfoDialog : MSGComponentBase +{ + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] + public DataMandatoryInfo Info { get; set; } = new(); + + private void Accept() => this.MudDialog.Close(DialogResult.Ok(true)); + + private void Reject() => this.MudDialog.Close(DialogResult.Ok(false)); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index 0fc41f7cd..e46930702 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -53,6 +53,8 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan private UpdateResponse? currentUpdateResponse; private MudThemeProvider themeProvider = null!; private bool useDarkMode; + private bool startupCompleted; + private readonly SemaphoreSlim mandatoryInfoDialogSemaphore = new(1, 1); private IReadOnlyCollection navItems = []; @@ -91,8 +93,8 @@ protected override async Task OnInitializedAsync() this.MessageBus.ApplyFilters(this, [], [ Event.UPDATE_AVAILABLE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED, Event.SHOW_ERROR, - Event.SHOW_ERROR, Event.SHOW_WARNING, Event.SHOW_SUCCESS, Event.STARTUP_PLUGIN_SYSTEM, - Event.PLUGINS_RELOADED, Event.INSTALL_UPDATE, + Event.SHOW_WARNING, Event.SHOW_SUCCESS, Event.STARTUP_PLUGIN_SYSTEM, Event.PLUGINS_RELOADED, + Event.INSTALL_UPDATE, Event.STARTUP_COMPLETED, ]); // Set the snackbar for the update service: @@ -174,6 +176,8 @@ await this.InvokeAsync(async () => await this.UpdateThemeConfiguration(); this.LoadNavItems(); this.StateHasChanged(); + if (this.startupCompleted) + _ = this.EnsureMandatoryInfosAcceptedAsync(); break; case Event.COLOR_THEME_CHANGED: @@ -261,6 +265,13 @@ await this.InvokeAsync(async () => this.LoadNavItems(); await this.InvokeAsync(this.StateHasChanged); + if (this.startupCompleted) + _ = this.EnsureMandatoryInfosAcceptedAsync(); + break; + + case Event.STARTUP_COMPLETED: + this.startupCompleted = true; + _ = this.EnsureMandatoryInfosAcceptedAsync(); break; } }); @@ -368,12 +379,87 @@ private async Task UpdateThemeConfiguration() await this.MessageBus.SendMessage(this, Event.COLOR_THEME_CHANGED); this.StateHasChanged(); } + + private async Task EnsureMandatoryInfosAcceptedAsync() + { + if (!await this.mandatoryInfoDialogSemaphore.WaitAsync(0)) + return; + + try + { + while (true) + { + var pendingInfos = this.GetPendingMandatoryInfos().ToList(); + if (pendingInfos.Count == 0) + return; + + foreach (var info in pendingInfos) + { + var wasAccepted = await this.ShowMandatoryInfoDialog(info); + if (!wasAccepted) + { + await this.RustService.ExitApplication(); + return; + } + + await this.StoreMandatoryInfoAcceptance(info); + } + } + } + finally + { + this.mandatoryInfoDialogSemaphore.Release(); + } + } + + private IEnumerable GetPendingMandatoryInfos() + { + return PluginFactory.GetMandatoryInfos() + .Where(info => + { + var acceptance = this.SettingsManager.ConfigurationData.MandatoryInformation.FindAcceptance(info.Id); + return acceptance is null || acceptance.AcceptedVersion != info.VersionText; + }); + } + + private async Task ShowMandatoryInfoDialog(DataMandatoryInfo info) + { + var dialogParameters = new DialogParameters + { + { x => x.Info, info }, + }; + + var dialogReference = await this.DialogService.ShowAsync(null, dialogParameters, DialogOptions.BLOCKING_FULLSCREEN); + var dialogResult = await dialogReference.Result; + return dialogResult is { Canceled: false, Data: true }; + } + + private async Task StoreMandatoryInfoAcceptance(DataMandatoryInfo info) + { + var acceptances = this.SettingsManager.ConfigurationData.MandatoryInformation.Acceptances; + var acceptance = new DataMandatoryInfoAcceptance + { + InfoId = info.Id, + AcceptedVersion = info.VersionText, + AcceptedAtUtc = DateTimeOffset.UtcNow, + EnterpriseConfigurationPluginId = info.EnterpriseConfigurationPluginId, + }; + + var existingIndex = acceptances.FindIndex(item => item.InfoId == info.Id); + if (existingIndex >= 0) + acceptances[existingIndex] = acceptance; + else + acceptances.Add(acceptance); + + await this.SettingsManager.StoreSettings(); + } #region Implementation of IDisposable public void Dispose() { this.MessageBus.Unregister(this); + this.mandatoryInfoDialogSemaphore.Dispose(); } #endregion diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor index b7b9aea41..07ddec738 100644 --- a/app/MindWork AI Studio/Pages/Information.razor +++ b/app/MindWork AI Studio/Pages/Information.razor @@ -222,6 +222,19 @@ + + @foreach (var mandatoryInfoPanel in this.mandatoryInfoPanels) + { + + + @string.Format(T("Provided by configuration plugin: {0}"), mandatoryInfoPanel.PluginName) + + + + } diff --git a/app/MindWork AI Studio/Pages/Information.razor.cs b/app/MindWork AI Studio/Pages/Information.razor.cs index b91722174..17c1dedd4 100644 --- a/app/MindWork AI Studio/Pages/Information.razor.cs +++ b/app/MindWork AI Studio/Pages/Information.razor.cs @@ -2,6 +2,7 @@ using AIStudio.Components; using AIStudio.Dialogs; +using AIStudio.Settings.DataModel; using AIStudio.Tools.Databases; using AIStudio.Tools.Metadata; using AIStudio.Tools.PluginSystem; @@ -77,9 +78,13 @@ public partial class Information : MSGComponentBase .ToList(); private List enterpriseEnvironments = EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.ToList(); + + private List mandatoryInfoPanels = []; private sealed record DatabaseDisplayInfo(string Label, string Value); + private sealed record MandatoryInfoPanelData(string HeaderText, string PluginName, DataMandatoryInfo Info, DataMandatoryInfoAcceptance? Acceptance); + private readonly List databaseDisplayInfo = new(); private bool HasAnyActiveEnvironment => this.enterpriseEnvironments.Any(e => e.IsActive); @@ -117,7 +122,7 @@ private bool HasEnterpriseConfigurationDetails protected override async Task OnInitializedAsync() { - this.ApplyFilters([], [ Event.ENTERPRISE_ENVIRONMENTS_CHANGED ]); + this.ApplyFilters([], [ Event.ENTERPRISE_ENVIRONMENTS_CHANGED, Event.CONFIGURATION_CHANGED ]); await base.OnInitializedAsync(); this.RefreshEnterpriseConfigurationState(); @@ -145,6 +150,7 @@ protected override async Task OnInitializedAsync() { case Event.PLUGINS_RELOADED: case Event.ENTERPRISE_ENVIRONMENTS_CHANGED: + case Event.CONFIGURATION_CHANGED: this.RefreshEnterpriseConfigurationState(); await this.InvokeAsync(this.StateHasChanged); break; @@ -163,6 +169,16 @@ private void RefreshEnterpriseConfigurationState() .ToList(); this.enterpriseEnvironments = EnterpriseEnvironmentService.CURRENT_ENVIRONMENTS.ToList(); + this.mandatoryInfoPanels = PluginFactory.GetMandatoryInfos() + .Select(info => + { + var plugin = this.configPlugins.FirstOrDefault(item => item.Id == info.EnterpriseConfigurationPluginId); + var pluginName = plugin?.Name ?? T("Unknown configuration plugin"); + var acceptance = this.SettingsManager.ConfigurationData.MandatoryInformation.FindAcceptance(info.Id); + var headerText = $"{pluginName}: {info.Title}"; + return new MandatoryInfoPanelData(headerText, pluginName, info, acceptance); + }) + .ToList(); } private async Task DeterminePandocVersion() diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 03a9b0f45..3faa4c82d 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -266,6 +266,30 @@ CONFIG["CHAT_TEMPLATES"] = {} -- Document analysis policies for this configuration: CONFIG["DOCUMENT_ANALYSIS_POLICIES"] = {} +-- Mandatory infos that users must explicitly accept before using AI Studio: +CONFIG["MANDATORY_INFOS"] = {} + +-- An example mandatory info: +-- CONFIG["MANDATORY_INFOS"][#CONFIG["MANDATORY_INFOS"]+1] = { +-- ["Id"] = "00000000-0000-0000-0000-000000000000", +-- ["Title"] = "AI Usage Requirements", +-- ["Version"] = "1", +-- ["Markdown"] = [===[ +-- ## Usage Requirements +-- +-- Before using this AI offering, please ensure that: +-- +-- - you have completed the required internal training, +-- - generated output is clearly labeled where necessary, +-- - results are reviewed by a human before reuse, +-- - all internal policies and applicable law are followed. +-- +-- Further information is available in the [internal wiki](https://example.org/wiki). +-- ]===], +-- ["AcceptButtonText"] = "Yes, I comply with these requirements", +-- ["RejectButtonText"] = "Stop. I do not agree to these requirements" +-- } + -- An example document analysis policy: -- CONFIG["DOCUMENT_ANALYSIS_POLICIES"][#CONFIG["DOCUMENT_ANALYSIS_POLICIES"]+1] = { -- ["Id"] = "00000000-0000-0000-0000-000000000000", diff --git a/app/MindWork AI Studio/Settings/DataModel/Data.cs b/app/MindWork AI Studio/Settings/DataModel/Data.cs index d6339739f..36f819151 100644 --- a/app/MindWork AI Studio/Settings/DataModel/Data.cs +++ b/app/MindWork AI Studio/Settings/DataModel/Data.cs @@ -107,6 +107,8 @@ public sealed class Data public DataDocumentAnalysis DocumentAnalysis { get; init; } = new(); + public DataMandatoryInformation MandatoryInformation { get; init; } = new(); + public DataTextSummarizer TextSummarizer { get; init; } = new(); public DataTextContentCleaner TextContentCleaner { get; init; } = new(); diff --git a/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs b/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs new file mode 100644 index 000000000..ad49b008a --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs @@ -0,0 +1,96 @@ +using Lua; + +namespace AIStudio.Settings.DataModel; + +public sealed record DataMandatoryInfo +{ + private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(); + + /// + /// The stable ID of the mandatory info. + /// + public string Id { get; private init; } = string.Empty; + + /// + /// The ID of the enterprise configuration plugin that provides this info. + /// + public Guid EnterpriseConfigurationPluginId { get; private init; } = Guid.Empty; + + /// + /// The title shown to the user. + /// + public string Title { get; private init; } = string.Empty; + + /// + /// The configured version string. When it changes, the user must accept the text again. + /// + public string VersionText { get; private init; } = string.Empty; + + /// + /// The Markdown content shown to the user. + /// + public string Markdown { get; private init; } = string.Empty; + + /// + /// The label of the acceptance button. + /// + public string AcceptButtonText { get; private init; } = string.Empty; + + /// + /// The label of the reject button. + /// + public string RejectButtonText { get; private init; } = string.Empty; + + public static bool TryParseConfiguration(int idx, LuaTable table, Guid configPluginId, out DataMandatoryInfo mandatoryInfo) + { + mandatoryInfo = new DataMandatoryInfo(); + if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead(out var idText) || !Guid.TryParse(idText, out var id)) + { + LOG.LogWarning("The configured mandatory info {InfoIndex} does not contain a valid ID. The ID must be a valid GUID.", idx); + return false; + } + + if (!table.TryGetValue("Title", out var titleValue) || !titleValue.TryRead(out var title) || string.IsNullOrWhiteSpace(title)) + { + LOG.LogWarning("The configured mandatory info {InfoIndex} does not contain a valid Title field.", idx); + return false; + } + + if (!table.TryGetValue("Version", out var versionValue) || !versionValue.TryRead(out var versionText) || string.IsNullOrWhiteSpace(versionText)) + { + LOG.LogWarning("The configured mandatory info {InfoIndex} does not contain a valid Version field.", idx); + return false; + } + + if (!table.TryGetValue("Markdown", out var markdownValue) || !markdownValue.TryRead(out var markdown) || string.IsNullOrWhiteSpace(markdown)) + { + LOG.LogWarning("The configured mandatory info {InfoIndex} does not contain a valid Markdown field.", idx); + return false; + } + + if (!table.TryGetValue("AcceptButtonText", out var acceptButtonValue) || !acceptButtonValue.TryRead(out var acceptButtonText) || string.IsNullOrWhiteSpace(acceptButtonText)) + { + LOG.LogWarning("The configured mandatory info {InfoIndex} does not contain a valid AcceptButtonText field.", idx); + return false; + } + + if (!table.TryGetValue("RejectButtonText", out var rejectButtonValue) || !rejectButtonValue.TryRead(out var rejectButtonText) || string.IsNullOrWhiteSpace(rejectButtonText)) + { + LOG.LogWarning("The configured mandatory info {InfoIndex} does not contain a valid RejectButtonText field.", idx); + return false; + } + + mandatoryInfo = new DataMandatoryInfo + { + Id = id.ToString(), + Title = title, + VersionText = versionText, + Markdown = markdown, + AcceptButtonText = acceptButtonText, + RejectButtonText = rejectButtonText, + EnterpriseConfigurationPluginId = configPluginId, + }; + + return true; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfoAcceptance.cs b/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfoAcceptance.cs new file mode 100644 index 000000000..765816c7b --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfoAcceptance.cs @@ -0,0 +1,24 @@ +namespace AIStudio.Settings.DataModel; + +public sealed record DataMandatoryInfoAcceptance +{ + /// + /// The ID of the mandatory info that was accepted. + /// + public string InfoId { get; init; } = string.Empty; + + /// + /// The accepted version string. + /// + public string AcceptedVersion { get; init; } = string.Empty; + + /// + /// The UTC time of the acceptance. + /// + public DateTimeOffset AcceptedAtUtc { get; init; } + + /// + /// The plugin that provided the accepted info at the time of acceptance. + /// + public Guid EnterpriseConfigurationPluginId { get; init; } = Guid.Empty; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInformation.cs b/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInformation.cs new file mode 100644 index 000000000..fe348944c --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInformation.cs @@ -0,0 +1,24 @@ +namespace AIStudio.Settings.DataModel; + +public sealed class DataMandatoryInformation +{ + /// + /// Persisted user acceptances for configured mandatory infos. + /// + public List Acceptances { get; set; } = []; + + public DataMandatoryInfoAcceptance? FindAcceptance(string infoId) + { + return this.Acceptances.LastOrDefault(acceptance => string.Equals(acceptance.InfoId, infoId, StringComparison.OrdinalIgnoreCase)); + } + + public bool RemoveLeftOverAcceptances(IEnumerable mandatoryInfos) + { + var validInfoIds = mandatoryInfos + .Select(info => info.Id) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + var removedCount = this.Acceptances.RemoveAll(acceptance => !validInfoIds.Contains(acceptance.InfoId)); + return removedCount > 0; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs index 99031624c..e96967eac 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -1,4 +1,5 @@ using AIStudio.Settings; +using AIStudio.Settings.DataModel; using AIStudio.Tools.Services; using Lua; @@ -12,12 +13,18 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(nameof(PluginConfiguration)); private List configObjects = []; + private List mandatoryInfos = []; /// /// The list of configuration objects. Configuration objects are, e.g., providers or chat templates. /// public IEnumerable ConfigObjects => this.configObjects; + /// + /// The list of mandatory infos provided by this configuration plugin. + /// + public IReadOnlyList MandatoryInfos => this.mandatoryInfos; + /// /// True/false when explicitly configured in the plugin, otherwise null. /// @@ -91,6 +98,7 @@ private sealed record TemporarySecretId(string SecretId, string SecretName) : IS private bool TryProcessConfiguration(bool dryRun, out string message) { this.configObjects.Clear(); + this.mandatoryInfos.Clear(); // Ensure that the main CONFIG table exists and is a valid Lua table: if (!this.state.Environment["CONFIG"].TryRead(out var mainTable)) @@ -150,6 +158,9 @@ private bool TryProcessConfiguration(bool dryRun, out string message) // Handle configured document analysis policies: PluginConfigurationObject.TryParse(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, x => x.NextDocumentAnalysisPolicyNum, mainTable, this.Id, ref this.configObjects, dryRun); + + // Handle configured mandatory infos: + this.TryReadMandatoryInfos(mainTable); // Config: preselected provider? ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun); @@ -163,4 +174,25 @@ private bool TryProcessConfiguration(bool dryRun, out string message) message = string.Empty; return true; } + + private void TryReadMandatoryInfos(LuaTable mainTable) + { + if (!mainTable.TryGetValue("MANDATORY_INFOS", out var mandatoryInfosValue) || !mandatoryInfosValue.TryRead(out var mandatoryInfosTable)) + return; + + for (var i = 1; i <= mandatoryInfosTable.ArrayLength; i++) + { + var luaMandatoryInfoValue = mandatoryInfosTable[i]; + if (!luaMandatoryInfoValue.TryRead(out var luaMandatoryInfoTable)) + { + LOG.LogWarning("The table 'MANDATORY_INFOS' entry at index {Index} is not a valid table (config plugin id: {ConfigPluginId}).", i, this.Id); + continue; + } + + if (DataMandatoryInfo.TryParseConfiguration(i, luaMandatoryInfoTable, this.Id, out var mandatoryInfo)) + this.mandatoryInfos.Add(mandatoryInfo); + else + LOG.LogWarning("The table 'MANDATORY_INFOS' entry at index {Index} does not contain a valid mandatory info (config plugin id: {ConfigPluginId}).", i, this.Id); + } + } } diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index f110e766b..1e6d6ad15 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -186,6 +186,10 @@ plugin.Type is PluginType.CONFIGURATION && // Check document analysis policies: if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY, x => x.DocumentAnalysis.Policies, AVAILABLE_PLUGINS, configObjectList)) wasConfigurationChanged = true; + + // Check left-over mandatory info acceptances: + if (SETTINGS_MANAGER.ConfigurationData.MandatoryInformation.RemoveLeftOverAcceptances(GetMandatoryInfos())) + wasConfigurationChanged = true; // Check for a preselected provider: if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.PreselectedProvider, AVAILABLE_PLUGINS)) diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs index 4b4f6a08a..a707ab061 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs @@ -1,4 +1,5 @@ using AIStudio.Settings; +using AIStudio.Settings.DataModel; namespace AIStudio.Tools.PluginSystem; @@ -127,4 +128,12 @@ public static void Dispose() HOT_RELOAD_WATCHER.Dispose(); } + + public static IReadOnlyList GetMandatoryInfos() + { + return RUNNING_PLUGINS + .OfType() + .SelectMany(plugin => plugin.MandatoryInfos) + .ToList(); + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Rust/AppExitResponse.cs b/app/MindWork AI Studio/Tools/Rust/AppExitResponse.cs new file mode 100644 index 000000000..ef3c67028 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/AppExitResponse.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools.Rust; + +public sealed record AppExitResponse(bool Success, string ErrorMessage); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/RustService.App.cs b/app/MindWork AI Studio/Tools/Services/RustService.App.cs index 8671e8979..1602ecc4e 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.App.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.App.cs @@ -1,5 +1,7 @@ using System.Security.Cryptography; +using AIStudio.Tools.Rust; + namespace AIStudio.Tools.Services; public sealed partial class RustService @@ -117,4 +119,35 @@ public async Task GetDataDirectory() return await response.Content.ReadAsStringAsync(); } + + /// + /// Requests the Rust runtime to exit the entire desktop application. + /// + public async Task ExitApplication() + { + try + { + var response = await this.http.PostAsync("/app/exit", null); + if (!response.IsSuccessStatusCode) + { + this.logger?.LogError("Failed to exit the app due to network error: {StatusCode}.", response.StatusCode); + return false; + } + + var result = await response.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); + if (result is null || !result.Success) + { + this.logger?.LogError("Failed to exit the app: {Error}", result?.ErrorMessage ?? "Unknown error"); + return false; + } + + this.logger?.LogInformation("Exit request sent to Rust runtime."); + return true; + } + catch (Exception ex) + { + this.logger?.LogError(ex, "Exception while requesting application exit."); + return false; + } + } } \ No newline at end of file diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index 0066cfaed..0d962e5f2 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -727,6 +727,13 @@ pub struct ShortcutResponse { error_message: String, } +/// Response for application exit requests. +#[derive(Serialize)] +pub struct AppExitResponse { + success: bool, + error_message: String, +} + /// Internal helper function to register a shortcut with its callback. /// This is used by both `register_shortcut` and `resume_shortcuts` to /// avoid code duplication. @@ -755,6 +762,34 @@ fn register_shortcut_with_callback( } } +/// Requests a controlled shutdown of the entire desktop application. +#[post("/app/exit")] +pub fn exit_app(_token: APIToken) -> Json { + let main_window_lock = MAIN_WINDOW.lock().unwrap(); + let main_window = match main_window_lock.as_ref() { + Some(window) => window, + None => { + error!(Source = "Tauri"; "Cannot exit app: main window not available."); + return Json(AppExitResponse { + success: false, + error_message: "Main window not available".to_string(), + }); + } + }; + + let app_handle = main_window.app_handle(); + info!(Source = "Tauri"; "Controlled app exit was requested by the UI."); + tauri::async_runtime::spawn(async move { + time::sleep(Duration::from_millis(50)).await; + app_handle.exit(0); + }); + + Json(AppExitResponse { + success: true, + error_message: String::new(), + }) +} + /// Registers or updates a global shortcut. If the shortcut string is empty, /// the existing shortcut for that name will be unregistered. #[post("/shortcuts/register", data = "")] diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs index 64bc8174a..aa7433458 100644 --- a/runtime/src/runtime_api.rs +++ b/runtime/src/runtime_api.rs @@ -76,6 +76,7 @@ pub fn start_runtime_api() { crate::app_window::select_file, crate::app_window::select_files, crate::app_window::save_file, + crate::app_window::exit_app, crate::secret::get_secret, crate::secret::store_secret, crate::secret::delete_secret, From 56805874c28faf1741425923f44ba65ba94e6387 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 9 Apr 2026 15:15:48 +0200 Subject: [PATCH 02/15] Normalize markdown indentation --- .../Settings/DataModel/DataMandatoryInfo.cs | 3 +- app/MindWork AI Studio/Tools/Markdown.cs | 53 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs b/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs index ad49b008a..167f9dc37 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs @@ -80,12 +80,13 @@ public static bool TryParseConfiguration(int idx, LuaTable table, Guid configPlu return false; } + var normalizedMarkdown = AIStudio.Tools.Markdown.RemoveSharedIndentation(markdown); mandatoryInfo = new DataMandatoryInfo { Id = id.ToString(), Title = title, VersionText = versionText, - Markdown = markdown, + Markdown = normalizedMarkdown, AcceptButtonText = acceptButtonText, RejectButtonText = rejectButtonText, EnterpriseConfigurationPluginId = configPluginId, diff --git a/app/MindWork AI Studio/Tools/Markdown.cs b/app/MindWork AI Studio/Tools/Markdown.cs index 49a2309c6..13b42ea8a 100644 --- a/app/MindWork AI Studio/Tools/Markdown.cs +++ b/app/MindWork AI Studio/Tools/Markdown.cs @@ -26,4 +26,57 @@ public static class Markdown }, } }; + + public static string RemoveSharedIndentation(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return string.Empty; + + var normalized = value.Replace("\r\n", "\n"); + var lines = normalized.Split('\n'); + + var firstContentLine = 0; + while (firstContentLine < lines.Length && string.IsNullOrWhiteSpace(lines[firstContentLine])) + firstContentLine++; + + var lastContentLine = lines.Length - 1; + while (lastContentLine >= firstContentLine && string.IsNullOrWhiteSpace(lines[lastContentLine])) + lastContentLine--; + + if (firstContentLine > lastContentLine) + return string.Empty; + + var commonIndentation = int.MaxValue; + for (var i = firstContentLine; i <= lastContentLine; i++) + { + var line = lines[i]; + if (string.IsNullOrWhiteSpace(line)) + continue; + + var indentation = 0; + while (indentation < line.Length && char.IsWhiteSpace(line[indentation])) + indentation++; + + commonIndentation = Math.Min(commonIndentation, indentation); + } + + if (commonIndentation == int.MaxValue) + commonIndentation = 0; + + for (var i = firstContentLine; i <= lastContentLine; i++) + { + var line = lines[i]; + if (string.IsNullOrWhiteSpace(line)) + { + lines[i] = string.Empty; + continue; + } + + lines[i] = line.Length <= commonIndentation + ? string.Empty + : line[commonIndentation..]; + } + + return string.Join('\n', lines[firstContentLine..(lastContentLine + 1)]); + } } \ No newline at end of file From 2c54df46b9b1f928925eeea40e14fb4a801d0b42 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Thu, 9 Apr 2026 15:47:38 +0200 Subject: [PATCH 03/15] Optimized --- app/MindWork AI Studio/Tools/Markdown.cs | 125 +++++++++++++++++------ 1 file changed, 96 insertions(+), 29 deletions(-) diff --git a/app/MindWork AI Studio/Tools/Markdown.cs b/app/MindWork AI Studio/Tools/Markdown.cs index 13b42ea8a..10a90163d 100644 --- a/app/MindWork AI Studio/Tools/Markdown.cs +++ b/app/MindWork AI Studio/Tools/Markdown.cs @@ -1,4 +1,5 @@ using Markdig; +using System.Text; namespace AIStudio.Tools; @@ -32,51 +33,117 @@ public static string RemoveSharedIndentation(string value) if (string.IsNullOrWhiteSpace(value)) return string.Empty; - var normalized = value.Replace("\r\n", "\n"); - var lines = normalized.Split('\n'); - - var firstContentLine = 0; - while (firstContentLine < lines.Length && string.IsNullOrWhiteSpace(lines[firstContentLine])) - firstContentLine++; - - var lastContentLine = lines.Length - 1; - while (lastContentLine >= firstContentLine && string.IsNullOrWhiteSpace(lines[lastContentLine])) - lastContentLine--; - - if (firstContentLine > lastContentLine) - return string.Empty; + return RemoveSharedIndentation(value.AsSpan()); + } + private static string RemoveSharedIndentation(ReadOnlySpan value) + { + var firstContentLineStart = -1; + var lastContentLineStart = -1; + var lastContentLineEnd = -1; var commonIndentation = int.MaxValue; - for (var i = firstContentLine; i <= lastContentLine; i++) + var position = 0; + + while (TryGetNextLine(value, position, out var lineStart, out var currentLineEnd, out var nextPosition)) { - var line = lines[i]; - if (string.IsNullOrWhiteSpace(line)) + var lineContent = value[lineStart..currentLineEnd]; + if (IsWhiteSpace(lineContent)) + { + position = nextPosition; continue; + } - var indentation = 0; - while (indentation < line.Length && char.IsWhiteSpace(line[indentation])) - indentation++; + if (firstContentLineStart < 0) + firstContentLineStart = lineStart; - commonIndentation = Math.Min(commonIndentation, indentation); + lastContentLineStart = lineStart; + lastContentLineEnd = currentLineEnd; + commonIndentation = Math.Min(commonIndentation, CountIndentation(lineContent)); + position = nextPosition; } + if (firstContentLineStart < 0) + return string.Empty; + if (commonIndentation == int.MaxValue) commonIndentation = 0; - for (var i = firstContentLine; i <= lastContentLine; i++) + var builder = new StringBuilder(lastContentLineEnd - firstContentLineStart); + var shouldAppendLineBreak = false; + position = firstContentLineStart; + + while (TryGetNextLine(value, position, out var lineStart, out var lineEnd, out var nextPosition)) { - var line = lines[i]; - if (string.IsNullOrWhiteSpace(line)) + var lineContent = value[lineStart..lineEnd]; + + if (shouldAppendLineBreak) + builder.Append('\n'); + + if (IsWhiteSpace(lineContent)) + shouldAppendLineBreak = true; + else if (lineContent.Length > commonIndentation) { - lines[i] = string.Empty; - continue; + builder.Append(lineContent[commonIndentation..]); + shouldAppendLineBreak = true; } + else + shouldAppendLineBreak = true; + + if (lineStart == lastContentLineStart) + break; + + position = nextPosition; + } + + return builder.ToString(); + } + + private static bool IsWhiteSpace(ReadOnlySpan value) + { + foreach (var character in value) + { + if (!char.IsWhiteSpace(character)) + return false; + } + + return true; + } + + private static int CountIndentation(ReadOnlySpan value) + { + var indentation = 0; + while (indentation < value.Length && char.IsWhiteSpace(value[indentation])) + indentation++; + + return indentation; + } + + private static bool TryGetNextLine(ReadOnlySpan value, int position, out int lineStart, out int lineEnd, out int nextPosition) + { + if (position > value.Length) + { + lineStart = 0; + lineEnd = 0; + nextPosition = position; + return false; + } + + lineStart = position; + for (var i = position; i < value.Length; i++) + { + if (value[i] != '\n') + continue; + + lineEnd = i > lineStart && value[i - 1] == '\r' + ? i - 1 + : i; - lines[i] = line.Length <= commonIndentation - ? string.Empty - : line[commonIndentation..]; + nextPosition = i + 1; + return true; } - return string.Join('\n', lines[firstContentLine..(lastContentLine + 1)]); + lineEnd = value.Length; + nextPosition = value.Length + 1; + return true; } } \ No newline at end of file From bfa9a764f272c6d3ad2b83ee2f5f7a48e476043a Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 10 Apr 2026 15:38:59 +0200 Subject: [PATCH 04/15] Enable full-width option and set max width to medium --- app/MindWork AI Studio/Dialogs/DialogOptions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/app/MindWork AI Studio/Dialogs/DialogOptions.cs b/app/MindWork AI Studio/Dialogs/DialogOptions.cs index eda952450..e2373824d 100644 --- a/app/MindWork AI Studio/Dialogs/DialogOptions.cs +++ b/app/MindWork AI Studio/Dialogs/DialogOptions.cs @@ -19,5 +19,6 @@ public static class DialogOptions { BackdropClick = false, CloseOnEscapeKey = false, + FullWidth = true, MaxWidth = MaxWidth.Medium, }; } \ No newline at end of file From d7919828bed4f39d8205bf613793ec868e40089e Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 10 Apr 2026 15:41:41 +0200 Subject: [PATCH 05/15] Refactored HttpClient usage for efficiency --- app/MindWork AI Studio/Tools/Pandoc.cs | 27 ++++++++++++-------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/app/MindWork AI Studio/Tools/Pandoc.cs b/app/MindWork AI Studio/Tools/Pandoc.cs index ef6b9deb4..c5826eaa8 100644 --- a/app/MindWork AI Studio/Tools/Pandoc.cs +++ b/app/MindWork AI Studio/Tools/Pandoc.cs @@ -34,6 +34,8 @@ public static partial class Pandoc /// private static bool HAS_LOGGED_AVAILABILITY_CHECK_ONCE; + private static readonly HttpClient WEB_CLIENT = new(); + /// /// Prepares a Pandoc process by using the Pandoc process builder. /// @@ -181,21 +183,18 @@ public static async Task InstallAsync(RustService rustService) // Download the latest Pandoc archive from GitHub: // var uri = await GenerateArchiveUriAsync(); - using (var client = new HttpClient()) + var response = await WEB_CLIENT.GetAsync(uri); + if (!response.IsSuccessStatusCode) { - var response = await client.GetAsync(uri); - if (!response.IsSuccessStatusCode) - { - await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, TB("Pandoc was not installed successfully, because the archive was not found."))); - LOG.LogError("Pandoc was not installed successfully, because the archive was not found (status code {0}): url='{1}', message='{2}'", response.StatusCode, uri, response.RequestMessage); - return; - } - - // Download the archive to the temporary file: - await using var tempFileStream = File.Create(pandocTempDownloadFile); - await response.Content.CopyToAsync(tempFileStream); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, TB("Pandoc was not installed successfully, because the archive was not found."))); + LOG.LogError("Pandoc was not installed successfully, because the archive was not found (status code {0}): url='{1}', message='{2}'", response.StatusCode, uri, response.RequestMessage); + return; } + // Download the archive to the temporary file: + await using var tempFileStream = File.Create(pandocTempDownloadFile); + await response.Content.CopyToAsync(tempFileStream); + if (uri.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) { ZipFile.ExtractToDirectory(pandocTempDownloadFile, installDir); @@ -245,9 +244,7 @@ private static void ClearFolder(string path) /// Version numbers can have the following formats: x.x, x.x.x or x.x.x.x /// Latest Pandoc version number public static async Task FetchLatestVersionAsync() { - using var client = new HttpClient(); - var response = await client.GetAsync(LATEST_URL); - + var response = await WEB_CLIENT.GetAsync(LATEST_URL); if (!response.IsSuccessStatusCode) { LOG.LogError("Code {StatusCode}: Could not fetch Pandoc's latest page: {Response}", response.StatusCode, response.RequestMessage); From 4476097340a55437abeb6d182dbc2a6f89f7ca31 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 10 Apr 2026 15:46:44 +0200 Subject: [PATCH 06/15] Fixed height --- app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor b/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor index 1319e9078..7ac306bd0 100644 --- a/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor +++ b/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor @@ -2,7 +2,7 @@ -
+
From e384260cfbf177d3a1140ed720b5c0a80c2888fd Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 10 Apr 2026 15:55:58 +0200 Subject: [PATCH 07/15] Fixed header of dialog --- app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor | 5 ----- .../Components/MandatoryInfoDisplay.razor.cs | 3 --- app/MindWork AI Studio/Layout/MainLayout.razor.cs | 2 +- app/MindWork AI Studio/Pages/Information.razor | 1 - 4 files changed, 1 insertion(+), 10 deletions(-) diff --git a/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor b/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor index ac6713478..ca881111c 100644 --- a/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor +++ b/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor @@ -1,11 +1,6 @@ @inherits MSGComponentBase - @if (this.ShowTitle) - { - @this.Info.Title - } - @T("Version"): @this.Info.VersionText diff --git a/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor.cs b/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor.cs index f2ac014ca..fa69c9944 100644 --- a/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor.cs +++ b/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor.cs @@ -12,9 +12,6 @@ public partial class MandatoryInfoDisplay [Parameter] public DataMandatoryInfoAcceptance? Acceptance { get; set; } - [Parameter] - public bool ShowTitle { get; set; } = true; - [Parameter] public bool ShowAcceptanceMetadata { get; set; } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index e46930702..da13385d3 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -429,7 +429,7 @@ private async Task ShowMandatoryInfoDialog(DataMandatoryInfo info) { x => x.Info, info }, }; - var dialogReference = await this.DialogService.ShowAsync(null, dialogParameters, DialogOptions.BLOCKING_FULLSCREEN); + var dialogReference = await this.DialogService.ShowAsync(info.Title, dialogParameters, DialogOptions.BLOCKING_FULLSCREEN); var dialogResult = await dialogReference.Result; return dialogResult is { Canceled: false, Data: true }; } diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor index 07ddec738..3170be0f2 100644 --- a/app/MindWork AI Studio/Pages/Information.razor +++ b/app/MindWork AI Studio/Pages/Information.razor @@ -231,7 +231,6 @@ } From c8692c97ced11c4911b657f8f35af9977ce62a98 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 10 Apr 2026 16:14:00 +0200 Subject: [PATCH 08/15] Add MudJustifiedMarkdown component and apply justified styling --- .../Components/MandatoryInfoDisplay.razor | 2 +- .../Components/MudJustifiedMarkdown.razor | 3 +++ .../Components/MudJustifiedMarkdown.razor.cs | 9 +++++++++ app/MindWork AI Studio/wwwroot/app.css | 19 +++++++++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 app/MindWork AI Studio/Components/MudJustifiedMarkdown.razor create mode 100644 app/MindWork AI Studio/Components/MudJustifiedMarkdown.razor.cs diff --git a/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor b/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor index ca881111c..8c53f702c 100644 --- a/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor +++ b/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor @@ -33,5 +33,5 @@ } } - + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/MudJustifiedMarkdown.razor b/app/MindWork AI Studio/Components/MudJustifiedMarkdown.razor new file mode 100644 index 000000000..1f99ff441 --- /dev/null +++ b/app/MindWork AI Studio/Components/MudJustifiedMarkdown.razor @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/app/MindWork AI Studio/Components/MudJustifiedMarkdown.razor.cs b/app/MindWork AI Studio/Components/MudJustifiedMarkdown.razor.cs new file mode 100644 index 000000000..0770c5022 --- /dev/null +++ b/app/MindWork AI Studio/Components/MudJustifiedMarkdown.razor.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +public partial class MudJustifiedMarkdown +{ + [Parameter] + public string Value { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/app.css b/app/MindWork AI Studio/wwwroot/app.css index 909d350d5..787fb2721 100644 --- a/app/MindWork AI Studio/wwwroot/app.css +++ b/app/MindWork AI Studio/wwwroot/app.css @@ -116,6 +116,25 @@ margin-bottom:2em; } +.justified-markdown .mud-markdown-body p, +.justified-markdown .mud-markdown-body li, +.justified-markdown .mud-markdown-body blockquote p { + text-align: justify; + hyphens: auto; +} + +.justified-markdown .mud-markdown-body pre, +.justified-markdown .mud-markdown-body code, +.justified-markdown .mud-markdown-body h1, +.justified-markdown .mud-markdown-body h2, +.justified-markdown .mud-markdown-body h3, +.justified-markdown .mud-markdown-body h4, +.justified-markdown .mud-markdown-body h5, +.justified-markdown .mud-markdown-body h6, +.justified-markdown .mud-markdown-body table { + text-align: left; +} + .code-block { background-color: #2d2d2d; color: #f8f8f2; From 5e38603fea8cfc14cb096eab079cac19f619c5cd Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 10 Apr 2026 16:25:31 +0200 Subject: [PATCH 09/15] Update header text to include localized "Consent" prefix --- app/MindWork AI Studio/Pages/Information.razor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Pages/Information.razor.cs b/app/MindWork AI Studio/Pages/Information.razor.cs index 17c1dedd4..8f2192a53 100644 --- a/app/MindWork AI Studio/Pages/Information.razor.cs +++ b/app/MindWork AI Studio/Pages/Information.razor.cs @@ -175,7 +175,7 @@ private void RefreshEnterpriseConfigurationState() var plugin = this.configPlugins.FirstOrDefault(item => item.Id == info.EnterpriseConfigurationPluginId); var pluginName = plugin?.Name ?? T("Unknown configuration plugin"); var acceptance = this.SettingsManager.ConfigurationData.MandatoryInformation.FindAcceptance(info.Id); - var headerText = $"{pluginName}: {info.Title}"; + var headerText = $"{T("Consent:")} {info.Title}"; return new MandatoryInfoPanelData(headerText, pluginName, info, acceptance); }) .ToList(); From 277cc01cbf81bde9825c6f385f81e5846b649267 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 10 Apr 2026 16:31:13 +0200 Subject: [PATCH 10/15] Add hash-based acceptance to track content changes --- .../Components/MandatoryInfoDisplay.razor | 20 +++++++++++---- .../Components/MandatoryInfoDisplay.razor.cs | 25 +++++++++++++++++++ .../Layout/MainLayout.razor.cs | 3 ++- .../Plugins/configuration/plugin.lua | 2 ++ .../Settings/DataModel/DataMandatoryInfo.cs | 13 +++++++++- .../DataModel/DataMandatoryInfoAcceptance.cs | 5 ++++ 6 files changed, 61 insertions(+), 7 deletions(-) diff --git a/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor b/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor index 8c53f702c..24d529abe 100644 --- a/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor +++ b/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor @@ -7,18 +7,28 @@ @if (this.ShowAcceptanceMetadata) { - @if (this.Acceptance is null) + @if (this.AcceptanceStatus is MandatoryInfoAcceptanceStatus.MISSING) { @T("This mandatory info has not been accepted yet.") } - else if (!string.Equals(this.Acceptance.AcceptedVersion, this.Info.VersionText, StringComparison.Ordinal)) + else if (this.AcceptanceStatus is MandatoryInfoAcceptanceStatus.VERSION_CHANGED) { - @T("The current version has not been accepted yet.") + @T("A new version of the terms is available. Please review it again.")
- @T("Last accepted version"): @this.Acceptance.AcceptedVersion + @T("Last accepted version"): @this.Acceptance!.AcceptedVersion +
+ @T("Accepted at (UTC)"): @this.Acceptance.AcceptedAtUtc.UtcDateTime.ToString("u") +
+ } + else if (this.AcceptanceStatus is MandatoryInfoAcceptanceStatus.CONTENT_CHANGED) + { + + @T("Please review this text again. The content was changed.") +
+ @T("Last accepted version"): @this.Acceptance!.AcceptedVersion
@T("Accepted at (UTC)"): @this.Acceptance.AcceptedAtUtc.UtcDateTime.ToString("u")
@@ -26,7 +36,7 @@ else { - @T("Accepted version"): @this.Acceptance.AcceptedVersion + @T("Accepted version"): @this.Acceptance!.AcceptedVersion
@T("Accepted at (UTC)"): @this.Acceptance.AcceptedAtUtc.UtcDateTime.ToString("u")
diff --git a/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor.cs b/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor.cs index fa69c9944..a9d97cdef 100644 --- a/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor.cs +++ b/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor.cs @@ -6,6 +6,14 @@ namespace AIStudio.Components; public partial class MandatoryInfoDisplay { + private enum MandatoryInfoAcceptanceStatus + { + MISSING, + VERSION_CHANGED, + CONTENT_CHANGED, + ACCEPTED, + } + [Parameter] public DataMandatoryInfo Info { get; set; } = new(); @@ -14,4 +22,21 @@ public partial class MandatoryInfoDisplay [Parameter] public bool ShowAcceptanceMetadata { get; set; } + + private MandatoryInfoAcceptanceStatus AcceptanceStatus + { + get + { + if (this.Acceptance is null) + return MandatoryInfoAcceptanceStatus.MISSING; + + if (!string.Equals(this.Acceptance.AcceptedVersion, this.Info.VersionText, StringComparison.Ordinal)) + return MandatoryInfoAcceptanceStatus.VERSION_CHANGED; + + if (!string.Equals(this.Acceptance.AcceptedHash, this.Info.GetAcceptanceHash(), StringComparison.Ordinal)) + return MandatoryInfoAcceptanceStatus.CONTENT_CHANGED; + + return MandatoryInfoAcceptanceStatus.ACCEPTED; + } + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index da13385d3..3d8d6b2ad 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -418,7 +418,7 @@ private IEnumerable GetPendingMandatoryInfos() .Where(info => { var acceptance = this.SettingsManager.ConfigurationData.MandatoryInformation.FindAcceptance(info.Id); - return acceptance is null || acceptance.AcceptedVersion != info.VersionText; + return acceptance is null || !string.Equals(acceptance.AcceptedHash, info.GetAcceptanceHash(), StringComparison.Ordinal); }); } @@ -441,6 +441,7 @@ private async Task StoreMandatoryInfoAcceptance(DataMandatoryInfo info) { InfoId = info.Id, AcceptedVersion = info.VersionText, + AcceptedHash = info.GetAcceptanceHash(), AcceptedAtUtc = DateTimeOffset.UtcNow, EnterpriseConfigurationPluginId = info.EnterpriseConfigurationPluginId, }; diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 3faa4c82d..552d6462f 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -267,6 +267,8 @@ CONFIG["CHAT_TEMPLATES"] = {} CONFIG["DOCUMENT_ANALYSIS_POLICIES"] = {} -- Mandatory infos that users must explicitly accept before using AI Studio: +-- AI Studio asks users again when Version, Title, or Markdown change. +-- Changing Version additionally allows the UI to communicate that a new version is available. CONFIG["MANDATORY_INFOS"] = {} -- An example mandatory info: diff --git a/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs b/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs index 167f9dc37..a633e2d30 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs @@ -1,3 +1,6 @@ +using System.Security.Cryptography; +using System.Text; + using Lua; namespace AIStudio.Settings.DataModel; @@ -22,7 +25,8 @@ public sealed record DataMandatoryInfo public string Title { get; private init; } = string.Empty; /// - /// The configured version string. When it changes, the user must accept the text again. + /// The configured version string shown to the user. A changed version triggers re-acceptance + /// and allows the UI to distinguish a new version from a content-only change. /// public string VersionText { get; private init; } = string.Empty; @@ -41,6 +45,13 @@ public sealed record DataMandatoryInfo /// public string RejectButtonText { get; private init; } = string.Empty; + public string GetAcceptanceHash() + { + var content = $"Version:{this.VersionText}\nTitle:{this.Title}\nMarkdown:{this.Markdown}"; + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(content)); + return Convert.ToHexString(hash); + } + public static bool TryParseConfiguration(int idx, LuaTable table, Guid configPluginId, out DataMandatoryInfo mandatoryInfo) { mandatoryInfo = new DataMandatoryInfo(); diff --git a/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfoAcceptance.cs b/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfoAcceptance.cs index 765816c7b..e24969d0a 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfoAcceptance.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfoAcceptance.cs @@ -12,6 +12,11 @@ public sealed record DataMandatoryInfoAcceptance /// public string AcceptedVersion { get; init; } = string.Empty; + /// + /// The accepted hash of the mandatory info content. + /// + public string AcceptedHash { get; init; } = string.Empty; + /// /// The UTC time of the acceptance. /// From b68c62693f472aa59b221835d7ffa93d4b570649 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 10 Apr 2026 16:48:00 +0200 Subject: [PATCH 11/15] Replace `GetAcceptanceHash` with `AcceptanceHash` property --- .../Components/MandatoryInfoDisplay.razor.cs | 2 +- .../Dialogs/MandatoryInfoDialog.razor | 2 +- app/MindWork AI Studio/Layout/MainLayout.razor.cs | 4 ++-- .../Settings/DataModel/DataMandatoryInfo.cs | 15 ++++++++++++--- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor.cs b/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor.cs index a9d97cdef..a8a7664e1 100644 --- a/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor.cs +++ b/app/MindWork AI Studio/Components/MandatoryInfoDisplay.razor.cs @@ -33,7 +33,7 @@ private MandatoryInfoAcceptanceStatus AcceptanceStatus if (!string.Equals(this.Acceptance.AcceptedVersion, this.Info.VersionText, StringComparison.Ordinal)) return MandatoryInfoAcceptanceStatus.VERSION_CHANGED; - if (!string.Equals(this.Acceptance.AcceptedHash, this.Info.GetAcceptanceHash(), StringComparison.Ordinal)) + if (!string.Equals(this.Acceptance.AcceptedHash, this.Info.AcceptanceHash, StringComparison.Ordinal)) return MandatoryInfoAcceptanceStatus.CONTENT_CHANGED; return MandatoryInfoAcceptanceStatus.ACCEPTED; diff --git a/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor b/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor index 7ac306bd0..bfd16fe32 100644 --- a/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor +++ b/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor @@ -3,7 +3,7 @@
- +
diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index 3d8d6b2ad..d56ec0a9d 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -418,7 +418,7 @@ private IEnumerable GetPendingMandatoryInfos() .Where(info => { var acceptance = this.SettingsManager.ConfigurationData.MandatoryInformation.FindAcceptance(info.Id); - return acceptance is null || !string.Equals(acceptance.AcceptedHash, info.GetAcceptanceHash(), StringComparison.Ordinal); + return acceptance is null || !string.Equals(acceptance.AcceptedHash, info.AcceptanceHash, StringComparison.Ordinal); }); } @@ -441,7 +441,7 @@ private async Task StoreMandatoryInfoAcceptance(DataMandatoryInfo info) { InfoId = info.Id, AcceptedVersion = info.VersionText, - AcceptedHash = info.GetAcceptanceHash(), + AcceptedHash = info.AcceptanceHash, AcceptedAtUtc = DateTimeOffset.UtcNow, EnterpriseConfigurationPluginId = info.EnterpriseConfigurationPluginId, }; diff --git a/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs b/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs index a633e2d30..638ba6d84 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataMandatoryInfo.cs @@ -45,10 +45,17 @@ public sealed record DataMandatoryInfo /// public string RejectButtonText { get; private init; } = string.Empty; - public string GetAcceptanceHash() + /// + /// The current hash used to determine whether the user needs to re-accept the info. + /// + public string AcceptanceHash { get; private init; } = string.Empty; + + private static string CreateAcceptanceHash(string versionText, string title, string markdown) { - var content = $"Version:{this.VersionText}\nTitle:{this.Title}\nMarkdown:{this.Markdown}"; - var hash = SHA256.HashData(Encoding.UTF8.GetBytes(content)); + var content = $"Version:{versionText}\nTitle:{title}\nMarkdown:{markdown}"; + var bytes = Encoding.UTF8.GetBytes(content); + var hash = SHA256.HashData(bytes); + return Convert.ToHexString(hash); } @@ -92,6 +99,7 @@ public static bool TryParseConfiguration(int idx, LuaTable table, Guid configPlu } var normalizedMarkdown = AIStudio.Tools.Markdown.RemoveSharedIndentation(markdown); + var acceptanceHash = CreateAcceptanceHash(versionText, title, normalizedMarkdown); mandatoryInfo = new DataMandatoryInfo { Id = id.ToString(), @@ -101,6 +109,7 @@ public static bool TryParseConfiguration(int idx, LuaTable table, Guid configPlu AcceptButtonText = acceptButtonText, RejectButtonText = rejectButtonText, EnterpriseConfigurationPluginId = configPluginId, + AcceptanceHash = acceptanceHash, }; return true; From 09895063961f0de4edb8f44f04d4d41f2d6baab5 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 10 Apr 2026 16:54:56 +0200 Subject: [PATCH 12/15] Add acceptance parameter to MandatoryInfoDialog --- app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor | 2 +- app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor.cs | 3 +++ app/MindWork AI Studio/Layout/MainLayout.razor.cs | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor b/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor index bfd16fe32..6dd11241f 100644 --- a/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor +++ b/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor @@ -3,7 +3,7 @@
- +
diff --git a/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor.cs b/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor.cs index 4ed3451ee..a25d43b55 100644 --- a/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/MandatoryInfoDialog.razor.cs @@ -13,6 +13,9 @@ public partial class MandatoryInfoDialog : MSGComponentBase [Parameter] public DataMandatoryInfo Info { get; set; } = new(); + [Parameter] + public DataMandatoryInfoAcceptance? Acceptance { get; set; } + private void Accept() => this.MudDialog.Close(DialogResult.Ok(true)); private void Reject() => this.MudDialog.Close(DialogResult.Ok(false)); diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index d56ec0a9d..a1659f347 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -424,9 +424,11 @@ private IEnumerable GetPendingMandatoryInfos() private async Task ShowMandatoryInfoDialog(DataMandatoryInfo info) { + var acceptance = this.SettingsManager.ConfigurationData.MandatoryInformation.FindAcceptance(info.Id); var dialogParameters = new DialogParameters { { x => x.Info, info }, + { x => x.Acceptance, acceptance }, }; var dialogReference = await this.DialogService.ShowAsync(info.Title, dialogParameters, DialogOptions.BLOCKING_FULLSCREEN); From 6011ffefa9b400bf02aa870fccbbd27763de84cb Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 10 Apr 2026 16:58:11 +0200 Subject: [PATCH 13/15] Updated I18N --- .../Assistants/I18N/allTexts.lua | 12 +++++-- .../plugin.lua | 36 +++++++++++++++++-- .../plugin.lua | 30 ++++++++++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index fac5d66a8..c1234d7cd 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -2101,21 +2101,24 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANAGEPANDOCDEPENDENCY::T986578435"] = "I -- Version UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T1573770551"] = "Version" +-- A new version of the terms is available. Please review it again. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T1711766303"] = "A new version of the terms is available. Please review it again." + -- This mandatory info has not been accepted yet. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T1870532312"] = "This mandatory info has not been accepted yet." -- Accepted version UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T203086476"] = "Accepted version" --- The current version has not been accepted yet. -UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T2158667957"] = "The current version has not been accepted yet." - -- Last accepted version UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T3407978086"] = "Last accepted version" -- Accepted at (UTC) UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T3511160492"] = "Accepted at (UTC)" +-- Please review this text again. The content was changed. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T941885055"] = "Please review this text again. The content was changed." + -- Given that my employer's workplace uses both Windows and Linux, I wanted a cross-platform solution that would work seamlessly across all major operating systems, including macOS. Additionally, I wanted to demonstrate that it is possible to create modern, efficient, cross-platform applications without resorting to Electron bloatware. The combination of .NET and Rust with Tauri proved to be an excellent technology stack for building such robust applications. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1057189794"] = "Given that my employer's workplace uses both Windows and Linux, I wanted a cross-platform solution that would work seamlessly across all major operating systems, including macOS. Additionally, I wanted to demonstrate that it is possible to create modern, efficient, cross-platform applications without resorting to Electron bloatware. The combination of .NET and Rust with Tauri proved to be an excellent technology stack for building such robust applications." @@ -5746,6 +5749,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1629800076"] = "Building on .NET -- AI Studio creates a log file at startup, in which events during startup are recorded. After startup, another log file is created that records all events that occur during the use of the app. This includes any errors that may occur. Depending on when an error occurs (at startup or during use), the contents of these log files can be helpful for troubleshooting. Sensitive information such as passwords is not included in the log files. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio creates a log file at startup, in which events during startup are recorded. After startup, another log file is created that records all events that occur during the use of the app. This includes any errors that may occur. Depending on when an error occurs (at startup or during use), the contents of these log files can be helpful for troubleshooting. Sensitive information such as passwords is not included in the log files." +-- Consent: +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Consent:" + -- This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1772678682"] = "This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant." 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 75c38a6d9..b6a62c821 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 @@ -1524,7 +1524,7 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2823798965 -- This assistant helps you create clear, structured slides from long texts or documents. Enter a presentation title and provide the content either as text or with one or more documents. Important aspects allow you to add instructions to the LLM regarding output or formatting. Set the number of slides either directly or based on your desired presentation duration. You can also specify the number of bullet points. If the default value of 0 is not changed, the LLM will independently determine how many slides or bullet points to generate. The output can be flexibly generated in various languages and tailored to a specific audience. UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2910177051"] = "Dieser Assistent hilft Ihnen, aus langen Texten oder Dokumenten klare, strukturierte Folien zu erstellen. Geben Sie einen Titel für die Präsentation ein und stellen Sie den Inhalt entweder als Text oder über ein oder mehrere Dokumente bereit. Unter „Wichtige Aspekte“ können Sie dem LLM Anweisungen zur Ausgabe oder Formatierung geben. Legen Sie die Anzahl der Folien entweder direkt oder anhand der gewünschten Präsentationsdauer fest. Sie können auch die Anzahl der Aufzählungspunkte angeben. Wenn der Standardwert 0 nicht geändert wird, bestimmt das LLM selbstständig, wie viele Folien oder Aufzählungspunkte erstellt werden. Die Ausgabe kann flexibel in verschiedenen Sprachen erzeugt und auf eine bestimmte Zielgruppe zugeschnitten werden." --- Folienplaner-Assistent +-- Slide Planner Assistant UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::SLIDEBUILDER::SLIDEASSISTANT::T2924755246"] = "Folienplaner-Assistent" -- The result of your previous slide builder session. @@ -2100,6 +2100,27 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANAGEPANDOCDEPENDENCY::T527187983"] = " -- Install Pandoc UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANAGEPANDOCDEPENDENCY::T986578435"] = "Pandoc installieren" +-- Version +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T1573770551"] = "Version" + +-- A new version of the terms is available. Please review it again. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T1711766303"] = "Eine neue Version der Bedingungen ist verfügbar. Bitte lesen Sie die Bedingungen erneut durch." + +-- This mandatory info has not been accepted yet. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T1870532312"] = "Diese Pflichtangabe wurde noch nicht akzeptiert." + +-- Accepted version +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T203086476"] = "Akzeptierte Version" + +-- Last accepted version +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T3407978086"] = "Zuletzt akzeptierte Version" + +-- Accepted at (UTC) +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T3511160492"] = "Akzeptiert am (UTC)" + +-- Please review this text again. The content was changed. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T941885055"] = "Bitte lesen Sie diesen Text erneut durch. Der Inhalt wurde geändert." + -- Given that my employer's workplace uses both Windows and Linux, I wanted a cross-platform solution that would work seamlessly across all major operating systems, including macOS. Additionally, I wanted to demonstrate that it is possible to create modern, efficient, cross-platform applications without resorting to Electron bloatware. The combination of .NET and Rust with Tauri proved to be an excellent technology stack for building such robust applications. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1057189794"] = "Da mein Arbeitgeber sowohl Windows als auch Linux am Arbeitsplatz nutzt, wollte ich eine plattformübergreifende Lösung, die nahtlos auf allen wichtigen Betriebssystemen, einschließlich macOS, funktioniert. Außerdem wollte ich zeigen, dass es möglich ist, moderne, effiziente und plattformübergreifende Anwendungen zu erstellen, ohne auf Software-Ballast, wie z.B. das Electron-Framework, zurückzugreifen. Die Kombination aus .NET und Rust mit Tauri hat sich dabei als hervorragender Technologie-Stack für den Bau solch robuster Anwendungen erwiesen." @@ -5508,7 +5529,7 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T2830810750"] = "AI Studio Entwick -- Generate a job posting for a given job description. UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T2831103254"] = "Erstellen Sie eine Stellenanzeige anhand einer vorgegebenen Stellenbeschreibung." --- Folienplaner-Assistent +-- Slide Planner Assistant UI_TEXT_CONTENT["AISTUDIO::PAGES::ASSISTANTS::T2924755246"] = "Folienplaner-Assistent" -- Installed Assistants @@ -5697,6 +5718,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID-Konflikt: Die -- This is a private AI Studio installation. It runs without an enterprise configuration. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "Dies ist eine private AI Studio-Installation. Sie läuft ohne Unternehmenskonfiguration." +-- Unknown configuration plugin +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unbekanntes Konfigurations-Plugin" + -- 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::INFORMATION::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." @@ -5727,6 +5751,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1629800076"] = "Basierend auf .N -- AI Studio creates a log file at startup, in which events during startup are recorded. After startup, another log file is created that records all events that occur during the use of the app. This includes any errors that may occur. Depending on when an error occurs (at startup or during use), the contents of these log files can be helpful for troubleshooting. Sensitive information such as passwords is not included in the log files. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio erstellt beim Start eine Protokolldatei, in der Ereignisse während des Starts aufgezeichnet werden. Nach dem Start wird eine weitere Protokolldatei erstellt, die alle Ereignisse während der Nutzung der App dokumentiert. Dazu gehören auch eventuell auftretende Fehler. Je nachdem, wann ein Fehler auftritt (beim Start oder während der Nutzung), können die Inhalte dieser Protokolldateien bei der Fehlerbehebung hilfreich sein. Sensible Informationen wie Passwörter werden nicht in den Protokolldateien gespeichert." +-- Consent: +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Zustimmung:" + -- This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1772678682"] = "Diese Bibliothek wird verwendet, um die Unterschiede zwischen zwei Texten anzuzeigen. Das ist zum Beispiel für den Grammatik- und Rechtschreibassistenten notwendig." @@ -5946,6 +5973,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T788846912"] = "Kopiert die Konfi -- installed by AI Studio UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T833849470"] = "installiert von AI Studio" +-- Provided by configuration plugin: {0} +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T836298648"] = "Bereitgestellt vom Konfigurations-Plugin: {0}" + -- We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "Wir verwenden diese Bibliothek, um PowerPoint-Dateien lesen zu können. So ist es möglich, Inhalte aus Folien in Prompts einzufügen und PowerPoint-Dateien in RAG-Prozessen zu berücksichtigen. Wir danken Nils Kruthoff für seine Arbeit an diesem Rust-Crate." @@ -6492,7 +6522,7 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T2684676843"] = "Texte z -- Synonym Assistant UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T2921123194"] = "Synonym-Assistent" --- Folienplaner-Assistent +-- Slide Planner Assistant UI_TEXT_CONTENT["AISTUDIO::TOOLS::COMPONENTSEXTENSIONS::T2924755246"] = "Folienplaner-Assistent" -- Document Analysis Assistant 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 8e7c757ff..fdf1acf33 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 @@ -2100,6 +2100,27 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANAGEPANDOCDEPENDENCY::T527187983"] = "C -- Install Pandoc UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANAGEPANDOCDEPENDENCY::T986578435"] = "Install Pandoc" +-- Version +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T1573770551"] = "Version" + +-- A new version of the terms is available. Please review it again. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T1711766303"] = "A new version of the terms is available. Please review it again." + +-- This mandatory info has not been accepted yet. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T1870532312"] = "This mandatory info has not been accepted yet." + +-- Accepted version +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T203086476"] = "Accepted version" + +-- Last accepted version +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T3407978086"] = "Last accepted version" + +-- Accepted at (UTC) +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T3511160492"] = "Accepted at (UTC)" + +-- Please review this text again. The content was changed. +UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MANDATORYINFODISPLAY::T941885055"] = "Please review this text again. The content was changed." + -- Given that my employer's workplace uses both Windows and Linux, I wanted a cross-platform solution that would work seamlessly across all major operating systems, including macOS. Additionally, I wanted to demonstrate that it is possible to create modern, efficient, cross-platform applications without resorting to Electron bloatware. The combination of .NET and Rust with Tauri proved to be an excellent technology stack for building such robust applications. UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::MOTIVATION::T1057189794"] = "Given that my employer's workplace uses both Windows and Linux, I wanted a cross-platform solution that would work seamlessly across all major operating systems, including macOS. Additionally, I wanted to demonstrate that it is possible to create modern, efficient, cross-platform applications without resorting to Electron bloatware. The combination of .NET and Rust with Tauri proved to be an excellent technology stack for building such robust applications." @@ -5697,6 +5718,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1137744461"] = "ID mismatch: the -- This is a private AI Studio installation. It runs without an enterprise configuration. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1209549230"] = "This is a private AI Studio installation. It runs without an enterprise configuration." +-- Unknown configuration plugin +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1290340974"] = "Unknown configuration plugin" + -- 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::INFORMATION::T1388816916"] = "This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat." @@ -5727,6 +5751,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1629800076"] = "Building on .NET -- AI Studio creates a log file at startup, in which events during startup are recorded. After startup, another log file is created that records all events that occur during the use of the app. This includes any errors that may occur. Depending on when an error occurs (at startup or during use), the contents of these log files can be helpful for troubleshooting. Sensitive information such as passwords is not included in the log files. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio creates a log file at startup, in which events during startup are recorded. After startup, another log file is created that records all events that occur during the use of the app. This includes any errors that may occur. Depending on when an error occurs (at startup or during use), the contents of these log files can be helpful for troubleshooting. Sensitive information such as passwords is not included in the log files." +-- Consent: +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Consent:" + -- This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1772678682"] = "This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant." @@ -5946,6 +5973,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T788846912"] = "Copies the config -- installed by AI Studio UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T833849470"] = "installed by AI Studio" +-- Provided by configuration plugin: {0} +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T836298648"] = "Provided by configuration plugin: {0}" + -- We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate." From fec3f922247b7859ecb2a8895fe9df5a7c211c40 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 10 Apr 2026 17:08:37 +0200 Subject: [PATCH 14/15] Updated changelog --- app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index 115c3edd0..d36e8bba6 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -9,8 +9,10 @@ - Added pre-call validation to check if the selected model exists for the provider before making the request. - Added math rendering in chats for LaTeX display formulas, including block formats such as `$$ ... $$` and `\[ ... \]`. - Added the latest OpenAI models. +- Added support for mandatory information notices in configuration plugins. Organizations can now require users to read and confirm important information before continuing in AI Studio. - Released the document analysis assistant after an intense testing phase. - Improved enterprise deployment for organizations: administrators can now provide up to 10 centrally managed enterprise configuration slots, use policy files on Linux and macOS, and continue using older configuration formats as a fallback during migration. +- Improved enterprise transparency on the information page by showing configured mandatory notices together with their source and confirmation status. - Improved the profile selection for assistants and the chat. You can now explicitly choose between the app default profile, no profile, or a specific profile. - Improved the performance by caching the OS language detection and requesting the user language only once per app start. - Improved the chat performance by reducing unnecessary UI updates, making chats smoother and more responsive, especially in longer conversations. From 9b68b6518ae39c39e120ad6a8e69cb7a7344f2fc Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 10 Apr 2026 17:10:30 +0200 Subject: [PATCH 15/15] Updated changelog --- app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index d36e8bba6..f35531ee3 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -12,7 +12,7 @@ - Added support for mandatory information notices in configuration plugins. Organizations can now require users to read and confirm important information before continuing in AI Studio. - Released the document analysis assistant after an intense testing phase. - Improved enterprise deployment for organizations: administrators can now provide up to 10 centrally managed enterprise configuration slots, use policy files on Linux and macOS, and continue using older configuration formats as a fallback during migration. -- Improved enterprise transparency on the information page by showing configured mandatory notices together with their source and confirmation status. +- Improved transparency on the information page by showing configured mandatory notices together with their source and confirmation status. - Improved the profile selection for assistants and the chat. You can now explicitly choose between the app default profile, no profile, or a specific profile. - Improved the performance by caching the OS language detection and requesting the user language only once per app start. - Improved the chat performance by reducing unnecessary UI updates, making chats smoother and more responsive, especially in longer conversations.