mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-06-27 15:56:28 +00:00
Added support for organization-managed introduction texts
This commit is contained in:
parent
e04879fd7f
commit
9b29c9896c
@ -10,6 +10,8 @@
|
|||||||
<InnerScrolling>
|
<InnerScrolling>
|
||||||
<MudExpansionPanels Class="mb-3" MultiExpansion="@false">
|
<MudExpansionPanels Class="mb-3" MultiExpansion="@false">
|
||||||
|
|
||||||
|
@if (this.SettingsManager.ConfigurationData.App.ShowIntroduction)
|
||||||
|
{
|
||||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.MenuBook" HeaderText="@T("Introduction")" IsExpanded="@true">
|
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.MenuBook" HeaderText="@T("Introduction")" IsExpanded="@true">
|
||||||
<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!")
|
||||||
@ -25,6 +27,14 @@
|
|||||||
@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>
|
||||||
|
}
|
||||||
|
|
||||||
|
@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")">
|
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.EventNote" HeaderText="@T("Last Changelog")">
|
||||||
<MudMarkdown Value="@this.LastChangeContent" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
<MudMarkdown Value="@this.LastChangeContent" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||||
|
|||||||
@ -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,18 @@ public partial class Home : MSGComponentBase
|
|||||||
|
|
||||||
private TextItem[] itemsAdvantages = [];
|
private TextItem[] itemsAdvantages = [];
|
||||||
|
|
||||||
|
private List<HomeIntroductionPanelData> introductionPanels = [];
|
||||||
|
|
||||||
|
private sealed record HomeIntroductionPanelData(string HeaderText, DataIntroduction Introduction);
|
||||||
|
|
||||||
#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();
|
||||||
|
|
||||||
// Read the last change content asynchronously
|
// Read the last change content asynchronously
|
||||||
// without blocking the UI thread:
|
// without blocking the UI thread:
|
||||||
@ -69,10 +76,12 @@ public partial class Home : MSGComponentBase
|
|||||||
{
|
{
|
||||||
case Event.PLUGINS_RELOADED:
|
case Event.PLUGINS_RELOADED:
|
||||||
this.InitializeAdvantagesItems();
|
this.InitializeAdvantagesItems();
|
||||||
|
this.RefreshIntroductionPanels();
|
||||||
await this.InvokeAsync(this.StateHasChanged);
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Event.CONFIGURATION_CHANGED:
|
case Event.CONFIGURATION_CHANGED:
|
||||||
|
this.RefreshIntroductionPanels();
|
||||||
await this.InvokeAsync(this.StateHasChanged);
|
await this.InvokeAsync(this.StateHasChanged);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -80,6 +89,17 @@ public partial class Home : MSGComponentBase
|
|||||||
|
|
||||||
#endregion
|
#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()
|
private async Task ReadLastChangeAsync()
|
||||||
{
|
{
|
||||||
var latest = Changelog.LOGS.MaxBy(n => n.Build);
|
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.
|
-- 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.
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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<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.
|
||||||
@ -25,6 +26,11 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyList<DataMandatoryInfo> MandatoryInfos => this.mandatoryInfos;
|
public IReadOnlyList<DataMandatoryInfo> MandatoryInfos => this.mandatoryInfos;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of introductions provided by this configuration plugin.
|
||||||
|
/// </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 +136,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 +161,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 +217,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 +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);
|
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))
|
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;
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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.
|
||||||
Loading…
Reference in New Issue
Block a user