DEV Community

Jan Zimprich
Jan Zimprich

Posted on

Streaming AI straight into Blazor UI: typed structured generation and live AI-built dashboards

DRYL Overview

Most AI UIs today are the same thing: a chat bubble filling up with Markdown. That's fine for text — but your app isn't made of text. It's made of cards, charts, badges, timelines, forms.

So I built DRYL — an open-source, AI-native component library for Blazor (Server & WebAssembly) — around one idea:

The AI shouldn't stream strings at your users. It should stream UI.

In this post I want to show you the two features I'm most excited about:

  1. DrylAiGenerate<T> — typed structured generation that streams a partial object into real Blazor components, token by token.
  2. DrylAiCanvas — the model builds a live, interactive dashboard as a chat artifact, and clicks inside it route back to the model.

(All GIFs below are recorded from the live docs at components.dryl.dev — no mockups.)


The problem with streaming text

Token streaming is a solved problem for chat. But the moment you want the model to produce something structured — a recipe, a product draft, a report — you usually end up with one of these:

  • Wait for the full JSON, deserialize, then render. The user stares at a spinner for 20 seconds.
  • Stream Markdown and lose all structure — no charts, no badges, no forms, no interactivity.

What you actually want: the model emits JSON for your C# type, and your UI renders the object while it's still incomplete — title appears first, then the description grows character by character, then list items pop in one by one.

That's exactly what DrylAiGenerate<T> does.

Typed structured generation: DrylAiGenerate<T>

You define a plain C# record:

public record Recipe(
    string Title,
    string Description,
    int Minutes,
    int Serves,
    List<Ingredient> Ingredients,
    List<Step> Steps);
Enter fullscreen mode Exit fullscreen mode

Ask the runner for a stream (it sets a JSON-schema response format for T and yields raw tokens):

// Works with any Microsoft.Agents.AI provider — OpenAI, Azure, Ollama, ...
_stream = Runner.GenerateStreamingAsync<Recipe>(agent, session, prompt, aiKey: "recipe");
Enter fullscreen mode Exit fullscreen mode

And bind it to your UI:

<DrylAiGenerate T="Recipe" Source="_stream" Key="recipe">
  <ChildContent Context="snap">
    <DrylCard Ai="@snap.State">
      <h3>@snap.Value?.Title</h3>
      <p>@snap.Value?.Description</p>

      <DrylTimeline>
        @foreach (var step in snap.Value?.Steps ?? [])
        {
          <DrylTimelineItem Title="@step.Title">@step.Text</DrylTimelineItem>
        }
      </DrylTimeline>
    </DrylCard>
  </ChildContent>
</DrylAiGenerate>
Enter fullscreen mode Exit fullscreen mode

Structured Generation

snap is a GenerationSnapshot<T>: on every chunk you get the best-possible partial Recipe. Under the hood a tolerant PartialJsonReader<T> repairs the incomplete JSON on the fly — half-open strings, unclosed arrays, truncated objects. If a chunk lands mid-token and the parse fails, it simply holds the last good snapshot. No flicker, ever.

A few things I care a lot about here:

  • It's typed. No dynamic, no JObject spelunking. snap.Value is your Recipe, IntelliSense and all.
  • It's just an IAsyncEnumerable<string>. No SDK lock-in in the component — in the docs the same demo runs against a canned replay and a real local model via Ollama. Tests don't need a model at all.
  • The UI knows the AI is working. Every DRYL component accepts a shared AiState (Thinking / Streaming / Generated). Pass Ai="@snap.State" and the card wears a breathing gradient aura while streaming and plays a one-shot reveal when done. One vocabulary across the whole library — users feel where the AI is at work without reading a label.
  • Progressive disclosure is trivial. Wrap sections in DrylPresence gated on the partial value (Visible="@(snap.Value?.Ingredients is { Count: > 0 })") and each part of the card animates in the moment the model produces it.

The showstopper: DrylAiCanvas — AI-built interactive artifacts

Structured generation renders your layout. The canvas goes one step further: the model designs the layout.

DrylAiCanvas gives your agent two tools — create_artifact and update_artifact. When the user asks for something visual, the model streams a component spec (a curated tree of DRYL components: charts, stat tiles, tables, inputs, buttons) and the canvas builds it node by node, live, next to the chat:

<DrylChat> ... </DrylChat>

<DrylAiCanvas Run="_run"
              OnInteraction="i => Send(i.ToPromptMessage())" />
Enter fullscreen mode Exit fullscreen mode
var tools = DrylCanvasTools.Create(_run);
var agent = new ChatClientAgent(chatClient,
    instructions: "...", tools: tools.All);
Enter fullscreen mode Exit fullscreen mode

DRYL Canvas

Here's the part that makes it more than a pretty renderer: the artifact is interactive, and interactions round-trip to the model.

Buttons inside the artifact carry an intent. When the user picks a region in the generated filter and clicks "Break down by region", you get a CanvasInteraction — including the current values of every form control in the artifact. interaction.ToPromptMessage() turns it into the next chat turn; the model answers with update_artifact, streaming a patch. Changed nodes glow, new nodes glide into place (FLIP-animated — nothing snaps), untouched nodes stay put.

DRYL Canvas Interactive

So the loop is:

user asks → model builds UI → user clicks inside that UI → model updates it.

That's the "artifacts" experience you know from modern AI chat apps — as a drop-in Blazor component, running on your own agent and your own components.

Safety-wise: the model never emits HTML or code. It emits a JSON spec validated against a curated component whitelist. Invalid nodes never render — the model gets a corrective error string and retries.

The rest of the iceberg

These two are my favorites, but the AI layer runs deeper — all speaking the same AiState vocabulary:

  • DrylAiStream — token streaming into DrylMarkdown with a paced smooth mode (some providers deliver tool-call turns in one burst; this keeps it reading as a stream).
  • Human-in-the-loop tools — ready-made AIFunctions that pop real dialogs mid-run (AskChoice, AskText, RequestPermission) and await the user's answer.
  • Display tools — the model answers with live charts, stat rows and timelines inline in the conversation (show_line_chart, show_stats, …).
  • DrylHandoffTrace — multi-agent sequential/concurrent flows rendered as a living timeline.
  • DrylAiField — wrap any input, get an unobtrusive ✨ affordance: generate into an empty field, or transform just the selected text.

And underneath it all sits a full general-purpose component library (60+ components): glass surfaces, token-driven dark/light modes, a fixed motion vocabulary, container-query responsiveness, zero npm dependencies.

Try it

dotnet add package DRYL.Components          # core library
dotnet add package DRYL.Components.Agents   # AI layer (Microsoft Agent Framework bridge)
Enter fullscreen mode Exit fullscreen mode
builder.Services.AddDrylComponents().AddDrylAgents();
Enter fullscreen mode Exit fullscreen mode

The core is stable and dependency-free (Markdig only); the agents package is experimental and versioned independently while the Microsoft Agent Framework itself matures. You bring your own AIAgent — OpenAI, Azure, or a local Ollama.

If you're building anything AI-flavored in Blazor, I'd genuinely love feedback — especially on the canvas spec format and what components you'd want the model to be able to build. Issues and PRs welcome. ⭐

Top comments (0)