Added support for organization-managed introduction texts (#814)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer 2026-06-20 20:28:22 +02:00 committed by GitHub
parent e04879fd7f
commit 5045da3a91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 291 additions and 27 deletions

View File

@ -112,12 +112,16 @@ Plugins can configure:
- Chat templates - Chat templates
- etc. - etc.
When adding configuration options, update: Configuration plugins provide three kinds of values:
- `app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs`: In method `TryProcessConfiguration` register new options. - **Managed settings:** simple values such as booleans, numbers, strings, enums, lists, or sets handled through `ManagedConfiguration`. These values may be locked or used as organization defaults.
- `app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs`: In method `LoadAll` check for leftover configuration. - **Managed configuration objects:** complex Lua tables that are persisted into `SettingsManager.ConfigurationData`, implement `IConfigurationObject`, and are cleaned up through `PluginConfigurationObject.CleanLeftOverConfigurationObjects(...)`. Examples include providers, profiles, chat templates, data sources, and document analysis policies.
- The corresponding data class in `app/MindWork AI Studio/Settings/DataModel/` to call `ManagedConfiguration.Register(...)`, when adding config options (in contrast to complex config. objects) - **Live plugin content:** complex Lua tables that implement `ILivePluginContent` and are read live from running plugins instead of being persisted to `ConfigurationData`. Examples include `MANDATORY_INFOS` and `INTRODUCTIONS`. If live plugin content creates persistent side data, add a dedicated cleanup path for that side data, like mandatory-info acceptances.
- `app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs` for parsing logic of complex configuration objects.
- `app/MindWork AI Studio/Plugins/configuration/plugin.lua` to document the new configuration option. When adding configuration plugin capabilities:
- For managed settings, update the corresponding data class in `app/MindWork AI Studio/Settings/DataModel/` to call `ManagedConfiguration.Register(...)`, process the setting in `PluginConfiguration.TryProcessConfiguration`, and check for leftover managed configuration in `PluginFactory.Loading.LoadAll`.
- For managed configuration objects, update `PluginConfigurationObject.cs` and `PluginConfigurationObjectType.cs`, persist them in the appropriate `ConfigurationData` collection, and add cleanup via `PluginConfigurationObject.CleanLeftOverConfigurationObjects(...)`.
- For live plugin content, add a data type implementing `ILivePluginContent`, parse it in `PluginConfiguration`, expose it through `PluginFactory`, and add any required cleanup only for persistent side data.
- Always document the new capability in `app/MindWork AI Studio/Plugins/configuration/plugin.lua`.
## RAG (Retrieval-Augmented Generation) ## RAG (Retrieval-Augmented Generation)

View File

@ -6067,6 +6067,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T144565305"] = "The app requires minimal
-- You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit. -- You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T149711988"] = "You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit." UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T149711988"] = "You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit."
-- Version
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1573770551"] = "Version"
-- Assistants -- Assistants
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1614176092"] = "Assistants" UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1614176092"] = "Assistants"

View File

@ -8,9 +8,11 @@
</MudText> </MudText>
<InnerScrolling> <InnerScrolling>
<MudExpansionPanels Class="mb-3" MultiExpansion="@false"> <MudExpansionPanels @key="@this.expansionPanelsRenderKey" Class="mb-3" MultiExpansion="@false">
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.MenuBook" HeaderText="@T("Introduction")" IsExpanded="@true"> @if (this.SettingsManager.ConfigurationData.App.ShowIntroduction)
{
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.MenuBook" HeaderText="@T("Introduction")" IsExpanded="@this.IsPanelExpanded(PANEL_ID_BUILT_IN_INTRODUCTION)" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(PANEL_ID_BUILT_IN_INTRODUCTION, isExpanded))">
<MudText Typo="Typo.h5" Class="mb-3"> <MudText Typo="Typo.h5" Class="mb-3">
@T("Welcome to MindWork AI Studio!") @T("Welcome to MindWork AI Studio!")
</MudText> </MudText>
@ -25,18 +27,29 @@
@T("We hope you enjoy using MindWork AI Studio to bring your AI projects to life!") @T("We hope you enjoy using MindWork AI Studio to bring your AI projects to life!")
</MudText> </MudText>
</ExpansionPanel> </ExpansionPanel>
}
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.EventNote" HeaderText="@T("Last Changelog")"> @foreach (var introduction in this.introductions)
{
<ExpansionPanel @key="@introduction.Id" HeaderIcon="@Icons.Material.Filled.Info" HeaderText="@introduction.Title" IsExpanded="@this.IsPanelExpanded(IntroductionPanelId(introduction))" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(IntroductionPanelId(introduction), isExpanded))">
<MudText Typo="Typo.body2" Class="mb-3">
@T("Version"): @introduction.VersionText
</MudText>
<MudJustifiedMarkdown Value="@introduction.Markdown" />
</ExpansionPanel>
}
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.EventNote" HeaderText="@T("Last Changelog")" IsExpanded="@this.IsPanelExpanded(PANEL_ID_LAST_CHANGELOG)" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(PANEL_ID_LAST_CHANGELOG, isExpanded))">
<MudMarkdown Value="@this.LastChangeContent" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/> <MudMarkdown Value="@this.LastChangeContent" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
</ExpansionPanel> </ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Lightbulb" HeaderText="@T("Vision")"> <ExpansionPanel HeaderIcon="@Icons.Material.Filled.Lightbulb" HeaderText="@T("Vision")" IsExpanded="@this.IsPanelExpanded(PANEL_ID_VISION)" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(PANEL_ID_VISION, isExpanded))">
<Vision/> <Vision/>
</ExpansionPanel> </ExpansionPanel>
@if (this.SettingsManager.ConfigurationData.App.ShowQuickStartGuide) @if (this.SettingsManager.ConfigurationData.App.ShowQuickStartGuide)
{ {
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.RocketLaunch" HeaderText="@T("Quick Start Guide")"> <ExpansionPanel HeaderIcon="@Icons.Material.Filled.RocketLaunch" HeaderText="@T("Quick Start Guide")" IsExpanded="@this.IsPanelExpanded(PANEL_ID_QUICK_START_GUIDE)" ExpandedChanged="@(isExpanded => this.SetPanelExpanded(PANEL_ID_QUICK_START_GUIDE, isExpanded))">
<MudMarkdown Props="Markdown.DefaultConfig" Value="@QUICK_START_GUIDE" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/> <MudMarkdown Props="Markdown.DefaultConfig" Value="@QUICK_START_GUIDE" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
</ExpansionPanel> </ExpansionPanel>
} }

View File

@ -1,5 +1,6 @@
using AIStudio.Components; using AIStudio.Components;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools.PluginSystem;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
@ -19,12 +20,24 @@ public partial class Home : MSGComponentBase
private TextItem[] itemsAdvantages = []; private TextItem[] itemsAdvantages = [];
private List<DataIntroduction> introductions = [];
private string expandedPanelId = string.Empty;
private int expansionPanelsRenderKey;
private const string PANEL_ID_BUILT_IN_INTRODUCTION = "built-in-introduction";
private const string PANEL_ID_LAST_CHANGELOG = "last-changelog";
private const string PANEL_ID_VISION = "vision";
private const string PANEL_ID_QUICK_START_GUIDE = "quick-start-guide";
#region Overrides of ComponentBase #region Overrides of ComponentBase
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED ]);
await base.OnInitializedAsync(); await base.OnInitializedAsync();
this.InitializeAdvantagesItems(); this.InitializeAdvantagesItems();
this.RefreshIntroductionPanels();
this.EnsureDefaultExpandedPanel();
// Read the last change content asynchronously // Read the last change content asynchronously
// without blocking the UI thread: // without blocking the UI thread:
@ -69,10 +82,14 @@ public partial class Home : MSGComponentBase
{ {
case Event.PLUGINS_RELOADED: case Event.PLUGINS_RELOADED:
this.InitializeAdvantagesItems(); this.InitializeAdvantagesItems();
this.RefreshIntroductionPanels();
this.EnsureDefaultExpandedPanel();
await this.InvokeAsync(this.StateHasChanged); await this.InvokeAsync(this.StateHasChanged);
break; break;
case Event.CONFIGURATION_CHANGED: case Event.CONFIGURATION_CHANGED:
this.RefreshIntroductionPanels();
this.EnsureDefaultExpandedPanel();
await this.InvokeAsync(this.StateHasChanged); await this.InvokeAsync(this.StateHasChanged);
break; break;
} }
@ -80,6 +97,42 @@ public partial class Home : MSGComponentBase
#endregion #endregion
private void RefreshIntroductionPanels()
{
this.introductions = PluginFactory.GetIntroductions().ToList();
}
private string GetDefaultExpandedPanelId()
{
if (this.SettingsManager.ConfigurationData.App.ShowIntroduction)
return PANEL_ID_BUILT_IN_INTRODUCTION;
var firstIntroduction = this.introductions.FirstOrDefault();
return firstIntroduction is not null
? IntroductionPanelId(firstIntroduction)
: PANEL_ID_LAST_CHANGELOG;
}
private void EnsureDefaultExpandedPanel()
{
this.expandedPanelId = this.GetDefaultExpandedPanelId();
this.expansionPanelsRenderKey++;
}
private bool IsPanelExpanded(string panelId) => string.Equals(this.expandedPanelId, panelId, StringComparison.Ordinal);
private Task SetPanelExpanded(string panelId, bool isExpanded)
{
if (isExpanded)
this.expandedPanelId = panelId;
else if (this.IsPanelExpanded(panelId))
this.expandedPanelId = string.Empty;
return Task.CompletedTask;
}
private static string IntroductionPanelId(DataIntroduction introduction) => $"introduction:{introduction.Id}";
private async Task ReadLastChangeAsync() private async Task ReadLastChangeAsync()
{ {
var latest = Changelog.LOGS.MaxBy(n => n.Build); var latest = Changelog.LOGS.MaxBy(n => n.Build);

View File

@ -207,6 +207,9 @@ CONFIG["SETTINGS"] = {}
-- Configure whether the quick start guide is shown on the welcome page. -- Configure whether the quick start guide is shown on the welcome page.
-- CONFIG["SETTINGS"]["DataApp.ShowQuickStartGuide"] = false -- 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: -- Configure the user permission to add providers:
-- CONFIG["SETTINGS"]["DataApp.AllowUserToAddProvider"] = false -- 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: -- Mandatory infos that users must explicitly accept before using AI Studio:
-- AI Studio asks users again when Version, Title, or Markdown change. -- 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. -- Changing Version additionally allows the UI to communicate that a new version is available.

View File

@ -6069,6 +6069,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T144565305"] = "Die App benötigt nur we
-- You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit. -- You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T149711988"] = "Sie zahlen nur für das, was Sie tatsächlich nutzen das kann günstiger sein als monatliche Abos wie ChatGPT Plus, vor allem bei gelegentlicher Nutzung. Aber Vorsicht: Bei sehr intensiver Nutzung können die API-Kosten deutlich höher ausfallen. Leider bieten die Anbieter derzeit keine Möglichkeit, die aktuellen Kosten direkt in der App anzuzeigen. Prüfen Sie deshalb regelmäßig Ihr Konto beim jeweiligen Anbieter, um ihre Ausgaben im Blick zu behalten. Nutzen Sie, wenn möglich, Prepaid-Optionen und legen Sie ein Ausgabenlimit fest." UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T149711988"] = "Sie zahlen nur für das, was Sie tatsächlich nutzen das kann günstiger sein als monatliche Abos wie ChatGPT Plus, vor allem bei gelegentlicher Nutzung. Aber Vorsicht: Bei sehr intensiver Nutzung können die API-Kosten deutlich höher ausfallen. Leider bieten die Anbieter derzeit keine Möglichkeit, die aktuellen Kosten direkt in der App anzuzeigen. Prüfen Sie deshalb regelmäßig Ihr Konto beim jeweiligen Anbieter, um ihre Ausgaben im Blick zu behalten. Nutzen Sie, wenn möglich, Prepaid-Optionen und legen Sie ein Ausgabenlimit fest."
-- Version
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1573770551"] = "Version"
-- Assistants -- Assistants
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1614176092"] = "Assistenten" UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1614176092"] = "Assistenten"

View File

@ -6069,6 +6069,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T144565305"] = "The app requires minimal
-- You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit. -- You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit.
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T149711988"] = "You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit." UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T149711988"] = "You only pay for what you use, which can be cheaper than monthly subscription services like ChatGPT Plus, especially if used infrequently. But beware, here be dragons: For extremely intensive usage, the API costs can be significantly higher. Unfortunately, providers currently do not offer a way to display current costs in the app. Therefore, check your account with the respective provider to see how your costs are developing. When available, use prepaid and set a cost limit."
-- Version
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1573770551"] = "Version"
-- Assistants -- Assistants
UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1614176092"] = "Assistants" UI_TEXT_CONTENT["AISTUDIO::PAGES::HOME::T1614176092"] = "Assistants"

View File

@ -57,6 +57,11 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
/// </summary> /// </summary>
public StartPage StartPage { get; set; } = ManagedConfiguration.Register(configSelection, n => n.StartPage, StartPage.HOME); public StartPage StartPage { get; set; } = ManagedConfiguration.Register(configSelection, n => n.StartPage, StartPage.HOME);
/// <summary>
/// Should the built-in introduction be visible on the home page?
/// </summary>
public bool ShowIntroduction { get; set; } = ManagedConfiguration.Register(configSelection, n => n.ShowIntroduction, true);
/// <summary> /// <summary>
/// Should the quick start guide be visible on the home page? /// Should the quick start guide be visible on the home page?
/// </summary> /// </summary>

View File

@ -0,0 +1,87 @@
using AIStudio.Tools.PluginSystem;
using Lua;
namespace AIStudio.Settings.DataModel;
public sealed record DataIntroduction : ILivePluginContent
{
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger<DataIntroduction>();
/// <summary>
/// The stable ID of the introduction.
/// </summary>
public string Id { get; private init; } = string.Empty;
/// <summary>
/// The ID of the enterprise configuration plugin that provides this introduction.
/// </summary>
public Guid EnterpriseConfigurationPluginId { get; private init; } = Guid.Empty;
/// <summary>
/// The title shown to the user.
/// </summary>
public string Title { get; private init; } = string.Empty;
/// <summary>
/// The configured version string shown to the user.
/// </summary>
public string VersionText { get; private init; } = string.Empty;
/// <summary>
/// The sort index used on the home page.
/// </summary>
public int Index { get; private init; } = 1;
/// <summary>
/// The Markdown content shown to the user.
/// </summary>
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<string>(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<string>(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<string>(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<string>(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;
}
}

View File

@ -1,11 +1,13 @@
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using AIStudio.Tools.PluginSystem;
using Lua; using Lua;
namespace AIStudio.Settings.DataModel; namespace AIStudio.Settings.DataModel;
public sealed record DataMandatoryInfo public sealed record DataMandatoryInfo : ILivePluginContent
{ {
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger<DataMandatoryInfo>(); private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger<DataMandatoryInfo>();

View File

@ -0,0 +1,18 @@
namespace AIStudio.Tools.PluginSystem;
/// <summary>
/// Represents complex content from a configuration plugin that is read live from
/// running plugins and is not persisted to the settings data model.
/// </summary>
public interface ILivePluginContent
{
/// <summary>
/// The stable ID of the live plugin content.
/// </summary>
public string Id { get; }
/// <summary>
/// The ID of the enterprise configuration plugin that provides this content.
/// </summary>
public Guid EnterpriseConfigurationPluginId { get; }
}

View File

@ -14,6 +14,7 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
private List<PluginConfigurationObject> configObjects = []; private List<PluginConfigurationObject> configObjects = [];
private List<DataMandatoryInfo> mandatoryInfos = []; private List<DataMandatoryInfo> mandatoryInfos = [];
private List<DataIntroduction> introductions = [];
/// <summary> /// <summary>
/// The list of configuration objects. Configuration objects are, e.g., providers or chat templates. /// The list of configuration objects. Configuration objects are, e.g., providers or chat templates.
@ -22,9 +23,16 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
/// <summary> /// <summary>
/// The list of mandatory infos provided by this configuration plugin. /// The list of mandatory infos provided by this configuration plugin.
/// Mandatory infos are live plugin content and are not persisted to ConfigurationData.
/// </summary> /// </summary>
public IReadOnlyList<DataMandatoryInfo> MandatoryInfos => this.mandatoryInfos; public IReadOnlyList<DataMandatoryInfo> MandatoryInfos => this.mandatoryInfos;
/// <summary>
/// The list of introductions provided by this configuration plugin.
/// Introductions are live plugin content and are not persisted to ConfigurationData.
/// </summary>
public IReadOnlyList<DataIntroduction> Introductions => this.introductions;
/// <summary> /// <summary>
/// True/false when explicitly configured in the plugin, otherwise null. /// True/false when explicitly configured in the plugin, otherwise null.
/// </summary> /// </summary>
@ -130,6 +138,7 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
{ {
this.configObjects.Clear(); this.configObjects.Clear();
this.mandatoryInfos.Clear(); this.mandatoryInfos.Clear();
this.introductions.Clear();
// Ensure that the main CONFIG table exists and is a valid Lua table: // Ensure that the main CONFIG table exists and is a valid Lua table:
if (!this.State.Environment["CONFIG"].TryRead<LuaTable>(out var mainTable)) if (!this.State.Environment["CONFIG"].TryRead<LuaTable>(out var mainTable))
@ -154,6 +163,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Config: what should be the start page? // Config: what should be the start page?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.StartPage, this.Id, settingsTable, dryRun); 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? // Config: show quick start guide on the home page?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShowQuickStartGuide, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.ShowQuickStartGuide, this.Id, settingsTable, dryRun);
@ -207,6 +219,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Handle configured mandatory infos: // Handle configured mandatory infos:
this.TryReadMandatoryInfos(mainTable); this.TryReadMandatoryInfos(mainTable);
// Handle configured introductions:
this.TryReadIntroductions(mainTable);
// Config: preselected provider? // Config: preselected provider?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun); ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);
@ -240,4 +255,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); 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<LuaTable>(out var introductionsTable))
return;
for (var i = 1; i <= introductionsTable.ArrayLength; i++)
{
var luaIntroductionValue = introductionsTable[i];
if (!luaIntroductionValue.TryRead<LuaTable>(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);
}
}
} }

View File

@ -214,6 +214,10 @@ public static partial class PluginFactory
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.StartPage, AVAILABLE_PLUGINS)) if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.StartPage, AVAILABLE_PLUGINS))
wasConfigurationChanged = true; 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: // Check for the quick start guide visibility:
if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShowQuickStartGuide, AVAILABLE_PLUGINS)) if(ManagedConfiguration.IsConfigurationLeftOver(x => x.App, x => x.ShowQuickStartGuide, AVAILABLE_PLUGINS))
wasConfigurationChanged = true; wasConfigurationChanged = true;

View File

@ -136,4 +136,13 @@ public static partial class PluginFactory
.SelectMany(plugin => plugin.MandatoryInfos) .SelectMany(plugin => plugin.MandatoryInfos)
.ToList(); .ToList();
} }
public static IReadOnlyList<DataIntroduction> GetIntroductions()
{
return RUNNING_PLUGINS
.OfType<PluginConfiguration>()
.SelectMany(plugin => plugin.Introductions)
.OrderBy(introduction => introduction.Index)
.ToList();
}
} }

View File

@ -1,4 +1,5 @@
# v26.6.2, build 242 (2026-06-xx xx:xx UTC) # 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 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 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. - 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.