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:
-
DrylAiGenerate<T>— typed structured generation that streams a partial object into real Blazor components, token by token. -
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);
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");
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>
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, noJObjectspelunking.snap.Valueis yourRecipe, 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). PassAi="@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
DrylPresencegated 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())" />
var tools = DrylCanvasTools.Create(_run);
var agent = new ChatClientAgent(chatClient,
instructions: "...", tools: tools.All);
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.
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 intoDrylMarkdownwith 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
- Docs & live demos: components.dryl.dev — the AI demos on the site run against real streams
- Source: github.com/Zimpi/DRYL.Components
- NuGet:
dotnet add package DRYL.Components # core library
dotnet add package DRYL.Components.Agents # AI layer (Microsoft Agent Framework bridge)
builder.Services.AddDrylComponents().AddDrylAgents();
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)