diff --git a/app/MindWork AI Studio/Pages/Home.razor b/app/MindWork AI Studio/Pages/Home.razor index abf7ffb7..b9b65beb 100644 --- a/app/MindWork AI Studio/Pages/Home.razor +++ b/app/MindWork AI Studio/Pages/Home.razor @@ -10,21 +10,31 @@ - - - @T("Welcome to MindWork AI Studio!") - - - @T("Thank you for considering MindWork AI Studio for your AI needs. This app is designed to help you harness the power of Large Language Models (LLMs). Please note that this app doesn't come with an integrated LLM. Instead, you will need to bring an API key from a suitable provider.") - - - @T("Here's what makes MindWork AI Studio stand out:") - - - - @T("We hope you enjoy using MindWork AI Studio to bring your AI projects to life!") - - + @if (this.SettingsManager.ConfigurationData.App.ShowIntroduction) + { + + + @T("Welcome to MindWork AI Studio!") + + + @T("Thank you for considering MindWork AI Studio for your AI needs. This app is designed to help you harness the power of Large Language Models (LLMs). Please note that this app doesn't come with an integrated LLM. Instead, you will need to bring an API key from a suitable provider.") + + + @T("Here's what makes MindWork AI Studio stand out:") + + + + @T("We hope you enjoy using MindWork AI Studio to bring your AI projects to life!") + + + } + + @foreach (var introductionPanel in this.introductionPanels) + { + + + + } diff --git a/app/MindWork AI Studio/Pages/Home.razor.cs b/app/MindWork AI Studio/Pages/Home.razor.cs index b44724d0..42336a7a 100644 --- a/app/MindWork AI Studio/Pages/Home.razor.cs +++ b/app/MindWork AI Studio/Pages/Home.razor.cs @@ -1,5 +1,6 @@ using AIStudio.Components; using AIStudio.Settings.DataModel; +using AIStudio.Tools.PluginSystem; using Microsoft.AspNetCore.Components; @@ -18,13 +19,19 @@ public partial class Home : MSGComponentBase private string LastChangeContent { get; set; } = string.Empty; private TextItem[] itemsAdvantages = []; + + private List introductionPanels = []; + + private sealed record HomeIntroductionPanelData(string HeaderText, DataIntroduction Introduction); #region Overrides of ComponentBase protected override async Task OnInitializedAsync() { + this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]); await base.OnInitializedAsync(); this.InitializeAdvantagesItems(); + this.RefreshIntroductionPanels(); // Read the last change content asynchronously // without blocking the UI thread: @@ -69,10 +76,12 @@ public partial class Home : MSGComponentBase { case Event.PLUGINS_RELOADED: this.InitializeAdvantagesItems(); + this.RefreshIntroductionPanels(); await this.InvokeAsync(this.StateHasChanged); break; case Event.CONFIGURATION_CHANGED: + this.RefreshIntroductionPanels(); await this.InvokeAsync(this.StateHasChanged); break; } @@ -80,6 +89,17 @@ public partial class Home : MSGComponentBase #endregion + private void RefreshIntroductionPanels() + { + this.introductionPanels = PluginFactory.GetIntroductions() + .Select(introduction => + { + var headerText = $"{introduction.Title} ({T("Version")} {introduction.VersionText})"; + return new HomeIntroductionPanelData(headerText, introduction); + }) + .ToList(); + } + private async Task ReadLastChangeAsync() { var latest = Changelog.LOGS.MaxBy(n => n.Build); diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 070952ce..e2953424 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -207,6 +207,9 @@ CONFIG["SETTINGS"] = {} -- Configure whether the quick start guide is shown on the welcome page. -- CONFIG["SETTINGS"]["DataApp.ShowQuickStartGuide"] = false +-- Configure whether the built-in introduction is shown on the welcome page. +-- CONFIG["SETTINGS"]["DataApp.ShowIntroduction"] = false + -- Configure the user permission to add providers: -- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false @@ -336,6 +339,26 @@ CONFIG["CHAT_TEMPLATES"] = {} -- } -- } +-- Introduction texts shown as expansion panels on the welcome page: +CONFIG["INTRODUCTIONS"] = {} + +-- An example introduction: +-- CONFIG["INTRODUCTIONS"][#CONFIG["INTRODUCTIONS"]+1] = { +-- ["Id"] = "00000000-0000-0000-0000-000000000000", +-- ["Title"] = "Welcome to Your Organization's AI Studio", +-- ["Version"] = "1", +-- ["Index"] = 1, +-- ["Markdown"] = [===[ +-- ## Getting Started +-- +-- This AI Studio installation is managed by your organization. +-- Please use the preconfigured providers and follow your internal +-- AI usage guidelines. +-- +-- Further information is available in the [internal wiki](https://example.org/wiki). +-- ]===] +-- } + -- 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. diff --git a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs index c9352514..a0c2c58e 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs @@ -57,6 +57,11 @@ public sealed class DataApp(Expression>? configSelection = n /// public StartPage StartPage { get; set; } = ManagedConfiguration.Register(configSelection, n => n.StartPage, StartPage.HOME); + /// + /// Should the built-in introduction be visible on the home page? + /// + public bool ShowIntroduction { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShowIntroduction, true); + /// /// Should the quick start guide be visible on the home page? /// diff --git a/app/MindWork AI Studio/Settings/DataModel/DataIntroduction.cs b/app/MindWork AI Studio/Settings/DataModel/DataIntroduction.cs new file mode 100644 index 00000000..1b09baef --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/DataIntroduction.cs @@ -0,0 +1,85 @@ +using Lua; + +namespace AIStudio.Settings.DataModel; + +public sealed record DataIntroduction +{ + private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger(); + + /// + /// The stable ID of the introduction. + /// + public string Id { get; private init; } = string.Empty; + + /// + /// The ID of the enterprise configuration plugin that provides this introduction. + /// + 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 shown to the user. + /// + public string VersionText { get; private init; } = string.Empty; + + /// + /// The sort index used on the home page. + /// + public int Index { get; private init; } = 1; + + /// + /// The Markdown content shown to the user. + /// + public string Markdown { get; private init; } = string.Empty; + + public static bool TryParseConfiguration(int idx, LuaTable table, Guid configPluginId, out DataIntroduction introduction) + { + introduction = new DataIntroduction(); + if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead(out var idText) || !Guid.TryParse(idText, out var id)) + { + LOG.LogWarning("The configured introduction {IntroductionIndex} 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 introduction {IntroductionIndex} 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 introduction {IntroductionIndex} 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 introduction {IntroductionIndex} does not contain a valid Markdown field.", idx); + return false; + } + + var index = 1; + if (table.TryGetValue("Index", out var indexValue) && !indexValue.TryRead(out index)) + { + LOG.LogWarning("The configured introduction {IntroductionIndex} does not contain a valid Index field. The Index must be an integer.", idx); + return false; + } + + introduction = new DataIntroduction + { + Id = id.ToString(), + Title = title, + VersionText = versionText, + Index = index, + Markdown = AIStudio.Tools.Markdown.RemoveSharedIndentation(markdown), + EnterpriseConfigurationPluginId = configPluginId, + }; + + return true; + } +} \ 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 8bd55c93..95996415 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -14,6 +14,7 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT private List configObjects = []; private List mandatoryInfos = []; + private List introductions = []; /// /// The list of configuration objects. Configuration objects are, e.g., providers or chat templates. @@ -25,6 +26,11 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT /// public IReadOnlyList MandatoryInfos => this.mandatoryInfos; + /// + /// The list of introductions provided by this configuration plugin. + /// + public IReadOnlyList Introductions => this.introductions; + /// /// True/false when explicitly configured in the plugin, otherwise null. /// @@ -130,6 +136,7 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT { this.configObjects.Clear(); this.mandatoryInfos.Clear(); + this.introductions.Clear(); // Ensure that the main CONFIG table exists and is a valid Lua table: if (!this.State.Environment["CONFIG"].TryRead(out var mainTable)) @@ -154,6 +161,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT // Config: what should be the start page? ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.StartPage, this.Id, settingsTable, dryRun); + // Config: show built-in introduction on the home page? + ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShowIntroduction, this.Id, settingsTable, dryRun); + // Config: show quick start guide on the home page? ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShowQuickStartGuide, this.Id, settingsTable, dryRun); @@ -206,6 +216,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT // Handle configured mandatory infos: this.TryReadMandatoryInfos(mainTable); + + // Handle configured introductions: + this.TryReadIntroductions(mainTable); // Config: preselected provider? ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun); @@ -240,4 +253,25 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT LOG.LogWarning("The table 'MANDATORY_INFOS' entry at index {Index} does not contain a valid mandatory info (config plugin id: {ConfigPluginId}).", i, this.Id); } } + + private void TryReadIntroductions(LuaTable mainTable) + { + if (!mainTable.TryGetValue("INTRODUCTIONS", out var introductionsValue) || !introductionsValue.TryRead(out var introductionsTable)) + return; + + for (var i = 1; i <= introductionsTable.ArrayLength; i++) + { + var luaIntroductionValue = introductionsTable[i]; + if (!luaIntroductionValue.TryRead(out var luaIntroductionTable)) + { + LOG.LogWarning("The table 'INTRODUCTIONS' entry at index {Index} is not a valid table (config plugin id: {ConfigPluginId}).", i, this.Id); + continue; + } + + if (DataIntroduction.TryParseConfiguration(i, luaIntroductionTable, this.Id, out var introduction)) + this.introductions.Add(introduction); + else + LOG.LogWarning("The table 'INTRODUCTIONS' entry at index {Index} does not contain a valid introduction (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 f7a5aabf..391288cf 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -214,6 +214,10 @@ public static partial class PluginFactory if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.StartPage, AVAILABLE_PLUGINS)) wasConfigurationChanged = true; + // Check for the built-in introduction visibility: + if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShowIntroduction, AVAILABLE_PLUGINS)) + wasConfigurationChanged = true; + // Check for the quick start guide visibility: if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShowQuickStartGuide, AVAILABLE_PLUGINS)) wasConfigurationChanged = true; diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs index ce0ae866..9efa9e9b 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.cs @@ -136,4 +136,13 @@ public static partial class PluginFactory .SelectMany(plugin => plugin.MandatoryInfos) .ToList(); } + + public static IReadOnlyList GetIntroductions() + { + return RUNNING_PLUGINS + .OfType() + .SelectMany(plugin => plugin.Introductions) + .OrderBy(introduction => introduction.Index) + .ToList(); + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.6.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.6.2.md index a93e1895..f8aeb0ca 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.6.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.6.2.md @@ -1,4 +1,5 @@ # v26.6.2, build 242 (2026-06-xx xx:xx UTC) - Added a read-only view for organization-managed profiles and chat templates, so users can inspect the content while the organization remains in control of changes. +- Added support for organization-managed introduction texts on the home page. Configuration plugins can now add custom Markdown introductions and hide the built-in introduction. - Fixed organization-managed chat templates not showing the correct icon in the chat template selection menu. - Fixed self-hosted provider API keys sometimes being stored under a localized name. AI Studio now uses a stable key name, keeps correct entries working, and automatically migrates known localized entries for LLM, transcription, and embedding providers. Organizations using configuration plugins do not need to change their plugins; affected users who still see an invalid API key warning should open the provider, transcription, or embedding settings and update the API key once. \ No newline at end of file