diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingArtifactService.Assembly.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingArtifactService.Assembly.cs index e635e897e..585b1657f 100644 --- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingArtifactService.Assembly.cs +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingArtifactService.Assembly.cs @@ -381,26 +381,24 @@ public sealed partial class VisualBriefingArtifactService dictionary.Remove("footerTemplates"); dictionary.Remove("protectionLabel"); dictionary.Remove("_mwai"); - dictionary["_mwai"] = JsonSerializer.SerializeToElement(new - { - schemaVersion = VisualBriefingVersions.SCHEMA, - runtimeVersion = VisualBriefingVersions.RUNTIME, - aiStudioVersion = Assembly.GetExecutingAssembly().GetCustomAttribute()?.Version ?? "unknown", - assets = request.EmbeddedAssets ?? new Dictionary(StringComparer.Ordinal), - assetMetadata = (request.AssetPlan ?? []).ToDictionary( + dictionary["_mwai"] = JsonSerializer.SerializeToElement(new VisualBriefingProtectedData( + VisualBriefingVersions.SCHEMA, + VisualBriefingVersions.RUNTIME, + Assembly.GetExecutingAssembly().GetCustomAttribute()?.Version ?? "unknown", + request.EmbeddedAssets ?? new Dictionary(StringComparer.Ordinal), + (request.AssetPlan ?? []).ToDictionary( asset => asset.AssetId, - asset => new { asset.Description, asset.AltText }, + asset => new VisualBriefingAssetMetadata(asset.Description, asset.AltText), StringComparer.Ordinal), - footer = BuildFooter(manifest, request), - }, JSON_OPTIONS); - + BuildFooter(manifest, request)), JSON_OPTIONS); + return JsonSerializer.SerializeToElement(dictionary, JSON_OPTIONS); } /// /// Defines BuildFooter for the visual briefing feature. /// - private static object BuildFooter(VisualBriefingManifest manifest, VisualBriefingRevisionRequest request) + private static Dictionary BuildFooter(VisualBriefingManifest manifest, VisualBriefingRevisionRequest request) { var source = request.ExportMetadataSource; var protectionLevel = source?.ProtectionLevel ?? manifest.Settings.ProtectionLevel; diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingAssetMetadata.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingAssetMetadata.cs new file mode 100644 index 000000000..7804f4e67 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingAssetMetadata.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Assistants.VisualBriefing; + +/// +/// What the protected data of an exported briefing tells about one of its pictures. +/// +/// What the picture shows. +/// The text for people who cannot see the picture. +internal sealed record VisualBriefingAssetMetadata(string Description, string AltText); \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingAssistant.razor.Projects.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingAssistant.razor.Projects.cs index 19d505155..c2b89f135 100644 --- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingAssistant.razor.Projects.cs +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingAssistant.razor.Projects.cs @@ -382,16 +382,15 @@ public partial class VisualBriefingAssistant /// The fingerprint is serialized from exactly the values that SaveCurrentAsync /// hands to the store. That is deliberate: a handwritten field list would silently stop /// auto-saving whenever a new setting is added and someone forgets to list it here. Sources are - /// projected into a named shape because System.Text.Json ignores tuple fields and would + /// projected into a named record because System.Text.Json ignores tuple fields and would /// otherwise serialize every source list into the same empty object. /// /// The fingerprint of the current editor state. private string BuildPersistenceFingerprint() => JsonSerializer.Serialize( - new - { + new VisualBriefingPersistenceFingerprint( this.editor.Name, this.editor.Author, - Settings = this.editor.ToSettings(), - Sources = this.editor.ToSources().Select(source => new { source.Path, source.Kind }).ToArray(), - }, VisualBriefingJson.Canonical); + this.editor.ToSettings(), + [.. this.editor.ToSources().Select(source => new VisualBriefingSourceFingerprint(source.Path, source.Kind))]), + VisualBriefingJson.Canonical); } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingBuildOrchestrator.Recompile.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingBuildOrchestrator.Recompile.cs index dbd5adf13..be8ada150 100644 --- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingBuildOrchestrator.Recompile.cs +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingBuildOrchestrator.Recompile.cs @@ -121,21 +121,7 @@ internal sealed partial class VisualBriefingBuildOrchestrator var validationDataProperties = compiled.Data.EnumerateObject() .ToDictionary(property => property.Name, property => property.Value.Clone(), StringComparer.Ordinal); - validationDataProperties["_mwai"] = JsonSerializer.SerializeToElement(new - { - schemaVersion = VisualBriefingVersions.SCHEMA, - runtimeVersion = VisualBriefingVersions.RUNTIME, - aiStudioVersion = "validation", - assets = content.AssetPlan.ToDictionary(asset => asset.AssetId, _ => "data:image/png;base64,AA==", StringComparer.Ordinal), - footer = new - { - createdWith = "validation", - models = "validation", - createdAt = "validation", - authors = "validation", - protection = "validation", - }, - }, VisualBriefingJson.Canonical); + validationDataProperties["_mwai"] = JsonSerializer.SerializeToElement(VisualBriefingProtectedData.ForValidation(content.AssetPlan), VisualBriefingJson.Canonical); VisualBriefingCompilerInvariant.Guard( VisualBriefingBuildStage.COMPILATION, diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingChartCompiler.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingChartCompiler.cs index 17f398aa8..b3e36c945 100644 --- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingChartCompiler.cs +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingChartCompiler.cs @@ -1,10 +1,16 @@ using System.Text.Json; +using System.Text.Json.Nodes; namespace AIStudio.Assistants.VisualBriefing; /// /// Turns a validated chart specification into a branded chart-library option object. /// +/// +/// The option object is built as JSON nodes rather than as types of its own: it follows the options of Apache +/// ECharts, which differ for every kind of chart, and nothing in AI Studio reads it again. The order of its +/// properties is part of the compiled data, which gets hashed. +/// internal static class VisualBriefingChartCompiler { /// @@ -14,114 +20,116 @@ internal static class VisualBriefingChartCompiler /// The branded chart option. internal static JsonElement Compile(VisualBriefingChartSpec chart) { - object series = chart.Kind switch + var series = chart.Kind switch { - VisualBriefingChartKind.PIE or VisualBriefingChartKind.DONUT => - chart.Categories.Select((category, index) => new + VisualBriefingChartKind.PIE or VisualBriefingChartKind.DONUT => new JsonArray( + [ + .. chart.Categories.Select((category, index) => new JsonObject { - name = category, - value = chart.Series[0].Values[index], - }).ToArray(), + ["name"] = category, + ["value"] = chart.Series[0].Values[index], + }), + ]), - VisualBriefingChartKind.RADAR => chart.Series.Select(item => new - { - name = item.Name, - type = "radar", - data = new[] + VisualBriefingChartKind.RADAR => new JsonArray( + [ + .. chart.Series.Select(item => new JsonObject { - new + ["name"] = item.Name, + ["type"] = "radar", + ["data"] = new JsonArray(new JsonObject { - value = item.Values, - name = item.Name, - }, - }, - }).ToArray(), + ["value"] = Numbers(item.Values), + ["name"] = item.Name, + }), + }), + ]), - _ => chart.Series.Select(item => new - { - name = item.Name, - type = SeriesType(chart.Kind), - stack = chart.Kind is VisualBriefingChartKind.STACKED_BAR ? "total" : null, - areaStyle = chart.Kind is VisualBriefingChartKind.AREA ? new { opacity = 0.18 } : null, - smooth = chart.Kind is VisualBriefingChartKind.LINE or VisualBriefingChartKind.AREA, - showSymbol = chart.Kind is VisualBriefingChartKind.SCATTER, - symbolSize = chart.Kind is VisualBriefingChartKind.SCATTER ? 10 : 6, - itemStyle = chart.Kind is VisualBriefingChartKind.BAR or VisualBriefingChartKind.STACKED_BAR - ? new { borderRadius = new[] { 6, 6, 0, 0 } } : null, - data = item.Values, - }).ToArray(), + _ => new JsonArray( + [ + .. chart.Series.Select(item => new JsonObject + { + ["name"] = item.Name, + ["type"] = SeriesType(chart.Kind), + ["stack"] = chart.Kind is VisualBriefingChartKind.STACKED_BAR ? "total" : null, + ["areaStyle"] = chart.Kind is VisualBriefingChartKind.AREA ? new JsonObject { ["opacity"] = 0.18 } : null, + ["smooth"] = chart.Kind is VisualBriefingChartKind.LINE or VisualBriefingChartKind.AREA, + ["showSymbol"] = chart.Kind is VisualBriefingChartKind.SCATTER, + ["symbolSize"] = chart.Kind is VisualBriefingChartKind.SCATTER ? 10 : 6, + ["itemStyle"] = chart.Kind is VisualBriefingChartKind.BAR or VisualBriefingChartKind.STACKED_BAR + ? new JsonObject { ["borderRadius"] = new JsonArray(6, 6, 0, 0) } : null, + ["data"] = Numbers(item.Values), + }), + ]), }; - var option = new + var isCircular = chart.Kind is VisualBriefingChartKind.PIE or VisualBriefingChartKind.DONUT; + var hasAxes = chart.Kind is not (VisualBriefingChartKind.PIE or VisualBriefingChartKind.DONUT or VisualBriefingChartKind.RADAR); + var option = new JsonObject { - color = new[] { "#236A50", "#F2D264", "#79AE90", "#C97857", "#4E7894", "#9B6B8F" }, - backgroundColor = "transparent", - textStyle = new + ["color"] = Texts("#236A50", "#F2D264", "#79AE90", "#C97857", "#4E7894", "#9B6B8F"), + ["backgroundColor"] = "transparent", + ["textStyle"] = new JsonObject { - color = "#172A24", - fontFamily = "system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif", + ["color"] = "#172A24", + ["fontFamily"] = "system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif", }, - tooltip = new + ["tooltip"] = new JsonObject { - trigger = chart.Kind is VisualBriefingChartKind.PIE or VisualBriefingChartKind.DONUT ? "item" : "axis", - borderColor = "#D6E2DC", - backgroundColor = "#FFFEFA", - textStyle = new { color = "#172A24" }, + ["trigger"] = isCircular ? "item" : "axis", + ["borderColor"] = "#D6E2DC", + ["backgroundColor"] = "#FFFEFA", + ["textStyle"] = new JsonObject { ["color"] = "#172A24" }, }, - legend = new { show = true, top = 0, textStyle = new { color = "#4F635B" } }, - grid = chart.Kind is VisualBriefingChartKind.PIE or VisualBriefingChartKind.DONUT or VisualBriefingChartKind.RADAR - ? null - : new { left = 8, right = 16, top = 48, bottom = 8, containLabel = true }, + ["legend"] = new JsonObject { ["show"] = true, ["top"] = 0, ["textStyle"] = new JsonObject { ["color"] = "#4F635B" } }, + ["grid"] = hasAxes + ? new JsonObject { ["left"] = 8, ["right"] = 16, ["top"] = 48, ["bottom"] = 8, ["containLabel"] = true } + : null, - xAxis = chart.Kind is VisualBriefingChartKind.PIE or VisualBriefingChartKind.DONUT or VisualBriefingChartKind.RADAR - ? null - : new + ["xAxis"] = hasAxes + ? new JsonObject { - type = "category", - data = chart.Categories, - axisLine = new { lineStyle = new { color = "#B8C9C0" } }, - axisTick = new { show = false }, - axisLabel = new { color = "#5E7169" }, - }, - - yAxis = chart.Kind is VisualBriefingChartKind.PIE or VisualBriefingChartKind.DONUT or VisualBriefingChartKind.RADAR - ? null - : new - { - type = "value", - axisLine = new { show = false }, - axisTick = new { show = false }, - axisLabel = new { color = "#5E7169" }, - splitLine = new { lineStyle = new { color = "#E1EAE5" } }, - }, - - radar = chart.Kind is VisualBriefingChartKind.RADAR - ? new - { - indicator = chart.Categories.Select(name => new { name }).ToArray(), - splitArea = new { areaStyle = new { color = new[] { "#FFFEFA", "#EAF1EC" } } }, - axisName = new { color = "#5E7169" }, - splitLine = new { lineStyle = new { color = "#B8C9C0" } }, + ["type"] = "category", + ["data"] = Texts(chart.Categories), + ["axisLine"] = new JsonObject { ["lineStyle"] = new JsonObject { ["color"] = "#B8C9C0" } }, + ["axisTick"] = new JsonObject { ["show"] = false }, + ["axisLabel"] = new JsonObject { ["color"] = "#5E7169" }, } : null, - series = chart.Kind is VisualBriefingChartKind.PIE or VisualBriefingChartKind.DONUT - ? new[] + ["yAxis"] = hasAxes + ? new JsonObject { - new - { - type = "pie", - radius = chart.Kind is VisualBriefingChartKind.DONUT - ? new[] { "45%", "70%" } - : new[] { "0%", "70%" }, - padAngle = 2, - itemStyle = new { borderColor = "#FFFEFA", borderWidth = 2, borderRadius = 5 }, - label = new { color = "#4F635B" }, - data = series, - }, + ["type"] = "value", + ["axisLine"] = new JsonObject { ["show"] = false }, + ["axisTick"] = new JsonObject { ["show"] = false }, + ["axisLabel"] = new JsonObject { ["color"] = "#5E7169" }, + ["splitLine"] = new JsonObject { ["lineStyle"] = new JsonObject { ["color"] = "#E1EAE5" } }, } + : null, + + ["radar"] = chart.Kind is VisualBriefingChartKind.RADAR + ? new JsonObject + { + ["indicator"] = new JsonArray([.. chart.Categories.Select(name => new JsonObject { ["name"] = name })]), + ["splitArea"] = new JsonObject { ["areaStyle"] = new JsonObject { ["color"] = Texts("#FFFEFA", "#EAF1EC") } }, + ["axisName"] = new JsonObject { ["color"] = "#5E7169" }, + ["splitLine"] = new JsonObject { ["lineStyle"] = new JsonObject { ["color"] = "#B8C9C0" } }, + } + : null, + + ["series"] = isCircular + ? new JsonArray(new JsonObject + { + ["type"] = "pie", + ["radius"] = chart.Kind is VisualBriefingChartKind.DONUT ? Texts("45%", "70%") : Texts("0%", "70%"), + ["padAngle"] = 2, + ["itemStyle"] = new JsonObject { ["borderColor"] = "#FFFEFA", ["borderWidth"] = 2, ["borderRadius"] = 5 }, + ["label"] = new JsonObject { ["color"] = "#4F635B" }, + ["data"] = series, + }) : series, }; @@ -141,4 +149,8 @@ internal static class VisualBriefingChartCompiler VisualBriefingChartKind.RADAR => "radar", _ => "line", }; + + private static JsonArray Texts(params IEnumerable texts) => new([.. texts.Select(text => JsonValue.Create(text))]); + + private static JsonArray Numbers(IEnumerable numbers) => new([.. numbers.Select(number => JsonValue.Create(number))]); } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingCompiledData.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingCompiledData.cs new file mode 100644 index 000000000..972001e14 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingCompiledData.cs @@ -0,0 +1,20 @@ +using System.Text.Json; + +namespace AIStudio.Assistants.VisualBriefing; + +/// +/// The data of a compiled briefing, which its template binds to. +/// +/// The content of each slot, by its ID. +/// The chart-library option object of each chart, by the ID of its component. +/// The controls, their state, and the formulas of the briefing. +/// The text for people who cannot see a component, by the ID of the component. +/// The sources of each component, by the ID of the component. +/// The fixed labels which the runtime of the briefing shows. +internal sealed record VisualBriefingCompiledData( + Dictionary Slots, + Dictionary Charts, + JsonElement Interactions, + Dictionary Accessibility, + Dictionary> SourceReferences, + VisualBriefingLabels Labels); \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingContentData.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingContentData.cs new file mode 100644 index 000000000..3f9a01245 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingContentData.cs @@ -0,0 +1,37 @@ +using System.Text.Json; + +namespace AIStudio.Assistants.VisualBriefing; + +/// +/// The data of a content artifact, as the content stage keeps it along with the artifact. +/// +/// The content of each slot, by its ID. +/// The validated chart specifications. +/// The validated interaction controls. +/// The validated formula specifications. +/// The text for people who cannot see a component, by the ID of the component. +/// The sources of each component, by the ID of the component. +/// The fixed labels which the runtime of the briefing shows. +internal sealed record VisualBriefingContentData( + Dictionary Slots, + List Charts, + List Controls, + List Formulas, + Dictionary Accessibility, + Dictionary> SourceReferences, + VisualBriefingLabels Labels) +{ + /// + /// Gathers the data of a content artifact. + /// + /// The content artifact. + /// The data of the artifact. + internal static VisualBriefingContentData Of(VisualBriefingContentArtifact artifact) => new( + artifact.Slots.ToDictionary(slot => slot.SlotId, slot => slot.Value, StringComparer.Ordinal), + artifact.Charts, + artifact.Controls, + artifact.Formulas, + artifact.AccessibilityTexts, + artifact.SourceReferences, + VisualBriefingLabels.Of(artifact.ResetLabel)); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingContentStage.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingContentStage.cs index 916f5efce..5e971666c 100644 --- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingContentStage.cs +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingContentStage.cs @@ -52,20 +52,7 @@ internal sealed class VisualBriefingContentStage(StructuredLlmStageRunner stageR artifact.SourceCoverage = evidence.SourceCoverage; artifact.StructuralSignature = plan.StructuralSignature; artifact.Model = VisualBriefingModelNames.ExportLabel(provider); - artifact.Data = JsonSerializer.SerializeToElement(new - { - slots = artifact.Slots.ToDictionary(slot => slot.SlotId, slot => slot.Value, StringComparer.Ordinal), - charts = artifact.Charts, - controls = artifact.Controls, - formulas = artifact.Formulas, - accessibility = artifact.AccessibilityTexts, - sourceReferences = artifact.SourceReferences, - labels = new - { - reset = artifact.ResetLabel, - brand = "MindWork AI Studio", - }, - }, VisualBriefingJson.Canonical); + artifact.Data = JsonSerializer.SerializeToElement(VisualBriefingContentData.Of(artifact), VisualBriefingJson.Canonical); artifact.PayloadHash = VisualBriefingPayloadHash.ForContent(artifact.Slots, artifact.Charts, artifact.Controls, artifact.Formulas, artifact.AccessibilityTexts, artifact.SourceReferences, artifact.ResetLabel, artifact.SourceCoverage, artifact.AssetPlan, artifact.StructuralSignature); @@ -117,21 +104,10 @@ internal sealed class VisualBriefingContentStage(StructuredLlmStageRunner stageR var requiredSlots = plan.Sections .SelectMany(section => new[] { - new - { - SlotId = section.TitleSlotId, - Role = VisualBriefingSlotRole.TITLE, - Type = VisualBriefingSlotType.TEXT, - }, - - new - { - SlotId = section.SummarySlotId, - Role = VisualBriefingSlotRole.SUMMARY, - Type = VisualBriefingSlotType.TEXT, - }, - }.Concat(section.Components.SelectMany(component => component.Slots.Select(slot => new { slot.SlotId, slot.Role, Type = VisualBriefingSlotTypes.Expected(slot), } - )))).ToArray(); + new VisualBriefingRequiredSlot(section.TitleSlotId, VisualBriefingSlotRole.TITLE, VisualBriefingSlotType.TEXT), + new VisualBriefingRequiredSlot(section.SummarySlotId, VisualBriefingSlotRole.SUMMARY, VisualBriefingSlotType.TEXT), + }.Concat(section.Components.SelectMany(component => component.Slots.Select(slot => new VisualBriefingRequiredSlot(slot.SlotId, slot.Role, VisualBriefingSlotTypes.Expected(slot)))))) + .ToArray(); var chartComponentIds = components .Where(component => component.Kind is VisualBriefingComponentKind.CHART) @@ -141,21 +117,20 @@ internal sealed class VisualBriefingContentStage(StructuredLlmStageRunner stageR // Filterable tables are absent here: AI Studio derives their controls from the table data: var controlRequirements = components .Where(component => component.Kind is VisualBriefingComponentKind.TABS or VisualBriefingComponentKind.SIMULATION) - .Select(component => new - { + .Select(component => new VisualBriefingControlRequirement( component.ComponentId, component.Kind, - - PanelSlotIds = component.Slots + + PanelSlotIds: component.Slots .Where(slot => slot.Role is VisualBriefingSlotRole.PANEL) .Select(slot => slot.SlotId) .ToArray(), - - ResultSlotIds = component.Slots + + ResultSlotIds: component.Slots .Where(slot => slot.Role is VisualBriefingSlotRole.RESULT) .Select(slot => slot.SlotId) - .ToArray(), - }).ToArray(); + .ToArray())) + .ToArray(); return $""" Target language: {manifest.Settings.TargetLanguage.PromptGeneralPurpose(manifest.Settings.CustomTargetLanguage)} @@ -167,7 +142,7 @@ internal sealed class VisualBriefingContentStage(StructuredLlmStageRunner stageR Exact chart component IDs, each to receive exactly one chart: {JsonSerializer.Serialize(chartComponentIds, VisualBriefingJson.Canonical)} Exact control and formula requirements, no controls for any other component: {JsonSerializer.Serialize(controlRequirements, VisualBriefingJson.Canonical)} Plan: {JsonSerializer.Serialize(plan.Sections, VisualBriefingJson.Canonical)} - Evidence: {JsonSerializer.Serialize(new { evidence.Facts, evidence.Metrics, evidence.Tables, evidence.AssetPlan }, VisualBriefingJson.Canonical)} + Evidence: {JsonSerializer.Serialize(VisualBriefingEvidencePrompt.Of(evidence), VisualBriefingJson.Canonical)} """; } @@ -234,21 +209,7 @@ internal sealed class VisualBriefingContentStage(StructuredLlmStageRunner stageR var compiled = VisualBriefingCompilerInvariant.Guard(VisualBriefingBuildStage.CONTENT, () => VisualBriefingLayoutCompiler.Compile(plan, projection, layout, VisualBriefingDesignProfile.EDITORIAL)); var data = compiled.Data.EnumerateObject().ToDictionary(property => property.Name, property => property.Value.Clone(), StringComparer.Ordinal); - data["_mwai"] = JsonSerializer.SerializeToElement(new - { - schemaVersion = VisualBriefingVersions.SCHEMA, - runtimeVersion = VisualBriefingVersions.RUNTIME, - aiStudioVersion = "validation", - assets = evidence.AssetPlan.ToDictionary(asset => asset.AssetId, _ => "data:image/png;base64,AA==", StringComparer.Ordinal), - footer = new - { - createdWith = "validation", - models = "validation", - createdAt = "validation", - authors = "validation", - protection = "validation", - }, - }, VisualBriefingJson.Canonical); + data["_mwai"] = JsonSerializer.SerializeToElement(VisualBriefingProtectedData.ForValidation(evidence.AssetPlan), VisualBriefingJson.Canonical); var validationData = JsonSerializer.SerializeToElement(data, VisualBriefingJson.Canonical); VisualBriefingCompilerInvariant.Guard(VisualBriefingBuildStage.CONTENT, diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingControlRequirement.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingControlRequirement.cs new file mode 100644 index 000000000..a4657a521 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingControlRequirement.cs @@ -0,0 +1,10 @@ +namespace AIStudio.Assistants.VisualBriefing; + +/// +/// A component for which the content stage asks its model to write controls and formulas. +/// +/// The ID of the component. +/// The kind of the component. +/// The IDs of the panel slots of the component, in their order. +/// The IDs of the result slots of the component, in their order. +internal sealed record VisualBriefingControlRequirement(string ComponentId, VisualBriefingComponentKind Kind, string[] PanelSlotIds, string[] ResultSlotIds); \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEvidencePrompt.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEvidencePrompt.cs new file mode 100644 index 000000000..acf437bb0 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEvidencePrompt.cs @@ -0,0 +1,22 @@ +namespace AIStudio.Assistants.VisualBriefing; + +/// +/// The evidence which the plan and content stages show their models. +/// +/// The facts of the evidence. +/// The metrics of the evidence. +/// The tables of the evidence. +/// The planned pictures of the briefing. +internal sealed record VisualBriefingEvidencePrompt( + List Facts, + List Metrics, + List Tables, + List AssetPlan) +{ + /// + /// Gathers what a model gets to see of an evidence artifact. + /// + /// The evidence artifact. + /// The evidence for the prompt. + internal static VisualBriefingEvidencePrompt Of(VisualBriefingEvidenceArtifact evidence) => new(evidence.Facts, evidence.Metrics, evidence.Tables, evidence.AssetPlan); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEvidenceStage.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEvidenceStage.cs index 9eb858d4a..560ae93ae 100644 --- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEvidenceStage.cs +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingEvidenceStage.cs @@ -102,13 +102,11 @@ internal sealed class VisualBriefingEvidenceStage(StructuredLlmStageRunner stage // The model never sees internal source GUIDs, only short handles. The file name is what lets // it tell the attached documents apart, which are supplied in the same canonical order: var handles = VisualBriefingSourceHandles.Map(manifest); - var sources = handles.Select(item => new - { - sourceId = item.Handle, + var sources = handles.Select(item => new VisualBriefingSourcePrompt( + item.Handle, item.Source.Kind, - assetId = string.IsNullOrWhiteSpace(item.Source.AssetId) ? null : item.Source.AssetId, - name = Path.GetFileName(item.Source.Path), - }); + string.IsNullOrWhiteSpace(item.Source.AssetId) ? null : item.Source.AssetId, + Path.GetFileName(item.Source.Path))); var transcripts = handles .Where(item => preparedSources.Transcripts.ContainsKey(item.Source.SourceId)) diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingInteractionCompiler.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingInteractionCompiler.cs index f53286a19..edda623d9 100644 --- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingInteractionCompiler.cs +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingInteractionCompiler.cs @@ -27,12 +27,7 @@ internal static class VisualBriefingInteractionCompiler formula => formula.Formula, StringComparer.Ordinal); - return JsonSerializer.SerializeToElement(new - { - controls, - state, - formulas = formulaMap, - }, VisualBriefingJson.Canonical); + return JsonSerializer.SerializeToElement(new VisualBriefingInteractionData(controls, state, formulaMap), VisualBriefingJson.Canonical); } /// diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingInteractionData.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingInteractionData.cs new file mode 100644 index 000000000..7f172d5fc --- /dev/null +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingInteractionData.cs @@ -0,0 +1,14 @@ +using System.Text.Json; + +namespace AIStudio.Assistants.VisualBriefing; + +/// +/// The interactions in the data of a briefing: its controls, their state, and the formulas which depend on them. +/// +/// The validated interaction controls. +/// The initial value of each control, by its ID. +/// The formula of each result slot, by the ID of the slot. +internal sealed record VisualBriefingInteractionData( + IReadOnlyList Controls, + Dictionary State, + Dictionary Formulas); \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingLabels.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingLabels.cs new file mode 100644 index 000000000..e831234f2 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingLabels.cs @@ -0,0 +1,16 @@ +namespace AIStudio.Assistants.VisualBriefing; + +/// +/// The fixed labels in the data of a briefing, which its runtime shows. +/// +/// The label of the buttons which reset a simulation. +/// The name of the app which created the briefing. +internal sealed record VisualBriefingLabels(string Reset, string Brand) +{ + /// + /// Creates the labels of a briefing. + /// + /// The label of the buttons which reset a simulation. + /// The labels. + internal static VisualBriefingLabels Of(string resetLabel) => new(resetLabel, "MindWork AI Studio"); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingLayoutCompiler.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingLayoutCompiler.cs index 7334e34db..c0749219f 100644 --- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingLayoutCompiler.cs +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingLayoutCompiler.cs @@ -49,19 +49,9 @@ internal sealed class VisualBriefingLayoutCompiler var interactions = VisualBriefingInteractionCompiler.Compile(content.Controls, content.Formulas); - var data = JsonSerializer.SerializeToElement(new - { - slots, - charts = chartOptions, - interactions, - accessibility = content.AccessibilityTexts, - sourceReferences = content.SourceReferences, - labels = new - { - reset = content.ResetLabel, - brand = "MindWork AI Studio", - }, - }, VisualBriefingJson.Canonical); + var data = JsonSerializer.SerializeToElement( + new VisualBriefingCompiledData(slots, chartOptions, interactions, content.AccessibilityTexts, content.SourceReferences, VisualBriefingLabels.Of(content.ResetLabel)), + VisualBriefingJson.Canonical); var html = CompileNode(layout, sections, components, content, true); var css = CompileCss(profile, layout); diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingParentDesign.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingParentDesign.cs new file mode 100644 index 000000000..269968bab --- /dev/null +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingParentDesign.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Assistants.VisualBriefing; + +/// +/// The design of the previous revision, which the presentation stage shows its model when the design changes. +/// +/// The layout tree of the previous revision. +/// The design profile of the previous revision. +internal sealed record VisualBriefingParentDesign(VisualBriefingLayoutNode Layout, VisualBriefingDesignProfile Profile); \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPersistenceFingerprint.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPersistenceFingerprint.cs new file mode 100644 index 000000000..f8f98e1cc --- /dev/null +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPersistenceFingerprint.cs @@ -0,0 +1,10 @@ +namespace AIStudio.Assistants.VisualBriefing; + +/// +/// What the editor of the Visual Briefing assistant hands to the store, so that it notices any change worth saving. +/// +/// The name of the briefing. +/// The author of the briefing. +/// The settings of the briefing. +/// The sources of the briefing. +internal sealed record VisualBriefingPersistenceFingerprint(string Name, string Author, VisualBriefingLocalSettings Settings, VisualBriefingSourceFingerprint[] Sources); \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPlanStage.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPlanStage.cs index 22adebf27..5768de4eb 100644 --- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPlanStage.cs +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPlanStage.cs @@ -112,6 +112,6 @@ internal sealed class VisualBriefingPlanStage(StructuredLlmStageRunner stageRunn $""" Audience: {manifest.Settings.AudienceProfile}; {manifest.Settings.AudienceAgeGroup}; {manifest.Settings.AudienceOrganizationalLevel}; {manifest.Settings.AudienceExpertise} Scope instruction: {manifest.Settings.Instruction} - Evidence: {JsonSerializer.Serialize(new { evidence.Facts, evidence.Metrics, evidence.Tables, evidence.AssetPlan }, VisualBriefingJson.Canonical)} + Evidence: {JsonSerializer.Serialize(VisualBriefingEvidencePrompt.Of(evidence), VisualBriefingJson.Canonical)} """; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPresentationStage.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPresentationStage.cs index 7ae5eac88..d37686ef3 100644 --- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPresentationStage.cs +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingPresentationStage.cs @@ -127,26 +127,7 @@ internal sealed class VisualBriefingPresentationStage(StructuredLlmStageRunner s () => VisualBriefingLayoutCompiler.Compile(plan, content, response.Layout, response.Profile)); var data = compiled.Data.EnumerateObject().ToDictionary(property => property.Name, property => property.Value.Clone(), StringComparer.Ordinal); - data["_mwai"] = JsonSerializer.SerializeToElement(new - { - schemaVersion = VisualBriefingVersions.SCHEMA, - runtimeVersion = VisualBriefingVersions.RUNTIME, - aiStudioVersion = "validation", - - assets = content.AssetPlan.ToDictionary( - asset => asset.AssetId, - _ => "data:image/png;base64,AA==", - StringComparer.Ordinal), - - footer = new - { - createdWith = "validation", - models = "validation", - createdAt = "validation", - authors = "validation", - protection = "validation", - }, - }, VisualBriefingJson.Canonical); + data["_mwai"] = JsonSerializer.SerializeToElement(VisualBriefingProtectedData.ForValidation(content.AssetPlan), VisualBriefingJson.Canonical); var validationData = JsonSerializer.SerializeToElement(data, VisualBriefingJson.Canonical); VisualBriefingCompilerInvariant.Guard(VisualBriefingBuildStage.DESIGN, @@ -184,7 +165,7 @@ internal sealed class VisualBriefingPresentationStage(StructuredLlmStageRunner s // Internal, so that a test can check the JSON this prompt carries: internal static string BuildPrompt(VisualBriefingManifest manifest, VisualBriefingPlanArtifact plan, VisualBriefingPresentationArtifact? parent) { - var parentJson = parent is null ? "none" : JsonSerializer.Serialize(new { parent.Layout, parent.Profile }, VisualBriefingJson.Canonical); + var parentJson = parent is null ? "none" : JsonSerializer.Serialize(new VisualBriefingParentDesign(parent.Layout, parent.Profile), VisualBriefingJson.Canonical); return $""" Operation: {(parent is null ? "CREATE_DESIGN" : "CHANGE_DESIGN")} Design instruction: {manifest.Settings.Instruction} diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingProtectedData.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingProtectedData.cs new file mode 100644 index 000000000..8e8f44187 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingProtectedData.cs @@ -0,0 +1,45 @@ +using System.Text.Json.Serialization; + +namespace AIStudio.Assistants.VisualBriefing; + +/// +/// The data of a briefing which AI Studio writes itself and no model may change, under the key _mwai. +/// +/// The version of the schema of the briefing data. +/// The version of the runtime script of the briefing. +/// The version of AI Studio which created the briefing. +/// The embedded pictures, by their asset IDs. +/// What the briefing tells about each picture, or null where the data only gets validated. +/// The texts of the footer, by their keys. +internal sealed record VisualBriefingProtectedData( + int SchemaVersion, + int RuntimeVersion, + string AIStudioVersion, + IReadOnlyDictionary Assets, + [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] Dictionary? AssetMetadata, + Dictionary Footer) +{ + private const string VALIDATION_VALUE = "validation"; + + private const string VALIDATION_ASSET = "data:image/png;base64,AA=="; + + /// + /// Creates stand-in protected data for validating a compiled briefing before it gets assembled. + /// + /// The planned pictures of the briefing. + /// The stand-in protected data. + internal static VisualBriefingProtectedData ForValidation(IEnumerable assetPlan) => new( + VisualBriefingVersions.SCHEMA, + VisualBriefingVersions.RUNTIME, + VALIDATION_VALUE, + assetPlan.ToDictionary(asset => asset.AssetId, _ => VALIDATION_ASSET, StringComparer.Ordinal), + null, + new Dictionary(StringComparer.Ordinal) + { + ["createdWith"] = VALIDATION_VALUE, + ["models"] = VALIDATION_VALUE, + ["createdAt"] = VALIDATION_VALUE, + ["authors"] = VALIDATION_VALUE, + ["protection"] = VALIDATION_VALUE, + }); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingRequiredSlot.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingRequiredSlot.cs new file mode 100644 index 000000000..7cf1f4ba0 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingRequiredSlot.cs @@ -0,0 +1,9 @@ +namespace AIStudio.Assistants.VisualBriefing; + +/// +/// A slot which the content stage asks its model to fill, with what the slot is for. +/// +/// The ID of the slot. +/// The semantic role of the slot. +/// The type of content the slot takes. +internal sealed record VisualBriefingRequiredSlot(string SlotId, VisualBriefingSlotRole Role, VisualBriefingSlotType Type); \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingSourceFingerprint.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingSourceFingerprint.cs new file mode 100644 index 000000000..c403cc921 --- /dev/null +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingSourceFingerprint.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Assistants.VisualBriefing; + +/// +/// A source as the fingerprint of the editor names it. +/// +/// The path of the source. +/// The kind of the source. +internal sealed record VisualBriefingSourceFingerprint(string Path, VisualBriefingSourceKind Kind); \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingSourcePrompt.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingSourcePrompt.cs new file mode 100644 index 000000000..86368aead --- /dev/null +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingSourcePrompt.cs @@ -0,0 +1,13 @@ +namespace AIStudio.Assistants.VisualBriefing; + +/// +/// A source as the evidence stage shows it its model. +/// +/// +/// The model never sees the internal ID of a source, only its short handle. +/// +/// The short handle of the source. +/// The kind of the source. +/// The asset ID of the source, or null when it is no picture of the briefing. +/// The file name of the source. +internal sealed record VisualBriefingSourcePrompt(string SourceId, VisualBriefingSourceKind Kind, string? AssetId, string Name); \ No newline at end of file diff --git a/app/Tests/Serialization/Fixtures/visual-briefing-content-data.json b/app/Tests/Serialization/Fixtures/visual-briefing-content-data.json new file mode 100644 index 000000000..444326085 --- /dev/null +++ b/app/Tests/Serialization/Fixtures/visual-briefing-content-data.json @@ -0,0 +1 @@ +{"slots":{"overview-title":"Text of overview-title, with Gr\u00F6\u00DFe and \u20AC","overview-summary":"Text of overview-summary, with Gr\u00F6\u00DFe and \u20AC","chart-line-title":"Text of chart-line-title, with Gr\u00F6\u00DFe and \u20AC","chart-line-caption":"Text of chart-line-caption, with Gr\u00F6\u00DFe and \u20AC","chart-area-title":"Text of chart-area-title, with Gr\u00F6\u00DFe and \u20AC","chart-area-caption":"Text of chart-area-caption, with Gr\u00F6\u00DFe and \u20AC","chart-bar-title":"Text of chart-bar-title, with Gr\u00F6\u00DFe and \u20AC","chart-bar-caption":"Text of chart-bar-caption, with Gr\u00F6\u00DFe and \u20AC","chart-stacked_bar-title":"Text of chart-stacked_bar-title, with Gr\u00F6\u00DFe and \u20AC","chart-stacked_bar-caption":"Text of chart-stacked_bar-caption, with Gr\u00F6\u00DFe and \u20AC","chart-scatter-title":"Text of chart-scatter-title, with Gr\u00F6\u00DFe and \u20AC","chart-scatter-caption":"Text of chart-scatter-caption, with Gr\u00F6\u00DFe and \u20AC","chart-pie-title":"Text of chart-pie-title, with Gr\u00F6\u00DFe and \u20AC","chart-pie-caption":"Text of chart-pie-caption, with Gr\u00F6\u00DFe and \u20AC","chart-donut-title":"Text of chart-donut-title, with Gr\u00F6\u00DFe and \u20AC","chart-donut-caption":"Text of chart-donut-caption, with Gr\u00F6\u00DFe and \u20AC","chart-radar-title":"Text of chart-radar-title, with Gr\u00F6\u00DFe and \u20AC","chart-radar-caption":"Text of chart-radar-caption, with Gr\u00F6\u00DFe and \u20AC","simulation-title":"Text of simulation-title, with Gr\u00F6\u00DFe and \u20AC","simulation-summary":"Text of simulation-summary, with Gr\u00F6\u00DFe and \u20AC","simulation-result":"Text of simulation-result, with Gr\u00F6\u00DFe and \u20AC"},"charts":[{"componentId":"chart-line","kind":"LINE","categories":["Q1","Q2","Q3"],"series":[{"name":"Revenue","values":[12.5,14,9.75]},{"name":"Costs","values":[8,7.25,10]}]},{"componentId":"chart-area","kind":"AREA","categories":["Q1","Q2","Q3"],"series":[{"name":"Revenue","values":[12.5,14,9.75]},{"name":"Costs","values":[8,7.25,10]}]},{"componentId":"chart-bar","kind":"BAR","categories":["Q1","Q2","Q3"],"series":[{"name":"Revenue","values":[12.5,14,9.75]},{"name":"Costs","values":[8,7.25,10]}]},{"componentId":"chart-stacked_bar","kind":"STACKED_BAR","categories":["Q1","Q2","Q3"],"series":[{"name":"Revenue","values":[12.5,14,9.75]},{"name":"Costs","values":[8,7.25,10]}]},{"componentId":"chart-scatter","kind":"SCATTER","categories":["Q1","Q2","Q3"],"series":[{"name":"Revenue","values":[12.5,14,9.75]},{"name":"Costs","values":[8,7.25,10]}]},{"componentId":"chart-pie","kind":"PIE","categories":["Q1","Q2","Q3"],"series":[{"name":"Revenue","values":[12.5,14,9.75]},{"name":"Costs","values":[8,7.25,10]}]},{"componentId":"chart-donut","kind":"DONUT","categories":["Q1","Q2","Q3"],"series":[{"name":"Revenue","values":[12.5,14,9.75]},{"name":"Costs","values":[8,7.25,10]}]},{"componentId":"chart-radar","kind":"RADAR","categories":["Q1","Q2","Q3"],"series":[{"name":"Revenue","values":[12.5,14,9.75]},{"name":"Costs","values":[8,7.25,10]}]}],"controls":[{"controlId":"growth","componentId":"simulation","kind":"RANGE","initialValue":3,"options":[]},{"controlId":"scenario","componentId":"simulation","kind":"SELECT","initialValue":"base","options":[{"value":"base","label":"Base case"},{"value":"high","label":"Gr\u00F6\u00DFeres Wachstum"}]}],"formulas":[{"componentId":"simulation","outputSlotId":"simulation-result","formula":{"formulaVersion":1,"op":"multiply","args":[{"path":"interactions.state.growth"},{"value":2}]}}],"accessibility":{"chart-line":"Revenue rises in Q2 and falls in Q3.","simulation":"The growth changes the result."},"sourceReferences":{"chart-line":["S1","S2"]},"labels":{"reset":"Zur\u00FCcksetzen","brand":"MindWork AI Studio"}} \ No newline at end of file diff --git a/app/Tests/Serialization/Fixtures/visual-briefing-persistence-fingerprint.json b/app/Tests/Serialization/Fixtures/visual-briefing-persistence-fingerprint.json new file mode 100644 index 000000000..0daf25189 --- /dev/null +++ b/app/Tests/Serialization/Fixtures/visual-briefing-persistence-fingerprint.json @@ -0,0 +1 @@ +{"name":"name sample 3","author":"author sample 4","settings":{"providerId":"providerId sample 7","modelId":"modelId sample 8","profileId":"profileId sample 9","targetLanguage":1,"customTargetLanguage":"customTargetLanguage sample 10","audienceProfile":1,"audienceAgeGroup":1,"audienceOrganizationalLevel":1,"audienceExpertise":1,"showSourceReferences":true,"optimizeImages":true,"instruction":"instruction sample 11","protectionLevel":"INTERNAL","customProtectionLevel":"customProtectionLevel sample 12"},"sources":[{"path":"/samples/path-17.txt","kind":"VISUAL_ASSET"},{"path":"/samples/path-22.txt","kind":"VISUAL_ASSET"}]} \ No newline at end of file diff --git a/app/Tests/Serialization/Fixtures/visual-briefing-validation-protected-data.json b/app/Tests/Serialization/Fixtures/visual-briefing-validation-protected-data.json new file mode 100644 index 000000000..c8b8b836b --- /dev/null +++ b/app/Tests/Serialization/Fixtures/visual-briefing-validation-protected-data.json @@ -0,0 +1 @@ +{"schemaVersion":2,"runtimeVersion":1,"aiStudioVersion":"validation","assets":{"assetId sample 48":"data:image/png;base64,AA==","assetId sample 51":"data:image/png;base64,AA=="},"footer":{"createdWith":"validation","models":"validation","createdAt":"validation","authors":"validation","protection":"validation"}} \ No newline at end of file diff --git a/app/Tests/Serialization/VisualBriefingShapeTests.cs b/app/Tests/Serialization/VisualBriefingShapeTests.cs new file mode 100644 index 000000000..6708e934a --- /dev/null +++ b/app/Tests/Serialization/VisualBriefingShapeTests.cs @@ -0,0 +1,47 @@ +using System.Text.Json; + +using AIStudio.Assistants.VisualBriefing; + +namespace AIStudio.Tests.Serialization; + +/// +/// Checks that the records of the Visual Briefing assistant write the same JSON as the anonymous shapes they replaced. +/// +/// +/// The files in the folder Fixtures were written by the anonymous shapes themselves, before the records existed. The +/// content data and the protected data for the validation reach the HTML of a briefing, and the fingerprint of the +/// editor decides whether a briefing gets saved: a field under another name or in another place changes all three. +/// +[TestFixture] +public sealed class VisualBriefingShapeTests +{ + [Test] + public void TheContentDataGetsWrittenAsBefore() + { + var data = JsonSerializer.SerializeToElement(VisualBriefingContentData.Of(VisualBriefingSamples.Content()), VisualBriefingJson.Canonical); + Assert.That(data.GetRawText(), Is.EqualTo(JsonGoldenFileTests.ReadFixture("visual-briefing-content-data.json"))); + } + + [Test] + public void TheProtectedDataForTheValidationGetsWrittenAsBefore() + { + var evidence = Read("visual-briefing-evidence-artifact.json"); + var data = JsonSerializer.SerializeToElement(VisualBriefingProtectedData.ForValidation(evidence.AssetPlan), VisualBriefingJson.Canonical); + Assert.That(data.GetRawText(), Is.EqualTo(JsonGoldenFileTests.ReadFixture("visual-briefing-validation-protected-data.json"))); + } + + [Test] + public void TheFingerprintOfTheEditorGetsWrittenAsBefore() + { + var manifest = Read("visual-briefing-manifest.json"); + var fingerprint = new VisualBriefingPersistenceFingerprint( + manifest.Name, + manifest.Author, + manifest.Settings, + [.. manifest.Sources.Select(source => new VisualBriefingSourceFingerprint(source.Path, source.Kind))]); + + Assert.That(JsonSerializer.Serialize(fingerprint, VisualBriefingJson.Canonical), Is.EqualTo(JsonGoldenFileTests.ReadFixture("visual-briefing-persistence-fingerprint.json"))); + } + + private static T Read(string fileName) => JsonSerializer.Deserialize(JsonGoldenFileTests.ReadFixture(fileName), VisualBriefingJson.Persistence)!; +} \ No newline at end of file