Skip to content

AI scenarios

Generative UI

Bit.BlazorUI.AI Scripted transport

The model chooses a component from a registry and supplies its properties. Anything it names that is not registered renders nothing.

Live demo

Ask about three cities: the answer is three weather cards the model asked for by name.

\n**Components the model chose**: ```razor @implements IAsyncDisposable
``` ```csharp private readonly BitAiChatSession session = new(AiDemoInteractiveTransports.GenerativeUi()); private readonly string[] suggestions = ["What is the weather like in Oslo, Lisbon and Auckland?"]; public ValueTask DisposeAsync() => session.DisposeAsync(); ``` `Program.cs`: ```csharp builder.Services.AddBitBlazorUIAIServices(ai => { ai.UseTransport(AiDemoTransports.Echo()); // The components model output is allowed to instantiate, and the properties it must supply. ai.ConfigureGenerativeUi(RegisterGenerativeComponents); }); // Register reads the component's parameters by reflection, which it marks as unsafe to trim. // Safe here: the annotation of Register keeps ForecastCard whole, and its parameters are plain data. [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode", Justification = "ForecastCard is kept whole by the annotation of Register, and its bindable parameters are strings and integers.")] [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "ForecastCard's bindable parameters are strings and integers, which need no code generated at run time.")] static void RegisterGenerativeComponents(BitAiGenerativeUiRegistry registry) { registry.Register("forecast_card", "Shows the current weather for one city.", "City"); } ``` `ForecastCard.razor`: ```razor
@City
@Degrees°
@Condition @if (Humidity > 0) { @Humidity% humidity }
@code { // The descriptions go into the schema the model is shown. [Parameter, System.ComponentModel.Description("The city name.")] public string City { get; set; } = string.Empty; [Parameter, System.ComponentModel.Description("Temperature in degrees Celsius.")] public int Degrees { get; set; } [Parameter, System.ComponentModel.Description("One word summary, for example Clear, Snow, Showers.")] public string Condition { get; set; } = string.Empty; [Parameter, System.ComponentModel.Description("Relative humidity as a percentage.")] public int Humidity { get; set; } } ``` `AiDemoInteractiveTransports.cs`: ```csharp // AiScriptedTransport and AiScript are the demo's helpers - see the Basic chat scenario. public static IBitAiTransport GenerativeUi() => new AiScriptedTransport((request, token) => AiScript.Stream(token, async yielder => { await yielder(new BitAiMessageStartedEvent()); await foreach (var evt in AiScript.Write("Here is the weather right now in the three cities you asked about.\n\n", 4, 12, token)) await yielder(evt); // A component, named as it was registered, and the properties to bind onto it. await yielder(new BitAiGenerativeUiEvent("forecast_card", AiScript.Json("""{"city":"Oslo","degrees":-3,"condition":"Snow","humidity":81}"""))); await yielder(new BitAiGenerativeUiEvent("forecast_card", AiScript.Json("""{"city":"Lisbon","degrees":17,"condition":"Clear","humidity":54}"""))); await yielder(new BitAiGenerativeUiEvent("forecast_card", AiScript.Json("""{"city":"Auckland","degrees":22,"condition":"Showers","humidity":72}"""))); await foreach (var evt in AiScript.Write("\n\nOslo is the outlier. If the trip is flexible, move it a month.", 4, 12, token)) await yielder(evt); await yielder(new BitAiMessageCompletedEvent()); })); ``` `Binding a payload`: ```csharp // The binding BitAiGenerativeUiView runs on every change, run by the app: here, to log what a model got wrong while a // prompt is being developed. Called with each reply as its turn ends (session.TurnCompleted += CheckCards). [Inject] private IBitAiGenerativeUiRegistry Registry { get; set; } = default!; [Inject] private ILogger Logger { get; set; } = default!; private void CheckCards(BitAiMessage reply) { foreach (var part in reply.PartsOf()) { // A name nothing is registered under is never rendered at all. if (Registry.Find(part.ComponentName) is not { } component) continue; var result = BitAiGenerativePropsBinder.Bind(component, part.Props); // Missing: a required property left out. Unreadable: a value of the wrong shape, with the serializer's Detail. foreach (var failure in result.Failures) { Logger.LogWarning("{Component}.{Property} {Kind}: {Detail}", failure.ComponentName, failure.PropertyName, failure.Kind, failure.Detail); } // Names the component does not declare, never passed to it: often a sign the schema the model has is out of date. foreach (var name in result.IgnoredProperties) { Logger.LogInformation("{Component} ignored {Property}", component.Name, name); } } } ```

How it works

The registry is the boundary. A model can only instantiate a component the application registered, and can only set the parameters that registration exposed. Event callbacks, render fragments, unmatched-attribute catch-alls and anything marked [BitAiGenerativeIgnore] are excluded, so a generated payload cannot wire up handlers or inject attributes. ForecastCard is an ordinary component of this site with ordinary parameters; nothing about it knows it is generative.

The transport writes its introduction as text, then sends one BitAiGenerativeUiEvent per card - the registered name, forecast_card, and the properties as JSON - and ends with more text. Each event becomes a BitAiGenerativeUiPart in the reply, between the text parts. Properties a model writes as it goes can stream as well: the same event with IsPartial set, sent again as more arrives, or BitAiGenerativeUiDeltaEvents carrying the raw JSON a piece at a time; the card is bound again as far as its properties have arrived.

registry.DescribeAll() produces the JSON Schema to put in the prompt, or to use as the schema of a render tool. Give the model the schema and it stops guessing component names. City is required here (the last argument of Register, or a [Required] on the parameter), and the descriptions of the parameters go into the schema.

Binding is BitAiGenerativePropsBinder.Bind(component, props), which the view runs on every change. Its BitAiGenerativeBindResult holds the Parameters the card renders with, and what did not bind: each Failures entry is a BitAiGenerativeBindError - its Kind (BitAiGenerativeBindErrorKind.Missing, a required property left out, or Unreadable, a value of the wrong shape, with the serializer's Detail and the ExpectedType), its PropertyName and ComponentName - and IgnoredProperties lists the names the component does not declare, which never reach it. IsComplete says whether anything failed. None of it is fatal: the card renders with what did bind. ShowBindingErrors of the view lists the failures under the card; an app runs the binder itself to check a payload - to log what a model got wrong while a prompt is developed, as the Binding a payload tab does.

Registering reads the component's parameters by reflection, so Register is marked as unsafe to trim. In a trimmed app, register the components from a method of your own that says why it is safe there - the component type is kept whole by the annotation of Register, so what remains to check is that its bindable parameters are plain data - as this site does for ForecastCard, whose parameters are strings and integers.

Components used

  • BitAiGenerativeUiView - renders the component the model named, with the properties it supplied, and lists what did not bind (ShowBindingErrors).
  • BitAiRendererBoundary - keeps a component that throws on what the model gave it to itself.

Feedback

Found a mistake, a gap, or something that could be clearer? Every page and every component is one click from its source.