Added an option for timelines

This commit is contained in:
Thorsten Sommer 2026-07-31 20:13:05 +02:00
parent 3f2cae123b
commit 4bbeccf54c
Signed by untrusted user who does not match committer: tsommer
GPG Key ID: 371BBA77A02C0108
13 changed files with 133 additions and 18 deletions

View File

@ -37,4 +37,7 @@ public enum VisualBriefingComponentKind
/// <summary>Provides deterministic interactive controls and calculated results.</summary>
SIMULATION,
/// <summary>Displays an ordered chronological sequence without a chart runtime.</summary>
TIMELINE,
}

View File

@ -101,6 +101,7 @@ internal sealed class VisualBriefingContentStage(StructuredLlmStageRunner stageR
Fulfil every required slot from the plan exactly once and add no other slots. Every slot has a declared type in the user message.
A TEXT slot value is a JSON string, number, or boolean. Write plain prose without markup, without angle brackets, and without programming syntax.
A TABLE slot value is the object {"columns": ["..."], "rows": [{"cells": ["..."]}]}. It has no other properties, every row has exactly one cell per column, and every cell is a string, number, or boolean.
A TIMELINE slot value is the object {"items": [{"period": "...", "title": "...", "description": "..."}]}. It has no other properties, contains at least two items in chronological order, and every item has exactly those three non-empty target-language strings.
For a FILTERABLE_TABLE component the first column is what readers filter by, so make it a repeating text category and give every row a string in that column.
Charts contain componentId, kind (LINE, AREA, BAR, STACKED_BAR, SCATTER, PIE, DONUT, RADAR), categories, and series. Never return chart-library options.
Controls contain controlId, componentId, kind (TAB, NUMBER, RANGE, SELECT), initialValue, and typed options with value and label. controlId is a unique lowercase identifier. An option value is the short unique value the control selects, and the option label is its visible target-language text.
@ -112,6 +113,7 @@ internal sealed class VisualBriefingContentStage(StructuredLlmStageRunner stageR
An accessibilityTexts entry is never shown on screen. It reaches people who cannot see the component, so it states what the component conveys: for a chart the trend and the decisive numbers, for a component with controls what those controls change.
Section TITLE and SUMMARY slots and component TITLE, LABEL, EYEBROW, and CAPTION slots are concise display copy. BODY and SUMMARY slots use short paragraphs suitable for screen reading.
For ACCORDION components, the TITLE slot supplies the visible summary and the BODY slot supplies the expandable content.
For TIMELINE components, preserve the evidence-backed chronology and express dates, ranges, or named phases in period without inventing precision.
Do not return source references, reset controls, filter controls, or entries for ASSET components; AI Studio creates all of them deterministically.
""";

View File

@ -97,6 +97,7 @@ internal sealed class VisualBriefingEvidenceStage(StructuredLlmStageRunner stage
tables contain evidenceId, title, columns, rows, sourceIds; every row has exactly the column count.
sourceCoverage contains each supplied source exactly once with coverage USED, CONTEXTUAL, or OUT_OF_SCOPE and a short reason.
assetPlan contains each supplied visual asset exactly once with assetId, description, and target-language altText.
Preserve material dates, periods, phases, milestones, durations, and their chronological order in the facts or tables that best represent them.
Include only facts supported by the supplied material.
""";

View File

@ -83,7 +83,18 @@ internal sealed class VisualBriefingLayoutCompiler
var componentId = HtmlEncoder.Default.Encode(component.ComponentId);
var body = CompileComponent(component, content);
var componentClasses = CompileLayoutClasses(node, $"mwai-component mwai-{component.Kind.ToString().ToLowerInvariant()}");
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 $"<article id=\"{id}\" class=\"{componentClasses}\" data-mwai-region=\"{componentId}\">{body}</article>";
}
@ -136,6 +147,7 @@ internal sealed class VisualBriefingLayoutCompiler
VisualBriefingComponentKind.TABS => CompileTabs(component, content.Controls),
VisualBriefingComponentKind.ACCORDION => $"<details><summary><span data-mwai-text=\"slots.{Slot(component, VisualBriefingSlotRole.TITLE)}\"></span></summary><div class=\"mwai-accordion-body\"><p data-mwai-text=\"slots.{Slot(component, VisualBriefingSlotRole.BODY)}\"></p></div></details>",
VisualBriefingComponentKind.SIMULATION => CompileSimulation(component, controls, content),
VisualBriefingComponentKind.TIMELINE => CompileTimeline(component),
_ => string.Empty,
};
@ -207,6 +219,19 @@ internal sealed class VisualBriefingLayoutCompiler
return $"<fieldset><legend data-mwai-text=\"slots.{title}\"></legend><p data-mwai-text=\"slots.{summary}\"></p><div class=\"mwai-control-grid\">{controls}</div><div class=\"mwai-results\">{outputs}</div>{VisualBriefingInteractionCompiler.CompileResetMarkup(component.ComponentId)}</fieldset>";
}
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 $"<header class=\"mwai-component-heading\"><h3 data-mwai-text=\"slots.{title}\"></h3><p data-mwai-text=\"slots.{summary}\"></p></header>" +
$"<ol class=\"mwai-timeline-track\" role=\"list\"><template data-mwai-each=\"slots.{dataSlot}.items\"><li class=\"mwai-timeline-item\">" +
"<span class=\"mwai-timeline-marker\" aria-hidden=\"true\"></span><div class=\"mwai-timeline-content\">" +
"<p class=\"mwai-timeline-period\" data-mwai-text=\".period\"></p><h4 data-mwai-text=\".title\"></h4>" +
"<p class=\"mwai-timeline-description\" data-mwai-text=\".description\"></p></div></li></template></ol>";
}
private static string Slot(VisualBriefingPlanComponent component, VisualBriefingSlotRole role, int occurrence = 0)
{
var slot = component.Slots.Where(candidate => candidate.Role == role).ElementAtOrDefault(occurrence) ?? throw new InvalidDataException($"A {component.Kind} component is missing its {role} slot.");
@ -255,7 +280,7 @@ internal sealed class VisualBriefingLayoutCompiler
.mwai-component-heading h3,.mwai-callout h3{font-size:clamp(1.3rem,2.2vw,1.75rem);}
.mwai-component-heading p,.mwai-copy,.mwai-context,.mwai-callout p{margin:0;max-width:70ch;}
.mwai-text{max-width:72ch;padding-block:.5rem;}
.mwai-metric,.mwai-chart,.mwai-asset,.mwai-table,.mwai-filterable_table,.mwai-tabs,.mwai-accordion,.mwai-simulation{padding:clamp(1.25rem,2.5vw,2rem);border:1px solid var(--mwai-line);border-radius:1.25rem;background:color-mix(in srgb,var(--mwai-paper),transparent 3%);box-shadow:0 18px 55px rgba(22,75,59,.07);}
.mwai-metric,.mwai-chart,.mwai-asset,.mwai-table,.mwai-filterable_table,.mwai-tabs,.mwai-accordion,.mwai-simulation,.mwai-timeline{padding:clamp(1.25rem,2.5vw,2rem);border:1px solid var(--mwai-line);border-radius:1.25rem;background:color-mix(in srgb,var(--mwai-paper),transparent 3%);box-shadow:0 18px 55px rgba(22,75,59,.07);}
.mwai-metric{position:relative;overflow:hidden;border-block-start:5px solid var(--mwai-sun);box-shadow:none;}
.mwai-metric-body{display:flex;flex-direction:column;margin:0;}
.mwai-metric dt{order:2;color:var(--mwai-muted);font-size:.82rem;font-weight:700;letter-spacing:.07em;text-transform:uppercase;}
@ -295,6 +320,15 @@ internal sealed class VisualBriefingLayoutCompiler
.mwai-control-grid{display:flex;flex-wrap:wrap;gap:1rem;margin-block:1.25rem;}
.mwai-results{display:flex;flex-wrap:wrap;gap:.75rem;margin-block:1rem;}
.mwai-results output{display:block;min-width:8rem;padding:1rem;border-radius:.8rem;background:var(--mwai-cream);color:var(--mwai-forest);font-size:1.45rem;font-weight:750;}
.mwai-timeline-track{display:flex;flex-direction:column;list-style:none;margin:0;padding:0;padding-inline-start:.55rem;}
.mwai-timeline-item{position:relative;min-width:0;padding:0;padding-block-end:1.75rem;padding-inline-start:1.75rem;border-inline-start:2px solid var(--mwai-line);}
.mwai-timeline-item:last-child{padding-block-end:0;}
.mwai-timeline-marker{position:absolute;inset-block-start:.18rem;inset-inline-start:-.52rem;width:.95rem;height:.95rem;border:3px solid var(--mwai-paper);border-radius:50%;background:var(--mwai-pine);box-shadow:0 0 0 2px var(--mwai-sage);}
.mwai-timeline-content{display:flex;flex-direction:column;gap:.4rem;}
.mwai-timeline-period,.mwai-timeline-description{margin:0;}
.mwai-timeline-period{color:var(--mwai-pine);font-size:.78rem;font-weight:760;letter-spacing:.07em;text-transform:uppercase;}
.mwai-timeline-content h4{margin:0;color:var(--mwai-forest);font-size:1.08rem;line-height:1.25;}
.mwai-timeline-description{color:var(--mwai-muted);line-height:1.55;}
.mwai-sources{display:block;padding-block-start:.8rem;border-block-start:1px solid var(--mwai-line);color:var(--mwai-muted);font-size:.76rem;line-height:1.5;}
.mwai-emphasized{border-color:var(--mwai-sun);box-shadow:0 18px 55px rgba(22,75,59,.12);}
.mwai-align-start{align-items:start;}.mwai-align-center{align-items:center;}.mwai-align-end{align-items:end;}.mwai-align-stretch{align-items:stretch;}
@ -321,8 +355,9 @@ internal sealed class VisualBriefingLayoutCompiler
}
css.Append("""
@media(max-width:47.99rem){#mwai-briefing-root{padding:.75rem}.mwai-section-inner{padding:1.5rem}.mwai-section-hero .mwai-section-inner{min-height:34rem}.mwai-metric,.mwai-chart,.mwai-asset,.mwai-table,.mwai-filterable_table,.mwai-tabs,.mwai-accordion,.mwai-simulation{padding:1rem}[data-mwai-chart]{min-height:19rem}th,td{padding:.7rem .75rem}}
@media print{@page{margin:14mm}#mwai-briefing-root{max-width:none;padding:0;font-size:10pt}.mwai-document{gap:8mm}.mwai-masthead{padding:0 0 4mm}.mwai-section{border:0;box-shadow:none;background:transparent;color:var(--mwai-ink);break-inside:auto}.mwai-section-inner{padding:6mm 0}.mwai-section-heading{margin-block-end:5mm}.mwai-section-heading h1{font-size:28pt}.mwai-section-heading h2{font-size:21pt}.mwai-section-heading p,.mwai-section-hero .mwai-section-heading p,.mwai-section-conclusion .mwai-section-heading p{color:var(--mwai-muted)}.mwai-component,.mwai-component figure,.mwai-table-wrap{break-inside:avoid}.mwai-metric,.mwai-chart,.mwai-asset,.mwai-table,.mwai-filterable_table,.mwai-tabs,.mwai-accordion,.mwai-simulation{box-shadow:none;background:var(--mwai-paper)}[data-mwai-tab-panel][hidden]{display:block!important}details:not([open])>.mwai-accordion-body{display:block!important}[data-mwai-reset]{display:none!important}thead th{position:static}*{print-color-adjust:exact}}
@media screen and (min-width:48rem){.mwai-timeline-horizontal .mwai-timeline-track{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(13rem,1fr);overflow-x:auto;padding:.55rem 0 .5rem;padding-inline-start:.55rem}.mwai-timeline-horizontal .mwai-timeline-item{padding:0;padding-block-start:1.5rem;padding-inline-end:1rem;border-block-start:2px solid var(--mwai-line);border-inline-start:0}.mwai-timeline-horizontal .mwai-timeline-marker{inset-block-start:-.52rem;inset-inline-start:-.52rem}}
@media(max-width:47.99rem){#mwai-briefing-root{padding:.75rem}.mwai-section-inner{padding:1.5rem}.mwai-section-hero .mwai-section-inner{min-height:34rem}.mwai-metric,.mwai-chart,.mwai-asset,.mwai-table,.mwai-filterable_table,.mwai-tabs,.mwai-accordion,.mwai-simulation,.mwai-timeline{padding:1rem}[data-mwai-chart]{min-height:19rem}th,td{padding:.7rem .75rem}}
@media print{@page{margin:14mm}#mwai-briefing-root{max-width:none;padding:0;font-size:10pt}.mwai-document{gap:8mm}.mwai-masthead{padding:0 0 4mm}.mwai-section{border:0;box-shadow:none;background:transparent;color:var(--mwai-ink);break-inside:auto}.mwai-section-inner{padding:6mm 0}.mwai-section-heading{margin-block-end:5mm}.mwai-section-heading h1{font-size:28pt}.mwai-section-heading h2{font-size:21pt}.mwai-section-heading p,.mwai-section-hero .mwai-section-heading p,.mwai-section-conclusion .mwai-section-heading p{color:var(--mwai-muted)}.mwai-component,.mwai-component figure,.mwai-table-wrap{break-inside:avoid}.mwai-timeline{break-inside:auto}.mwai-timeline-item{break-inside:avoid}.mwai-metric,.mwai-chart,.mwai-asset,.mwai-table,.mwai-filterable_table,.mwai-tabs,.mwai-accordion,.mwai-simulation,.mwai-timeline{box-shadow:none;background:var(--mwai-paper)}[data-mwai-tab-panel][hidden]{display:block!important}details:not([open])>.mwai-accordion-body{display:block!important}[data-mwai-reset]{display:none!important}thead th{position:static}*{print-color-adjust:exact}}
""");
return css.ToString();

View File

@ -27,4 +27,8 @@ public sealed class VisualBriefingPlanComponent
/// <summary>Gets or sets the optional embedded asset identifier.</summary>
[JsonRequired]
public string? AssetId { get; set; }
/// <summary>Gets or sets the orientation used only by timeline components.</summary>
[JsonRequired]
public VisualBriefingTimelineOrientation? TimelineOrientation { get; set; }
}

View File

@ -55,7 +55,7 @@ internal sealed class VisualBriefingPlanStage(StructuredLlmStageRunner stageRunn
var structuralSignature = VisualBriefingHashing.Compute(string.Join('\u001f', sections.Select(section => $"{section.SectionId}:{section.Role}:{section.TitleSlotId}:{section.SummarySlotId}")
.Concat(sections.SelectMany(section => section.Components)
.Select(component =>
$"{component.ComponentId}:{component.Kind}:{component.AssetId}:{string.Join(',', component.Slots.Select(slot => $"{slot.SlotId}:{slot.Role}"))}"))));
$"{component.ComponentId}:{component.Kind}:{component.AssetId}:{component.TimelineOrientation}:{string.Join(',', component.Slots.Select(slot => $"{slot.SlotId}:{slot.Role}"))}"))));
var artifact = new VisualBriefingPlanArtifact
{
@ -88,9 +88,9 @@ internal sealed class VisualBriefingPlanStage(StructuredLlmStageRunner stageRunn
Section roles are HERO, EXECUTIVE_SUMMARY, NARRATIVE, EVIDENCE, EXPLORATION, or CONCLUSION.
The first section is the only HERO. EXECUTIVE_SUMMARY may occur once directly after it. CONCLUSION may occur once as the final section.
Every titleSlotId and summarySlotId is a unique content slot ID.
Each component has exactly componentId, kind, evidenceIds, slots, and assetId.
Every slot has exactly slotId and role. Slot roles are EYEBROW, TITLE, SUMMARY, BODY, LABEL, VALUE, CONTEXT, CAPTION, TABLE_DATA, PANEL, or RESULT.
Allowed kinds: TEXT, METRIC, TABLE, CHART, ASSET, CALLOUT, TABS, ACCORDION, FILTERABLE_TABLE, SIMULATION.
Each component has exactly componentId, kind, evidenceIds, slots, assetId, and timelineOrientation.
Every slot has exactly slotId and role. Slot roles are EYEBROW, TITLE, SUMMARY, BODY, LABEL, VALUE, CONTEXT, CAPTION, TABLE_DATA, PANEL, RESULT, or TIMELINE_DATA.
Allowed kinds: TEXT, METRIC, TABLE, CHART, ASSET, CALLOUT, TABS, ACCORDION, FILTERABLE_TABLE, SIMULATION, TIMELINE.
IDs are stable lowercase identifiers matching ^[a-z][a-z0-9_-]{0,63}$. Reference only supplied evidence IDs.
Slot IDs are unique across the whole briefing, including section title and summary slots.
Use these exact component slot patterns:
@ -102,7 +102,11 @@ internal sealed class VisualBriefingPlanStage(StructuredLlmStageRunner stageRunn
TABS: TITLE, SUMMARY, then one or more PANEL slots.
ACCORDION: TITLE, BODY.
SIMULATION: TITLE, SUMMARY, then one or more RESULT slots.
TIMELINE: TITLE, SUMMARY, TIMELINE_DATA.
assetId is null except for ASSET components; include every supplied assetId in exactly one ASSET component.
timelineOrientation is null except for TIMELINE components, where it is HORIZONTAL or VERTICAL.
Use TIMELINE for sourced events, milestones, phases, or historical developments whose sequence matters; use CHART instead for quantitative trends over time.
Choose HORIZONTAL for a concise overview with few milestones and VERTICAL for longer or explanation-rich chronological narratives.
""";
private static string BuildPrompt(VisualBriefingManifest manifest, VisualBriefingEvidenceArtifact evidence) =>

View File

@ -180,6 +180,7 @@ internal sealed class VisualBriefingPresentationStage(StructuredLlmStageRunner s
The layout root is one STACK. Its direct children are one SECTION for every planned section,
in plan order, with the matching sectionId. A section may contain STACK and GRID containers,
and must reference exactly its own components. Reference every supplied component exactly once.
Give a HORIZONTAL TIMELINE enough width for its ordered track; do not place it in a narrow grid column.
Prefer editorial rhythm over a wall of cards. Use emphasis sparingly for decisive metrics or insights.
MindWork AI Studio owns all colors, typography, surfaces, and chart styling.
""";

View File

@ -40,4 +40,7 @@ public enum VisualBriefingSlotRole
/// <summary>Provides a calculated simulation result.</summary>
RESULT,
/// <summary>Provides the ordered entries of a chronological timeline.</summary>
TIMELINE_DATA,
}

View File

@ -13,4 +13,7 @@ public enum VisualBriefingSlotType
/// <summary>A tabular object with columns and rows.</summary>
TABLE,
/// <summary>An ordered object containing chronological timeline items.</summary>
TIMELINE,
}

View File

@ -12,7 +12,12 @@ internal static class VisualBriefingSlotTypes
/// </summary>
/// <param name="slot">The planned semantic slot.</param>
/// <returns>The required slot type.</returns>
internal static VisualBriefingSlotType Expected(VisualBriefingPlanSlot slot) => slot.Role is VisualBriefingSlotRole.TABLE_DATA ? VisualBriefingSlotType.TABLE : VisualBriefingSlotType.TEXT;
internal static VisualBriefingSlotType Expected(VisualBriefingPlanSlot slot) => slot.Role switch
{
VisualBriefingSlotRole.TABLE_DATA => VisualBriefingSlotType.TABLE,
VisualBriefingSlotRole.TIMELINE_DATA => VisualBriefingSlotType.TIMELINE,
_ => VisualBriefingSlotType.TEXT,
};
/// <summary>
/// Determines whether a slot carries the tabular data of a table component.
@ -51,6 +56,7 @@ internal static class VisualBriefingSlotTypes
internal static string Describe(VisualBriefingSlotType type) => type switch
{
VisualBriefingSlotType.TABLE => "object with a columns array and a rows array of cells arrays",
VisualBriefingSlotType.TIMELINE => "object with an items array of period, title, and description strings",
_ => "string, number, or boolean",
};
@ -66,6 +72,9 @@ internal static class VisualBriefingSlotTypes
return value.ValueKind is JsonValueKind.String or JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False
? string.Empty : "A text slot requires a string, number, or boolean value.";
if (type is VisualBriefingSlotType.TIMELINE)
return ValidateTimeline(value);
if (value.ValueKind is not JsonValueKind.Object)
return "A table slot requires an object with columns and rows.";
@ -100,4 +109,34 @@ internal static class VisualBriefingSlotTypes
return string.Empty;
}
/// <summary>
/// Checks the fixed timeline content shape used by the deterministic compiler.
/// </summary>
/// <param name="value">The timeline slot value returned by the model.</param>
/// <returns>A short reason when the value does not match, otherwise an empty string.</returns>
private static string ValidateTimeline(JsonElement value)
{
if (value.ValueKind is not JsonValueKind.Object || value.EnumerateObject().Select(property => property.Name).ToArray() is not ["items"])
return "A timeline slot requires exactly one items array.";
var items = value.GetProperty("items");
if (items.ValueKind is not JsonValueKind.Array || items.GetArrayLength() < 2)
return "A timeline requires at least two ordered items.";
foreach (var item in items.EnumerateArray())
{
if (item.ValueKind is not JsonValueKind.Object)
return "Every timeline item requires period, title, and description strings.";
var properties = item.EnumerateObject().Select(property => property.Name).ToArray();
if (properties.Length != 3 || !properties.ToHashSet(StringComparer.Ordinal).SetEquals(["period", "title", "description"]))
return "Every timeline item requires exactly period, title, and description.";
if (properties.Any(property => item.GetProperty(property).ValueKind is not JsonValueKind.String || string.IsNullOrWhiteSpace(item.GetProperty(property).GetString())))
return "Every timeline period, title, and description requires a non-empty string.";
}
return string.Empty;
}
}

View File

@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
namespace AIStudio.Assistants.VisualBriefing;
/// <summary>
/// Selects the desktop presentation direction of a chronological timeline.
/// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<VisualBriefingTimelineOrientation>))]
public enum VisualBriefingTimelineOrientation
{
/// <summary>Places timeline items along a horizontal track on sufficiently wide screens.</summary>
HORIZONTAL,
/// <summary>Places timeline items along a vertical track.</summary>
VERTICAL,
}

View File

@ -197,9 +197,12 @@ internal static partial class VisualBriefingValidation
item.EvidenceIds.Count == 0 ||
item.EvidenceIds.Distinct(StringComparer.Ordinal).Count() != item.EvidenceIds.Count ||
item.EvidenceIds.Any(id => !evidenceIds.Contains(id)) ||
!HasValidSlotPattern(item)))
!HasValidSlotPattern(item) ||
item.Kind is VisualBriefingComponentKind.TIMELINE &&
item.TimelineOrientation is not (VisualBriefingTimelineOrientation.HORIZONTAL or VisualBriefingTimelineOrientation.VERTICAL) ||
item.Kind is not VisualBriefingComponentKind.TIMELINE && item.TimelineOrientation is not null))
return Invalid(
"Every component must reference valid evidence and use the exact slot roles for its kind.",
"Every component must reference valid evidence and use the exact slots and orientation for its kind.",
VisualBriefingValidationRule.REFERENCE_INVALID);
var plannedAssetIds = components
@ -674,6 +677,7 @@ internal static partial class VisualBriefingValidation
VisualBriefingComponentKind.TABS => roles is [VisualBriefingSlotRole.TITLE, VisualBriefingSlotRole.SUMMARY, _, ..] && roles.Skip(2).All(role => role is VisualBriefingSlotRole.PANEL),
VisualBriefingComponentKind.ACCORDION => roles.SequenceEqual([VisualBriefingSlotRole.TITLE, VisualBriefingSlotRole.BODY]),
VisualBriefingComponentKind.SIMULATION => roles is [VisualBriefingSlotRole.TITLE, VisualBriefingSlotRole.SUMMARY, _, ..] && roles.Skip(2).All(role => role is VisualBriefingSlotRole.RESULT),
VisualBriefingComponentKind.TIMELINE => roles.SequenceEqual([VisualBriefingSlotRole.TITLE, VisualBriefingSlotRole.SUMMARY, VisualBriefingSlotRole.TIMELINE_DATA]),
_ => false,
};

View File

@ -12,13 +12,13 @@ public static class VisualBriefingVersions
public const int MANIFEST = 1;
/// <summary>Gets the canonical data schema version.</summary>
public const int SCHEMA = 1;
public const int SCHEMA = 2;
/// <summary>
/// Gets the deterministic HTML, CSS, chart, and interaction compiler version. Increment this
/// whenever compiler behavior changes so interrupted recompiles cannot resume across versions.
/// </summary>
public const int COMPILER = 1;
public const int COMPILER = 2;
/// <summary>
/// Gets the embedded AI Studio runtime bundle version. Increment this for changes to the
@ -33,17 +33,17 @@ public static class VisualBriefingVersions
public const int BUILD = 1;
/// <summary>Gets the immutable intermediate-artifact contract version.</summary>
public const int INTERMEDIATE_ARTIFACT = 1;
public const int INTERMEDIATE_ARTIFACT = 2;
/// <summary>Gets the evidence-agent response contract version.</summary>
public const int EVIDENCE_CONTRACT = 1;
public const int EVIDENCE_CONTRACT = 2;
/// <summary>Gets the plan-agent response contract version.</summary>
public const int PLAN_CONTRACT = 1;
public const int PLAN_CONTRACT = 2;
/// <summary>Gets the content-agent response contract version.</summary>
public const int CONTENT_CONTRACT = 1;
public const int CONTENT_CONTRACT = 2;
/// <summary>Gets the design-agent response contract version.</summary>
public const int DESIGN_CONTRACT = 1;
public const int DESIGN_CONTRACT = 2;
}