AI Chat Primitives
Streaming-ready building blocks for LLM-powered chat interfaces. Render tokens as they arrive, show tool calls, surface reasoning, and let users send prompts — all over Blazor Server's SignalR pipe.
Installation
dotnet add package Lumeo
One-time app setup (AddLumeo(), CSS & JS) is covered in the
installation guide.
When to Use
- Building any chat, copilot, or agentic UI on top of a streaming LLM.
- You want token-by-token rendering without the SSE plumbing React apps require — Blazor Server pipes chunks over SignalR with one
StateHasChanged(). - Displaying tool calls (function calls, RAG lookups, SQL runs) as collapsible cards inline with the conversation.
- Showing chain-of-thought / reasoning segments that users can expand on demand.
- Giving users a prompt input with auto-grow, Enter-to-send, attach slot, and a token counter.
Reasoned for 2.4s
User wants EMEA revenue breakdown. I should call the sales tool to pull Q1–Q4 numbers, then compute growth per sub-region. DACH looked strong last quarter.
query_sales_db success842ms
{
"region": "EMEA",
"period": "2026-Q1"
}{
"DACH": 12_400_000,
"UK": 9_800_000,
"Nordics": 4_200_000,
"Iberia": 3_100_000
}search_web running
{
"query": "blazor server streaming"
}fetch_url success312ms
{
"url": "https://example.com"
}200 OK — 14.2KB payloadrun_python error28ms
print(x)NameError: name 'x' is not definedThinking…
Let me break this down — first I need to identify the entities, then decide which tool to invoke. Given the user asked about EMEA revenue…
Reasoned for 3.8s
Fast path: query_sales_db with region=EMEA, period=2026-Q1. That returns per-sub-region totals. Then rank by YoY growth.
Wiring StreamingText to a SignalR hub
Blazor Server already runs each circuit over SignalR. Capture a @ref to your parent component's streaming state
and call InvokeAsync(StateHasChanged) when a new chunk arrives. That's the whole streaming story.
// In your Blazor component:
@inject IHubContext<ChatHub> Hub
private string _assistantText = "";
private bool _isStreaming;
protected override void OnInitialized()
{
ChatHub.OnChunk += async chunk =>
{
_assistantText += chunk;
await InvokeAsync(StateHasChanged); // Blazor Server pushes over SignalR automatically
};
ChatHub.OnComplete += async () =>
{
_isStreaming = false;
await InvokeAsync(StateHasChanged);
};
}
// In your markup:
<StreamingText Text="@_assistantText" IsStreaming="@_isStreaming" />API Reference
PromptInput
| Property | Type | Default | Description |
|---|---|---|---|
| Value | string? | null | Current text. Two-way bindable via @bind-Value. |
| Placeholder | string | "Ask anything…" | Placeholder when empty. |
| MaxHeight | int | 240 | Pixel cap before the textarea scrolls internally. |
| MinHeight | int | 56 | Minimum pixel height of the textarea. |
| IsLoading | bool | false | Disables input and shows a spinner on the send button. |
| OnSend | EventCallback<string> | — | Fires with the current Value on Enter or Send click. |
| DisableSendOnEmpty | bool | true | Disables the send button when Value is blank. |
| LeadingContent | RenderFragment? | — | Slot on the left of the toolbar (attach button, etc.). |
| TrailingContent | RenderFragment? | — | Slot on the right of the toolbar (token count, model selector). |
| Class | string? | — | Extra CSS classes on the root container. |
| ValueChanged | EventCallback<string?> | — | Two-way binding callback; fires as the user types. |
StreamingText
| Property | Type | Default | Description |
|---|---|---|---|
| Text | string? | null | Full accumulated text. Newly-appended suffix fades in. |
| IsStreaming | bool | false | When true, shows a pulsing caret at the end of the text. |
| Prose | bool | false | Applies prose typography (Tailwind) to the content. |
AgentMessageList
| Property | Type | Default | Description |
|---|---|---|---|
| ChildContent | RenderFragment? | — | Usually one or more <AgentMessage> children. |
| AutoScroll | bool | true | Sticks to the bottom when new content arrives, using a MutationObserver. |
AgentMessage
| Property | Type | Default | Description |
|---|---|---|---|
| Role | AgentMessageRole | Assistant | One of User, Assistant, System, Tool. |
| Avatar | RenderFragment? | — | Optional avatar rendered alongside the bubble. |
| Name | string? | — | Display name shown above the bubble. |
| Timestamp | DateTimeOffset? | — | Optional timestamp shown next to the name. |
| IsStreaming | bool | false | Appends a pulsing caret to the body. |
| ChildContent | RenderFragment? | — | Message body. Usually plain text or <StreamingText>. |
ToolCallCard
| Property | Type | Default | Description |
|---|---|---|---|
| ToolName | string | "" | Name of the tool being invoked (e.g. search_web). |
| Status | ToolCallStatus | Pending | Pending, Running, Success, or Error. Drives chip colors and icons. |
| Input | string? | — | Arguments to the tool call, shown in a <pre> block. |
| Output | string? | — | Tool result shown when expanded. |
| ErrorMessage | string? | — | Error details shown when Status=Error. |
| DurationMs | long? | — | Wall-clock duration of the call. |
| DefaultOpen | bool | false | Whether the card starts expanded. |
ReasoningDisplay
| Property | Type | Default | Description |
|---|---|---|---|
| Text | string? | — | Reasoning / chain-of-thought text. |
| IsStreaming | bool | false | Applies a subtle opacity pulse while the model is still thinking. |
| Summary | string? | — | Override the default summary label. |
| DurationMs | long? | — | When set, summary becomes "Reasoned for Xs". |
| DefaultOpen | bool | false | Whether the reasoning section starts expanded. |