using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
namespace AIStudio.Assistants.VisualBriefing;
///
/// Compiles validated content into the fixed MindWork editorial presentation system.
///
internal sealed class VisualBriefingLayoutCompiler
{
///
/// Compiles semantic plan, content, layout, and profile artifacts into standalone parts.
///
/// The validated semantic plan.
/// The validated content.
/// The validated layout tree.
/// The bounded MindWork design profile.
/// The deterministic compiled parts and hashes.
internal static VisualBriefingCompilationResult Compile(VisualBriefingPlanArtifact plan, VisualBriefingContentArtifact content, VisualBriefingLayoutNode layout, VisualBriefingDesignProfile profile)
{
var slots = content.Slots.ToDictionary(item => item.SlotId, item => item.Value.Clone(), StringComparer.Ordinal);
var plannedSlotIds = plan.Sections
.SelectMany(section => new[] { section.TitleSlotId, section.SummarySlotId }
.Concat(section.Components.SelectMany(component => component.Slots.Select(slot => slot.SlotId))))
.ToArray();
var missingSlot = plannedSlotIds.FirstOrDefault(slotId => !slots.ContainsKey(slotId));
if (missingSlot is not null)
throw new InvalidDataException("A planned content slot is missing during compilation.");
var components = plan.Sections.SelectMany(section => section.Components)
.ToDictionary(item => item.ComponentId, StringComparer.Ordinal);
var sections = plan.Sections.ToDictionary(item => item.SectionId, StringComparer.Ordinal);
var charts = content.Charts.ToDictionary(item => item.ComponentId, StringComparer.Ordinal);
var missingChart = components.Values
.Where(component => component.Kind is VisualBriefingComponentKind.CHART)
.Select(component => component.ComponentId)
.FirstOrDefault(componentId => !charts.ContainsKey(componentId));
if (missingChart is not null)
throw new InvalidDataException("A planned chart is missing during compilation.");
var chartOptions = content.Charts.ToDictionary(
item => item.ComponentId,
item => VisualBriefingChartCompiler.Compile(item),
StringComparer.Ordinal);
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 html = CompileNode(layout, sections, components, content, true);
var css = CompileCss(profile, layout);
return new(
data,
html,
css,
VisualBriefingHashing.Compute(html),
VisualBriefingHashing.Compute(css));
}
private static string CompileNode(VisualBriefingLayoutNode node, IReadOnlyDictionary sections, IReadOnlyDictionary components, VisualBriefingContentArtifact content, bool isRoot = false)
{
var id = HtmlEncoder.Default.Encode(node.NodeId);
if (node.Kind is VisualBriefingLayoutNodeKind.COMPONENT)
{
if (node.ComponentId is null || !components.TryGetValue(node.ComponentId, out var component))
throw new InvalidDataException("The layout references an unknown component.");
var componentId = HtmlEncoder.Default.Encode(component.ComponentId);
var body = CompileComponent(component, content);
var semanticClasses = $"mwai-component mwai-{component.Kind.ToString().ToLowerInvariant()}";
if (component.Kind is VisualBriefingComponentKind.TIMELINE)
{
semanticClasses += component.TimelineOrientation switch
{
VisualBriefingTimelineOrientation.HORIZONTAL => " mwai-timeline-horizontal",
VisualBriefingTimelineOrientation.VERTICAL => " mwai-timeline-vertical",
_ => throw new InvalidDataException("A timeline component has an invalid orientation."),
};
}
var componentClasses = CompileLayoutClasses(node, semanticClasses);
return $"{body}";
}
var children = string.Concat(node.Children.OrderBy(child => child.Order)
.Select(child => CompileNode(child, sections, components, content)));
if (node.Kind is VisualBriefingLayoutNodeKind.SECTION)
{
if (node.SectionId is null || !sections.TryGetValue(node.SectionId, out var section))
throw new InvalidDataException("The layout references an unknown section.");
var title = HtmlEncoder.Default.Encode(section.TitleSlotId);
var summary = HtmlEncoder.Default.Encode(section.SummarySlotId);
var headingTag = section.Role is VisualBriefingSectionRole.HERO ? "h1" : "h2";
var role = section.Role.ToString().ToLowerInvariant().Replace('_', '-');
var classes = CompileLayoutClasses(node, $"mwai-layout mwai-section mwai-section-{role}");
return $"
",
VisualBriefingComponentKind.SIMULATION => CompileSimulation(component, controls, content),
VisualBriefingComponentKind.TIMELINE => CompileTimeline(component),
_ => string.Empty,
};
var references = content.SourceReferences.ContainsKey(component.ComponentId)
? $" "
: string.Empty;
return $"{body}{references}";
}
private static string CompileTable(VisualBriefingPlanComponent component, string controls, VisualBriefingContentArtifact content)
{
var title = Slot(component, VisualBriefingSlotRole.TITLE);
var summary = Slot(component, VisualBriefingSlotRole.SUMMARY);
var dataSlot = Slot(component, VisualBriefingSlotRole.TABLE_DATA);
var filterControl = content.Controls.FirstOrDefault(control =>
control.ComponentId == component.ComponentId &&
control.Kind is VisualBriefingControlKind.FILTER);
var filterAttributes = filterControl is null
? string.Empty
: $" data-mwai-filter=\"$root.interactions.state.{HtmlEncoder.Default.Encode(filterControl.ControlId)}\" data-mwai-filter-value=\".cells.0\"";
var toolbar = string.IsNullOrEmpty(controls) ? string.Empty : $"
{controls}
";
return $"{toolbar}
" +
$"
" +
$"
" +
$"
" +
"
";
}
private static string CompileTabs(VisualBriefingPlanComponent component, IReadOnlyList controls)
{
var indexedControl = controls.Select((control, index) => (Control: control, Index: index))
.First(item =>
item.Control.ComponentId == component.ComponentId &&
item.Control.Kind is VisualBriefingControlKind.TAB);
var initial = indexedControl.Control.InitialValue.GetString();
var componentId = HtmlEncoder.Default.Encode(component.ComponentId);
var title = Slot(component, VisualBriefingSlotRole.TITLE);
var summary = Slot(component, VisualBriefingSlotRole.SUMMARY);
var panelsSlots = component.Slots.Where(slot => slot.Role is VisualBriefingSlotRole.PANEL).ToArray();
var buttons = new StringBuilder();
var panels = new StringBuilder();
for (var index = 0; index < indexedControl.Control.Options.Count; index++)
{
var option = indexedControl.Control.Options[index];
var panelId = $"{componentId}-tab-{index}";
var selected = string.Equals(option.Value, initial, StringComparison.Ordinal);
buttons.Append($"");
panels.Append($"");
}
return $"
{buttons}
{panels}
";
}
private static string CompileSimulation(VisualBriefingPlanComponent component, string controls, VisualBriefingContentArtifact content)
{
var title = Slot(component, VisualBriefingSlotRole.TITLE);
var summary = Slot(component, VisualBriefingSlotRole.SUMMARY);
var outputs = string.Concat(content.Formulas
.Where(formula => formula.ComponentId == component.ComponentId)
.Select(formula => $""));
return $"";
}
private static string CompileTimeline(VisualBriefingPlanComponent component)
{
var title = Slot(component, VisualBriefingSlotRole.TITLE);
var summary = Slot(component, VisualBriefingSlotRole.SUMMARY);
var dataSlot = Slot(component, VisualBriefingSlotRole.TIMELINE_DATA);
return $"" +
$"