Blazor Interfaces for Slow, Fallible AI
An AI interface can be technically responsive and still be dishonest. It can animate a progress bar while nothing measurable is progressing. It can display “cancelled” while a server continues working. It can replace a current answer with a late result from a previous request.
Explore the mechanism ↓
Reading tools Listen or translate
Read aloud
Uses an installed voice on your device.
Translate article
Machine translation. Code and evidence notes stay in English.
Original English text.
START WITH THE MECHANISM
Interactive companion and laboratory update: 12 November 2026. The editorial date places the topic on the timeline; this current explanation and its measured evidence retain their actual preparation and execution dates.
THE FULL INVESTIGATION
Blazor Interfaces for Slow, Fallible AI
Dates and technology context
This is a retrospective editorial timeline. Topic context: 10 April 2026. First publicly available: 12 November 2026. Current text, visuals and laboratory revisions were prepared on 12 November 2026.
The real Blazor circuit and browser checks use SDK10.0.400/runtime10.0.15, executed November2026. April2026 contains only the earlier interaction contract and .NET10.0.100/10.0.0 baseline.
An AI interface can be technically responsive and still be dishonest. It can animate a progress bar while nothing measurable is progressing. It can display “cancelled” while a server continues working. It can replace a current answer with a late result from a previous request.
These failures are not primarily about styling. They come from an incomplete model of the operation. A slow, fallible AI task needs an interface that can express uncertainty, preserve user intent and recover after interruption. Blazor supplies the component model; the application must supply the truth about what is happening.
Consider an assistant that analyzes a document and produces a structured recommendation. Uploading, validating, retrieving evidence, calling a model and checking the result are different stages. A useful interface tells the user which stage has been reached without claiming more certainty than the backend can provide.
The examples target the established component and asynchronous programming model in Blazor on .NET 10. They are illustrative fragments, not a complete application or an executed accessibility test. The platform release establishes the version baseline. Microsoft .NET 10 announcement.
Model states before designing the screen
Begin with a state machine that distinguishes idle, submitted, running, cancellation requested, cancelled, succeeded, failed and outcome unknown. Some applications need fewer states, but collapsing them should be a deliberate decision.
“Submitted” means the client sent a request. “Accepted” means the server durably owns it. “Running” means execution has started. These events can happen at different times, and a disconnected browser may observe only some of them.
An unknown outcome is especially important when the task can create effects. If the connection fails after the server commits a result, the client cannot conclude that the task failed. The appropriate next action is to look up the operation, not to offer an unconditional “try again” button that creates duplicate work.
Keep user intent separate from server outcome. Clicking Cancel records a request to stop. It does not prove that all in-flight work stopped. The UI should display a pending cancellation state until the server confirms the terminal outcome or explains why part of the operation already completed.
Do not invent a percentage
A progress bar is meaningful when the denominator is known and the completed work is comparable. Processing 40 of 100 similarly sized files can support a count-based indicator. Waiting for a model response generally cannot support an honest claim that the work is 73 percent complete.
Use stage descriptions and elapsed time when the total work is unknown. “Retrieving source material” or “Checking the response” is more useful than a smoothly advancing number that stalls at 99 percent. If a stage can legitimately repeat, say so instead of making the repetition look like the application has gone backward.
Estimated time can be helpful, but label it as an estimate and explain the basis when it matters. A rolling average from previous operations is not a guarantee for this operation. Large documents, cold dependencies and retries can produce very different durations.
The interface should also avoid implying that streamed text is a final answer. A model may emit a plausible opening and later encounter missing evidence. If the product validates the complete output, distinguish draft streaming content from an accepted result. The user should know which content is safe to copy or act on.
Give each operation an identity and a revision
01 / See the idea
Intent and outcome are different states
A hypothetical document analysis is submitted before the server accepts it. A later cancel click expresses intent, while the terminal result must still come from the operation record.
Interpretation and evidence
- Submitted is not accepted.
- Cancel requested is not cancelled.
- Unknown requires a lookup.
Asynchronous updates can arrive late, out of order or after the user starts another task. A component should not treat the last callback to run as the authoritative state.
Use an operation identifier and an increasing server-side revision or event sequence. Apply an update only if it belongs to the current operation and is newer than the state already displayed. This is a small rule with a large effect on UI correctness.
// Application-owned state. Illustrative, not executed.
public sealed record OperationView(
Guid OperationId,
long Revision,
string Stage,
string Status,
string? ResultReference);
private OperationView? current;
private void Apply(OperationView update)
{
if (current is not null &&
(update.OperationId != current.OperationId ||
update.Revision <= current.Revision))
return;
current = update;
}
The complete component must establish current when it starts a new operation. Otherwise an unsolicited first update could become the current one. Server revisions should reflect durable operation state, not a local counter that resets whenever a worker restarts.
This rule protects the display from stale information. It does not authorize access to an operation. Every server endpoint that returns state or results must independently verify that the caller may read them.
Respect the component's asynchronous lifetime
Blazor's synchronization context does not make every asynchronous method atomic. A component can be re-entered when it awaits incomplete work, and lifecycle events can occur while an earlier operation is still in progress. Microsoft's guidance explicitly discusses this re-entrancy and the need to leave the component in a valid state before awaiting. Blazor synchronization context.
Set the state that the renderer needs before starting an await. Keep references to owned cancellation sources and subscriptions. When a callback comes from outside the normal component event flow, marshal the state update through InvokeAsync rather than assuming it is running in the correct context.
// Fragment for updates raised by an external subscription.
private Task OnOperationUpdateAsync(OperationView update) =>
InvokeAsync(() =>
{
if (disposed)
return;
Apply(update);
StateHasChanged();
});
disposed is component-owned state, and the subscription must be detached during disposal. This fragment is not sufficient on its own: the owner still needs to observe callback failures and close the subscription. The purpose is to show that renderer access and operation identity are separate checks.
Do not call StateHasChanged blindly during disposal. A component being removed has a different lifecycle from one displaying a new result. Microsoft's disposal guidance cautions against relying on exact disposal timing and against assuming asynchronous initialization has completed. Razor component disposal.
Cancellation needs a server contract
02 / A concrete example
Late messages must not overwrite today
The display is showing operation B at revision 3. An update for A at revision 9 and B at revision 2 are ignored; B at revision 4 is accepted.
Interpretation and evidence
- Match the operation ID first.
- Reject equal or older revisions.
- State authority remains on the server.
Cancelling a client-side wait can improve responsiveness, but it does not necessarily cancel server work. An HTTP request abort, an operation cancellation endpoint and a model provider's cancellation behavior are different mechanisms.
For a durable job, expose cancellation as an authenticated command against the operation identifier. The command should be idempotent and return the state the server knows. If the task is already complete, return that result rather than pretending the cancellation rewound it.
A component can stop listening immediately when the user navigates away while allowing the server-owned job to continue. That may be the right product behavior. Alternatively, leaving the page may request cancellation. Either choice is defensible if it is explicit. Surprise background work and surprise loss of work are both poor defaults.
The UI should explain the scope: “Stop waiting” is different from “Request cancellation.” If some results have already been committed, report partial completion. A single red toast saying “Cancelled” can conceal a complicated and important state transition.
Recovery starts with a durable operation record
If the operation exists only in component memory, reconnecting to the application creates a mystery. The browser knows it was waiting, but the server may have finished, failed or restarted.
Use a durable operation record for work that must outlive the connection. It should include the actor or tenant scope, submitted input reference, current state, revision, timestamps and any result reference. The browser can then reconnect and request the latest state.
Do not persist sensitive document content in browser storage merely to make reconnection easy. A small operation identifier may be sufficient. Even that identifier should not function as a bearer credential; possession of it must not bypass authorization.
On recovery, show what is known and what is being reconciled. “We are checking the result of your previous request” is more accurate than silently launching another run. A good recovery experience reduces duplicate submissions because the application can account for its own work.
Make status understandable without animation
03 / Watch the mechanism
Cancellation cannot rewind a commit
Static view. Play to see how the mechanism unfolds.
In this hypothetical race, the server commits result R7 before receiving Cancel. The browser first shows cancellation requested, then the confirmed completed state rather than a false cancelled label.
Interpretation and evidence
- Show the pending request honestly.
- Keep status outside the busy region.
- Use the confirmed terminal outcome.
A spinner communicates activity visually, but it does not necessarily communicate meaning. Use a text status that remains understandable to assistive technology and to a person who has stepped away and returned.
In a Razor component, a small status region can announce meaningful transitions. Avoid announcing every token or every tiny progress event, which can overwhelm the user. Announce stage changes, completion, cancellation and actionable failures.
@* Illustrative markup; accessibility testing not performed. *@
<p role="status" aria-live="polite">@StatusMessage</p>
<section aria-busy="@(IsRunning ? "true" : "false")">
<button type="button"
disabled="@(!CanRequestCancellation)"
@onclick="RequestCancellationAsync">
Request cancellation
</button>
</section>
Keep the status region outside the busy result container. Assistive technology may defer changes owned by an element while its aria-busy value is true, so placing stage announcements inside that element can defeat the intended feedback. The result region can remain busy while its separate status announces meaningful transitions. WAI-ARIA 1.2, aria-busy.
The W3C status-message criterion explains why important updates should be programmatically available without forcing focus changes. Markup is only part of that work: test actual announcements, keyboard navigation, focus placement and error recovery with the chosen UI. W3C guidance on status messages.
Keep controls stable where possible. If a button disappears and a new result region appears, focus may end up in a confusing place. Decide where focus should go after submission, cancellation and completion, and verify the behavior rather than assuming the browser will make a sensible choice.
Design failure messages around the next decision
“Something went wrong” is sometimes the only safe public message, but it rarely tells the user what to do. Distinguish an invalid document, insufficient evidence, a temporary dependency failure and an unknown outcome.
For an invalid document, explain the input requirement. For insufficient evidence, offer clarification or a way to provide the missing source. For a temporary failure, say whether retry is safe and whether the previous operation is definitely terminal. For an unknown outcome, begin reconciliation.
Do not expose raw model or infrastructure errors. They can contain internal details and may be misleading outside their original context. Map them into a stable application error contract while keeping a correlation identifier for support.
Also preserve the user's work. A long-running request should not erase a carefully written prompt merely because a dependency timed out. Retain the input locally only under an appropriate data policy, and allow the user to revise it without accidentally mutating a running operation's recorded input.
Test time and disorder, not only content
Build a fake operation service that can emit updates in a controlled order. Test a delayed completion from operation A after operation B has started. Test duplicated revisions, a disconnected stream, a cancellation race and a terminal result arriving after the cancellation request.
Test component removal during every await boundary that owns resources. Verify that subscriptions close, callbacks do not update disposed components and exceptions are observed. A test that only checks the final answer text misses the lifecycle in which most of these bugs live.
For usability, observe people completing the task under realistic delays. Ask what they believe the system is doing, whether they think cancellation has taken effect and whether they know a result is final. A technically correct state model can still be communicated badly.
No runtime or user testing has been performed for this manuscript. The proposed tests are an acceptance plan. A later implementation should retain actual results from component tests, reconnect scenarios and assistive-technology checks rather than promoting the presence of ARIA attributes into an accessibility claim.
The interface should help the user remain in control
Slow AI does not require pretending that uncertainty has disappeared. It requires communicating uncertainty in a way that supports action. A clear stage, a truthful cancellation state and a recoverable operation record can create more confidence than an elaborate animation.
The engineering objective is to preserve a coherent relationship between user intent, server responsibility and visible outcome. Blazor can express that relationship cleanly, but it cannot invent it. Once the state model is honest, the interface can be calm even when the operation is not fast.
A later Blazor application makes the interaction observable
Implementation note, prepared 12 November 2026. The original April 2026 sections describe the interaction contract. The application below is a later implementation using SDK 10.0.400 and runtime 10.0.15; it is not evidence of execution at the earlier contextual date.
The small Blazor application accompanying this article turns the central states into observable behavior. It has a request field, a choice of scripted provider behavior, start and cancellation controls, a result region and an explicit status message. It does not call a hosted AI service.
The provider is deliberately controlled by the local interface. A release button completes a pending operation. Another button releases the oldest pending operation, even when a newer request is already displayed. This lets the browser test produce the important ordering directly rather than hoping a sleep is long enough to create a race.
The application runs as interactive server-side Blazor on a temporary loopback address. The test wrapper starts it, discovers its assigned port, uses one browser page and closes both browser and server afterward. It is a functional demonstration, not a deployed service or a latency benchmark.
The absence of a real model is a precise limitation, not a hidden substitution. This experiment tests the interface's response to a provider contract: waiting, completion, cancellation and failure. Model quality, remote network behavior and provider-specific streaming require separate evidence.
The view state belongs to one operation
01 / Watch the mechanism
A late result must pass the identity gate
Static view. Play to see how the mechanism unfolds.
The browser test completes the second request first, then deliberately releases the older request. The old response cannot change the current result, identity or revision.
Interpretation and evidence
- Check identity after the awaited operation.
- A new request does not prove the old provider stopped.
- Test a stale response by making it arrive.
The implemented state record carries an operation identity and a revision alongside the visible state:
public sealed record OperationState(
Guid Id, long Revision, string Status,
bool Busy, string Result, string Prompt);
Starting a request creates a new identity, resets its local revision to one and enters Waiting. The result is empty until the current operation produces an accepted completion. A second request replaces the displayed operation immediately and requests cancellation of the earlier one.
Replacement is not proof that the earlier provider stopped. The older operation may still finish, especially in the deliberately uncooperative fixture mode. Its continuation therefore checks identity before changing the visible state:
private bool IsCurrent(Guid id) => !disposed && state.Id == id;
The check is repeated after the provider's awaited operation finishes. If the result belongs to an older request, it is ignored for display. The fixture increments a diagnostic count so the browser test can establish that the late completion really occurred, rather than simply failing to arrive during the test.
The revision is local to the current operation. It advances when cancellation is requested or a terminal outcome is accepted. The implementation has one final provider response per operation; it does not claim to implement a general streamed-event protocol with arbitrary within-operation reordering. The earlier revision-filtering discussion identifies the additional contract such a protocol would need.
This distinction keeps the example honest. A request identity protects against one class of stale update. It does not, by itself, solve every ordering problem inside a long-running operation.
Cancellation has an observed state and a requested state
The cancellation control first changes the status to CancelRequested and signals the operation's cancellation source. It does not immediately claim that the provider has stopped.
In cooperative mode, the scripted provider connects that token to its pending completion source. The awaited operation then ends with cancellation, and the component enters Cancelled with no accepted result.
In late-response mode, the provider intentionally ignores cancellation. The interface continues to report that cancellation has been requested and that it is waiting for the provider to finish or acknowledge the request. When the scripted result eventually arrives, the component checks the current token and declines to accept the result as successful.
This behavior is appropriate for the demonstration because the provider has no external effects. A real operation that writes data or sends an instruction would need a richer contract. Declining to display its result would not undo the effect. The interface would need to report and reconcile what actually happened.
The same care applies to supersession. Starting another request is a decision about which result the user currently wants to see. It does not erase the earlier operation's possible obligations. In this fixture, there are no remote obligations, so cancelling and ignoring stale display updates is enough for the stated boundary.
The test should make that scope visible. It verifies display correctness under cooperative and uncooperative completion. It does not certify cancellation semantics for a particular AI provider, storage system or business action.
An await does not freeze the component
Blazor's synchronization context provides a single logical execution context within a server-side circuit, but an incomplete await allows the component's control flow to be re-entered. Another event or disposal can occur before the original operation resumes. Microsoft documents this distinction explicitly. Blazor synchronization context.
The implementation therefore sets a valid Waiting state before awaiting the provider. It captures the request text, provider mode and operation identity into local variables. Later edits to the input do not silently change what the in-flight operation means.
The continuation checks both disposal and identity before applying an outcome. The cancellation source belongs to that invocation and is disposed in its own finally block. It clears the component's current cancellation reference only if that reference still points to the same source. Otherwise an old operation could accidentally remove the cancellation handle for the new one.
These details are small but consequential. The code is managing several overlapping lifetimes: the component, the displayed request, an earlier provider operation and the cancellation objects associated with each attempt. A single boolean called loading would not express all of them.
The provider service is scoped to the circuit and uses completion sources to gate responses. Component disposal cancels pending fixture work. This is not a durable operation store; a circuit disappearing does not leave an operation that another browser can recover. That remains a separate feature from the state-correlation behavior demonstrated here.
Put the status outside the busy result region
02 / See the idea
Status remains outside the busy result region
The status message explains waiting while the result region is marked busy. The test checks that the live status has no aria-busy ancestor and that no percentage is fabricated.
Interpretation and evidence
- Keep status announcements outside the busy result region.
- Describe the known state rather than inventing progress.
- Separate DOM checks from actual assistive-technology testing.
The status message is a sibling of the result region. It has role="status", explicit polite live-region behavior and aria-atomic="true". The result region carries aria-busy while the current operation is waiting.
<p id="status" role="status" aria-live="polite" aria-atomic="true">
Current operation status
</p>
<div id="result-region" aria-busy="true">
Result content
</div>
The separation is intentional. The message describing the wait is not placed inside the region whose updates are being marked busy. The browser test checks this structural relationship rather than merely searching for an aria-live attribute somewhere on the page.
W3C's status-message technique explains the use of the status role to convey updates without moving focus and notes the value of explicit atomic behavior. This supports the markup choice; it does not mean that an automated DOM check proves the experience of every screen-reader and browser combination. W3C ARIA22.
The text describes states, not fabricated percentages. Waiting says that no completion estimate is available. CancelRequested says that the application is still waiting for completion or acknowledgement. Failed names the next available action, while Completed states that the displayed result belongs to the current request.
The controls also remain usable while work is pending. The user can request cancellation or start another request. That is a deliberate interaction contract, and the identity checks make its consequence safe within this local example.
Test the late result by making it arrive
The browser scenario starts a first request in the mode that ignores cancellation. It then starts a second request, verifies that the operation identity changed and releases the current operation. The second result becomes visible.
Only then does the test release the oldest pending operation. The stale-completion counter must increase, while the displayed result, current identity and revision remain unchanged. This is stronger evidence than waiting briefly and observing that the second result is still present. The test establishes that the late result was actually processed and rejected for display.
The cancellation scenarios use the same principle. Cooperative cancellation must reach Cancelled. Uncooperative cancellation must remain visibly pending until the test releases the provider, after which the result is not accepted as successful.
Failure and retry are separate. The fail-once mode raises a controlled error before returning a result. The interface exposes Retry and leaves the result region non-busy. Retrying creates a new operation identity; releasing that new operation then supplies a successful result. The fixture does not imply that every real failure is safe to retry automatically.
The wrapper also verifies a narrow mobile condition: at a 390-pixel viewport, the document does not overflow horizontally. It captures real waiting, completed and mobile-error states for review. These images document an executed interface, not a generated mockup or a claim about model behavior.
The page must become interactive before a test can count
A server-rendered page can look complete before the interactive circuit is ready. Testing an early click without establishing that boundary can produce a misleading result: the button exists, but the application has not yet attached the behavior under examination.
The reference page exposes a small diagnostic marker after its first interactive render. The test waits for that marker before starting a request. The marker is not a user-facing progress estimate; it identifies a test precondition.
During implementation, the first browser attempts rendered the page but did not reach interactivity because the framework's static asset was not mapped correctly. Those attempts were not counted as successful UI tests. Correcting asset delivery and waiting for the actual interactive boundary are part of making the evidence credible.
This is a useful reminder for application testing in general. An HTTP success response, visible markup and an attached interaction are different observations. The test should establish the one its claim requires.
The completed demonstration remains deliberately small. It has no authentication system, persisted conversation, network reconnect recovery or streaming partial-answer protocol. Its value is a runnable, inspectable implementation of the state boundaries that a more complete application would need to preserve.
The executed browser checks and captured states
The final browser run passed nine named functional groups with no reported page or resource errors. It checked waiting, the placement of the live status, absence of fabricated percentage progress, cooperative cancellation, supersession, a deliberately released stale completion, uncooperative cancellation, failure and retry, and the narrow mobile overflow condition. These are grouped behavioral checks, not nine claims of general accessibility compliance.
The waiting capture shows an actual pending scripted operation. Its result area is busy while the status explains that no completion estimate is available.

The completed capture was taken after the second request finished and the earlier request was deliberately released. The displayed answer remains attached to the second request; the ignored-stale-completion count records that the first result did arrive.

The mobile capture records the controlled error state before retry. The test subsequently retried with a new operation identity and accepted the released result.

The test wrapper closed its browser and stopped the temporary Kestrel process, which exited successfully. No timing benchmark or hosted model call was performed. Screen-reader behavior was not manually evaluated; the executed accessibility-related checks concern DOM semantics and layout. That boundary should remain visible when these screenshots are reused in a presentation or portfolio.
Historical versions and verification boundaries
These pins existed before the context date. Availability and static API checks do not establish a successful build or a production-safe patch level. Execution claims are limited to explicitly identified companion checks.
| Component | Version | Available by | Source |
|---|---|---|---|
| .NET SDK | 10.0.100 | 2025-11-11 | Primary record |
| .NET runtime | 10.0.0 | 2025-11-11 | Primary record |
| ASP.NET Core shared framework | 10.0.0 | 2025-11-11 | Primary record |
| C# | 12.0 | 2024-12-03 | Primary record |