mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-06-27 14:56:27 +00:00
Added support for organization-managed introduction texts
This commit is contained in:
parent
e04879fd7f
commit
9b29c9896c
@ -10,6 +10,8 @@
|
||||
<InnerScrolling>
|
||||
<MudExpansionPanels Class="mb-3" MultiExpansion="@false">
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.App.ShowIntroduction)
|
||||
{
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.MenuBook" HeaderText="@T("Introduction")" IsExpanded="@true">
|
||||
<MudText Typo="Typo.h5" Class="mb-3">
|
||||
@T("Welcome to MindWork AI Studio!")
|
||||
@ -25,6 +27,14 @@
|
||||
@T("We hope you enjoy using MindWork AI Studio to bring your AI projects to life!")
|
||||
</MudText>
|
||||
</ExpansionPanel>
|
||||
}
|
||||
|
||||
@foreach (var introductionPanel in this.introductionPanels)
|
||||
{
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Info" HeaderText="@introductionPanel.HeaderText">
|
||||
<MudJustifiedMarkdown Value="@introductionPanel.Introduction.Markdown" />
|
||||
</ExpansionPanel>
|
||||
}
|
||||
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.EventNote" HeaderText="@T("Last Changelog")">
|
||||
<MudMarkdown Value="@this.LastChangeContent" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
using AIStudio.Components;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.PluginSystem;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
@ -19,12 +20,18 @@ public partial class Home : MSGComponentBase
|
||||
|
||||
private TextItem[] itemsAdvantages = [];
|
||||
|
||||
private List<HomeIntroductionPanelData> 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);
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -57,6 +57,11 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
|
||||
/// </summary>
|
||||
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>
|
||||
/// Should the quick start guide be visible on the home page?
|
||||
/// </summary>
|
||||
|
||||
@ -0,0 +1,85 @@
|
||||
using Lua;
|
||||
|
||||
namespace AIStudio.Settings.DataModel;
|
||||
|
||||
public sealed record DataIntroduction
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
||||
|
||||
private List<PluginConfigurationObject> configObjects = [];
|
||||
private List<DataMandatoryInfo> mandatoryInfos = [];
|
||||
private List<DataIntroduction> introductions = [];
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public IReadOnlyList<DataMandatoryInfo> MandatoryInfos => this.mandatoryInfos;
|
||||
|
||||
/// <summary>
|
||||
/// The list of introductions provided by this configuration plugin.
|
||||
/// </summary>
|
||||
public IReadOnlyList<DataIntroduction> Introductions => this.introductions;
|
||||
|
||||
/// <summary>
|
||||
/// True/false when explicitly configured in the plugin, otherwise null.
|
||||
/// </summary>
|
||||
@ -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<LuaTable>(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);
|
||||
|
||||
@ -207,6 +217,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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -136,4 +136,13 @@ public static partial class PluginFactory
|
||||
.SelectMany(plugin => plugin.MandatoryInfos)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static IReadOnlyList<DataIntroduction> GetIntroductions()
|
||||
{
|
||||
return RUNNING_PLUGINS
|
||||
.OfType<PluginConfiguration>()
|
||||
.SelectMany(plugin => plugin.Introductions)
|
||||
.OrderBy(introduction => introduction.Index)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
Loading…
Reference in New Issue
Block a user