Lumeo

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.
Session started 21:36
21:34You
Summarise the quarterly revenue numbers for EMEA and tell me which region grew fastest.
Lumeo Copilot21:34
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
Input
{
  "region": "EMEA",
  "period": "2026-Q1"
}
Output
{
  "DACH": 12_400_000,
  "UK": 9_800_000,
  "Nordics": 4_200_000,
  "Iberia": 3_100_000
}
Lumeo Copilot
The fastest grower was DACH at +18% YoY, driven mainly by…
0 / 2000
0 tokens
Lumeo streams tokens straight through SignalR — no SSE plumbing, no fetch() wrappers, just one StateHasChanged() per chunk. 
The caret and suffix fade-in re-render as chunks append.
search_web
running
Input
{
  "query": "blazor server streaming"
}
fetch_url success312ms
Input
{
  "url": "https://example.com"
}
Output
200 OK — 14.2KB payload
run_python error28ms
Input
print(x)
Error
NameError: name 'x' is not defined
Thinking…

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
Valuestring?nullCurrent text. Two-way bindable via @bind-Value.
Placeholderstring"Ask anything…"Placeholder when empty.
MaxHeightint240Pixel cap before the textarea scrolls internally.
MinHeightint56Minimum pixel height of the textarea.
IsLoadingboolfalseDisables input and shows a spinner on the send button.
OnSendEventCallback<string>Fires with the current Value on Enter or Send click.
DisableSendOnEmptybooltrueDisables the send button when Value is blank.
LeadingContentRenderFragment?Slot on the left of the toolbar (attach button, etc.).
TrailingContentRenderFragment?Slot on the right of the toolbar (token count, model selector).
Classstring?Extra CSS classes on the root container.
ValueChanged EventCallback<string?> Two-way binding callback; fires as the user types.

StreamingText

Property Type Default Description
Textstring?nullFull accumulated text. Newly-appended suffix fades in.
IsStreamingboolfalseWhen true, shows a pulsing caret at the end of the text.
ProseboolfalseApplies prose typography (Tailwind) to the content.

AgentMessageList

Property Type Default Description
ChildContentRenderFragment?Usually one or more <AgentMessage> children.
AutoScrollbooltrueSticks to the bottom when new content arrives, using a MutationObserver.

AgentMessage

Property Type Default Description
RoleAgentMessageRoleAssistantOne of User, Assistant, System, Tool.
AvatarRenderFragment?Optional avatar rendered alongside the bubble.
Namestring?Display name shown above the bubble.
TimestampDateTimeOffset?Optional timestamp shown next to the name.
IsStreamingboolfalseAppends a pulsing caret to the body.
ChildContentRenderFragment?Message body. Usually plain text or <StreamingText>.

ToolCallCard

Property Type Default Description
ToolNamestring""Name of the tool being invoked (e.g. search_web).
StatusToolCallStatusPendingPending, Running, Success, or Error. Drives chip colors and icons.
Inputstring?Arguments to the tool call, shown in a <pre> block.
Outputstring?Tool result shown when expanded.
ErrorMessagestring?Error details shown when Status=Error.
DurationMslong?Wall-clock duration of the call.
DefaultOpenboolfalseWhether the card starts expanded.

ReasoningDisplay

Property Type Default Description
Textstring?Reasoning / chain-of-thought text.
IsStreamingboolfalseApplies a subtle opacity pulse while the model is still thinking.
Summarystring?Override the default summary label.
DurationMslong?When set, summary becomes "Reasoned for Xs".
DefaultOpenboolfalseWhether the reasoning section starts expanded.