[Blazor][Components.AI] Allow custom handlers to replace structured RichContentBlock content
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 10h
- Merged PRs (30d)
- 281
Description
## Background and Motivation
`ContentBlockHandler` allows applications to consume model updates before the built-in handlers. This appears to be the natural extension point for converting streamed `TextContent` into application-defined structured rich text, such as Markdown parsed into `RichTextNode` values.
A custom handler can accumulate text by calling `RichContentBlock.AppendText()`, but it cannot update the block's structured representation:
- `RichContentBlock.Content` has an internal setter.
- `RichContentBlock.ReplaceContent()` is internal.
- The structured-node renderer used by `MessageList` is not exposed as a reusable component.
As a result, an application cannot use a custom handler to populate a standard `RichContentBlock`. It must either:
1. Transform the `IChatClient` response stream into `RichTextContent` snapshots before `UIAgent` processes it, as the Components AI Dojo currently does.
2. Define a custom block and duplicate the package's structured-node renderer.
Transforming the chat-client stream couples presentation formatting to model updates. `UIAgent` builds its internal conversation history from those transformed updates, so cumulative presentation snapshots can be retained alongside the original provider text.
Related work:
- #68324 introduced rich-text rendering and demonstrates the `FormattedChatClient` approach.
- #68418 tracks related rich-node renderer extensibility, but not custom-handler access to `RichContentBlock` content.
## Proposed API
Make the existing atomic replacement method public:
```diff
namespace Microsoft.AspNetCore.Components.AI;
public class RichContentBlock : ContentBlock
{
public string RawText { get; }
public IReadOnlyList Content { get; internal set; }
public void AppendText(string text);
- internal void ReplaceContent(
+ public void ReplaceContent(
string text,
IReadOnlyList content);
}
```
Keeping the `Content` setter internal while exposing `ReplaceContent` ensures that raw text and its structured representation are replaced atomically.
## Usage Examples
```csharp
internal sealed class MarkdownContentHandler
: ContentBlockHandler
{
public override BlockMappingResult Handle(
BlockMappingContext context,
RichContentBlock state)
{
TextContent? textContent = null;
foreach (var content in context.UnhandledContents)
{
if (content is TextContent text)
{
textContent = text;
break;
}
}
if (textContent is null)
{
return state.Id.Length > 0
? BlockMappingResult.Complete()
: BlockMappingResult.Pass();
}
context.MarkHandled(textContent);
state.AppendText(textContent.Text ?? string.Empty);
state.ReplaceContent(
state.RawText,
MarkdownParser.Parse(state.RawText));
if (state.Id.Length == 0)
{
state.Id = context.Update.MessageId ?? Guid.NewGuid().ToString("N");
return BlockMappingResult.Emit(state, state);
}
return BlockMappingResult.Update(state);
}
}
```
This keeps provider updates and `UIAgent` history as ordinary text while applying formatting only in the presentation pipeline.
## Alternative Designs
- Add a parser callback to `UIAgentOptions` or the built-in text handler.
- Expose the structured-node renderer as a reusable component for custom blocks.
- Introduce a separate public mutable structured-content block type.
A public `ReplaceContent` method is the smallest change and uses the atomic snapshot model already implemented by `RichTextContentHandler`.
## Risks
Applications could provide nodes that do not correspond to the supplied raw text. This is already possible through `RichTextContent`, and `ReplaceContent` preserves the same snapshot contract.
Contributor guide
Research direction
Start with RichContentBlock and the existing RichTextContentHandler, which already uses the atomic replacement model described here. Expose ReplaceContent with the shown signature while keeping Content's setter internal, then verify a custom ContentBlockHandler can replace raw text and structured nodes atomically.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- frontend
- Issue type
- Feature
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100