Skip to content

AI scenarios

Forms

Bit.BlazorUI.AI Scripted transport

The model proposes a form from a JSON Schema, the user corrects it, and the corrected values are what execute.

Live demo

Ask it to book something: check the form it proposes, correct it, and submit. The values that would execute appear beside the chat, under the schema's property names.

\n**A booking to confirm**: ```razor @implements IAsyncDisposable
@if (values is not null) {

Submitted values

    @foreach (var (key, value) in values) {
  • @key: @value
  • }
} ``` ```csharp private readonly BitAiChatSession session = new(AiDemoInteractiveTransports.ProposesForm()); private IReadOnlyDictionary? values; private EventCallback> onSubmitted; private readonly string[] suggestions = ["Book a table for Friday"]; // What BookingForm calls with the corrected values - the page decides what a booking means. protected override void OnInitialized() => onSubmitted = EventCallback.Factory.Create>(this, submitted => values = submitted); public ValueTask DisposeAsync() => session.DisposeAsync(); ``` `Program.cs`: ```csharp builder.Services.AddBitBlazorUIAIServices(ai => { ai.UseTransport(AiDemoTransports.Echo()); // A schema the model fills in becomes a form the user checks, instead of raw JSON. ai.ConfigureRenderers(renderers => renderers.RegisterStructured("booking_form", context => builder => { builder.OpenComponent(0); builder.AddComponentParameter(1, nameof(BookingForm.Part), context.Part); builder.CloseComponent(); })); }); ``` `BookingForm.razor`: ```razor @* Not submitted before the whole schema has arrived, and not edited after it was. *@ @code { private BitAiFormDefinition definition = new(); private bool submitted; [Parameter, EditorRequired] public BitAiStructuredDataPart Part { get; set; } = default!; [CascadingParameter(Name = "BookingSubmitted")] public EventCallback> Submitted { get; set; } // Rebuilt as the schema streams in; the form keeps whatever the user already typed. protected override void OnParametersSet() => definition = Part.Value is { } schema ? BitAiFormDefinition.FromJsonSchema(schema) : new BitAiFormDefinition(); private async Task OnSubmitAsync(IReadOnlyDictionary values) { submitted = true; await Submitted.InvokeAsync(values); } } ``` `AiDemoInteractiveTransports.cs`: ```csharp // AiScriptedTransport and AiScript are the demo's helpers - see the Basic chat scenario. public static IBitAiTransport ProposesForm() => new AiScriptedTransport((request, token) => AiScript.Stream(token, async yielder => { await yielder(new BitAiMessageStartedEvent()); await foreach (var evt in AiScript.Write("I can book that. Check the details first, since I inferred two of them.\n\n", 4, 12, token)) await yielder(evt); // The two details the model inferred go in as the defaults the form starts from: two guests, and the coming Friday. var today = DateTime.Today; var friday = today.AddDays(((int)DayOfWeek.Friday - (int)today.DayOfWeek + 7) % 7); var date = friday.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); var schema = $$""" { "title": "Book a table", "type": "object", "required": ["restaurant", "guests", "date"], "properties": { "restaurant": { "type": "string", "title": "Restaurant" }, "guests": { "type": "integer", "title": "Guests", "minimum": 1, "maximum": 12, "default": 2 }, "date": { "type": "string", "format": "date", "title": "Date", "default": "{{date}}" }, "seating": { "type": "string", "enum": ["Inside", "Terrace", "No preference"] }, "notes": { "type": "string", "format": "textarea", "title": "Notes for the restaurant" } } } """; // Named booking_form, which is what picks the registered renderer. foreach (var slice in AiScript.Slice(schema, 30)) { await AiScript.Pause(50, token); await yielder(new BitAiStructuredDataDeltaEvent(slice) { SchemaName = "booking_form" }); } await yielder(new BitAiMessageCompletedEvent()); })); ```

How it works

Confirmation, not guessing. The model produced a schema, not a booking: the form is built from that schema (BitAiFormDefinition.FromJsonSchema), validated by the application rather than by the model, and only submits once it is valid.

The transport writes a line of text, then streams the JSON Schema as BitAiStructuredDataDeltaEvents named booking_form. A structured renderer registered for that name (renderers.RegisterStructured("booking_form", context => ...), a BitAiStructuredDataRenderer handed a BitAiStructuredDataContext with the part and its message) replaces the default view, so the user sees a form rather than the JSON behind it. The renderer, BookingForm, hands the values - keyed by the schema's property names - to whatever page cascaded a BookingSubmitted callback, so it stays reusable and the page decides what a booking means: this one lists them beside the chat, where a real one would make the booking, and could tell the model what was booked with the session's SubmitAsync.

Fields appear as the schema streams in, and anything already typed survives a new field arriving. What the model inferred arrives as the schema's defaults, which the form starts from; it cannot be submitted until the whole schema is in (Busy), and takes no more edits once it has been (Disabled).

Components used

Feedback

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