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 (3)

Collapse
 
topstar_ai profile image
Luis Cruz

I'm impressed by how DrylAiGenerate<T> tackles the issue of streaming structured data into Blazor components, particularly the use of a tolerant PartialJsonReader<T> to repair incomplete JSON on the fly. The fact that it's typed and works with any Microsoft.Agents.AI provider makes it a versatile solution. I've worked with similar streaming scenarios in the past, and I can appreciate the value of having a component that can handle partial objects and provide a seamless user experience. Have you considered adding support for caching or throttling to handle cases where the AI model is slow to respond or the user's connection is unstable?

Collapse
 
dryl_dev profile image
Jan Zimprich

Thanks a lot for the kind words and for taking the time to read the post! I'm really glad the approach with PartialJsonReader<T> resonated with you — handling those incomplete, in-flight JSON chunks without causing UI flicker was definitely one of the most fun challenges to solve.

Regarding caching and throttling: that's a super interesting idea!

Right now, DrylAiGenerate<T> re-evaluates as soon as a valid new snapshot can be parsed from the stream. However, adding an opt-in throttling/buffering mechanism (like frame-pacing rapid token bursts or smoothing out erratic stream deliveries) makes total sense, especially over unstable SignalR/Blazor WebSocket connections. Caching or snapshot-persistence during high latency is also a great angle to make the streaming state even more resilient.

I'm definitely going to give this some thought and look into how i can incorporate a clean throttling or caching strategy into the DRYL.Components.Agents pipeline.

Thanks again for the awesome feedback and the great suggestion! 🙌

Some comments may only be visible to logged-in visitors. Sign in to view all comments.