Add hash-based acceptance to track content changes

This commit is contained in:
Thorsten Sommer 2026-04-10 16:31:13 +02:00
parent 5e38603fea
commit 277cc01cbf
No known key found for this signature in database
GPG Key ID: B0B7E2FC074BF1F5
6 changed files with 61 additions and 7 deletions

View File

@ -7,18 +7,28 @@
@if (this.ShowAcceptanceMetadata) @if (this.ShowAcceptanceMetadata)
{ {
@if (this.Acceptance is null) @if (this.AcceptanceStatus is MandatoryInfoAcceptanceStatus.MISSING)
{ {
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="@true"> <MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="@true">
@T("This mandatory info has not been accepted yet.") @T("This mandatory info has not been accepted yet.")
</MudAlert> </MudAlert>
} }
else if (!string.Equals(this.Acceptance.AcceptedVersion, this.Info.VersionText, StringComparison.Ordinal)) else if (this.AcceptanceStatus is MandatoryInfoAcceptanceStatus.VERSION_CHANGED)
{ {
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="@true"> <MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="@true">
@T("The current version has not been accepted yet.") @T("A new version of the terms is available. Please review it again.")
<br /> <br />
@T("Last accepted version"): @this.Acceptance.AcceptedVersion @T("Last accepted version"): @this.Acceptance!.AcceptedVersion
<br />
@T("Accepted at (UTC)"): @this.Acceptance.AcceptedAtUtc.UtcDateTime.ToString("u")
</MudAlert>
}
else if (this.AcceptanceStatus is MandatoryInfoAcceptanceStatus.CONTENT_CHANGED)
{
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="@true">
@T("Please review this text again. The content was changed.")
<br />
@T("Last accepted version"): @this.Acceptance!.AcceptedVersion
<br /> <br />
@T("Accepted at (UTC)"): @this.Acceptance.AcceptedAtUtc.UtcDateTime.ToString("u") @T("Accepted at (UTC)"): @this.Acceptance.AcceptedAtUtc.UtcDateTime.ToString("u")
</MudAlert> </MudAlert>
@ -26,7 +36,7 @@
else else
{ {
<MudAlert Severity="Severity.Success" Variant="Variant.Outlined" Dense="@true"> <MudAlert Severity="Severity.Success" Variant="Variant.Outlined" Dense="@true">
@T("Accepted version"): @this.Acceptance.AcceptedVersion @T("Accepted version"): @this.Acceptance!.AcceptedVersion
<br /> <br />
@T("Accepted at (UTC)"): @this.Acceptance.AcceptedAtUtc.UtcDateTime.ToString("u") @T("Accepted at (UTC)"): @this.Acceptance.AcceptedAtUtc.UtcDateTime.ToString("u")
</MudAlert> </MudAlert>

View File

@ -6,6 +6,14 @@ namespace AIStudio.Components;
public partial class MandatoryInfoDisplay public partial class MandatoryInfoDisplay
{ {
private enum MandatoryInfoAcceptanceStatus
{
MISSING,
VERSION_CHANGED,
CONTENT_CHANGED,
ACCEPTED,
}
[Parameter] [Parameter]
public DataMandatoryInfo Info { get; set; } = new(); public DataMandatoryInfo Info { get; set; } = new();
@ -14,4 +22,21 @@ public partial class MandatoryInfoDisplay
[Parameter] [Parameter]
public bool ShowAcceptanceMetadata { get; set; } 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;
}
}
} }

View File

@ -418,7 +418,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
.Where(info => .Where(info =>
{ {
var acceptance = this.SettingsManager.ConfigurationData.MandatoryInformation.FindAcceptance(info.Id); 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 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
{ {
InfoId = info.Id, InfoId = info.Id,
AcceptedVersion = info.VersionText, AcceptedVersion = info.VersionText,
AcceptedHash = info.GetAcceptanceHash(),
AcceptedAtUtc = DateTimeOffset.UtcNow, AcceptedAtUtc = DateTimeOffset.UtcNow,
EnterpriseConfigurationPluginId = info.EnterpriseConfigurationPluginId, EnterpriseConfigurationPluginId = info.EnterpriseConfigurationPluginId,
}; };

View File

@ -267,6 +267,8 @@ CONFIG["CHAT_TEMPLATES"] = {}
CONFIG["DOCUMENT_ANALYSIS_POLICIES"] = {} CONFIG["DOCUMENT_ANALYSIS_POLICIES"] = {}
-- 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.
-- Changing Version additionally allows the UI to communicate that a new version is available.
CONFIG["MANDATORY_INFOS"] = {} CONFIG["MANDATORY_INFOS"] = {}
-- An example mandatory info: -- An example mandatory info:

View File

@ -1,3 +1,6 @@
using System.Security.Cryptography;
using System.Text;
using Lua; using Lua;
namespace AIStudio.Settings.DataModel; namespace AIStudio.Settings.DataModel;
@ -22,7 +25,8 @@ public sealed record DataMandatoryInfo
public string Title { get; private init; } = string.Empty; public string Title { get; private init; } = string.Empty;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
public string VersionText { get; private init; } = string.Empty; public string VersionText { get; private init; } = string.Empty;
@ -41,6 +45,13 @@ public sealed record DataMandatoryInfo
/// </summary> /// </summary>
public string RejectButtonText { get; private init; } = string.Empty; 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) public static bool TryParseConfiguration(int idx, LuaTable table, Guid configPluginId, out DataMandatoryInfo mandatoryInfo)
{ {
mandatoryInfo = new DataMandatoryInfo(); mandatoryInfo = new DataMandatoryInfo();

View File

@ -12,6 +12,11 @@ public sealed record DataMandatoryInfoAcceptance
/// </summary> /// </summary>
public string AcceptedVersion { get; init; } = string.Empty; public string AcceptedVersion { get; init; } = string.Empty;
/// <summary>
/// The accepted hash of the mandatory info content.
/// </summary>
public string AcceptedHash { get; init; } = string.Empty;
/// <summary> /// <summary>
/// The UTC time of the acceptance. /// The UTC time of the acceptance.
/// </summary> /// </summary>